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 isInitialised($root, moduleName) {
78
8
  return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
79
9
  }
@@ -102,46 +32,13 @@
102
32
  function formatErrorMessage(Component, message) {
103
33
  return `${Component.moduleName}: ${message}`;
104
34
  }
105
-
106
- /**
107
- * Schema for component config
108
- *
109
- * @typedef {object} Schema
110
- * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
111
- * @property {SchemaCondition[]} [anyOf] - List of schema conditions
112
- */
113
-
114
- /**
115
- * Schema property for component config
116
- *
117
- * @typedef {object} SchemaProperty
118
- * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
119
- */
120
-
121
- /**
122
- * Schema condition for component config
123
- *
124
- * @typedef {object} SchemaCondition
125
- * @property {string[]} required - List of required config fields
126
- * @property {string} errorMessage - Error message when required config fields not provided
127
- */
128
35
  /**
129
36
  * @typedef ComponentWithModuleName
130
37
  * @property {string} moduleName - Name of the component
131
38
  */
132
-
133
- function normaliseDataset(Component, dataset) {
134
- const out = {};
135
- for (const [field, property] of Object.entries(Component.schema.properties)) {
136
- if (field in dataset) {
137
- out[field] = normaliseString(dataset[field], property);
138
- }
139
- if ((property == null ? void 0 : property.type) === 'object') {
140
- out[field] = extractConfigByNamespace(Component, dataset, field);
141
- }
142
- }
143
- return out;
144
- }
39
+ /**
40
+ * @import { ObjectNested } from './configuration.mjs'
41
+ */
145
42
 
146
43
  class GOVUKFrontendError extends Error {
147
44
  constructor(...args) {
@@ -161,6 +58,12 @@
161
58
  this.name = 'SupportError';
162
59
  }
163
60
  }
61
+ class ConfigError extends GOVUKFrontendError {
62
+ constructor(...args) {
63
+ super(...args);
64
+ this.name = 'ConfigError';
65
+ }
66
+ }
164
67
  class ElementError extends GOVUKFrontendError {
165
68
  constructor(messageOrOptions) {
166
69
  let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
@@ -187,10 +90,10 @@
187
90
  }
188
91
  }
189
92
  /**
190
- * @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
93
+ * @import { ComponentWithModuleName } from '../common/index.mjs'
191
94
  */
192
95
 
193
- class GOVUKFrontendComponent {
96
+ class Component {
194
97
  /**
195
98
  * Returns the root element of the component
196
99
  *
@@ -241,9 +144,152 @@
241
144
  */
242
145
 
243
146
  /**
244
- * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
147
+ * @typedef {typeof Component & ChildClass} ChildClassConstructor
148
+ */
149
+ Component.elementType = HTMLElement;
150
+
151
+ const configOverride = Symbol.for('configOverride');
152
+ class ConfigurableComponent extends Component {
153
+ [configOverride](param) {
154
+ return {};
155
+ }
156
+
157
+ /**
158
+ * Returns the root element of the component
159
+ *
160
+ * @protected
161
+ * @returns {ConfigurationType} - the root element of component
162
+ */
163
+ get config() {
164
+ return this._config;
165
+ }
166
+ constructor($root, config) {
167
+ super($root);
168
+ this._config = void 0;
169
+ const childConstructor = this.constructor;
170
+ if (!isObject(childConstructor.defaults)) {
171
+ throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
172
+ }
173
+ const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
174
+ this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
175
+ }
176
+ }
177
+ function normaliseString(value, property) {
178
+ const trimmedValue = value ? value.trim() : '';
179
+ let output;
180
+ let outputType = property == null ? void 0 : property.type;
181
+ if (!outputType) {
182
+ if (['true', 'false'].includes(trimmedValue)) {
183
+ outputType = 'boolean';
184
+ }
185
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
186
+ outputType = 'number';
187
+ }
188
+ }
189
+ switch (outputType) {
190
+ case 'boolean':
191
+ output = trimmedValue === 'true';
192
+ break;
193
+ case 'number':
194
+ output = Number(trimmedValue);
195
+ break;
196
+ default:
197
+ output = value;
198
+ }
199
+ return output;
200
+ }
201
+ function normaliseDataset(Component, dataset) {
202
+ if (!isObject(Component.schema)) {
203
+ throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined'));
204
+ }
205
+ const out = {};
206
+ const entries = Object.entries(Component.schema.properties);
207
+ for (const entry of entries) {
208
+ const [namespace, property] = entry;
209
+ const field = namespace.toString();
210
+ if (field in dataset) {
211
+ out[field] = normaliseString(dataset[field], property);
212
+ }
213
+ if ((property == null ? void 0 : property.type) === 'object') {
214
+ out[field] = extractConfigByNamespace(Component.schema, dataset, namespace);
215
+ }
216
+ }
217
+ return out;
218
+ }
219
+ function mergeConfigs(...configObjects) {
220
+ const formattedConfigObject = {};
221
+ for (const configObject of configObjects) {
222
+ for (const key of Object.keys(configObject)) {
223
+ const option = formattedConfigObject[key];
224
+ const override = configObject[key];
225
+ if (isObject(option) && isObject(override)) {
226
+ formattedConfigObject[key] = mergeConfigs(option, override);
227
+ } else {
228
+ formattedConfigObject[key] = override;
229
+ }
230
+ }
231
+ }
232
+ return formattedConfigObject;
233
+ }
234
+ function extractConfigByNamespace(schema, dataset, namespace) {
235
+ const property = schema.properties[namespace];
236
+ if ((property == null ? void 0 : property.type) !== 'object') {
237
+ return;
238
+ }
239
+ const newObject = {
240
+ [namespace]: {}
241
+ };
242
+ for (const [key, value] of Object.entries(dataset)) {
243
+ let current = newObject;
244
+ const keyParts = key.split('.');
245
+ for (const [index, name] of keyParts.entries()) {
246
+ if (isObject(current)) {
247
+ if (index < keyParts.length - 1) {
248
+ if (!isObject(current[name])) {
249
+ current[name] = {};
250
+ }
251
+ current = current[name];
252
+ } else if (key !== namespace) {
253
+ current[name] = normaliseString(value);
254
+ }
255
+ }
256
+ }
257
+ }
258
+ return newObject[namespace];
259
+ }
260
+ /**
261
+ * Schema for component config
262
+ *
263
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
264
+ * @typedef {object} Schema
265
+ * @property {Record<keyof ConfigurationType, SchemaProperty | undefined>} properties - Schema properties
266
+ * @property {SchemaCondition<ConfigurationType>[]} [anyOf] - List of schema conditions
267
+ */
268
+ /**
269
+ * Schema property for component config
270
+ *
271
+ * @typedef {object} SchemaProperty
272
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
273
+ */
274
+ /**
275
+ * Schema condition for component config
276
+ *
277
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
278
+ * @typedef {object} SchemaCondition
279
+ * @property {(keyof ConfigurationType)[]} required - List of required config fields
280
+ * @property {string} errorMessage - Error message when required config fields not provided
281
+ */
282
+ /**
283
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
284
+ * @typedef ChildClass
285
+ * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
286
+ * @property {Schema<ConfigurationType>} [schema] - The schema of the component configuration
287
+ * @property {ConfigurationType} [defaults] - The default values of the configuration of the component
288
+ */
289
+ /**
290
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
291
+ * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
245
292
  */
246
- GOVUKFrontendComponent.elementType = HTMLElement;
247
293
 
248
294
  const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
249
295
 
@@ -251,17 +297,16 @@
251
297
  * JavaScript enhancements for the Button component
252
298
  *
253
299
  * @preserve
300
+ * @augments ConfigurableComponent<ButtonConfig>
254
301
  */
255
- class Button extends GOVUKFrontendComponent {
302
+ class Button extends ConfigurableComponent {
256
303
  /**
257
304
  * @param {Element | null} $root - HTML element to use for button
258
305
  * @param {ButtonConfig} [config] - Button config
259
306
  */
260
307
  constructor($root, config = {}) {
261
- super($root);
262
- this.config = void 0;
308
+ super($root, config);
263
309
  this.debounceFormSubmitTimer = null;
264
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
265
310
  this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
266
311
  this.$root.addEventListener('click', event => this.debounce(event));
267
312
  }
@@ -298,7 +343,7 @@
298
343
  */
299
344
 
300
345
  /**
301
- * @typedef {import('../../common/index.mjs').Schema} Schema
346
+ * @import { Schema } from '../../common/configuration.mjs'
302
347
  */
303
348
  Button.moduleName = 'govuk-button';
304
349
  Button.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 isInitialised($root, moduleName) {
72
2
  return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
73
3
  }
@@ -96,46 +26,13 @@ function isObject(option) {
96
26
  function formatErrorMessage(Component, message) {
97
27
  return `${Component.moduleName}: ${message}`;
98
28
  }
99
-
100
- /**
101
- * Schema for component config
102
- *
103
- * @typedef {object} Schema
104
- * @property {{ [field: string]: SchemaProperty | undefined }} properties - Schema properties
105
- * @property {SchemaCondition[]} [anyOf] - List of schema conditions
106
- */
107
-
108
- /**
109
- * Schema property for component config
110
- *
111
- * @typedef {object} SchemaProperty
112
- * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
113
- */
114
-
115
- /**
116
- * Schema condition for component config
117
- *
118
- * @typedef {object} SchemaCondition
119
- * @property {string[]} required - List of required config fields
120
- * @property {string} errorMessage - Error message when required config fields not provided
121
- */
122
29
  /**
123
30
  * @typedef ComponentWithModuleName
124
31
  * @property {string} moduleName - Name of the component
125
32
  */
126
-
127
- function normaliseDataset(Component, dataset) {
128
- const out = {};
129
- for (const [field, property] of Object.entries(Component.schema.properties)) {
130
- if (field in dataset) {
131
- out[field] = normaliseString(dataset[field], property);
132
- }
133
- if ((property == null ? void 0 : property.type) === 'object') {
134
- out[field] = extractConfigByNamespace(Component, dataset, field);
135
- }
136
- }
137
- return out;
138
- }
33
+ /**
34
+ * @import { ObjectNested } from './configuration.mjs'
35
+ */
139
36
 
140
37
  class GOVUKFrontendError extends Error {
141
38
  constructor(...args) {
@@ -155,6 +52,12 @@ class SupportError extends GOVUKFrontendError {
155
52
  this.name = 'SupportError';
156
53
  }
157
54
  }
55
+ class ConfigError extends GOVUKFrontendError {
56
+ constructor(...args) {
57
+ super(...args);
58
+ this.name = 'ConfigError';
59
+ }
60
+ }
158
61
  class ElementError extends GOVUKFrontendError {
159
62
  constructor(messageOrOptions) {
160
63
  let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
@@ -181,10 +84,10 @@ class InitError extends GOVUKFrontendError {
181
84
  }
182
85
  }
183
86
  /**
184
- * @typedef {import('../common/index.mjs').ComponentWithModuleName} ComponentWithModuleName
87
+ * @import { ComponentWithModuleName } from '../common/index.mjs'
185
88
  */
186
89
 
187
- class GOVUKFrontendComponent {
90
+ class Component {
188
91
  /**
189
92
  * Returns the root element of the component
190
93
  *
@@ -235,9 +138,152 @@ class GOVUKFrontendComponent {
235
138
  */
236
139
 
237
140
  /**
238
- * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
141
+ * @typedef {typeof Component & ChildClass} ChildClassConstructor
142
+ */
143
+ Component.elementType = HTMLElement;
144
+
145
+ const configOverride = Symbol.for('configOverride');
146
+ class ConfigurableComponent extends Component {
147
+ [configOverride](param) {
148
+ return {};
149
+ }
150
+
151
+ /**
152
+ * Returns the root element of the component
153
+ *
154
+ * @protected
155
+ * @returns {ConfigurationType} - the root element of component
156
+ */
157
+ get config() {
158
+ return this._config;
159
+ }
160
+ constructor($root, config) {
161
+ super($root);
162
+ this._config = void 0;
163
+ const childConstructor = this.constructor;
164
+ if (!isObject(childConstructor.defaults)) {
165
+ throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
166
+ }
167
+ const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
168
+ this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
169
+ }
170
+ }
171
+ function normaliseString(value, property) {
172
+ const trimmedValue = value ? value.trim() : '';
173
+ let output;
174
+ let outputType = property == null ? void 0 : property.type;
175
+ if (!outputType) {
176
+ if (['true', 'false'].includes(trimmedValue)) {
177
+ outputType = 'boolean';
178
+ }
179
+ if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
180
+ outputType = 'number';
181
+ }
182
+ }
183
+ switch (outputType) {
184
+ case 'boolean':
185
+ output = trimmedValue === 'true';
186
+ break;
187
+ case 'number':
188
+ output = Number(trimmedValue);
189
+ break;
190
+ default:
191
+ output = value;
192
+ }
193
+ return output;
194
+ }
195
+ function normaliseDataset(Component, dataset) {
196
+ if (!isObject(Component.schema)) {
197
+ throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined'));
198
+ }
199
+ const out = {};
200
+ const entries = Object.entries(Component.schema.properties);
201
+ for (const entry of entries) {
202
+ const [namespace, property] = entry;
203
+ const field = namespace.toString();
204
+ if (field in dataset) {
205
+ out[field] = normaliseString(dataset[field], property);
206
+ }
207
+ if ((property == null ? void 0 : property.type) === 'object') {
208
+ out[field] = extractConfigByNamespace(Component.schema, dataset, namespace);
209
+ }
210
+ }
211
+ return out;
212
+ }
213
+ function mergeConfigs(...configObjects) {
214
+ const formattedConfigObject = {};
215
+ for (const configObject of configObjects) {
216
+ for (const key of Object.keys(configObject)) {
217
+ const option = formattedConfigObject[key];
218
+ const override = configObject[key];
219
+ if (isObject(option) && isObject(override)) {
220
+ formattedConfigObject[key] = mergeConfigs(option, override);
221
+ } else {
222
+ formattedConfigObject[key] = override;
223
+ }
224
+ }
225
+ }
226
+ return formattedConfigObject;
227
+ }
228
+ function extractConfigByNamespace(schema, dataset, namespace) {
229
+ const property = schema.properties[namespace];
230
+ if ((property == null ? void 0 : property.type) !== 'object') {
231
+ return;
232
+ }
233
+ const newObject = {
234
+ [namespace]: {}
235
+ };
236
+ for (const [key, value] of Object.entries(dataset)) {
237
+ let current = newObject;
238
+ const keyParts = key.split('.');
239
+ for (const [index, name] of keyParts.entries()) {
240
+ if (isObject(current)) {
241
+ if (index < keyParts.length - 1) {
242
+ if (!isObject(current[name])) {
243
+ current[name] = {};
244
+ }
245
+ current = current[name];
246
+ } else if (key !== namespace) {
247
+ current[name] = normaliseString(value);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ return newObject[namespace];
253
+ }
254
+ /**
255
+ * Schema for component config
256
+ *
257
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
258
+ * @typedef {object} Schema
259
+ * @property {Record<keyof ConfigurationType, SchemaProperty | undefined>} properties - Schema properties
260
+ * @property {SchemaCondition<ConfigurationType>[]} [anyOf] - List of schema conditions
261
+ */
262
+ /**
263
+ * Schema property for component config
264
+ *
265
+ * @typedef {object} SchemaProperty
266
+ * @property {'string' | 'boolean' | 'number' | 'object'} type - Property type
267
+ */
268
+ /**
269
+ * Schema condition for component config
270
+ *
271
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} ConfigurationType
272
+ * @typedef {object} SchemaCondition
273
+ * @property {(keyof ConfigurationType)[]} required - List of required config fields
274
+ * @property {string} errorMessage - Error message when required config fields not provided
275
+ */
276
+ /**
277
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
278
+ * @typedef ChildClass
279
+ * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
280
+ * @property {Schema<ConfigurationType>} [schema] - The schema of the component configuration
281
+ * @property {ConfigurationType} [defaults] - The default values of the configuration of the component
282
+ */
283
+ /**
284
+ * @template {Partial<Record<keyof ConfigurationType, unknown>>} [ConfigurationType=ObjectNested]
285
+ * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
239
286
  */
240
- GOVUKFrontendComponent.elementType = HTMLElement;
241
287
 
242
288
  const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
243
289
 
@@ -245,17 +291,16 @@ const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
245
291
  * JavaScript enhancements for the Button component
246
292
  *
247
293
  * @preserve
294
+ * @augments ConfigurableComponent<ButtonConfig>
248
295
  */
249
- class Button extends GOVUKFrontendComponent {
296
+ class Button extends ConfigurableComponent {
250
297
  /**
251
298
  * @param {Element | null} $root - HTML element to use for button
252
299
  * @param {ButtonConfig} [config] - Button config
253
300
  */
254
301
  constructor($root, config = {}) {
255
- super($root);
256
- this.config = void 0;
302
+ super($root, config);
257
303
  this.debounceFormSubmitTimer = null;
258
- this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
259
304
  this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
260
305
  this.$root.addEventListener('click', event => this.debounce(event));
261
306
  }
@@ -292,7 +337,7 @@ class Button extends GOVUKFrontendComponent {
292
337
  */
293
338
 
294
339
  /**
295
- * @typedef {import('../../common/index.mjs').Schema} Schema
340
+ * @import { Schema } from '../../common/configuration.mjs'
296
341
  */
297
342
  Button.moduleName = 'govuk-button';
298
343
  Button.defaults = Object.freeze({