@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,73 +1,267 @@
|
|
|
1
|
+
function isInitialised($root, moduleName) {
|
|
2
|
+
return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
*
|
|
6
|
+
* Checks if GOV.UK Frontend is supported on this page
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
8
|
+
* Some browsers will load and run our JavaScript but GOV.UK Frontend
|
|
9
|
+
* won't be supported.
|
|
6
10
|
*
|
|
7
|
-
* @param {
|
|
8
|
-
* @
|
|
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
|
|
9
13
|
*/
|
|
10
|
-
function
|
|
11
|
-
if (!$
|
|
12
|
-
return;
|
|
14
|
+
function isSupported($scope = document.body) {
|
|
15
|
+
if (!$scope) {
|
|
16
|
+
return false;
|
|
13
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
|
+
}
|
|
14
29
|
|
|
15
|
-
|
|
16
|
-
|
|
30
|
+
class GOVUKFrontendError extends Error {
|
|
31
|
+
constructor(...args) {
|
|
32
|
+
super(...args);
|
|
33
|
+
this.name = 'GOVUKFrontendError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
class SupportError extends GOVUKFrontendError {
|
|
37
|
+
/**
|
|
38
|
+
* Checks if GOV.UK Frontend is supported on this page
|
|
39
|
+
*
|
|
40
|
+
* @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
|
|
41
|
+
*/
|
|
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);
|
|
67
|
+
}
|
|
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
|
+
}
|
|
17
79
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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`);
|
|
95
|
+
}
|
|
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
|
+
}
|
|
23
122
|
}
|
|
24
123
|
}
|
|
25
124
|
|
|
26
125
|
/**
|
|
27
|
-
* @
|
|
28
|
-
* @
|
|
126
|
+
* @typedef ChildClass
|
|
127
|
+
* @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
|
|
29
128
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 {};
|
|
34
139
|
}
|
|
35
140
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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'));
|
|
42
156
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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);
|
|
238
|
+
}
|
|
50
239
|
}
|
|
51
|
-
$sibling = $sibling.previousElementSibling;
|
|
52
240
|
}
|
|
53
|
-
|
|
54
|
-
// If no match found in siblings, move up to parent
|
|
55
|
-
$currentElement = $currentElement.parentElement;
|
|
56
241
|
}
|
|
242
|
+
return newObject[namespace];
|
|
57
243
|
}
|
|
58
244
|
|
|
245
|
+
/**
|
|
246
|
+
* GOV.UK Frontend helpers
|
|
247
|
+
*
|
|
248
|
+
* @todo Import from GOV.UK Frontend
|
|
249
|
+
*/
|
|
250
|
+
|
|
59
251
|
/**
|
|
60
252
|
* Move focus to element
|
|
61
253
|
*
|
|
62
254
|
* Sets tabindex to -1 to make the element programmatically focusable,
|
|
63
255
|
* but removes it on blur as the element doesn't need to be focused again.
|
|
64
256
|
*
|
|
65
|
-
* @
|
|
257
|
+
* @template {HTMLElement} FocusElement
|
|
258
|
+
* @param {FocusElement} $element - HTML element
|
|
66
259
|
* @param {object} [options] - Handler options
|
|
67
|
-
* @param {function(this:
|
|
68
|
-
* @param {function(this:
|
|
260
|
+
* @param {function(this: FocusElement): void} [options.onBeforeFocus] - Callback before focus
|
|
261
|
+
* @param {function(this: FocusElement): void} [options.onBlur] - Callback on blur
|
|
69
262
|
*/
|
|
70
263
|
function setFocus($element, options = {}) {
|
|
264
|
+
var _options$onBeforeFocu;
|
|
71
265
|
const isFocusable = $element.getAttribute('tabindex');
|
|
72
266
|
if (!isFocusable) {
|
|
73
267
|
$element.setAttribute('tabindex', '-1');
|
|
@@ -86,9 +280,8 @@ function setFocus($element, options = {}) {
|
|
|
86
280
|
* Handle element blur
|
|
87
281
|
*/
|
|
88
282
|
function onBlur() {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
283
|
+
var _options$onBlur;
|
|
284
|
+
(_options$onBlur = options.onBlur) == null || _options$onBlur.call($element);
|
|
92
285
|
if (!isFocusable) {
|
|
93
286
|
$element.removeAttribute('tabindex');
|
|
94
287
|
}
|
|
@@ -100,46 +293,84 @@ function setFocus($element, options = {}) {
|
|
|
100
293
|
});
|
|
101
294
|
|
|
102
295
|
// Focus element
|
|
103
|
-
|
|
104
|
-
options.onBeforeFocus.call($element);
|
|
105
|
-
}
|
|
296
|
+
(_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
|
|
106
297
|
$element.focus();
|
|
107
298
|
}
|
|
108
299
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
300
|
+
/**
|
|
301
|
+
* @param {Element} $element - Element to remove attribute value from
|
|
302
|
+
* @param {string} attr - Attribute name
|
|
303
|
+
* @param {string} value - Attribute value
|
|
304
|
+
*/
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Find an elements preceding sibling
|
|
308
|
+
*
|
|
309
|
+
* Utility function to find an elements previous sibling matching the provided
|
|
310
|
+
* selector.
|
|
311
|
+
*
|
|
312
|
+
* @param {Element | null} $element - Element to find siblings for
|
|
313
|
+
* @param {string} [selector] - selector for required sibling
|
|
314
|
+
*/
|
|
315
|
+
function getPreviousSibling($element, selector) {
|
|
316
|
+
if (!$element || !($element instanceof HTMLElement)) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Get the previous sibling element
|
|
321
|
+
let $sibling = $element.previousElementSibling;
|
|
322
|
+
|
|
323
|
+
// If the sibling matches our selector, use it
|
|
324
|
+
// If not, jump to the next sibling and continue the loop
|
|
325
|
+
while ($sibling) {
|
|
326
|
+
if ($sibling.matches(selector)) return $sibling;
|
|
327
|
+
$sibling = $sibling.previousElementSibling;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @param {Element | null} $element
|
|
333
|
+
* @param {string} [selector]
|
|
334
|
+
*/
|
|
335
|
+
function findNearestMatchingElement($element, selector) {
|
|
336
|
+
// If no element or selector is provided, return
|
|
337
|
+
if (!$element || !($element instanceof HTMLElement) || false) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Start with the current element
|
|
342
|
+
let $currentElement = $element;
|
|
343
|
+
while ($currentElement) {
|
|
344
|
+
// First check the current element
|
|
345
|
+
if ($currentElement.matches(selector)) {
|
|
346
|
+
return $currentElement;
|
|
117
347
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
},
|
|
126
|
-
disableAutoFocus: {
|
|
127
|
-
type: 'boolean'
|
|
128
|
-
},
|
|
129
|
-
focusOnDismissSelector: {
|
|
130
|
-
type: 'string'
|
|
131
|
-
}
|
|
348
|
+
|
|
349
|
+
// Check all previous siblings
|
|
350
|
+
let $sibling = $currentElement.previousElementSibling;
|
|
351
|
+
while ($sibling) {
|
|
352
|
+
// Check if the sibling itself is a heading
|
|
353
|
+
if ($sibling.matches(selector)) {
|
|
354
|
+
return $sibling;
|
|
132
355
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
dismissible: false,
|
|
136
|
-
dismissText: 'Dismiss',
|
|
137
|
-
disableAutoFocus: false
|
|
138
|
-
};
|
|
356
|
+
$sibling = $sibling.previousElementSibling;
|
|
357
|
+
}
|
|
139
358
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
359
|
+
// If no match found in siblings, move up to parent
|
|
360
|
+
$currentElement = $currentElement.parentElement;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* @augments {ConfigurableComponent<AlertConfig>}
|
|
366
|
+
*/
|
|
367
|
+
class Alert extends ConfigurableComponent {
|
|
368
|
+
/**
|
|
369
|
+
* @param {Element | null} $root - HTML element to use for alert
|
|
370
|
+
* @param {AlertConfig} [config] - Alert config
|
|
371
|
+
*/
|
|
372
|
+
constructor($root, config = {}) {
|
|
373
|
+
super($root, config);
|
|
143
374
|
|
|
144
375
|
/**
|
|
145
376
|
* Focus the alert
|
|
@@ -152,14 +383,14 @@ class Alert {
|
|
|
152
383
|
* do this based on user research findings, or to avoid a clash with another
|
|
153
384
|
* element which should be focused when the page loads.
|
|
154
385
|
*/
|
|
155
|
-
if (this.$
|
|
156
|
-
setFocus(this.$
|
|
386
|
+
if (this.$root.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
|
|
387
|
+
setFocus(this.$root);
|
|
157
388
|
}
|
|
158
|
-
this.$dismissButton = this.$
|
|
389
|
+
this.$dismissButton = this.$root.querySelector('.moj-alert__dismiss');
|
|
159
390
|
if (this.config.dismissible && this.$dismissButton) {
|
|
160
391
|
this.$dismissButton.innerHTML = this.config.dismissText;
|
|
161
392
|
this.$dismissButton.removeAttribute('hidden');
|
|
162
|
-
this.$
|
|
393
|
+
this.$root.addEventListener('click', event => {
|
|
163
394
|
if (event.target instanceof Node && this.$dismissButton.contains(event.target)) {
|
|
164
395
|
this.dimiss();
|
|
165
396
|
}
|
|
@@ -180,7 +411,7 @@ class Alert {
|
|
|
180
411
|
|
|
181
412
|
// Is the next sibling another alert
|
|
182
413
|
if (!$elementToRecieveFocus) {
|
|
183
|
-
const $nextSibling = this.$
|
|
414
|
+
const $nextSibling = this.$root.nextElementSibling;
|
|
184
415
|
if ($nextSibling && $nextSibling.matches('.moj-alert')) {
|
|
185
416
|
$elementToRecieveFocus = $nextSibling;
|
|
186
417
|
}
|
|
@@ -188,13 +419,13 @@ class Alert {
|
|
|
188
419
|
|
|
189
420
|
// Else try to find any preceding sibling alert or heading
|
|
190
421
|
if (!$elementToRecieveFocus) {
|
|
191
|
-
$elementToRecieveFocus = getPreviousSibling(this.$
|
|
422
|
+
$elementToRecieveFocus = getPreviousSibling(this.$root, '.moj-alert, h1, h2, h3, h4, h5, h6');
|
|
192
423
|
}
|
|
193
424
|
|
|
194
425
|
// Else find the closest ancestor heading, or fallback to main, or last resort
|
|
195
426
|
// use the body element
|
|
196
427
|
if (!$elementToRecieveFocus) {
|
|
197
|
-
$elementToRecieveFocus = findNearestMatchingElement(this.$
|
|
428
|
+
$elementToRecieveFocus = findNearestMatchingElement(this.$root, 'h1, h2, h3, h4, h5, h6, main, body');
|
|
198
429
|
}
|
|
199
430
|
|
|
200
431
|
// If we have an element, place focus on it
|
|
@@ -203,111 +434,12 @@ class Alert {
|
|
|
203
434
|
}
|
|
204
435
|
|
|
205
436
|
// Remove the alert
|
|
206
|
-
this.$
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Normalise string
|
|
211
|
-
*
|
|
212
|
-
* 'If it looks like a duck, and it quacks like a duck…' 🦆
|
|
213
|
-
*
|
|
214
|
-
* If the passed value looks like a boolean or a number, convert it to a boolean
|
|
215
|
-
* or number.
|
|
216
|
-
*
|
|
217
|
-
* Designed to be used to convert config passed via data attributes (which are
|
|
218
|
-
* always strings) into something sensible.
|
|
219
|
-
*
|
|
220
|
-
* @internal
|
|
221
|
-
* @param {DOMStringMap[string]} value - The value to normalise
|
|
222
|
-
* @param {SchemaProperty} [property] - Component schema property
|
|
223
|
-
* @returns {string | boolean | number | undefined} Normalised data
|
|
224
|
-
*/
|
|
225
|
-
normaliseString(value, property) {
|
|
226
|
-
const trimmedValue = value ? value.trim() : '';
|
|
227
|
-
let output;
|
|
228
|
-
let outputType;
|
|
229
|
-
if (property && property.type) {
|
|
230
|
-
outputType = property.type;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// No schema type set? Determine automatically
|
|
234
|
-
if (!outputType) {
|
|
235
|
-
if (['true', 'false'].includes(trimmedValue)) {
|
|
236
|
-
outputType = 'boolean';
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Empty / whitespace-only strings are considered finite so we need to check
|
|
240
|
-
// the length of the trimmed string as well
|
|
241
|
-
if (trimmedValue.length > 0 && Number.isFinite(Number(trimmedValue))) {
|
|
242
|
-
outputType = 'number';
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
switch (outputType) {
|
|
246
|
-
case 'boolean':
|
|
247
|
-
output = trimmedValue === 'true';
|
|
248
|
-
break;
|
|
249
|
-
case 'number':
|
|
250
|
-
output = Number(trimmedValue);
|
|
251
|
-
break;
|
|
252
|
-
default:
|
|
253
|
-
output = value;
|
|
254
|
-
}
|
|
255
|
-
return output;
|
|
437
|
+
this.$root.remove();
|
|
256
438
|
}
|
|
257
439
|
|
|
258
440
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* Loop over an object and normalise each value using {@link normaliseString},
|
|
262
|
-
* optionally expanding nested `i18n.field`
|
|
263
|
-
*
|
|
264
|
-
* @param {Schema} schema - component schema
|
|
265
|
-
* @param {DOMStringMap} dataset - HTML element dataset
|
|
266
|
-
* @returns {object} Normalised dataset
|
|
441
|
+
* Name for the component used when initialising using data-module attributes.
|
|
267
442
|
*/
|
|
268
|
-
parseDataset(schema, dataset) {
|
|
269
|
-
const parsed = {};
|
|
270
|
-
for (const [field, property] of Object.entries(schema.properties)) {
|
|
271
|
-
if (field in dataset) {
|
|
272
|
-
if (dataset[field]) {
|
|
273
|
-
parsed[field] = this.normaliseString(dataset[field], property);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
return parsed;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Config merging function
|
|
282
|
-
*
|
|
283
|
-
* Takes any number of objects and combines them together, with
|
|
284
|
-
* greatest priority on the LAST item passed in.
|
|
285
|
-
*
|
|
286
|
-
* @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
|
|
287
|
-
* @returns {{ [key: string]: unknown }} A merged config object
|
|
288
|
-
*/
|
|
289
|
-
mergeConfigs(...configObjects) {
|
|
290
|
-
const formattedConfigObject = {};
|
|
291
|
-
|
|
292
|
-
// Loop through each of the passed objects
|
|
293
|
-
for (const configObject of configObjects) {
|
|
294
|
-
for (const key of Object.keys(configObject)) {
|
|
295
|
-
const option = formattedConfigObject[key];
|
|
296
|
-
const override = configObject[key];
|
|
297
|
-
|
|
298
|
-
// Push their keys one-by-one into formattedConfigObject. Any duplicate
|
|
299
|
-
// keys with object values will be merged, otherwise the new value will
|
|
300
|
-
// override the existing value.
|
|
301
|
-
if (typeof option === 'object' && typeof override === 'object') {
|
|
302
|
-
// @ts-expect-error Index signature for type 'string' is missing
|
|
303
|
-
formattedConfigObject[key] = this.mergeConfigs(option, override);
|
|
304
|
-
} else {
|
|
305
|
-
formattedConfigObject[key] = override;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return formattedConfigObject;
|
|
310
|
-
}
|
|
311
443
|
}
|
|
312
444
|
|
|
313
445
|
/**
|
|
@@ -318,5 +450,41 @@ class Alert {
|
|
|
318
450
|
* @property {string} [focusOnDismissSelector] - CSS Selector for element to be focused on dismiss
|
|
319
451
|
*/
|
|
320
452
|
|
|
453
|
+
/**
|
|
454
|
+
* @import { Schema } from 'govuk-frontend/dist/govuk/common/configuration.mjs'
|
|
455
|
+
*/
|
|
456
|
+
Alert.moduleName = 'moj-alert';
|
|
457
|
+
/**
|
|
458
|
+
* Alert default config
|
|
459
|
+
*
|
|
460
|
+
* @type {AlertConfig}
|
|
461
|
+
*/
|
|
462
|
+
Alert.defaults = Object.freeze({
|
|
463
|
+
dismissible: false,
|
|
464
|
+
dismissText: 'Dismiss',
|
|
465
|
+
disableAutoFocus: false
|
|
466
|
+
});
|
|
467
|
+
/**
|
|
468
|
+
* Alert config schema
|
|
469
|
+
*
|
|
470
|
+
* @satisfies {Schema<AlertConfig>}
|
|
471
|
+
*/
|
|
472
|
+
Alert.schema = Object.freeze(/** @type {const} */{
|
|
473
|
+
properties: {
|
|
474
|
+
dismissible: {
|
|
475
|
+
type: 'boolean'
|
|
476
|
+
},
|
|
477
|
+
dismissText: {
|
|
478
|
+
type: 'string'
|
|
479
|
+
},
|
|
480
|
+
disableAutoFocus: {
|
|
481
|
+
type: 'boolean'
|
|
482
|
+
},
|
|
483
|
+
focusOnDismissSelector: {
|
|
484
|
+
type: 'string'
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
321
489
|
export { Alert };
|
|
322
490
|
//# sourceMappingURL=alert.bundle.mjs.map
|