@ministryofjustice/frontend 3.5.0 → 3.6.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 (36) hide show
  1. package/moj/all.jquery.js +13378 -0
  2. package/moj/all.jquery.min.js +1 -81
  3. package/moj/all.js +2577 -2853
  4. package/moj/all.mjs +126 -0
  5. package/moj/components/add-another/add-another.js +111 -132
  6. package/moj/components/add-another/add-another.mjs +106 -0
  7. package/moj/components/alert/alert.js +352 -479
  8. package/moj/components/alert/alert.mjs +251 -0
  9. package/moj/components/alert/alert.spec.helper.js +6 -24
  10. package/moj/components/alert/alert.spec.helper.mjs +66 -0
  11. package/moj/components/button-menu/button-menu.js +326 -343
  12. package/moj/components/button-menu/button-menu.mjs +329 -0
  13. package/moj/components/date-picker/date-picker.js +905 -922
  14. package/moj/components/date-picker/date-picker.mjs +961 -0
  15. package/moj/components/filter-toggle-button/filter-toggle-button.js +98 -119
  16. package/moj/components/filter-toggle-button/filter-toggle-button.mjs +93 -0
  17. package/moj/components/form-validator/form-validator.js +201 -396
  18. package/moj/components/form-validator/form-validator.mjs +168 -0
  19. package/moj/components/multi-file-upload/multi-file-upload.js +227 -441
  20. package/moj/components/multi-file-upload/multi-file-upload.mjs +219 -0
  21. package/moj/components/multi-select/multi-select.js +82 -103
  22. package/moj/components/multi-select/multi-select.mjs +77 -0
  23. package/moj/components/password-reveal/password-reveal.js +40 -61
  24. package/moj/components/password-reveal/password-reveal.mjs +35 -0
  25. package/moj/components/rich-text-editor/rich-text-editor.js +162 -183
  26. package/moj/components/rich-text-editor/rich-text-editor.mjs +157 -0
  27. package/moj/components/search-toggle/search-toggle.js +52 -73
  28. package/moj/components/search-toggle/search-toggle.mjs +54 -0
  29. package/moj/components/sortable-table/sortable-table.js +143 -164
  30. package/moj/components/sortable-table/sortable-table.mjs +138 -0
  31. package/moj/helpers.js +196 -215
  32. package/moj/helpers.mjs +123 -0
  33. package/moj/moj-frontend.min.js +1 -81
  34. package/moj/version.js +6 -23
  35. package/moj/version.mjs +3 -0
  36. package/package.json +13 -1
@@ -1,482 +1,355 @@
1
1
  (function (global, factory) {
2
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3
- typeof define === 'function' && define.amd ? define(factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.MOJFrontend = factory());
5
- })(this, (function () { 'use strict';
6
-
7
- function getDefaultExportFromCjs (x) {
8
- return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
9
- }
10
-
11
- var helpers;
12
- var hasRequiredHelpers;
13
-
14
- function requireHelpers () {
15
- if (hasRequiredHelpers) return helpers;
16
- hasRequiredHelpers = 1;
17
- function removeAttributeValue(el, attr, value) {
18
- let re, m;
19
- if (el.getAttribute(attr)) {
20
- if (el.getAttribute(attr) === value) {
21
- el.removeAttribute(attr);
22
- } else {
23
- re = new RegExp(`(^|\\s)${value}(\\s|$)`);
24
- m = el.getAttribute(attr).match(re);
25
- if (m && m.length === 3) {
26
- el.setAttribute(
27
- attr,
28
- el.getAttribute(attr).replace(re, m[1] && m[2] ? ' ' : '')
29
- );
30
- }
31
- }
32
- }
33
- }
34
-
35
- function addAttributeValue(el, attr, value) {
36
- let re;
37
- if (!el.getAttribute(attr)) {
38
- el.setAttribute(attr, value);
39
- } else {
40
- re = new RegExp(`(^|\\s)${value}(\\s|$)`);
41
- if (!re.test(el.getAttribute(attr))) {
42
- el.setAttribute(attr, `${el.getAttribute(attr)} ${value}`);
43
- }
44
- }
45
- }
46
-
47
- function dragAndDropSupported() {
48
- const div = document.createElement('div');
49
- return typeof div.ondrop !== 'undefined'
50
- }
51
-
52
- function formDataSupported() {
53
- return typeof FormData === 'function'
54
- }
55
-
56
- function fileApiSupported() {
57
- const input = document.createElement('input');
58
- input.type = 'file';
59
- return typeof input.files !== 'undefined'
60
- }
61
-
62
- function nodeListForEach(nodes, callback) {
63
- if (window.NodeList.prototype.forEach) {
64
- return nodes.forEach(callback)
65
- }
66
- for (let i = 0; i < nodes.length; i++) {
67
- callback.call(window, nodes[i], i, nodes);
68
- }
69
- }
70
-
71
- /**
72
- * Find an elements next sibling
73
- *
74
- * Utility function to find an elements next sibling matching the provided
75
- * selector.
76
- *
77
- * @param {HTMLElement} $element - Element to find siblings for
78
- * @param {string} selector - selector for required sibling
79
- */
80
- function getNextSibling($element, selector) {
81
- if (!$element) return
82
- // Get the next sibling element
83
- let $sibling = $element.nextElementSibling;
84
-
85
- // If there's no selector, return the first sibling
86
- if (!selector) return $sibling
87
-
88
- // If the sibling matches our selector, use it
89
- // If not, jump to the next sibling and continue the loop
90
- while ($sibling) {
91
- if ($sibling.matches(selector)) return $sibling
92
- $sibling = $sibling.nextElementSibling;
93
- }
94
- }
95
-
96
- /**
97
- * Find an elements preceding sibling
98
- *
99
- * Utility function to find an elements previous sibling matching the provided
100
- * selector.
101
- *
102
- * @param {HTMLElement} $element - Element to find siblings for
103
- * @param {string} selector - selector for required sibling
104
- */
105
- function getPreviousSibling($element, selector) {
106
- if (!$element) return
107
- // Get the previous sibling element
108
- let $sibling = $element.previousElementSibling;
109
-
110
- // If there's no selector, return the first sibling
111
- if (!selector) return $sibling
112
-
113
- // If the sibling matches our selector, use it
114
- // If not, jump to the next sibling and continue the loop
115
- while ($sibling) {
116
- if ($sibling.matches(selector)) return $sibling
117
- $sibling = $sibling.previousElementSibling;
118
- }
119
- }
120
-
121
- function findNearestMatchingElement($element, selector) {
122
- // If no element or selector is provided, return null
123
- if (!$element) return
124
- if (!selector) return
125
-
126
- // Start with the current element
127
- let $currentElement = $element;
128
-
129
- while ($currentElement) {
130
- // First check the current element
131
- if ($currentElement.matches(selector)) {
132
- return $currentElement
133
- }
134
-
135
- // Check all previous siblings
136
- let $sibling = $currentElement.previousElementSibling;
137
- while ($sibling) {
138
- // Check if the sibling itself is a heading
139
- if ($sibling.matches(selector)) {
140
- return $sibling
141
- }
142
- $sibling = $sibling.previousElementSibling;
143
- }
144
-
145
- // If no match found in siblings, move up to parent
146
- $currentElement = $currentElement.parentElement;
147
- }
148
- }
149
-
150
- /**
151
- * Move focus to element
152
- *
153
- * Sets tabindex to -1 to make the element programmatically focusable,
154
- * but removes it on blur as the element doesn't need to be focused again.
155
- *
156
- * @param {HTMLElement} $element - HTML element
157
- * @param {object} [options] - Handler options
158
- * @param {function(this: HTMLElement): void} [options.onBeforeFocus] - Callback before focus
159
- * @param {function(this: HTMLElement): void} [options.onBlur] - Callback on blur
160
- */
161
- function setFocus($element, options = {}) {
162
- const isFocusable = $element.getAttribute('tabindex');
163
-
164
- if (!isFocusable) {
165
- $element.setAttribute('tabindex', '-1');
166
- }
167
-
168
- /**
169
- * Handle element focus
170
- */
171
- function onFocus() {
172
- $element.addEventListener('blur', onBlur, { once: true });
173
- }
174
-
175
- /**
176
- * Handle element blur
177
- */
178
- function onBlur() {
179
- if (options.onBlur) {
180
- options.onBlur.call($element);
181
- }
182
-
183
- if (!isFocusable) {
184
- $element.removeAttribute('tabindex');
185
- }
186
- }
187
-
188
- // Add listener to reset element on blur, after focus
189
- $element.addEventListener('focus', onFocus, { once: true });
190
-
191
- // Focus element
192
- if (options.onBeforeFocus) {
193
- options.onBeforeFocus.call($element);
194
- }
195
- $element.focus();
196
- }
197
-
198
- helpers = {
199
- removeAttributeValue,
200
- addAttributeValue,
201
- dragAndDropSupported,
202
- formDataSupported,
203
- fileApiSupported,
204
- nodeListForEach,
205
- getNextSibling,
206
- getPreviousSibling,
207
- findNearestMatchingElement,
208
- setFocus
209
- };
210
- return helpers;
211
- }
212
-
213
- var alert$1;
214
- var hasRequiredAlert;
215
-
216
- function requireAlert () {
217
- if (hasRequiredAlert) return alert$1;
218
- hasRequiredAlert = 1;
219
- const {
220
- findNearestMatchingElement,
221
- getPreviousSibling,
222
- setFocus
223
- } = requireHelpers();
224
-
225
- /**
226
- * @typedef {object} AlertConfig
227
- * @property {boolean} [dismissible=false] - Can the alert be dismissed by the user
228
- * @property {string} [dismissText=Dismiss] - the label text for the dismiss button
229
- * @property {boolean} [disableAutoFocus=false] - whether the alert will be autofocused
230
- * @property {string} [focusOnDismissSelector] - CSS Selector for element to be focused on dismiss
231
- */
232
-
233
- /**
234
- * @param {HTMLElement} $module - the Alert element
235
- * @param {AlertConfig} config - configuration options
236
- * @class
237
- */
238
- function Alert($module, config = {}) {
239
- if (!$module) {
240
- return this
241
- }
242
-
243
- const schema = Object.freeze({
244
- properties: {
245
- dismissible: { type: 'boolean' },
246
- dismissText: { type: 'string' },
247
- disableAutoFocus: { type: 'boolean' },
248
- focusOnDismissSelector: { type: 'string' }
249
- }
250
- });
251
-
252
- const defaults = {
253
- dismissible: false,
254
- dismissText: 'Dismiss',
255
- disableAutoFocus: false
256
- };
257
-
258
- // data attributes override JS config, which overrides defaults
259
- this.config = this.mergeConfigs(
260
- defaults,
261
- config,
262
- this.parseDataset(schema, $module.dataset)
263
- );
264
-
265
- this.$module = $module;
266
- }
267
-
268
- Alert.prototype.init = function () {
269
- /**
270
- * Focus the alert
271
- *
272
- * If `role="alert"` is set, focus the element to help some assistive
273
- * technologies prioritise announcing it.
274
- *
275
- * You can turn off the auto-focus functionality by setting
276
- * `data-disable-auto-focus="true"` in the component HTML. You might wish to
277
- * do this based on user research findings, or to avoid a clash with another
278
- * element which should be focused when the page loads.
279
- */
280
- if (
281
- this.$module.getAttribute('role') === 'alert' &&
282
- !this.config.disableAutoFocus
283
- ) {
284
- setFocus(this.$module);
285
- }
286
-
287
- this.$dismissButton = this.$module.querySelector('.moj-alert__dismiss');
288
-
289
- if (this.config.dismissible && this.$dismissButton) {
290
- this.$dismissButton.innerHTML = this.config.dismissText;
291
- this.$dismissButton.removeAttribute('hidden');
292
-
293
- this.$module.addEventListener('click', (event) => {
294
- if (this.$dismissButton.contains(event.target)) {
295
- this.dimiss();
296
- }
297
- });
298
- }
299
- };
300
-
301
- /**
302
- * Handle dismissing the alert
303
- */
304
- Alert.prototype.dimiss = function () {
305
- let $elementToRecieveFocus;
306
-
307
- // If a selector has been provided, attempt to find that element
308
- if (this.config.focusOnDismissSelector) {
309
- $elementToRecieveFocus = document.querySelector(
310
- this.config.focusOnDismissSelector
311
- );
312
- }
313
-
314
- // Is the next sibling another alert
315
- if (!$elementToRecieveFocus) {
316
- const $nextSibling = this.$module.nextElementSibling;
317
- if ($nextSibling && $nextSibling.matches('.moj-alert')) {
318
- $elementToRecieveFocus = $nextSibling;
319
- }
320
- }
321
-
322
- // Else try to find any preceding sibling alert or heading
323
- if (!$elementToRecieveFocus) {
324
- $elementToRecieveFocus = getPreviousSibling(
325
- this.$module,
326
- '.moj-alert, h1, h2, h3, h4, h5, h6'
327
- );
328
- }
329
-
330
- // Else find the closest ancestor heading, or fallback to main, or last resort
331
- // use the body element
332
- if (!$elementToRecieveFocus) {
333
- $elementToRecieveFocus = findNearestMatchingElement(
334
- this.$module,
335
- 'h1, h2, h3, h4, h5, h6, main, body'
336
- );
337
- }
338
-
339
- // If we have an element, place focus on it
340
- if ($elementToRecieveFocus) {
341
- setFocus($elementToRecieveFocus);
342
- }
343
-
344
- // Remove the alert
345
- this.$module.remove();
346
- };
347
-
348
- /**
349
- * Normalise string
350
- *
351
- * 'If it looks like a duck, and it quacks like a duck…' 🦆
352
- *
353
- * If the passed value looks like a boolean or a number, convert it to a boolean
354
- * or number.
355
- *
356
- * Designed to be used to convert config passed via data attributes (which are
357
- * always strings) into something sensible.
358
- *
359
- * @internal
360
- * @param {DOMStringMap[string]} value - The value to normalise
361
- * @param {SchemaProperty} [property] - Component schema property
362
- * @returns {string | boolean | number | undefined} Normalised data
363
- */
364
- Alert.prototype.normaliseString = function (value, property) {
365
- const trimmedValue = value ? value.trim() : '';
366
-
367
- let output;
368
- let outputType;
369
- if (property && property.type) {
370
- outputType = property.type;
371
- }
372
-
373
- // No schema type set? Determine automatically
374
- if (!outputType) {
375
- if (['true', 'false'].includes(trimmedValue)) {
376
- outputType = 'boolean';
377
- }
378
-
379
- // Empty / whitespace-only strings are considered finite so we need to check
380
- // the length of the trimmed string as well
381
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
382
- outputType = 'number';
383
- }
384
- }
385
-
386
- switch (outputType) {
387
- case 'boolean':
388
- output = trimmedValue === 'true';
389
- break
390
-
391
- case 'number':
392
- output = Number(trimmedValue);
393
- break
394
-
395
- default:
396
- output = value;
397
- }
398
-
399
- return output
400
- };
401
-
402
- /**
403
- * Parse dataset
404
- *
405
- * Loop over an object and normalise each value using {@link normaliseString},
406
- * optionally expanding nested `i18n.field`
407
- *
408
- * @param {Schema} schema - component schema
409
- * @param {DOMStringMap} dataset - HTML element dataset
410
- * @returns {object} Normalised dataset
411
- */
412
- Alert.prototype.parseDataset = function (schema, dataset) {
413
- const parsed = {};
414
-
415
- for (const [field, property] of Object.entries(schema.properties)) {
416
- if (field in dataset) {
417
- if (dataset[field]) {
418
- parsed[field] = this.normaliseString(dataset[field], property);
419
- }
420
- }
421
- }
422
-
423
- return parsed
424
- };
425
-
426
- /**
427
- * Config merging function
428
- *
429
- * Takes any number of objects and combines them together, with
430
- * greatest priority on the LAST item passed in.
431
- *
432
- * @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
433
- * @returns {{ [key: string]: unknown }} A merged config object
434
- */
435
- Alert.prototype.mergeConfigs = function (...configObjects) {
436
- const formattedConfigObject = {};
437
-
438
- // Loop through each of the passed objects
439
- for (const configObject of configObjects) {
440
- for (const key of Object.keys(configObject)) {
441
- const option = formattedConfigObject[key];
442
- const override = configObject[key];
443
-
444
- // Push their keys one-by-one into formattedConfigObject. Any duplicate
445
- // keys with object values will be merged, otherwise the new value will
446
- // override the existing value.
447
- if (typeof option === 'object' && typeof override === 'object') {
448
- // @ts-expect-error Index signature for type 'string' is missing
449
- formattedConfigObject[key] = this.mergeConfigs(option, override);
450
- } else {
451
- formattedConfigObject[key] = override;
452
- }
453
- }
454
- }
455
-
456
- return formattedConfigObject
457
- };
458
-
459
- alert$1 = { Alert };
460
-
461
- /**
462
- * Schema for component config
463
- *
464
- * @typedef {object} Schema
465
- * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
466
- */
467
-
468
- /**
469
- * Schema property for component config
470
- *
471
- * @typedef {object} SchemaProperty
472
- * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
473
- */
474
- return alert$1;
475
- }
476
-
477
- var alertExports = requireAlert();
478
- var alert = /*@__PURE__*/getDefaultExportFromCjs(alertExports);
479
-
480
- return alert;
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MOJFrontend = global.MOJFrontend || {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ /**
8
+ * Find an elements preceding sibling
9
+ *
10
+ * Utility function to find an elements previous sibling matching the provided
11
+ * selector.
12
+ *
13
+ * @param {HTMLElement} $element - Element to find siblings for
14
+ * @param {string} selector - selector for required sibling
15
+ */
16
+ function getPreviousSibling($element, selector) {
17
+ if (!$element) return
18
+ // Get the previous sibling element
19
+ let $sibling = $element.previousElementSibling;
20
+
21
+ // If the sibling matches our selector, use it
22
+ // If not, jump to the next sibling and continue the loop
23
+ while ($sibling) {
24
+ if ($sibling.matches(selector)) return $sibling
25
+ $sibling = $sibling.previousElementSibling;
26
+ }
27
+ }
28
+
29
+ function findNearestMatchingElement($element, selector) {
30
+ // If no element or selector is provided, return null
31
+ if (!$element) return
32
+
33
+ // Start with the current element
34
+ let $currentElement = $element;
35
+
36
+ while ($currentElement) {
37
+ // First check the current element
38
+ if ($currentElement.matches(selector)) {
39
+ return $currentElement
40
+ }
41
+
42
+ // Check all previous siblings
43
+ let $sibling = $currentElement.previousElementSibling;
44
+ while ($sibling) {
45
+ // Check if the sibling itself is a heading
46
+ if ($sibling.matches(selector)) {
47
+ return $sibling
48
+ }
49
+ $sibling = $sibling.previousElementSibling;
50
+ }
51
+
52
+ // If no match found in siblings, move up to parent
53
+ $currentElement = $currentElement.parentElement;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Move focus to element
59
+ *
60
+ * Sets tabindex to -1 to make the element programmatically focusable,
61
+ * but removes it on blur as the element doesn't need to be focused again.
62
+ *
63
+ * @param {HTMLElement} $element - HTML element
64
+ * @param {object} [options] - Handler options
65
+ * @param {function(this: HTMLElement): void} [options.onBeforeFocus] - Callback before focus
66
+ * @param {function(this: HTMLElement): void} [options.onBlur] - Callback on blur
67
+ */
68
+ function setFocus($element, options = {}) {
69
+ const isFocusable = $element.getAttribute('tabindex');
70
+
71
+ if (!isFocusable) {
72
+ $element.setAttribute('tabindex', '-1');
73
+ }
74
+
75
+ /**
76
+ * Handle element focus
77
+ */
78
+ function onFocus() {
79
+ $element.addEventListener('blur', onBlur, { once: true });
80
+ }
81
+
82
+ /**
83
+ * Handle element blur
84
+ */
85
+ function onBlur() {
86
+ if (options.onBlur) {
87
+ options.onBlur.call($element);
88
+ }
89
+
90
+ if (!isFocusable) {
91
+ $element.removeAttribute('tabindex');
92
+ }
93
+ }
94
+
95
+ // Add listener to reset element on blur, after focus
96
+ $element.addEventListener('focus', onFocus, { once: true });
97
+
98
+ // Focus element
99
+ if (options.onBeforeFocus) {
100
+ options.onBeforeFocus.call($element);
101
+ }
102
+ $element.focus();
103
+ }
104
+
105
+ /**
106
+ * @typedef {object} AlertConfig
107
+ * @property {boolean} [dismissible=false] - Can the alert be dismissed by the user
108
+ * @property {string} [dismissText=Dismiss] - the label text for the dismiss button
109
+ * @property {boolean} [disableAutoFocus=false] - whether the alert will be autofocused
110
+ * @property {string} [focusOnDismissSelector] - CSS Selector for element to be focused on dismiss
111
+ */
112
+
113
+ /**
114
+ * @param {HTMLElement} $module - the Alert element
115
+ * @param {AlertConfig} config - configuration options
116
+ * @class
117
+ */
118
+ function Alert($module, config = {}) {
119
+ if (!$module) {
120
+ return this
121
+ }
122
+
123
+ const schema = Object.freeze({
124
+ properties: {
125
+ dismissible: { type: 'boolean' },
126
+ dismissText: { type: 'string' },
127
+ disableAutoFocus: { type: 'boolean' },
128
+ focusOnDismissSelector: { type: 'string' }
129
+ }
130
+ });
131
+
132
+ const defaults = {
133
+ dismissible: false,
134
+ dismissText: 'Dismiss',
135
+ disableAutoFocus: false
136
+ };
137
+
138
+ // data attributes override JS config, which overrides defaults
139
+ this.config = this.mergeConfigs(
140
+ defaults,
141
+ config,
142
+ this.parseDataset(schema, $module.dataset)
143
+ );
144
+
145
+ this.$module = $module;
146
+ }
147
+
148
+ Alert.prototype.init = function () {
149
+ /**
150
+ * Focus the alert
151
+ *
152
+ * If `role="alert"` is set, focus the element to help some assistive
153
+ * technologies prioritise announcing it.
154
+ *
155
+ * You can turn off the auto-focus functionality by setting
156
+ * `data-disable-auto-focus="true"` in the component HTML. You might wish to
157
+ * do this based on user research findings, or to avoid a clash with another
158
+ * element which should be focused when the page loads.
159
+ */
160
+ if (
161
+ this.$module.getAttribute('role') === 'alert' &&
162
+ !this.config.disableAutoFocus
163
+ ) {
164
+ setFocus(this.$module);
165
+ }
166
+
167
+ this.$dismissButton = this.$module.querySelector('.moj-alert__dismiss');
168
+
169
+ if (this.config.dismissible && this.$dismissButton) {
170
+ this.$dismissButton.innerHTML = this.config.dismissText;
171
+ this.$dismissButton.removeAttribute('hidden');
172
+
173
+ this.$module.addEventListener('click', (event) => {
174
+ if (this.$dismissButton.contains(event.target)) {
175
+ this.dimiss();
176
+ }
177
+ });
178
+ }
179
+ };
180
+
181
+ /**
182
+ * Handle dismissing the alert
183
+ */
184
+ Alert.prototype.dimiss = function () {
185
+ let $elementToRecieveFocus;
186
+
187
+ // If a selector has been provided, attempt to find that element
188
+ if (this.config.focusOnDismissSelector) {
189
+ $elementToRecieveFocus = document.querySelector(
190
+ this.config.focusOnDismissSelector
191
+ );
192
+ }
193
+
194
+ // Is the next sibling another alert
195
+ if (!$elementToRecieveFocus) {
196
+ const $nextSibling = this.$module.nextElementSibling;
197
+ if ($nextSibling && $nextSibling.matches('.moj-alert')) {
198
+ $elementToRecieveFocus = $nextSibling;
199
+ }
200
+ }
201
+
202
+ // Else try to find any preceding sibling alert or heading
203
+ if (!$elementToRecieveFocus) {
204
+ $elementToRecieveFocus = getPreviousSibling(
205
+ this.$module,
206
+ '.moj-alert, h1, h2, h3, h4, h5, h6'
207
+ );
208
+ }
209
+
210
+ // Else find the closest ancestor heading, or fallback to main, or last resort
211
+ // use the body element
212
+ if (!$elementToRecieveFocus) {
213
+ $elementToRecieveFocus = findNearestMatchingElement(
214
+ this.$module,
215
+ 'h1, h2, h3, h4, h5, h6, main, body'
216
+ );
217
+ }
218
+
219
+ // If we have an element, place focus on it
220
+ if ($elementToRecieveFocus) {
221
+ setFocus($elementToRecieveFocus);
222
+ }
223
+
224
+ // Remove the alert
225
+ this.$module.remove();
226
+ };
227
+
228
+ /**
229
+ * Normalise string
230
+ *
231
+ * 'If it looks like a duck, and it quacks like a duck…' 🦆
232
+ *
233
+ * If the passed value looks like a boolean or a number, convert it to a boolean
234
+ * or number.
235
+ *
236
+ * Designed to be used to convert config passed via data attributes (which are
237
+ * always strings) into something sensible.
238
+ *
239
+ * @internal
240
+ * @param {DOMStringMap[string]} value - The value to normalise
241
+ * @param {SchemaProperty} [property] - Component schema property
242
+ * @returns {string | boolean | number | undefined} Normalised data
243
+ */
244
+ Alert.prototype.normaliseString = function (value, property) {
245
+ const trimmedValue = value ? value.trim() : '';
246
+
247
+ let output;
248
+ let outputType;
249
+ if (property && property.type) {
250
+ outputType = property.type;
251
+ }
252
+
253
+ // No schema type set? Determine automatically
254
+ if (!outputType) {
255
+ if (['true', 'false'].includes(trimmedValue)) {
256
+ outputType = 'boolean';
257
+ }
258
+
259
+ // Empty / whitespace-only strings are considered finite so we need to check
260
+ // the length of the trimmed string as well
261
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
262
+ outputType = 'number';
263
+ }
264
+ }
265
+
266
+ switch (outputType) {
267
+ case 'boolean':
268
+ output = trimmedValue === 'true';
269
+ break
270
+
271
+ case 'number':
272
+ output = Number(trimmedValue);
273
+ break
274
+
275
+ default:
276
+ output = value;
277
+ }
278
+
279
+ return output
280
+ };
281
+
282
+ /**
283
+ * Parse dataset
284
+ *
285
+ * Loop over an object and normalise each value using {@link normaliseString},
286
+ * optionally expanding nested `i18n.field`
287
+ *
288
+ * @param {Schema} schema - component schema
289
+ * @param {DOMStringMap} dataset - HTML element dataset
290
+ * @returns {object} Normalised dataset
291
+ */
292
+ Alert.prototype.parseDataset = function (schema, dataset) {
293
+ const parsed = {};
294
+
295
+ for (const [field, property] of Object.entries(schema.properties)) {
296
+ if (field in dataset) {
297
+ if (dataset[field]) {
298
+ parsed[field] = this.normaliseString(dataset[field], property);
299
+ }
300
+ }
301
+ }
302
+
303
+ return parsed
304
+ };
305
+
306
+ /**
307
+ * Config merging function
308
+ *
309
+ * Takes any number of objects and combines them together, with
310
+ * greatest priority on the LAST item passed in.
311
+ *
312
+ * @param {...{ [key: string]: unknown }} configObjects - Config objects to merge
313
+ * @returns {{ [key: string]: unknown }} A merged config object
314
+ */
315
+ Alert.prototype.mergeConfigs = function (...configObjects) {
316
+ const formattedConfigObject = {};
317
+
318
+ // Loop through each of the passed objects
319
+ for (const configObject of configObjects) {
320
+ for (const key of Object.keys(configObject)) {
321
+ const option = formattedConfigObject[key];
322
+ const override = configObject[key];
323
+
324
+ // Push their keys one-by-one into formattedConfigObject. Any duplicate
325
+ // keys with object values will be merged, otherwise the new value will
326
+ // override the existing value.
327
+ if (typeof option === 'object' && typeof override === 'object') {
328
+ // @ts-expect-error Index signature for type 'string' is missing
329
+ formattedConfigObject[key] = this.mergeConfigs(option, override);
330
+ } else {
331
+ formattedConfigObject[key] = override;
332
+ }
333
+ }
334
+ }
335
+
336
+ return formattedConfigObject
337
+ };
338
+
339
+ /**
340
+ * Schema for component config
341
+ *
342
+ * @typedef {object} Schema
343
+ * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
344
+ */
345
+
346
+ /**
347
+ * Schema property for component config
348
+ *
349
+ * @typedef {object} SchemaProperty
350
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
351
+ */
352
+
353
+ exports.Alert = Alert;
481
354
 
482
355
  }));