@ministryofjustice/frontend 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/moj/all.bundle.js +1549 -1062
  2. package/moj/all.bundle.js.map +1 -1
  3. package/moj/all.bundle.mjs +1845 -1054
  4. package/moj/all.bundle.mjs.map +1 -1
  5. package/moj/all.mjs +7 -90
  6. package/moj/all.mjs.map +1 -1
  7. package/moj/all.scss +1 -0
  8. package/moj/all.scss.map +1 -1
  9. package/moj/common/index.mjs +57 -0
  10. package/moj/common/index.mjs.map +1 -0
  11. package/moj/common/moj-frontend-version.mjs +14 -0
  12. package/moj/common/moj-frontend-version.mjs.map +1 -0
  13. package/moj/components/add-another/add-another.bundle.js +105 -76
  14. package/moj/components/add-another/add-another.bundle.js.map +1 -1
  15. package/moj/components/add-another/add-another.bundle.mjs +222 -71
  16. package/moj/components/add-another/add-another.bundle.mjs.map +1 -1
  17. package/moj/components/add-another/add-another.mjs +103 -72
  18. package/moj/components/add-another/add-another.mjs.map +1 -1
  19. package/moj/components/alert/alert.bundle.js +115 -191
  20. package/moj/components/alert/alert.bundle.js.map +1 -1
  21. package/moj/components/alert/alert.bundle.mjs +354 -186
  22. package/moj/components/alert/alert.bundle.mjs.map +1 -1
  23. package/moj/components/alert/alert.mjs +55 -140
  24. package/moj/components/alert/alert.mjs.map +1 -1
  25. package/moj/components/button-menu/README.md +3 -1
  26. package/moj/components/button-menu/button-menu.bundle.js +91 -120
  27. package/moj/components/button-menu/button-menu.bundle.js.map +1 -1
  28. package/moj/components/button-menu/button-menu.bundle.mjs +329 -114
  29. package/moj/components/button-menu/button-menu.bundle.mjs.map +1 -1
  30. package/moj/components/button-menu/button-menu.mjs +89 -116
  31. package/moj/components/button-menu/button-menu.mjs.map +1 -1
  32. package/moj/components/date-picker/date-picker.bundle.js +174 -154
  33. package/moj/components/date-picker/date-picker.bundle.js.map +1 -1
  34. package/moj/components/date-picker/date-picker.bundle.mjs +411 -147
  35. package/moj/components/date-picker/date-picker.bundle.mjs.map +1 -1
  36. package/moj/components/date-picker/date-picker.mjs +172 -150
  37. package/moj/components/date-picker/date-picker.mjs.map +1 -1
  38. package/moj/components/filter/template.njk +1 -1
  39. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js +133 -44
  40. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js.map +1 -1
  41. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs +374 -41
  42. package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs.map +1 -1
  43. package/moj/components/filter-toggle-button/filter-toggle-button.mjs +131 -40
  44. package/moj/components/filter-toggle-button/filter-toggle-button.mjs.map +1 -1
  45. package/moj/components/form-validator/form-validator.bundle.js +159 -69
  46. package/moj/components/form-validator/form-validator.bundle.js.map +1 -1
  47. package/moj/components/form-validator/form-validator.bundle.mjs +399 -65
  48. package/moj/components/form-validator/form-validator.bundle.mjs.map +1 -1
  49. package/moj/components/form-validator/form-validator.mjs +134 -54
  50. package/moj/components/form-validator/form-validator.mjs.map +1 -1
  51. package/moj/components/multi-file-upload/multi-file-upload.bundle.js +291 -117
  52. package/moj/components/multi-file-upload/multi-file-upload.bundle.js.map +1 -1
  53. package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs +527 -109
  54. package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs.map +1 -1
  55. package/moj/components/multi-file-upload/multi-file-upload.mjs +288 -101
  56. package/moj/components/multi-file-upload/multi-file-upload.mjs.map +1 -1
  57. package/moj/components/multi-file-upload/template.njk +1 -1
  58. package/moj/components/multi-select/multi-select.bundle.js +106 -41
  59. package/moj/components/multi-select/multi-select.bundle.js.map +1 -1
  60. package/moj/components/multi-select/multi-select.bundle.mjs +346 -37
  61. package/moj/components/multi-select/multi-select.bundle.mjs.map +1 -1
  62. package/moj/components/multi-select/multi-select.mjs +104 -37
  63. package/moj/components/multi-select/multi-select.mjs.map +1 -1
  64. package/moj/components/password-reveal/_password-reveal.scss +3 -1
  65. package/moj/components/password-reveal/_password-reveal.scss.map +1 -1
  66. package/moj/components/password-reveal/password-reveal.bundle.js +32 -29
  67. package/moj/components/password-reveal/password-reveal.bundle.js.map +1 -1
  68. package/moj/components/password-reveal/password-reveal.bundle.mjs +149 -24
  69. package/moj/components/password-reveal/password-reveal.bundle.mjs.map +1 -1
  70. package/moj/components/password-reveal/password-reveal.mjs +30 -25
  71. package/moj/components/password-reveal/password-reveal.mjs.map +1 -1
  72. package/moj/components/rich-text-editor/README.md +4 -3
  73. package/moj/components/rich-text-editor/rich-text-editor.bundle.js +127 -62
  74. package/moj/components/rich-text-editor/rich-text-editor.bundle.js.map +1 -1
  75. package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs +367 -58
  76. package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs.map +1 -1
  77. package/moj/components/rich-text-editor/rich-text-editor.mjs +125 -58
  78. package/moj/components/rich-text-editor/rich-text-editor.mjs.map +1 -1
  79. package/moj/components/search-toggle/search-toggle.bundle.js +94 -26
  80. package/moj/components/search-toggle/search-toggle.bundle.js.map +1 -1
  81. package/moj/components/search-toggle/search-toggle.bundle.mjs +334 -22
  82. package/moj/components/search-toggle/search-toggle.bundle.mjs.map +1 -1
  83. package/moj/components/search-toggle/search-toggle.mjs +92 -22
  84. package/moj/components/search-toggle/search-toggle.mjs.map +1 -1
  85. package/moj/components/sortable-table/sortable-table.bundle.js +151 -83
  86. package/moj/components/sortable-table/sortable-table.bundle.js.map +1 -1
  87. package/moj/components/sortable-table/sortable-table.bundle.mjs +390 -78
  88. package/moj/components/sortable-table/sortable-table.bundle.mjs.map +1 -1
  89. package/moj/components/sortable-table/sortable-table.mjs +149 -79
  90. package/moj/components/sortable-table/sortable-table.mjs.map +1 -1
  91. package/moj/core/_all.scss +3 -0
  92. package/moj/core/_all.scss.map +1 -0
  93. package/moj/core/_moj-frontend-properties.scss +7 -0
  94. package/moj/core/_moj-frontend-properties.scss.map +1 -0
  95. package/moj/filters/prototype-kit-13-filters.js +4 -3
  96. package/moj/helpers.bundle.js +22 -77
  97. package/moj/helpers.bundle.js.map +1 -1
  98. package/moj/helpers.bundle.mjs +23 -74
  99. package/moj/helpers.bundle.mjs.map +1 -1
  100. package/moj/helpers.mjs +23 -74
  101. package/moj/helpers.mjs.map +1 -1
  102. package/moj/moj-frontend.min.css +1 -1
  103. package/moj/moj-frontend.min.css.map +1 -1
  104. package/moj/moj-frontend.min.js +1 -1
  105. package/moj/moj-frontend.min.js.map +1 -1
  106. package/package.json +1 -1
  107. package/moj/version.bundle.js +0 -12
  108. package/moj/version.bundle.js.map +0 -1
  109. package/moj/version.bundle.mjs +0 -4
  110. package/moj/version.bundle.mjs.map +0 -1
  111. package/moj/version.mjs +0 -4
  112. package/moj/version.mjs.map +0 -1
@@ -1,66 +1,276 @@
1
- class DatePicker {
1
+ function isInitialised($root, moduleName) {
2
+ return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
3
+ }
4
+
5
+ /**
6
+ * Checks if GOV.UK Frontend is supported on this page
7
+ *
8
+ * Some browsers will load and run our JavaScript but GOV.UK Frontend
9
+ * won't be supported.
10
+ *
11
+ * @param {HTMLElement | null} [$scope] - (internal) `<body>` HTML element checked for browser support
12
+ * @returns {boolean} Whether GOV.UK Frontend is supported on this page
13
+ */
14
+ function isSupported($scope = document.body) {
15
+ if (!$scope) {
16
+ return false;
17
+ }
18
+ return $scope.classList.contains('govuk-frontend-supported');
19
+ }
20
+ function isArray(option) {
21
+ return Array.isArray(option);
22
+ }
23
+ function isObject(option) {
24
+ return !!option && typeof option === 'object' && !isArray(option);
25
+ }
26
+ function formatErrorMessage(Component, message) {
27
+ return `${Component.moduleName}: ${message}`;
28
+ }
29
+
30
+ class GOVUKFrontendError extends Error {
31
+ constructor(...args) {
32
+ super(...args);
33
+ this.name = 'GOVUKFrontendError';
34
+ }
35
+ }
36
+ class SupportError extends GOVUKFrontendError {
2
37
  /**
3
- * @param {Element | null} $module - HTML element to use for date picker
4
- * @param {DatePickerConfig} [config] - Date picker config
38
+ * Checks if GOV.UK Frontend is supported on this page
39
+ *
40
+ * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
5
41
  */
6
- constructor($module, config = {}) {
7
- if (!$module || !($module instanceof HTMLElement)) {
8
- return this;
42
+ constructor($scope = document.body) {
43
+ const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
44
+ super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
45
+ this.name = 'SupportError';
46
+ }
47
+ }
48
+ class ConfigError extends GOVUKFrontendError {
49
+ constructor(...args) {
50
+ super(...args);
51
+ this.name = 'ConfigError';
52
+ }
53
+ }
54
+ class ElementError extends GOVUKFrontendError {
55
+ constructor(messageOrOptions) {
56
+ let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
57
+ if (typeof messageOrOptions === 'object') {
58
+ const {
59
+ component,
60
+ identifier,
61
+ element,
62
+ expectedType
63
+ } = messageOrOptions;
64
+ message = identifier;
65
+ message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found';
66
+ message = formatErrorMessage(component, message);
9
67
  }
10
- const $input = $module.querySelector('.moj-js-datepicker-input');
68
+ super(message);
69
+ this.name = 'ElementError';
70
+ }
71
+ }
72
+ class InitError extends GOVUKFrontendError {
73
+ constructor(componentOrMessage) {
74
+ const message = typeof componentOrMessage === 'string' ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\`$root\`) already initialised`);
75
+ super(message);
76
+ this.name = 'InitError';
77
+ }
78
+ }
11
79
 
12
- // Check that required elements are present
13
- if (!$input || !($input instanceof HTMLInputElement)) {
14
- return this;
80
+ class Component {
81
+ /**
82
+ * Returns the root element of the component
83
+ *
84
+ * @protected
85
+ * @returns {RootElementType} - the root element of component
86
+ */
87
+ get $root() {
88
+ return this._$root;
89
+ }
90
+ constructor($root) {
91
+ this._$root = void 0;
92
+ const childConstructor = this.constructor;
93
+ if (typeof childConstructor.moduleName !== 'string') {
94
+ throw new InitError(`\`moduleName\` not defined in component`);
15
95
  }
16
- this.$module = $module;
17
- this.$input = $input;
18
- const schema = Object.freeze({
19
- properties: {
20
- excludedDates: {
21
- type: 'string'
22
- },
23
- excludedDays: {
24
- type: 'string'
25
- },
26
- leadingZeros: {
27
- type: 'string'
28
- },
29
- maxDate: {
30
- type: 'string'
31
- },
32
- minDate: {
33
- type: 'string'
34
- },
35
- weekStartDay: {
36
- type: 'string'
96
+ if (!($root instanceof childConstructor.elementType)) {
97
+ throw new ElementError({
98
+ element: $root,
99
+ component: childConstructor,
100
+ identifier: 'Root element (`$root`)',
101
+ expectedType: childConstructor.elementType.name
102
+ });
103
+ } else {
104
+ this._$root = $root;
105
+ }
106
+ childConstructor.checkSupport();
107
+ this.checkInitialised();
108
+ const moduleName = childConstructor.moduleName;
109
+ this.$root.setAttribute(`data-${moduleName}-init`, '');
110
+ }
111
+ checkInitialised() {
112
+ const constructor = this.constructor;
113
+ const moduleName = constructor.moduleName;
114
+ if (moduleName && isInitialised(this.$root, moduleName)) {
115
+ throw new InitError(constructor);
116
+ }
117
+ }
118
+ static checkSupport() {
119
+ if (!isSupported()) {
120
+ throw new SupportError();
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * @typedef ChildClass
127
+ * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
128
+ */
129
+
130
+ /**
131
+ * @typedef {typeof Component & ChildClass} ChildClassConstructor
132
+ */
133
+ Component.elementType = HTMLElement;
134
+
135
+ const configOverride = Symbol.for('configOverride');
136
+ class ConfigurableComponent extends Component {
137
+ [configOverride](param) {
138
+ return {};
139
+ }
140
+
141
+ /**
142
+ * Returns the root element of the component
143
+ *
144
+ * @protected
145
+ * @returns {ConfigurationType} - the root element of component
146
+ */
147
+ get config() {
148
+ return this._config;
149
+ }
150
+ constructor($root, config) {
151
+ super($root);
152
+ this._config = void 0;
153
+ const childConstructor = this.constructor;
154
+ if (!isObject(childConstructor.defaults)) {
155
+ throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
156
+ }
157
+ const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
158
+ this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
159
+ }
160
+ }
161
+ function normaliseString(value, property) {
162
+ const trimmedValue = value ? value.trim() : '';
163
+ let output;
164
+ let outputType = property == null ? void 0 : property.type;
165
+ if (!outputType) {
166
+ if (['true', 'false'].includes(trimmedValue)) {
167
+ outputType = 'boolean';
168
+ }
169
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
170
+ outputType = 'number';
171
+ }
172
+ }
173
+ switch (outputType) {
174
+ case 'boolean':
175
+ output = trimmedValue === 'true';
176
+ break;
177
+ case 'number':
178
+ output = Number(trimmedValue);
179
+ break;
180
+ default:
181
+ output = value;
182
+ }
183
+ return output;
184
+ }
185
+ function normaliseDataset(Component, dataset) {
186
+ if (!isObject(Component.schema)) {
187
+ throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined'));
188
+ }
189
+ const out = {};
190
+ const entries = Object.entries(Component.schema.properties);
191
+ for (const entry of entries) {
192
+ const [namespace, property] = entry;
193
+ const field = namespace.toString();
194
+ if (field in dataset) {
195
+ out[field] = normaliseString(dataset[field], property);
196
+ }
197
+ if ((property == null ? void 0 : property.type) === 'object') {
198
+ out[field] = extractConfigByNamespace(Component.schema, dataset, namespace);
199
+ }
200
+ }
201
+ return out;
202
+ }
203
+ function mergeConfigs(...configObjects) {
204
+ const formattedConfigObject = {};
205
+ for (const configObject of configObjects) {
206
+ for (const key of Object.keys(configObject)) {
207
+ const option = formattedConfigObject[key];
208
+ const override = configObject[key];
209
+ if (isObject(option) && isObject(override)) {
210
+ formattedConfigObject[key] = mergeConfigs(option, override);
211
+ } else {
212
+ formattedConfigObject[key] = override;
213
+ }
214
+ }
215
+ }
216
+ return formattedConfigObject;
217
+ }
218
+ function extractConfigByNamespace(schema, dataset, namespace) {
219
+ const property = schema.properties[namespace];
220
+ if ((property == null ? void 0 : property.type) !== 'object') {
221
+ return;
222
+ }
223
+ const newObject = {
224
+ [namespace]: {}
225
+ };
226
+ for (const [key, value] of Object.entries(dataset)) {
227
+ let current = newObject;
228
+ const keyParts = key.split('.');
229
+ for (const [index, name] of keyParts.entries()) {
230
+ if (isObject(current)) {
231
+ if (index < keyParts.length - 1) {
232
+ if (!isObject(current[name])) {
233
+ current[name] = {};
234
+ }
235
+ current = current[name];
236
+ } else if (key !== namespace) {
237
+ current[name] = normaliseString(value);
37
238
  }
38
239
  }
39
- });
40
- const defaults = {
41
- leadingZeros: false,
42
- weekStartDay: 'monday'
43
- };
240
+ }
241
+ }
242
+ return newObject[namespace];
243
+ }
44
244
 
45
- // data attributes override JS config, which overrides defaults
46
- this.config = this.mergeConfigs(defaults, config, this.parseDataset(schema, $module.dataset));
245
+ /**
246
+ * @augments {ConfigurableComponent<DatePickerConfig>}
247
+ */
248
+ class DatePicker extends ConfigurableComponent {
249
+ /**
250
+ * @param {Element | null} $root - HTML element to use for date picker
251
+ * @param {DatePickerConfig} [config] - Date picker config
252
+ */
253
+ constructor($root, config = {}) {
254
+ var _this$config$input$el;
255
+ super($root, config);
256
+ const $input = (_this$config$input$el = this.config.input.element) != null ? _this$config$input$el : this.$root.querySelector(this.config.input.selector);
257
+ if (!$input || !($input instanceof HTMLInputElement)) {
258
+ return this;
259
+ }
260
+ this.$input = $input;
47
261
  this.dayLabels = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
48
262
  this.monthLabels = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
49
263
  this.currentDate = new Date();
50
264
  this.currentDate.setHours(0, 0, 0, 0);
51
- this.calendarDays = [];
52
- this.excludedDates = [];
53
- this.excludedDays = [];
265
+ this.calendarDays = /** @type {DSCalendarDay[]} */[];
266
+ this.excludedDates = /** @type {Date[]} */[];
267
+ this.excludedDays = /** @type {number[]} */[];
54
268
  this.buttonClass = 'moj-datepicker__button';
55
269
  this.selectedDayButtonClass = 'moj-datepicker__button--selected';
56
270
  this.currentDayButtonClass = 'moj-datepicker__button--current';
57
271
  this.todayButtonClass = 'moj-datepicker__button--today';
58
- if (this.$module.dataset.initialized) {
59
- return this;
60
- }
61
272
  this.setOptions();
62
273
  this.initControls();
63
- this.$module.setAttribute('data-initialized', 'true');
64
274
  }
65
275
  initControls() {
66
276
  this.id = `datepicker-${this.$input.id}`;
@@ -75,15 +285,23 @@ class DatePicker {
75
285
  $inputWrapper.appendChild(this.$input);
76
286
  $inputWrapper.insertAdjacentHTML('beforeend', this.toggleTemplate());
77
287
  $componentWrapper.insertAdjacentElement('beforeend', this.$dialog);
78
- this.$calendarButton = this.$module.querySelector('.moj-js-datepicker-toggle');
79
- this.$dialogTitle = this.$dialog.querySelector('.moj-js-datepicker-month-year');
288
+ this.$calendarButton = /** @type {HTMLButtonElement} */
289
+ this.$root.querySelector('.moj-js-datepicker-toggle');
290
+ this.$dialogTitle = /** @type {HTMLHeadingElement} */
291
+ this.$dialog.querySelector('.moj-js-datepicker-month-year');
80
292
  this.createCalendar();
81
- this.$prevMonthButton = this.$dialog.querySelector('.moj-js-datepicker-prev-month');
82
- this.$prevYearButton = this.$dialog.querySelector('.moj-js-datepicker-prev-year');
83
- this.$nextMonthButton = this.$dialog.querySelector('.moj-js-datepicker-next-month');
84
- this.$nextYearButton = this.$dialog.querySelector('.moj-js-datepicker-next-year');
85
- this.$cancelButton = this.$dialog.querySelector('.moj-js-datepicker-cancel');
86
- this.$okButton = this.$dialog.querySelector('.moj-js-datepicker-ok');
293
+ this.$prevMonthButton = /** @type {HTMLButtonElement} */
294
+ this.$dialog.querySelector('.moj-js-datepicker-prev-month');
295
+ this.$prevYearButton = /** @type {HTMLButtonElement} */
296
+ this.$dialog.querySelector('.moj-js-datepicker-prev-year');
297
+ this.$nextMonthButton = /** @type {HTMLButtonElement} */
298
+ this.$dialog.querySelector('.moj-js-datepicker-next-month');
299
+ this.$nextYearButton = /** @type {HTMLButtonElement} */
300
+ this.$dialog.querySelector('.moj-js-datepicker-next-year');
301
+ this.$cancelButton = /** @type {HTMLButtonElement} */
302
+ this.$dialog.querySelector('.moj-js-datepicker-cancel');
303
+ this.$okButton = /** @type {HTMLButtonElement} */
304
+ this.$dialog.querySelector('.moj-js-datepicker-ok');
87
305
 
88
306
  // add event listeners
89
307
  this.$prevMonthButton.addEventListener('click', event => this.focusPreviousMonth(event, false));
@@ -97,9 +315,9 @@ class DatePicker {
97
315
  this.$okButton.addEventListener('click', () => {
98
316
  this.selectDate(this.currentDate);
99
317
  });
100
- const dialogButtons = this.$dialog.querySelectorAll('button:not([disabled="true"])');
101
- this.$firstButtonInDialog = dialogButtons[0];
102
- this.$lastButtonInDialog = dialogButtons[dialogButtons.length - 1];
318
+ const $dialogButtons = this.$dialog.querySelectorAll('button:not([disabled="true"])');
319
+ this.$firstButtonInDialog = $dialogButtons[0];
320
+ this.$lastButtonInDialog = $dialogButtons[$dialogButtons.length - 1];
103
321
  this.$firstButtonInDialog.addEventListener('keydown', event => this.firstButtonKeydown(event));
104
322
  this.$lastButtonInDialog.addEventListener('keydown', event => this.lastButtonKeydown(event));
105
323
  this.$calendarButton.addEventListener('click', event => this.toggleDialog(event));
@@ -245,7 +463,6 @@ class DatePicker {
245
463
  this.setMinAndMaxDatesOnCalendar();
246
464
  this.setExcludedDates();
247
465
  this.setExcludedDays();
248
- this.setLeadingZeros();
249
466
  this.setWeekStartDay();
250
467
  }
251
468
  setMinAndMaxDatesOnCalendar() {
@@ -270,10 +487,10 @@ class DatePicker {
270
487
  }
271
488
  }
272
489
 
273
- /*
490
+ /**
274
491
  * Parses a daterange string into an array of dates
275
- * @param {String} datestring - A daterange string in the format "dd/mm/yyyy-dd/mm/yyyy"
276
- * @returns {Date[]}
492
+ *
493
+ * @param {string} datestring - A daterange string in the format "dd/mm/yyyy-dd/mm/yyyy"
277
494
  */
278
495
  parseDateRangeString(datestring) {
279
496
  const dates = [];
@@ -300,17 +517,6 @@ class DatePicker {
300
517
  this.excludedDays = this.config.excludedDays.replace(/\s+/, ' ').toLowerCase().split(' ').map(item => weekDays.indexOf(item)).filter(item => item !== -1);
301
518
  }
302
519
  }
303
- setLeadingZeros() {
304
- if (typeof this.config.leadingZeros !== 'boolean') {
305
- if (this.config.leadingZeros.toLowerCase() === 'true') {
306
- this.config.leadingZeros = true;
307
- return;
308
- }
309
- if (this.config.leadingZeros.toLowerCase() === 'false') {
310
- this.config.leadingZeros = false;
311
- }
312
- }
313
- }
314
520
  setWeekStartDay() {
315
521
  const weekStartDayParam = this.config.weekStartDay;
316
522
  if (weekStartDayParam && weekStartDayParam.toLowerCase() === 'sunday') {
@@ -323,7 +529,7 @@ class DatePicker {
323
529
  }
324
530
 
325
531
  /**
326
- * Determine if a date is selecteable
532
+ * Determine if a date is selectable
327
533
  *
328
534
  * @param {Date} date - the date to check
329
535
  * @returns {boolean}
@@ -389,24 +595,36 @@ class DatePicker {
389
595
  /**
390
596
  * Get a human readable date in the format Monday 2 March 2024
391
597
  *
392
- * @param {Date} date - date to format
598
+ * @param {Date} date - Date to format
393
599
  * @returns {string}
394
600
  */
395
601
  formattedDateHuman(date) {
396
602
  return `${this.dayLabels[(date.getDay() + 6) % 7]} ${date.getDate()} ${this.monthLabels[date.getMonth()]} ${date.getFullYear()}`;
397
603
  }
604
+
605
+ /**
606
+ * @param {MouseEvent} event - Click event
607
+ */
398
608
  backgroundClick(event) {
399
609
  if (this.isOpen() && event.target instanceof Node && !this.$dialog.contains(event.target) && !this.$input.contains(event.target) && !this.$calendarButton.contains(event.target)) {
400
610
  event.preventDefault();
401
611
  this.closeDialog();
402
612
  }
403
613
  }
614
+
615
+ /**
616
+ * @param {KeyboardEvent} event - Keydown event
617
+ */
404
618
  firstButtonKeydown(event) {
405
619
  if (event.key === 'Tab' && event.shiftKey) {
406
620
  this.$lastButtonInDialog.focus();
407
621
  event.preventDefault();
408
622
  }
409
623
  }
624
+
625
+ /**
626
+ * @param {KeyboardEvent} event - Keydown event
627
+ */
410
628
  lastButtonKeydown(event) {
411
629
  if (event.key === 'Tab' && !event.shiftKey) {
412
630
  this.$firstButtonInDialog.focus();
@@ -436,49 +654,57 @@ class DatePicker {
436
654
  thisDay.setDate(thisDay.getDate() + 1);
437
655
  }
438
656
  }
657
+
658
+ /**
659
+ * @param {boolean} [focus] - Focus the day button
660
+ */
439
661
  setCurrentDate(focus = true) {
440
662
  const {
441
663
  currentDate
442
664
  } = this;
443
665
  this.calendarDays.forEach(calendarDay => {
444
- calendarDay.button.classList.add('moj-datepicker__button');
445
- calendarDay.button.classList.add('moj-datepicker__calendar-day');
446
- calendarDay.button.setAttribute('tabindex', '-1');
447
- calendarDay.button.classList.remove(this.selectedDayButtonClass);
666
+ calendarDay.$button.classList.add('moj-datepicker__button');
667
+ calendarDay.$button.classList.add('moj-datepicker__calendar-day');
668
+ calendarDay.$button.setAttribute('tabindex', '-1');
669
+ calendarDay.$button.classList.remove(this.selectedDayButtonClass);
448
670
  const calendarDayDate = calendarDay.date;
449
671
  calendarDayDate.setHours(0, 0, 0, 0);
450
672
  const today = new Date();
451
673
  today.setHours(0, 0, 0, 0);
452
674
  if (calendarDayDate.getTime() === currentDate.getTime() /* && !calendarDay.button.disabled */) {
453
675
  if (focus) {
454
- calendarDay.button.setAttribute('tabindex', '0');
455
- calendarDay.button.focus();
456
- calendarDay.button.classList.add(this.selectedDayButtonClass);
676
+ calendarDay.$button.setAttribute('tabindex', '0');
677
+ calendarDay.$button.focus();
678
+ calendarDay.$button.classList.add(this.selectedDayButtonClass);
457
679
  }
458
680
  }
459
681
  if (this.inputDate && calendarDayDate.getTime() === this.inputDate.getTime()) {
460
- calendarDay.button.classList.add(this.currentDayButtonClass);
461
- calendarDay.button.setAttribute('aria-current', 'date');
682
+ calendarDay.$button.classList.add(this.currentDayButtonClass);
683
+ calendarDay.$button.setAttribute('aria-current', 'date');
462
684
  } else {
463
- calendarDay.button.classList.remove(this.currentDayButtonClass);
464
- calendarDay.button.removeAttribute('aria-current');
685
+ calendarDay.$button.classList.remove(this.currentDayButtonClass);
686
+ calendarDay.$button.removeAttribute('aria-current');
465
687
  }
466
688
  if (calendarDayDate.getTime() === today.getTime()) {
467
- calendarDay.button.classList.add(this.todayButtonClass);
689
+ calendarDay.$button.classList.add(this.todayButtonClass);
468
690
  } else {
469
- calendarDay.button.classList.remove(this.todayButtonClass);
691
+ calendarDay.$button.classList.remove(this.todayButtonClass);
470
692
  }
471
693
  });
472
694
 
473
695
  // if no date is tab-able, make the first non-disabled date tab-able
474
696
  if (!focus) {
475
697
  const enabledDays = this.calendarDays.filter(calendarDay => {
476
- return window.getComputedStyle(calendarDay.button).display === 'block' && !calendarDay.button.disabled;
698
+ return window.getComputedStyle(calendarDay.$button).display === 'block' && !calendarDay.$button.disabled;
477
699
  });
478
- enabledDays[0].button.setAttribute('tabindex', '0');
700
+ enabledDays[0].$button.setAttribute('tabindex', '0');
479
701
  this.currentDate = enabledDays[0].date;
480
702
  }
481
703
  }
704
+
705
+ /**
706
+ * @param {Date} date - Date to select
707
+ */
482
708
  selectDate(date) {
483
709
  if (this.isExcludedDate(date)) {
484
710
  return;
@@ -495,6 +721,10 @@ class DatePicker {
495
721
  isOpen() {
496
722
  return this.$dialog.classList.contains('moj-datepicker__dialog--open');
497
723
  }
724
+
725
+ /**
726
+ * @param {MouseEvent} event - Click event
727
+ */
498
728
  toggleDialog(event) {
499
729
  event.preventDefault();
500
730
  if (this.isOpen()) {
@@ -529,6 +759,11 @@ class DatePicker {
529
759
  this.$calendarButton.setAttribute('aria-expanded', 'false');
530
760
  this.$calendarButton.focus();
531
761
  }
762
+
763
+ /**
764
+ * @param {Date} date - Date to go to
765
+ * @param {boolean} [focus] - Focus the day button
766
+ */
532
767
  goToDate(date, focus) {
533
768
  const current = this.currentDate;
534
769
  this.currentDate = date;
@@ -580,13 +815,23 @@ class DatePicker {
580
815
  this.goToDate(date);
581
816
  }
582
817
 
583
- // month navigation
818
+ /**
819
+ * Month navigation
820
+ *
821
+ * @param {KeyboardEvent | MouseEvent} event - Key press or click event
822
+ * @param {boolean} [focus] - Focus the day button
823
+ */
584
824
  focusNextMonth(event, focus = true) {
585
825
  event.preventDefault();
586
826
  const date = new Date(this.currentDate);
587
827
  date.setMonth(date.getMonth() + 1, 1);
588
828
  this.goToDate(date, focus);
589
829
  }
830
+
831
+ /**
832
+ * @param {KeyboardEvent | MouseEvent} event - Key press or click event
833
+ * @param {boolean} [focus] - Focus the day button
834
+ */
590
835
  focusPreviousMonth(event, focus = true) {
591
836
  event.preventDefault();
592
837
  const date = new Date(this.currentDate);
@@ -594,13 +839,23 @@ class DatePicker {
594
839
  this.goToDate(date, focus);
595
840
  }
596
841
 
597
- // year navigation
842
+ /**
843
+ * Year navigation
844
+ *
845
+ * @param {KeyboardEvent | MouseEvent} event - Key press or click event
846
+ * @param {boolean} [focus] - Focus the day button
847
+ */
598
848
  focusNextYear(event, focus = true) {
599
849
  event.preventDefault();
600
850
  const date = new Date(this.currentDate);
601
851
  date.setFullYear(date.getFullYear() + 1, date.getMonth(), 1);
602
852
  this.goToDate(date, focus);
603
853
  }
854
+
855
+ /**
856
+ * @param {KeyboardEvent | MouseEvent} event - Key press or click event
857
+ * @param {boolean} [focus] - Focus the day button
858
+ */
604
859
  focusPreviousYear(event, focus = true) {
605
860
  event.preventDefault();
606
861
  const date = new Date(this.currentDate);
@@ -609,72 +864,70 @@ class DatePicker {
609
864
  }
610
865
 
611
866
  /**
612
- * Parse dataset
613
- *
614
- * @param {Schema} schema - Component class
615
- * @param {DOMStringMap} dataset - HTML element dataset
616
- * @returns {object} Normalised dataset
867
+ * Name for the component used when initialising using data-module attributes.
617
868
  */
618
- parseDataset(schema, dataset) {
619
- const parsed = {};
620
- for (const [field,,] of Object.entries(schema.properties)) {
621
- if (field in dataset) {
622
- parsed[field] = dataset[field];
623
- }
624
- }
625
- return parsed;
869
+ }
870
+ DatePicker.moduleName = 'moj-date-picker';
871
+ /**
872
+ * Date picker default config
873
+ *
874
+ * @type {DatePickerConfig}
875
+ */
876
+ DatePicker.defaults = Object.freeze({
877
+ leadingZeros: false,
878
+ weekStartDay: 'monday',
879
+ input: {
880
+ selector: '.moj-js-datepicker-input'
626
881
  }
627
-
628
- /**
629
- * Config merging function
630
- *
631
- * Takes any number of objects and combines them together, with
632
- * greatest priority on the LAST item passed in.
633
- *
634
- * @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
635
- * @returns {{ [key: string]: unknown }} A merged config object
636
- */
637
- mergeConfigs(...configObjects) {
638
- const formattedConfigObject = {};
639
-
640
- // Loop through each of the passed objects
641
- for (const configObject of configObjects) {
642
- for (const key of Object.keys(configObject)) {
643
- const option = formattedConfigObject[key];
644
- const override = configObject[key];
645
-
646
- // Push their keys one-by-one into formattedConfigObject. Any duplicate
647
- // keys with object values will be merged, otherwise the new value will
648
- // override the existing value.
649
- if (typeof option === 'object' && typeof override === 'object') {
650
- // @ts-expect-error Index signature for type 'string' is missing
651
- formattedConfigObject[key] = this.mergeConfigs(option, override);
652
- } else {
653
- formattedConfigObject[key] = override;
654
- }
655
- }
882
+ });
883
+ /**
884
+ * Date picker config schema
885
+ *
886
+ * @satisfies {Schema<DatePickerConfig>}
887
+ */
888
+ DatePicker.schema = Object.freeze(/** @type {const} */{
889
+ properties: {
890
+ excludedDates: {
891
+ type: 'string'
892
+ },
893
+ excludedDays: {
894
+ type: 'string'
895
+ },
896
+ leadingZeros: {
897
+ type: 'boolean'
898
+ },
899
+ maxDate: {
900
+ type: 'string'
901
+ },
902
+ minDate: {
903
+ type: 'string'
904
+ },
905
+ weekStartDay: {
906
+ type: 'string'
907
+ },
908
+ input: {
909
+ type: 'object'
656
910
  }
657
- return formattedConfigObject;
658
911
  }
659
- }
912
+ });
660
913
  class DSCalendarDay {
661
914
  /**
662
915
  *
663
- * @param {HTMLElement} button
916
+ * @param {HTMLButtonElement} $button
664
917
  * @param {number} index
665
918
  * @param {number} row
666
919
  * @param {number} column
667
920
  * @param {DatePicker} picker
668
921
  */
669
- constructor(button, index, row, column, picker) {
922
+ constructor($button, index, row, column, picker) {
670
923
  this.index = index;
671
924
  this.row = row;
672
925
  this.column = column;
673
- this.button = button;
926
+ this.$button = $button;
674
927
  this.picker = picker;
675
928
  this.date = new Date();
676
- this.button.addEventListener('keydown', this.keyPress.bind(this));
677
- this.button.addEventListener('click', this.click.bind(this));
929
+ this.$button.addEventListener('keydown', this.keyPress.bind(this));
930
+ this.$button.addEventListener('click', this.click.bind(this));
678
931
  }
679
932
 
680
933
  /**
@@ -686,26 +939,34 @@ class DSCalendarDay {
686
939
  const label = day.getDate();
687
940
  let accessibleLabel = this.picker.formattedDateHuman(day);
688
941
  if (disabled) {
689
- this.button.setAttribute('aria-disabled', 'true');
942
+ this.$button.setAttribute('aria-disabled', 'true');
690
943
  accessibleLabel = `Excluded date, ${accessibleLabel}`;
691
944
  } else {
692
- this.button.removeAttribute('aria-disabled');
945
+ this.$button.removeAttribute('aria-disabled');
693
946
  }
694
947
  if (hidden) {
695
- this.button.style.display = 'none';
948
+ this.$button.style.display = 'none';
696
949
  } else {
697
- this.button.style.display = 'block';
950
+ this.$button.style.display = 'block';
698
951
  }
699
- this.button.setAttribute('data-testid', this.picker.formattedDateFromDate(day));
700
- this.button.innerHTML = `<span class="govuk-visually-hidden">${accessibleLabel}</span><span aria-hidden="true">${label}</span>`;
952
+ this.$button.setAttribute('data-testid', this.picker.formattedDateFromDate(day));
953
+ this.$button.innerHTML = `<span class="govuk-visually-hidden">${accessibleLabel}</span><span aria-hidden="true">${label}</span>`;
701
954
  this.date = new Date(day);
702
955
  }
956
+
957
+ /**
958
+ * @param {MouseEvent} event - Click event
959
+ */
703
960
  click(event) {
704
961
  this.picker.goToDate(this.date);
705
962
  this.picker.selectDate(this.date);
706
963
  event.stopPropagation();
707
964
  event.preventDefault();
708
965
  }
966
+
967
+ /**
968
+ * @param {KeyboardEvent} event - Keydown event
969
+ */
709
970
  keyPress(event) {
710
971
  let calendarNavKey = true;
711
972
  switch (event.key) {
@@ -762,14 +1023,17 @@ class DSCalendarDay {
762
1023
  * @typedef {object} DatePickerConfig
763
1024
  * @property {string} [excludedDates] - Dates that cannot be selected
764
1025
  * @property {string} [excludedDays] - Days that cannot be selected
765
- * @property {boolean} [leadingZeroes] - Whether to add leading zeroes when populating the field
1026
+ * @property {boolean} [leadingZeros] - Whether to add leading zeroes when populating the field
766
1027
  * @property {string} [minDate] - The earliest available date
767
1028
  * @property {string} [maxDate] - The latest available date
768
1029
  * @property {string} [weekStartDay] - First day of the week in calendar view
1030
+ * @property {object} [input] - Input config
1031
+ * @property {string} [input.selector] - Selector for the input element
1032
+ * @property {Element | null} [input.element] - HTML element for the input
769
1033
  */
770
1034
 
771
1035
  /**
772
- * @import { Schema } from '../../all.mjs'
1036
+ * @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
773
1037
  */
774
1038
 
775
1039
  export { DatePicker };