@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.
- package/moj/all.bundle.js +1549 -1062
- package/moj/all.bundle.js.map +1 -1
- package/moj/all.bundle.mjs +1845 -1054
- package/moj/all.bundle.mjs.map +1 -1
- package/moj/all.mjs +7 -90
- package/moj/all.mjs.map +1 -1
- package/moj/all.scss +1 -0
- package/moj/all.scss.map +1 -1
- package/moj/common/index.mjs +57 -0
- package/moj/common/index.mjs.map +1 -0
- package/moj/common/moj-frontend-version.mjs +14 -0
- package/moj/common/moj-frontend-version.mjs.map +1 -0
- package/moj/components/add-another/add-another.bundle.js +105 -76
- package/moj/components/add-another/add-another.bundle.js.map +1 -1
- package/moj/components/add-another/add-another.bundle.mjs +222 -71
- package/moj/components/add-another/add-another.bundle.mjs.map +1 -1
- package/moj/components/add-another/add-another.mjs +103 -72
- package/moj/components/add-another/add-another.mjs.map +1 -1
- package/moj/components/alert/alert.bundle.js +115 -191
- package/moj/components/alert/alert.bundle.js.map +1 -1
- package/moj/components/alert/alert.bundle.mjs +354 -186
- package/moj/components/alert/alert.bundle.mjs.map +1 -1
- package/moj/components/alert/alert.mjs +55 -140
- package/moj/components/alert/alert.mjs.map +1 -1
- package/moj/components/button-menu/README.md +3 -1
- package/moj/components/button-menu/button-menu.bundle.js +91 -120
- package/moj/components/button-menu/button-menu.bundle.js.map +1 -1
- package/moj/components/button-menu/button-menu.bundle.mjs +329 -114
- package/moj/components/button-menu/button-menu.bundle.mjs.map +1 -1
- package/moj/components/button-menu/button-menu.mjs +89 -116
- package/moj/components/button-menu/button-menu.mjs.map +1 -1
- package/moj/components/date-picker/date-picker.bundle.js +174 -154
- package/moj/components/date-picker/date-picker.bundle.js.map +1 -1
- package/moj/components/date-picker/date-picker.bundle.mjs +411 -147
- package/moj/components/date-picker/date-picker.bundle.mjs.map +1 -1
- package/moj/components/date-picker/date-picker.mjs +172 -150
- package/moj/components/date-picker/date-picker.mjs.map +1 -1
- package/moj/components/filter/template.njk +1 -1
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js +133 -44
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.js.map +1 -1
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs +374 -41
- package/moj/components/filter-toggle-button/filter-toggle-button.bundle.mjs.map +1 -1
- package/moj/components/filter-toggle-button/filter-toggle-button.mjs +131 -40
- package/moj/components/filter-toggle-button/filter-toggle-button.mjs.map +1 -1
- package/moj/components/form-validator/form-validator.bundle.js +159 -69
- package/moj/components/form-validator/form-validator.bundle.js.map +1 -1
- package/moj/components/form-validator/form-validator.bundle.mjs +399 -65
- package/moj/components/form-validator/form-validator.bundle.mjs.map +1 -1
- package/moj/components/form-validator/form-validator.mjs +134 -54
- package/moj/components/form-validator/form-validator.mjs.map +1 -1
- package/moj/components/multi-file-upload/multi-file-upload.bundle.js +291 -117
- package/moj/components/multi-file-upload/multi-file-upload.bundle.js.map +1 -1
- package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs +527 -109
- package/moj/components/multi-file-upload/multi-file-upload.bundle.mjs.map +1 -1
- package/moj/components/multi-file-upload/multi-file-upload.mjs +288 -101
- package/moj/components/multi-file-upload/multi-file-upload.mjs.map +1 -1
- package/moj/components/multi-file-upload/template.njk +1 -1
- package/moj/components/multi-select/multi-select.bundle.js +106 -41
- package/moj/components/multi-select/multi-select.bundle.js.map +1 -1
- package/moj/components/multi-select/multi-select.bundle.mjs +346 -37
- package/moj/components/multi-select/multi-select.bundle.mjs.map +1 -1
- package/moj/components/multi-select/multi-select.mjs +104 -37
- package/moj/components/multi-select/multi-select.mjs.map +1 -1
- package/moj/components/password-reveal/_password-reveal.scss +3 -1
- package/moj/components/password-reveal/_password-reveal.scss.map +1 -1
- package/moj/components/password-reveal/password-reveal.bundle.js +32 -29
- package/moj/components/password-reveal/password-reveal.bundle.js.map +1 -1
- package/moj/components/password-reveal/password-reveal.bundle.mjs +149 -24
- package/moj/components/password-reveal/password-reveal.bundle.mjs.map +1 -1
- package/moj/components/password-reveal/password-reveal.mjs +30 -25
- package/moj/components/password-reveal/password-reveal.mjs.map +1 -1
- package/moj/components/rich-text-editor/README.md +4 -3
- package/moj/components/rich-text-editor/rich-text-editor.bundle.js +127 -62
- package/moj/components/rich-text-editor/rich-text-editor.bundle.js.map +1 -1
- package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs +367 -58
- package/moj/components/rich-text-editor/rich-text-editor.bundle.mjs.map +1 -1
- package/moj/components/rich-text-editor/rich-text-editor.mjs +125 -58
- package/moj/components/rich-text-editor/rich-text-editor.mjs.map +1 -1
- package/moj/components/search-toggle/search-toggle.bundle.js +94 -26
- package/moj/components/search-toggle/search-toggle.bundle.js.map +1 -1
- package/moj/components/search-toggle/search-toggle.bundle.mjs +334 -22
- package/moj/components/search-toggle/search-toggle.bundle.mjs.map +1 -1
- package/moj/components/search-toggle/search-toggle.mjs +92 -22
- package/moj/components/search-toggle/search-toggle.mjs.map +1 -1
- package/moj/components/sortable-table/sortable-table.bundle.js +151 -83
- package/moj/components/sortable-table/sortable-table.bundle.js.map +1 -1
- package/moj/components/sortable-table/sortable-table.bundle.mjs +390 -78
- package/moj/components/sortable-table/sortable-table.bundle.mjs.map +1 -1
- package/moj/components/sortable-table/sortable-table.mjs +149 -79
- package/moj/components/sortable-table/sortable-table.mjs.map +1 -1
- package/moj/core/_all.scss +3 -0
- package/moj/core/_all.scss.map +1 -0
- package/moj/core/_moj-frontend-properties.scss +7 -0
- package/moj/core/_moj-frontend-properties.scss.map +1 -0
- package/moj/filters/prototype-kit-13-filters.js +4 -3
- package/moj/helpers.bundle.js +22 -77
- package/moj/helpers.bundle.js.map +1 -1
- package/moj/helpers.bundle.mjs +23 -74
- package/moj/helpers.bundle.mjs.map +1 -1
- package/moj/helpers.mjs +23 -74
- package/moj/helpers.mjs.map +1 -1
- package/moj/moj-frontend.min.css +1 -1
- package/moj/moj-frontend.min.css.map +1 -1
- package/moj/moj-frontend.min.js +1 -1
- package/moj/moj-frontend.min.js.map +1 -1
- package/package.json +1 -1
- package/moj/version.bundle.js +0 -12
- package/moj/version.bundle.js.map +0 -1
- package/moj/version.bundle.mjs +0 -4
- package/moj/version.bundle.mjs.map +0 -1
- package/moj/version.mjs +0 -4
- package/moj/version.mjs.map +0 -1
|
@@ -1,66 +1,276 @@
|
|
|
1
|
-
|
|
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
|
-
*
|
|
4
|
-
*
|
|
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($
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return newObject[namespace];
|
|
243
|
+
}
|
|
44
244
|
|
|
45
|
-
|
|
46
|
-
|
|
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 =
|
|
79
|
-
this.$
|
|
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 =
|
|
82
|
-
this.$
|
|
83
|
-
this.$
|
|
84
|
-
this.$
|
|
85
|
-
this.$
|
|
86
|
-
this.$
|
|
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
|
-
*
|
|
276
|
-
* @
|
|
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
|
|
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 -
|
|
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
|
|
445
|
-
calendarDay
|
|
446
|
-
calendarDay
|
|
447
|
-
calendarDay
|
|
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
|
|
455
|
-
calendarDay
|
|
456
|
-
calendarDay
|
|
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
|
|
461
|
-
calendarDay
|
|
682
|
+
calendarDay.$button.classList.add(this.currentDayButtonClass);
|
|
683
|
+
calendarDay.$button.setAttribute('aria-current', 'date');
|
|
462
684
|
} else {
|
|
463
|
-
calendarDay
|
|
464
|
-
calendarDay
|
|
685
|
+
calendarDay.$button.classList.remove(this.currentDayButtonClass);
|
|
686
|
+
calendarDay.$button.removeAttribute('aria-current');
|
|
465
687
|
}
|
|
466
688
|
if (calendarDayDate.getTime() === today.getTime()) {
|
|
467
|
-
calendarDay
|
|
689
|
+
calendarDay.$button.classList.add(this.todayButtonClass);
|
|
468
690
|
} else {
|
|
469
|
-
calendarDay
|
|
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
|
|
698
|
+
return window.getComputedStyle(calendarDay.$button).display === 'block' && !calendarDay.$button.disabled;
|
|
477
699
|
});
|
|
478
|
-
enabledDays[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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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 {
|
|
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
|
|
926
|
+
this.$button = $button;
|
|
674
927
|
this.picker = picker;
|
|
675
928
|
this.date = new Date();
|
|
676
|
-
this
|
|
677
|
-
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
|
|
942
|
+
this.$button.setAttribute('aria-disabled', 'true');
|
|
690
943
|
accessibleLabel = `Excluded date, ${accessibleLabel}`;
|
|
691
944
|
} else {
|
|
692
|
-
this
|
|
945
|
+
this.$button.removeAttribute('aria-disabled');
|
|
693
946
|
}
|
|
694
947
|
if (hidden) {
|
|
695
|
-
this
|
|
948
|
+
this.$button.style.display = 'none';
|
|
696
949
|
} else {
|
|
697
|
-
this
|
|
950
|
+
this.$button.style.display = 'block';
|
|
698
951
|
}
|
|
699
|
-
this
|
|
700
|
-
this
|
|
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} [
|
|
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 '
|
|
1036
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
773
1037
|
*/
|
|
774
1038
|
|
|
775
1039
|
export { DatePicker };
|