govuk_tech_docs 4.1.2 → 4.3.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/govuk_tech_docs.gemspec +1 -1
  4. data/lib/assets/stylesheets/_govuk_tech_docs.scss +3 -0
  5. data/lib/govuk_tech_docs/meta_tags.rb +5 -1
  6. data/lib/govuk_tech_docs/version.rb +1 -1
  7. data/lib/source/layouts/core.erb +21 -5
  8. data/node_modules/govuk-frontend/dist/govuk/all.bundle.js +508 -209
  9. data/node_modules/govuk-frontend/dist/govuk/all.bundle.mjs +505 -208
  10. data/node_modules/govuk-frontend/dist/govuk/all.mjs +3 -1
  11. data/node_modules/govuk-frontend/dist/govuk/all.scss +6 -0
  12. data/node_modules/govuk-frontend/dist/govuk/common/configuration.mjs +169 -0
  13. data/node_modules/govuk-frontend/dist/govuk/common/govuk-frontend-version.mjs +1 -1
  14. data/node_modules/govuk-frontend/dist/govuk/common/index.mjs +4 -87
  15. data/node_modules/govuk-frontend/dist/govuk/{govuk-frontend-component.mjs → component.mjs} +5 -5
  16. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js +161 -116
  17. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs +160 -115
  18. data/node_modules/govuk-frontend/dist/govuk/components/accordion/accordion.mjs +5 -8
  19. data/node_modules/govuk-frontend/dist/govuk/components/button/button.bundle.js +161 -116
  20. data/node_modules/govuk-frontend/dist/govuk/components/button/button.bundle.mjs +160 -115
  21. data/node_modules/govuk-frontend/dist/govuk/components/button/button.mjs +5 -8
  22. data/node_modules/govuk-frontend/dist/govuk/components/character-count/_index.scss +8 -0
  23. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js +187 -145
  24. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs +186 -144
  25. data/node_modules/govuk-frontend/dist/govuk/components/character-count/character-count.mjs +18 -17
  26. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js +9 -29
  27. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs +8 -28
  28. data/node_modules/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs +2 -2
  29. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js +161 -116
  30. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs +160 -115
  31. data/node_modules/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs +6 -8
  32. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js +161 -116
  33. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs +160 -115
  34. data/node_modules/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs +5 -8
  35. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/_index.scss +167 -0
  36. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.js +754 -0
  37. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.bundle.mjs +746 -0
  38. data/node_modules/govuk-frontend/dist/govuk/components/file-upload/file-upload.mjs +267 -0
  39. data/node_modules/govuk-frontend/dist/govuk/components/header/_index.scss +14 -10
  40. data/node_modules/govuk-frontend/dist/govuk/components/header/header.bundle.js +9 -29
  41. data/node_modules/govuk-frontend/dist/govuk/components/header/header.bundle.mjs +8 -28
  42. data/node_modules/govuk-frontend/dist/govuk/components/header/header.mjs +2 -2
  43. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js +161 -116
  44. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs +160 -115
  45. data/node_modules/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs +6 -8
  46. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js +161 -117
  47. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs +160 -116
  48. data/node_modules/govuk-frontend/dist/govuk/components/password-input/password-input.mjs +5 -9
  49. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.bundle.js +9 -29
  50. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs +8 -28
  51. data/node_modules/govuk-frontend/dist/govuk/components/radios/radios.mjs +2 -2
  52. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js +9 -29
  53. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs +8 -28
  54. data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.mjs +2 -2
  55. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js +10 -30
  56. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs +9 -29
  57. data/node_modules/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs +3 -3
  58. data/node_modules/govuk-frontend/dist/govuk/components/summary-list/_index.scss +12 -21
  59. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js +9 -29
  60. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs +8 -28
  61. data/node_modules/govuk-frontend/dist/govuk/components/tabs/tabs.mjs +2 -2
  62. data/node_modules/govuk-frontend/dist/govuk/core/_govuk-frontend-properties.scss +1 -1
  63. data/node_modules/govuk-frontend/dist/govuk/errors/index.mjs +1 -1
  64. data/node_modules/govuk-frontend/dist/govuk/govuk-frontend.min.js +1 -1
  65. data/node_modules/govuk-frontend/dist/govuk/helpers/_colour.scss +2 -2
  66. data/node_modules/govuk-frontend/dist/govuk/init.mjs +28 -24
  67. data/node_modules/govuk-frontend/dist/govuk/settings/_colours-organisations.scss +18 -5
  68. data/node_modules/govuk-frontend/dist/govuk/settings/_typography-responsive.scss +5 -10
  69. data/node_modules/govuk-frontend/dist/govuk-prototype-kit/init.scss +1 -1
  70. data/package-lock.json +8 -7
  71. data/package.json +1 -1
  72. metadata +12 -10
  73. data/node_modules/govuk-frontend/dist/govuk/common/normalise-dataset.mjs +0 -18
  74. data/node_modules/govuk-frontend/dist/govuk/common/normalise-string.mjs +0 -31
@@ -1,79 +1,9 @@
1
1
  (function (global, factory) {
2
2
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
3
  typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = {}));
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = global.GOVUKFrontend || {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
- function normaliseString(value, property) {
8
- const trimmedValue = value ? value.trim() : '';
9
- let output;
10
- let outputType = property == null ? void 0 : property.type;
11
- if (!outputType) {
12
- if (['true', 'false'].includes(trimmedValue)) {
13
- outputType = 'boolean';
14
- }
15
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
16
- outputType = 'number';
17
- }
18
- }
19
- switch (outputType) {
20
- case 'boolean':
21
- output = trimmedValue === 'true';
22
- break;
23
- case 'number':
24
- output = Number(trimmedValue);
25
- break;
26
- default:
27
- output = value;
28
- }
29
- return output;
30
- }
31
-
32
- /**
33
- * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
34
- */
35
-
36
- function mergeConfigs(...configObjects) {
37
- const formattedConfigObject = {};
38
- for (const configObject of configObjects) {
39
- for (const key of Object.keys(configObject)) {
40
- const option = formattedConfigObject[key];
41
- const override = configObject[key];
42
- if (isObject(option) && isObject(override)) {
43
- formattedConfigObject[key] = mergeConfigs(option, override);
44
- } else {
45
- formattedConfigObject[key] = override;
46
- }
47
- }
48
- }
49
- return formattedConfigObject;
50
- }
51
- function extractConfigByNamespace(Component, dataset, namespace) {
52
- const property = Component.schema.properties[namespace];
53
- if ((property == null ? void 0 : property.type) !== 'object') {
54
- return;
55
- }
56
- const newObject = {
57
- [namespace]: ({})
58
- };
59
- for (const [key, value] of Object.entries(dataset)) {
60
- let current = newObject;
61
- const keyParts = key.split('.');
62
- for (const [index, name] of keyParts.entries()) {
63
- if (typeof current === 'object') {
64
- if (index < keyParts.length - 1) {
65
- if (!isObject(current[name])) {
66
- current[name] = {};
67
- }
68
- current = current[name];
69
- } else if (key !== namespace) {
70
- current[name] = normaliseString(value);
71
- }
72
- }
73
- }
74
- }
75
- return newObject[namespace];
76
- }
77
7
  function getFragmentFromUrl(url) {
78
8
  if (!url.includes('#')) {
79
9
  return undefined;
@@ -132,46 +62,13 @@
132
62
  function formatErrorMessage(Component, message) {
133
63
  return `${Component.moduleName}: ${message}`;
134
64
  }
135
-
136
- /**
137
- * Schema for component config
138
- *
139
- * @typedef {object} Schema
140
- * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
141
- * @property {SchemaCondition[]} [anyOf] - List of schema conditions
142
- */
143
-
144
- /**
145
- * Schema property for component config
146
- *
147
- * @typedef {object} SchemaProperty
148
- * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
149
- */
150
-
151
- /**
152
- * Schema condition for component config
153
- *
154
- * @typedef {object} SchemaCondition
155
- * @property {string[]} required - List of required config fields
156
- * @property {string} errorMessage - Error message when required config fields not provided
157
- */
158
65
  /**
159
66
  * @typedef ComponentWithModuleName
160
67
  * @property {string} moduleName - Name of the component
161
68
  */
162
-
163
- function normaliseDataset(Component, dataset) {
164
- const out = {};
165
- for (const [field, property] of Object.entries(Component.schema.properties)) {
166
- if (field in dataset) {
167
- out[field] = normaliseString(dataset[field], property);
168
- }
169
- if ((property == null ? void 0 : property.type) === 'object') {
170
- out[field] = extractConfigByNamespace(Component, dataset, field);
171
- }
172
- }
173
- return out;
174
- }
69
+ /**
70
+ * @import { ObjectNested } from './configuration.mjs'
71
+ */
175
72
 
176
73
  class GOVUKFrontendError extends Error {
177
74
  constructor(...args) {
@@ -191,6 +88,12 @@
191
88
  this.name = 'SupportError';
192
89
  }
193
90
  }
91
+ class ConfigError extends GOVUKFrontendError {
92
+ constructor(...args) {
93
+ super(...args);
94
+ this.name = 'ConfigError';
95
+ }
96
+ }
194
97
  class ElementError extends GOVUKFrontendError {
195
98
  constructor(messageOrOptions) {
196
99
  let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
@@ -217,10 +120,10 @@
217
120
  }
218
121
  }
219
122
  /**
220
- * @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
123
+ * @import { ComponentWithModuleName } from '../common/index.mjs'
221
124
  */
222
125
 
223
- class GOVUKFrontendComponent {
126
+ class Component {
224
127
  /**
225
128
  * Returns the root element of the component
226
129
  *
@@ -271,9 +174,152 @@
271
174
  */
272
175
 
273
176
  /**
274
- * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
177
+ * @typedef {typeof Component & ChildClass} ChildClassConstructor
178
+ */
179
+ Component.elementType = HTMLElement;
180
+
181
+ const configOverride = Symbol.for('configOverride');
182
+ class ConfigurableComponent extends Component {
183
+ [configOverride](param) {
184
+ return {};
185
+ }
186
+
187
+ /**
188
+ * Returns the root element of the component
189
+ *
190
+ * @protected
191
+ * @returns {ConfigurationType} - the root element of component
192
+ */
193
+ get config() {
194
+ return this._config;
195
+ }
196
+ constructor($root, config) {
197
+ super($root);
198
+ this._config = void 0;
199
+ const childConstructor = this.constructor;
200
+ if (!isObject(childConstructor.defaults)) {
201
+ throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
202
+ }
203
+ const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
204
+ this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
205
+ }
206
+ }
207
+ function normaliseString(value, property) {
208
+ const trimmedValue = value ? value.trim() : '';
209
+ let output;
210
+ let outputType = property == null ? void 0 : property.type;
211
+ if (!outputType) {
212
+ if (['true', 'false'].includes(trimmedValue)) {
213
+ outputType = 'boolean';
214
+ }
215
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
216
+ outputType = 'number';
217
+ }
218
+ }
219
+ switch (outputType) {
220
+ case 'boolean':
221
+ output = trimmedValue === 'true';
222
+ break;
223
+ case 'number':
224
+ output = Number(trimmedValue);
225
+ break;
226
+ default:
227
+ output = value;
228
+ }
229
+ return output;
230
+ }
231
+ function normaliseDataset(Component, dataset) {
232
+ if (!isObject(Component.schema)) {
233
+ throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined'));
234
+ }
235
+ const out = {};
236
+ const entries = Object.entries(Component.schema.properties);
237
+ for (const entry of entries) {
238
+ const [namespace, property] = entry;
239
+ const field = namespace.toString();
240
+ if (field in dataset) {
241
+ out[field] = normaliseString(dataset[field], property);
242
+ }
243
+ if ((property == null ? void 0 : property.type) === 'object') {
244
+ out[field] = extractConfigByNamespace(Component.schema, dataset, namespace);
245
+ }
246
+ }
247
+ return out;
248
+ }
249
+ function mergeConfigs(...configObjects) {
250
+ const formattedConfigObject = {};
251
+ for (const configObject of configObjects) {
252
+ for (const key of Object.keys(configObject)) {
253
+ const option = formattedConfigObject[key];
254
+ const override = configObject[key];
255
+ if (isObject(option) && isObject(override)) {
256
+ formattedConfigObject[key] = mergeConfigs(option, override);
257
+ } else {
258
+ formattedConfigObject[key] = override;
259
+ }
260
+ }
261
+ }
262
+ return formattedConfigObject;
263
+ }
264
+ function extractConfigByNamespace(schema, dataset, namespace) {
265
+ const property = schema.properties[namespace];
266
+ if ((property == null ? void 0 : property.type) !== 'object') {
267
+ return;
268
+ }
269
+ const newObject = {
270
+ [namespace]: {}
271
+ };
272
+ for (const [key, value] of Object.entries(dataset)) {
273
+ let current = newObject;
274
+ const keyParts = key.split('.');
275
+ for (const [index, name] of keyParts.entries()) {
276
+ if (isObject(current)) {
277
+ if (index < keyParts.length - 1) {
278
+ if (!isObject(current[name])) {
279
+ current[name] = {};
280
+ }
281
+ current = current[name];
282
+ } else if (key !== namespace) {
283
+ current[name] = normaliseString(value);
284
+ }
285
+ }
286
+ }
287
+ }
288
+ return newObject[namespace];
289
+ }
290
+ /**
291
+ * Schema for component config
292
+ *
293
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
294
+ * @typedef {object} Schema
295
+ * @property {Record<keyof ConfigurationType, SchemaProperty | undefined>} properties - Schema properties
296
+ * @property {SchemaCondition<ConfigurationType>[]} [anyOf] - List of schema conditions
297
+ */
298
+ /**
299
+ * Schema property for component config
300
+ *
301
+ * @typedef {object} SchemaProperty
302
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
303
+ */
304
+ /**
305
+ * Schema condition for component config
306
+ *
307
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
308
+ * @typedef {object} SchemaCondition
309
+ * @property {(keyof ConfigurationType)[]} required - List of required config fields
310
+ * @property {string} errorMessage - Error message when required config fields not provided
311
+ */
312
+ /**
313
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
314
+ * @typedef ChildClass
315
+ * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
316
+ * @property {Schema<ConfigurationType>} [schema] - The schema of the component configuration
317
+ * @property {ConfigurationType} [defaults] - The default values of the configuration of the component
318
+ */
319
+ /**
320
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
321
+ * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
275
322
  */
276
- GOVUKFrontendComponent.elementType = HTMLElement;
277
323
 
278
324
  /**
279
325
  * Error summary component
@@ -282,16 +328,15 @@
282
328
  * configuration.
283
329
  *
284
330
  * @preserve
331
+ * @augments ConfigurableComponent<ErrorSummaryConfig>
285
332
  */
286
- class ErrorSummary extends GOVUKFrontendComponent {
333
+ class ErrorSummary extends ConfigurableComponent {
287
334
  /**
288
335
  * @param {Element | null} $root - HTML element to use for error summary
289
336
  * @param {ErrorSummaryConfig} [config] - Error summary config
290
337
  */
291
338
  constructor($root, config = {}) {
292
- super($root);
293
- this.config = void 0;
294
- this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, this.$root.dataset));
339
+ super($root, config);
295
340
  if (!this.config.disableAutoFocus) {
296
341
  setFocus(this.$root);
297
342
  }
@@ -358,7 +403,7 @@
358
403
  */
359
404
 
360
405
  /**
361
- * @typedef {import('../../common/index.mjs').Schema} Schema
406
+ * @import { Schema } from '../../common/configuration.mjs'
362
407
  */
363
408
  ErrorSummary.moduleName = 'govuk-error-summary';
364
409
  ErrorSummary.defaults = Object.freeze({
@@ -1,73 +1,3 @@
1
- function normaliseString(value, property) {
2
- const trimmedValue = value ? value.trim() : '';
3
- let output;
4
- let outputType = property == null ? void 0 : property.type;
5
- if (!outputType) {
6
- if (['true', 'false'].includes(trimmedValue)) {
7
- outputType = 'boolean';
8
- }
9
- if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
10
- outputType = 'number';
11
- }
12
- }
13
- switch (outputType) {
14
- case 'boolean':
15
- output = trimmedValue === 'true';
16
- break;
17
- case 'number':
18
- output = Number(trimmedValue);
19
- break;
20
- default:
21
- output = value;
22
- }
23
- return output;
24
- }
25
-
26
- /**
27
- * @typedef {import('./index.mjs').SchemaProperty} SchemaProperty
28
- */
29
-
30
- function mergeConfigs(...configObjects) {
31
- const formattedConfigObject = {};
32
- for (const configObject of configObjects) {
33
- for (const key of Object.keys(configObject)) {
34
- const option = formattedConfigObject[key];
35
- const override = configObject[key];
36
- if (isObject(option) && isObject(override)) {
37
- formattedConfigObject[key] = mergeConfigs(option, override);
38
- } else {
39
- formattedConfigObject[key] = override;
40
- }
41
- }
42
- }
43
- return formattedConfigObject;
44
- }
45
- function extractConfigByNamespace(Component, dataset, namespace) {
46
- const property = Component.schema.properties[namespace];
47
- if ((property == null ? void 0 : property.type) !== 'object') {
48
- return;
49
- }
50
- const newObject = {
51
- [namespace]: ({})
52
- };
53
- for (const [key, value] of Object.entries(dataset)) {
54
- let current = newObject;
55
- const keyParts = key.split('.');
56
- for (const [index, name] of keyParts.entries()) {
57
- if (typeof current === 'object') {
58
- if (index < keyParts.length - 1) {
59
- if (!isObject(current[name])) {
60
- current[name] = {};
61
- }
62
- current = current[name];
63
- } else if (key !== namespace) {
64
- current[name] = normaliseString(value);
65
- }
66
- }
67
- }
68
- }
69
- return newObject[namespace];
70
- }
71
1
  function getFragmentFromUrl(url) {
72
2
  if (!url.includes('#')) {
73
3
  return undefined;
@@ -126,46 +56,13 @@ function isObject(option) {
126
56
  function formatErrorMessage(Component, message) {
127
57
  return `${Component.moduleName}: ${message}`;
128
58
  }
129
-
130
- /**
131
- * Schema for component config
132
- *
133
- * @typedef {object} Schema
134
- * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
135
- * @property {SchemaCondition[]} [anyOf] - List of schema conditions
136
- */
137
-
138
- /**
139
- * Schema property for component config
140
- *
141
- * @typedef {object} SchemaProperty
142
- * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
143
- */
144
-
145
- /**
146
- * Schema condition for component config
147
- *
148
- * @typedef {object} SchemaCondition
149
- * @property {string[]} required - List of required config fields
150
- * @property {string} errorMessage - Error message when required config fields not provided
151
- */
152
59
  /**
153
60
  * @typedef ComponentWithModuleName
154
61
  * @property {string} moduleName - Name of the component
155
62
  */
156
-
157
- function normaliseDataset(Component, dataset) {
158
- const out = {};
159
- for (const [field, property] of Object.entries(Component.schema.properties)) {
160
- if (field in dataset) {
161
- out[field] = normaliseString(dataset[field], property);
162
- }
163
- if ((property == null ? void 0 : property.type) === 'object') {
164
- out[field] = extractConfigByNamespace(Component, dataset, field);
165
- }
166
- }
167
- return out;
168
- }
63
+ /**
64
+ * @import { ObjectNested } from './configuration.mjs'
65
+ */
169
66
 
170
67
  class GOVUKFrontendError extends Error {
171
68
  constructor(...args) {
@@ -185,6 +82,12 @@ class SupportError extends GOVUKFrontendError {
185
82
  this.name = 'SupportError';
186
83
  }
187
84
  }
85
+ class ConfigError extends GOVUKFrontendError {
86
+ constructor(...args) {
87
+ super(...args);
88
+ this.name = 'ConfigError';
89
+ }
90
+ }
188
91
  class ElementError extends GOVUKFrontendError {
189
92
  constructor(messageOrOptions) {
190
93
  let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
@@ -211,10 +114,10 @@ class InitError extends GOVUKFrontendError {
211
114
  }
212
115
  }
213
116
  /**
214
- * @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
117
+ * @import { ComponentWithModuleName } from '../common/index.mjs'
215
118
  */
216
119
 
217
- class GOVUKFrontendComponent {
120
+ class Component {
218
121
  /**
219
122
  * Returns the root element of the component
220
123
  *
@@ -265,9 +168,152 @@ class GOVUKFrontendComponent {
265
168
  */
266
169
 
267
170
  /**
268
- * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
171
+ * @typedef {typeof Component & ChildClass} ChildClassConstructor
172
+ */
173
+ Component.elementType = HTMLElement;
174
+
175
+ const configOverride = Symbol.for('configOverride');
176
+ class ConfigurableComponent extends Component {
177
+ [configOverride](param) {
178
+ return {};
179
+ }
180
+
181
+ /**
182
+ * Returns the root element of the component
183
+ *
184
+ * @protected
185
+ * @returns {ConfigurationType} - the root element of component
186
+ */
187
+ get config() {
188
+ return this._config;
189
+ }
190
+ constructor($root, config) {
191
+ super($root);
192
+ this._config = void 0;
193
+ const childConstructor = this.constructor;
194
+ if (!isObject(childConstructor.defaults)) {
195
+ throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
196
+ }
197
+ const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
198
+ this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
199
+ }
200
+ }
201
+ function normaliseString(value, property) {
202
+ const trimmedValue = value ? value.trim() : '';
203
+ let output;
204
+ let outputType = property == null ? void 0 : property.type;
205
+ if (!outputType) {
206
+ if (['true', 'false'].includes(trimmedValue)) {
207
+ outputType = 'boolean';
208
+ }
209
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
210
+ outputType = 'number';
211
+ }
212
+ }
213
+ switch (outputType) {
214
+ case 'boolean':
215
+ output = trimmedValue === 'true';
216
+ break;
217
+ case 'number':
218
+ output = Number(trimmedValue);
219
+ break;
220
+ default:
221
+ output = value;
222
+ }
223
+ return output;
224
+ }
225
+ function normaliseDataset(Component, dataset) {
226
+ if (!isObject(Component.schema)) {
227
+ throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined'));
228
+ }
229
+ const out = {};
230
+ const entries = Object.entries(Component.schema.properties);
231
+ for (const entry of entries) {
232
+ const [namespace, property] = entry;
233
+ const field = namespace.toString();
234
+ if (field in dataset) {
235
+ out[field] = normaliseString(dataset[field], property);
236
+ }
237
+ if ((property == null ? void 0 : property.type) === 'object') {
238
+ out[field] = extractConfigByNamespace(Component.schema, dataset, namespace);
239
+ }
240
+ }
241
+ return out;
242
+ }
243
+ function mergeConfigs(...configObjects) {
244
+ const formattedConfigObject = {};
245
+ for (const configObject of configObjects) {
246
+ for (const key of Object.keys(configObject)) {
247
+ const option = formattedConfigObject[key];
248
+ const override = configObject[key];
249
+ if (isObject(option) && isObject(override)) {
250
+ formattedConfigObject[key] = mergeConfigs(option, override);
251
+ } else {
252
+ formattedConfigObject[key] = override;
253
+ }
254
+ }
255
+ }
256
+ return formattedConfigObject;
257
+ }
258
+ function extractConfigByNamespace(schema, dataset, namespace) {
259
+ const property = schema.properties[namespace];
260
+ if ((property == null ? void 0 : property.type) !== 'object') {
261
+ return;
262
+ }
263
+ const newObject = {
264
+ [namespace]: {}
265
+ };
266
+ for (const [key, value] of Object.entries(dataset)) {
267
+ let current = newObject;
268
+ const keyParts = key.split('.');
269
+ for (const [index, name] of keyParts.entries()) {
270
+ if (isObject(current)) {
271
+ if (index < keyParts.length - 1) {
272
+ if (!isObject(current[name])) {
273
+ current[name] = {};
274
+ }
275
+ current = current[name];
276
+ } else if (key !== namespace) {
277
+ current[name] = normaliseString(value);
278
+ }
279
+ }
280
+ }
281
+ }
282
+ return newObject[namespace];
283
+ }
284
+ /**
285
+ * Schema for component config
286
+ *
287
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
288
+ * @typedef {object} Schema
289
+ * @property {Record<keyof ConfigurationType, SchemaProperty | undefined>} properties - Schema properties
290
+ * @property {SchemaCondition<ConfigurationType>[]} [anyOf] - List of schema conditions
291
+ */
292
+ /**
293
+ * Schema property for component config
294
+ *
295
+ * @typedef {object} SchemaProperty
296
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
297
+ */
298
+ /**
299
+ * Schema condition for component config
300
+ *
301
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
302
+ * @typedef {object} SchemaCondition
303
+ * @property {(keyof ConfigurationType)[]} required - List of required config fields
304
+ * @property {string} errorMessage - Error message when required config fields not provided
305
+ */
306
+ /**
307
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
308
+ * @typedef ChildClass
309
+ * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
310
+ * @property {Schema<ConfigurationType>} [schema] - The schema of the component configuration
311
+ * @property {ConfigurationType} [defaults] - The default values of the configuration of the component
312
+ */
313
+ /**
314
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
315
+ * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
269
316
  */
270
- GOVUKFrontendComponent.elementType = HTMLElement;
271
317
 
272
318
  /**
273
319
  * Error summary component
@@ -276,16 +322,15 @@ GOVUKFrontendComponent.elementType = HTMLElement;
276
322
  * configuration.
277
323
  *
278
324
  * @preserve
325
+ * @augments ConfigurableComponent<ErrorSummaryConfig>
279
326
  */
280
- class ErrorSummary extends GOVUKFrontendComponent {
327
+ class ErrorSummary extends ConfigurableComponent {
281
328
  /**
282
329
  * @param {Element | null} $root - HTML element to use for error summary
283
330
  * @param {ErrorSummaryConfig} [config] - Error summary config
284
331
  */
285
332
  constructor($root, config = {}) {
286
- super($root);
287
- this.config = void 0;
288
- this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, this.$root.dataset));
333
+ super($root, config);
289
334
  if (!this.config.disableAutoFocus) {
290
335
  setFocus(this.$root);
291
336
  }
@@ -352,7 +397,7 @@ class ErrorSummary extends GOVUKFrontendComponent {
352
397
  */
353
398
 
354
399
  /**
355
- * @typedef {import('../../common/index.mjs').Schema} Schema
400
+ * @import { Schema } from '../../common/configuration.mjs'
356
401
  */
357
402
  ErrorSummary.moduleName = 'govuk-error-summary';
358
403
  ErrorSummary.defaults = Object.freeze({
@@ -1,6 +1,5 @@
1
- import { mergeConfigs, setFocus, getFragmentFromUrl } from '../../common/index.mjs';
2
- import { normaliseDataset } from '../../common/normalise-dataset.mjs';
3
- import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
1
+ import { ConfigurableComponent } from '../../common/configuration.mjs';
2
+ import { setFocus, getFragmentFromUrl } from '../../common/index.mjs';
4
3
 
5
4
  /**
6
5
  * Error summary component
@@ -9,16 +8,15 @@ import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
9
8
  * configuration.
10
9
  *
11
10
  * @preserve
11
+ * @augments ConfigurableComponent<ErrorSummaryConfig>
12
12
  */
13
- class ErrorSummary extends GOVUKFrontendComponent {
13
+ class ErrorSummary extends ConfigurableComponent {
14
14
  /**
15
15
  * @param {Element | null} $root - HTML element to use for error summary
16
16
  * @param {ErrorSummaryConfig} [config] - Error summary config
17
17
  */
18
18
  constructor($root, config = {}) {
19
- super($root);
20
- this.config = void 0;
21
- this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, this.$root.dataset));
19
+ super($root, config);
22
20
  if (!this.config.disableAutoFocus) {
23
21
  setFocus(this.$root);
24
22
  }
@@ -85,7 +83,7 @@ class ErrorSummary extends GOVUKFrontendComponent {
85
83
  */
86
84
 
87
85
  /**
88
- * @typedef {import('../../common/index.mjs').Schema} Schema
86
+ * @import { Schema } from '../../common/configuration.mjs'
89
87
  */
90
88
  ErrorSummary.moduleName = 'govuk-error-summary';
91
89
  ErrorSummary.defaults = Object.freeze({