katalyst-govuk-formbuilder 1.19.0 → 1.20.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.
@@ -1,17 +1,20 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
1
3
  function getFragmentFromUrl(url) {
2
- if (!url.includes('#')) {
4
+ if (!url.includes("#")) {
3
5
  return undefined;
4
6
  }
5
- return url.split('#').pop();
7
+ return url.split("#").pop();
6
8
  }
9
+
7
10
  function setFocus($element, options = {}) {
8
11
  var _options$onBeforeFocu;
9
- const isFocusable = $element.getAttribute('tabindex');
12
+ const isFocusable = $element.getAttribute("tabindex");
10
13
  if (!isFocusable) {
11
- $element.setAttribute('tabindex', '-1');
14
+ $element.setAttribute("tabindex", "-1");
12
15
  }
13
16
  function onFocus() {
14
- $element.addEventListener('blur', onBlur, {
17
+ $element.addEventListener("blur", onBlur, {
15
18
  once: true
16
19
  });
17
20
  }
@@ -19,40 +22,35 @@ function setFocus($element, options = {}) {
19
22
  var _options$onBlur;
20
23
  (_options$onBlur = options.onBlur) == null || _options$onBlur.call($element);
21
24
  if (!isFocusable) {
22
- $element.removeAttribute('tabindex');
25
+ $element.removeAttribute("tabindex");
23
26
  }
24
27
  }
25
- $element.addEventListener('focus', onFocus, {
28
+ $element.addEventListener("focus", onFocus, {
26
29
  once: true
27
30
  });
28
31
  (_options$onBeforeFocu = options.onBeforeFocus) == null || _options$onBeforeFocu.call($element);
29
32
  $element.focus();
30
33
  }
34
+
31
35
  function isInitialised($root, moduleName) {
32
36
  return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
33
37
  }
34
38
 
35
- /**
36
- * Checks if GOV.UK Frontend is supported on this page
37
- *
38
- * Some browsers will load and run our JavaScript but GOV.UK Frontend
39
- * won't be supported.
40
- *
41
- * @param {HTMLElement | null} [$scope] - (internal) `<body>` HTML element checked for browser support
42
- * @returns {boolean} Whether GOV.UK Frontend is supported on this page
43
- */
44
39
  function isSupported($scope = document.body) {
45
40
  if (!$scope) {
46
41
  return false;
47
42
  }
48
- return $scope.classList.contains('govuk-frontend-supported');
43
+ return $scope.classList.contains("govuk-frontend-supported");
49
44
  }
45
+
50
46
  function isArray(option) {
51
47
  return Array.isArray(option);
52
48
  }
49
+
53
50
  function isObject(option) {
54
- return !!option && typeof option === 'object' && !isArray(option);
51
+ return !!option && typeof option === "object" && !isArray(option);
55
52
  }
53
+
56
54
  function formatErrorMessage(Component, message) {
57
55
  return `${Component.moduleName}: ${message}`;
58
56
  }
@@ -60,74 +58,62 @@ function formatErrorMessage(Component, message) {
60
58
  class GOVUKFrontendError extends Error {
61
59
  constructor(...args) {
62
60
  super(...args);
63
- this.name = 'GOVUKFrontendError';
61
+ this.name = "GOVUKFrontendError";
64
62
  }
65
63
  }
64
+
66
65
  class SupportError extends GOVUKFrontendError {
67
- /**
68
- * Checks if GOV.UK Frontend is supported on this page
69
- *
70
- * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
71
- */
72
66
  constructor($scope = document.body) {
73
- 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';
67
+ 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";
74
68
  super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
75
- this.name = 'SupportError';
69
+ this.name = "SupportError";
76
70
  }
77
71
  }
72
+
78
73
  class ConfigError extends GOVUKFrontendError {
79
74
  constructor(...args) {
80
75
  super(...args);
81
- this.name = 'ConfigError';
76
+ this.name = "ConfigError";
82
77
  }
83
78
  }
79
+
84
80
  class ElementError extends GOVUKFrontendError {
85
81
  constructor(messageOrOptions) {
86
- let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
87
- if (typeof messageOrOptions === 'object') {
88
- const {
89
- component,
90
- identifier,
91
- element,
92
- expectedType
93
- } = messageOrOptions;
82
+ let message = typeof messageOrOptions === "string" ? messageOrOptions : "";
83
+ if (typeof messageOrOptions === "object") {
84
+ const {component: component, identifier: identifier, element: element, expectedType: expectedType} = messageOrOptions;
94
85
  message = identifier;
95
- message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found';
86
+ message += element ? ` is not of type ${expectedType != null ? expectedType : "HTMLElement"}` : " not found";
96
87
  message = formatErrorMessage(component, message);
97
88
  }
98
89
  super(message);
99
- this.name = 'ElementError';
90
+ this.name = "ElementError";
100
91
  }
101
92
  }
93
+
102
94
  class InitError extends GOVUKFrontendError {
103
95
  constructor(componentOrMessage) {
104
- const message = typeof componentOrMessage === 'string' ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\`$root\`) already initialised`);
96
+ const message = typeof componentOrMessage === "string" ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\`$root\`) already initialised`);
105
97
  super(message);
106
- this.name = 'InitError';
98
+ this.name = "InitError";
107
99
  }
108
100
  }
109
101
 
110
102
  class Component {
111
- /**
112
- * Returns the root element of the component
113
- *
114
- * @protected
115
- * @returns {RootElementType} - the root element of component
116
- */
117
103
  get $root() {
118
104
  return this._$root;
119
105
  }
120
106
  constructor($root) {
121
107
  this._$root = void 0;
122
108
  const childConstructor = this.constructor;
123
- if (typeof childConstructor.moduleName !== 'string') {
109
+ if (typeof childConstructor.moduleName !== "string") {
124
110
  throw new InitError(`\`moduleName\` not defined in component`);
125
111
  }
126
112
  if (!($root instanceof childConstructor.elementType)) {
127
113
  throw new ElementError({
128
114
  element: $root,
129
115
  component: childConstructor,
130
- identifier: 'Root element (`$root`)',
116
+ identifier: "Root element (`$root`)",
131
117
  expectedType: childConstructor.elementType.name
132
118
  });
133
119
  } else {
@@ -136,7 +122,7 @@ class Component {
136
122
  childConstructor.checkSupport();
137
123
  this.checkInitialised();
138
124
  const moduleName = childConstructor.moduleName;
139
- this.$root.setAttribute(`data-${moduleName}-init`, '');
125
+ this.$root.setAttribute(`data-${moduleName}-init`, "");
140
126
  }
141
127
  checkInitialised() {
142
128
  const constructor = this.constructor;
@@ -147,33 +133,19 @@ class Component {
147
133
  }
148
134
  static checkSupport() {
149
135
  if (!isSupported()) {
150
- throw new SupportError();
136
+ throw new SupportError;
151
137
  }
152
138
  }
153
139
  }
154
140
 
155
- /**
156
- * @typedef ChildClass
157
- * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
158
- */
159
-
160
- /**
161
- * @typedef {typeof Component & ChildClass} ChildClassConstructor
162
- */
163
141
  Component.elementType = HTMLElement;
164
142
 
165
- const configOverride = Symbol.for('configOverride');
143
+ const configOverride = Symbol.for("configOverride");
144
+
166
145
  class ConfigurableComponent extends Component {
167
146
  [configOverride](param) {
168
147
  return {};
169
148
  }
170
-
171
- /**
172
- * Returns the root element of the component
173
- *
174
- * @protected
175
- * @returns {ConfigurationType} - the root element of component
176
- */
177
149
  get config() {
178
150
  return this._config;
179
151
  }
@@ -182,39 +154,43 @@ class ConfigurableComponent extends Component {
182
154
  this._config = void 0;
183
155
  const childConstructor = this.constructor;
184
156
  if (!isObject(childConstructor.defaults)) {
185
- throw new ConfigError(formatErrorMessage(childConstructor, 'Config passed as parameter into constructor but no defaults defined'));
157
+ throw new ConfigError(formatErrorMessage(childConstructor, "Config passed as parameter into constructor but no defaults defined"));
186
158
  }
187
159
  const datasetConfig = normaliseDataset(childConstructor, this._$root.dataset);
188
160
  this._config = mergeConfigs(childConstructor.defaults, config != null ? config : {}, this[configOverride](datasetConfig), datasetConfig);
189
161
  }
190
162
  }
163
+
191
164
  function normaliseString(value, property) {
192
- const trimmedValue = value ? value.trim() : '';
165
+ const trimmedValue = value ? value.trim() : "";
193
166
  let output;
194
167
  let outputType = property == null ? void 0 : property.type;
195
168
  if (!outputType) {
196
- if (['true', 'false'].includes(trimmedValue)) {
197
- outputType = 'boolean';
169
+ if ([ "true", "false" ].includes(trimmedValue)) {
170
+ outputType = "boolean";
198
171
  }
199
172
  if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
200
- outputType = 'number';
173
+ outputType = "number";
201
174
  }
202
175
  }
203
176
  switch (outputType) {
204
- case 'boolean':
205
- output = trimmedValue === 'true';
206
- break;
207
- case 'number':
208
- output = Number(trimmedValue);
209
- break;
210
- default:
211
- output = value;
177
+ case "boolean":
178
+ output = trimmedValue === "true";
179
+ break;
180
+
181
+ case "number":
182
+ output = Number(trimmedValue);
183
+ break;
184
+
185
+ default:
186
+ output = value;
212
187
  }
213
188
  return output;
214
189
  }
190
+
215
191
  function normaliseDataset(Component, dataset) {
216
192
  if (!isObject(Component.schema)) {
217
- throw new ConfigError(formatErrorMessage(Component, 'Config passed as parameter into constructor but no schema defined'));
193
+ throw new ConfigError(formatErrorMessage(Component, "Config passed as parameter into constructor but no schema defined"));
218
194
  }
219
195
  const out = {};
220
196
  const entries = Object.entries(Component.schema.properties);
@@ -224,12 +200,13 @@ function normaliseDataset(Component, dataset) {
224
200
  if (field in dataset) {
225
201
  out[field] = normaliseString(dataset[field], property);
226
202
  }
227
- if ((property == null ? void 0 : property.type) === 'object') {
203
+ if ((property == null ? void 0 : property.type) === "object") {
228
204
  out[field] = extractConfigByNamespace(Component.schema, dataset, namespace);
229
205
  }
230
206
  }
231
207
  return out;
232
208
  }
209
+
233
210
  function mergeConfigs(...configObjects) {
234
211
  const formattedConfigObject = {};
235
212
  for (const configObject of configObjects) {
@@ -245,29 +222,28 @@ function mergeConfigs(...configObjects) {
245
222
  }
246
223
  return formattedConfigObject;
247
224
  }
225
+
248
226
  function validateConfig(schema, config) {
249
227
  const validationErrors = [];
250
228
  for (const [name, conditions] of Object.entries(schema)) {
251
229
  const errors = [];
252
230
  if (Array.isArray(conditions)) {
253
- for (const {
254
- required,
255
- errorMessage
256
- } of conditions) {
231
+ for (const {required: required, errorMessage: errorMessage} of conditions) {
257
232
  if (!required.every(key => !!config[key])) {
258
233
  errors.push(errorMessage);
259
234
  }
260
235
  }
261
- if (name === 'anyOf' && !(conditions.length - errors.length >= 1)) {
236
+ if (name === "anyOf" && !(conditions.length - errors.length >= 1)) {
262
237
  validationErrors.push(...errors);
263
238
  }
264
239
  }
265
240
  }
266
241
  return validationErrors;
267
242
  }
243
+
268
244
  function extractConfigByNamespace(schema, dataset, namespace) {
269
245
  const property = schema.properties[namespace];
270
- if ((property == null ? void 0 : property.type) !== 'object') {
246
+ if ((property == null ? void 0 : property.type) !== "object") {
271
247
  return;
272
248
  }
273
249
  const newObject = {
@@ -275,7 +251,7 @@ function extractConfigByNamespace(schema, dataset, namespace) {
275
251
  };
276
252
  for (const [key, value] of Object.entries(dataset)) {
277
253
  let current = newObject;
278
- const keyParts = key.split('.');
254
+ const keyParts = key.split(".");
279
255
  for (const [index, name] of keyParts.entries()) {
280
256
  if (isObject(current)) {
281
257
  if (index < keyParts.length - 1) {
@@ -298,23 +274,23 @@ class I18n {
298
274
  this.translations = void 0;
299
275
  this.locale = void 0;
300
276
  this.translations = translations;
301
- this.locale = (_config$locale = config.locale) != null ? _config$locale : document.documentElement.lang || 'en';
277
+ this.locale = (_config$locale = config.locale) != null ? _config$locale : document.documentElement.lang || "en";
302
278
  }
303
279
  t(lookupKey, options) {
304
280
  if (!lookupKey) {
305
- throw new Error('i18n: lookup key missing');
281
+ throw new Error("i18n: lookup key missing");
306
282
  }
307
283
  let translation = this.translations[lookupKey];
308
- if (typeof (options == null ? void 0 : options.count) === 'number' && typeof translation === 'object') {
284
+ if (typeof (options == null ? void 0 : options.count) === "number" && typeof translation === "object") {
309
285
  const translationPluralForm = translation[this.getPluralSuffix(lookupKey, options.count)];
310
286
  if (translationPluralForm) {
311
287
  translation = translationPluralForm;
312
288
  }
313
289
  }
314
- if (typeof translation === 'string') {
290
+ if (typeof translation === "string") {
315
291
  if (translation.match(/%{(.\S+)}/)) {
316
292
  if (!options) {
317
- throw new Error('i18n: cannot replace placeholders in string if no option data provided');
293
+ throw new Error("i18n: cannot replace placeholders in string if no option data provided");
318
294
  }
319
295
  return this.replacePlaceholders(translation, options);
320
296
  }
@@ -324,13 +300,13 @@ class I18n {
324
300
  }
325
301
  replacePlaceholders(translationString, options) {
326
302
  const formatter = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : undefined;
327
- return translationString.replace(/%{(.\S+)}/g, function (placeholderWithBraces, placeholderKey) {
303
+ return translationString.replace(/%{(.\S+)}/g, function(placeholderWithBraces, placeholderKey) {
328
304
  if (Object.prototype.hasOwnProperty.call(options, placeholderKey)) {
329
305
  const placeholderValue = options[placeholderKey];
330
- if (placeholderValue === false || typeof placeholderValue !== 'number' && typeof placeholderValue !== 'string') {
331
- return '';
306
+ if (placeholderValue === false || typeof placeholderValue !== "number" && typeof placeholderValue !== "string") {
307
+ return "";
332
308
  }
333
- if (typeof placeholderValue === 'number') {
309
+ if (typeof placeholderValue === "number") {
334
310
  return formatter ? formatter.format(placeholderValue) : `${placeholderValue}`;
335
311
  }
336
312
  return placeholderValue;
@@ -339,21 +315,21 @@ class I18n {
339
315
  });
340
316
  }
341
317
  hasIntlPluralRulesSupport() {
342
- return Boolean('PluralRules' in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length);
318
+ return Boolean("PluralRules" in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length);
343
319
  }
344
320
  getPluralSuffix(lookupKey, count) {
345
321
  count = Number(count);
346
322
  if (!isFinite(count)) {
347
- return 'other';
323
+ return "other";
348
324
  }
349
325
  const translation = this.translations[lookupKey];
350
326
  const preferredForm = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(count) : this.selectPluralFormUsingFallbackRules(count);
351
- if (typeof translation === 'object') {
327
+ if (typeof translation === "object") {
352
328
  if (preferredForm in translation) {
353
329
  return preferredForm;
354
- } else if ('other' in translation) {
330
+ } else if ("other" in translation) {
355
331
  console.warn(`i18n: Missing plural form ".${preferredForm}" for "${this.locale}" locale. Falling back to ".other".`);
356
- return 'other';
332
+ return "other";
357
333
  }
358
334
  }
359
335
  throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`);
@@ -364,10 +340,10 @@ class I18n {
364
340
  if (ruleset) {
365
341
  return I18n.pluralRules[ruleset](count);
366
342
  }
367
- return 'other';
343
+ return "other";
368
344
  }
369
345
  getPluralRulesForLocale() {
370
- const localeShort = this.locale.split('-')[0];
346
+ const localeShort = this.locale.split("-")[0];
371
347
  for (const pluralRule in I18n.pluralRulesMap) {
372
348
  const languages = I18n.pluralRulesMap[pluralRule];
373
349
  if (languages.includes(this.locale) || languages.includes(localeShort)) {
@@ -376,112 +352,114 @@ class I18n {
376
352
  }
377
353
  }
378
354
  }
355
+
379
356
  I18n.pluralRulesMap = {
380
- arabic: ['ar'],
381
- chinese: ['my', 'zh', 'id', 'ja', 'jv', 'ko', 'ms', 'th', 'vi'],
382
- french: ['hy', 'bn', 'fr', 'gu', 'hi', 'fa', 'pa', 'zu'],
383
- german: ['af', 'sq', 'az', 'eu', 'bg', 'ca', 'da', 'nl', 'en', 'et', 'fi', 'ka', 'de', 'el', 'hu', 'lb', 'no', 'so', 'sw', 'sv', 'ta', 'te', 'tr', 'ur'],
384
- irish: ['ga'],
385
- russian: ['ru', 'uk'],
386
- scottish: ['gd'],
387
- spanish: ['pt-PT', 'it', 'es'],
388
- welsh: ['cy']
357
+ arabic: [ "ar" ],
358
+ chinese: [ "my", "zh", "id", "ja", "jv", "ko", "ms", "th", "vi" ],
359
+ french: [ "hy", "bn", "fr", "gu", "hi", "fa", "pa", "zu" ],
360
+ german: [ "af", "sq", "az", "eu", "bg", "ca", "da", "nl", "en", "et", "fi", "ka", "de", "el", "hu", "lb", "no", "so", "sw", "sv", "ta", "te", "tr", "ur" ],
361
+ irish: [ "ga" ],
362
+ russian: [ "ru", "uk" ],
363
+ scottish: [ "gd" ],
364
+ spanish: [ "pt-PT", "it", "es" ],
365
+ welsh: [ "cy" ]
389
366
  };
367
+
390
368
  I18n.pluralRules = {
391
369
  arabic(n) {
392
370
  if (n === 0) {
393
- return 'zero';
371
+ return "zero";
394
372
  }
395
373
  if (n === 1) {
396
- return 'one';
374
+ return "one";
397
375
  }
398
376
  if (n === 2) {
399
- return 'two';
377
+ return "two";
400
378
  }
401
379
  if (n % 100 >= 3 && n % 100 <= 10) {
402
- return 'few';
380
+ return "few";
403
381
  }
404
382
  if (n % 100 >= 11 && n % 100 <= 99) {
405
- return 'many';
383
+ return "many";
406
384
  }
407
- return 'other';
385
+ return "other";
408
386
  },
409
387
  chinese() {
410
- return 'other';
388
+ return "other";
411
389
  },
412
390
  french(n) {
413
- return n === 0 || n === 1 ? 'one' : 'other';
391
+ return n === 0 || n === 1 ? "one" : "other";
414
392
  },
415
393
  german(n) {
416
- return n === 1 ? 'one' : 'other';
394
+ return n === 1 ? "one" : "other";
417
395
  },
418
396
  irish(n) {
419
397
  if (n === 1) {
420
- return 'one';
398
+ return "one";
421
399
  }
422
400
  if (n === 2) {
423
- return 'two';
401
+ return "two";
424
402
  }
425
403
  if (n >= 3 && n <= 6) {
426
- return 'few';
404
+ return "few";
427
405
  }
428
406
  if (n >= 7 && n <= 10) {
429
- return 'many';
407
+ return "many";
430
408
  }
431
- return 'other';
409
+ return "other";
432
410
  },
433
411
  russian(n) {
434
412
  const lastTwo = n % 100;
435
413
  const last = lastTwo % 10;
436
414
  if (last === 1 && lastTwo !== 11) {
437
- return 'one';
415
+ return "one";
438
416
  }
439
417
  if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) {
440
- return 'few';
418
+ return "few";
441
419
  }
442
420
  if (last === 0 || last >= 5 && last <= 9 || lastTwo >= 11 && lastTwo <= 14) {
443
- return 'many';
421
+ return "many";
444
422
  }
445
- return 'other';
423
+ return "other";
446
424
  },
447
425
  scottish(n) {
448
426
  if (n === 1 || n === 11) {
449
- return 'one';
427
+ return "one";
450
428
  }
451
429
  if (n === 2 || n === 12) {
452
- return 'two';
430
+ return "two";
453
431
  }
454
432
  if (n >= 3 && n <= 10 || n >= 13 && n <= 19) {
455
- return 'few';
433
+ return "few";
456
434
  }
457
- return 'other';
435
+ return "other";
458
436
  },
459
437
  spanish(n) {
460
438
  if (n === 1) {
461
- return 'one';
439
+ return "one";
462
440
  }
463
- if (n % 1000000 === 0 && n !== 0) {
464
- return 'many';
441
+ if (n % 1e6 === 0 && n !== 0) {
442
+ return "many";
465
443
  }
466
- return 'other';
444
+ return "other";
467
445
  },
468
446
  welsh(n) {
469
447
  if (n === 0) {
470
- return 'zero';
448
+ return "zero";
471
449
  }
472
450
  if (n === 1) {
473
- return 'one';
451
+ return "one";
474
452
  }
475
453
  if (n === 2) {
476
- return 'two';
454
+ return "two";
477
455
  }
478
456
  if (n === 3) {
479
- return 'few';
457
+ return "few";
480
458
  }
481
459
  if (n === 6) {
482
- return 'many';
460
+ return "many";
483
461
  }
484
- return 'other';
462
+ return "other";
485
463
  }
486
464
  };
487
465
 
@@ -492,24 +470,19 @@ const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
492
470
  *
493
471
  * @preserve
494
472
  * @augments ConfigurableComponent<ButtonConfig>
495
- */
496
- class Button extends ConfigurableComponent {
497
- /**
498
- * @param {Element | null} $root - HTML element to use for button
499
- * @param {ButtonConfig} [config] - Button config
500
- */
473
+ */ class Button extends ConfigurableComponent {
501
474
  constructor($root, config = {}) {
502
475
  super($root, config);
503
476
  this.debounceFormSubmitTimer = null;
504
- this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
505
- this.$root.addEventListener('click', event => this.debounce(event));
477
+ this.$root.addEventListener("keydown", event => this.handleKeyDown(event));
478
+ this.$root.addEventListener("click", event => this.debounce(event));
506
479
  }
507
480
  handleKeyDown(event) {
508
481
  const $target = event.target;
509
- if (event.key !== ' ') {
482
+ if (event.key !== " ") {
510
483
  return;
511
484
  }
512
- if ($target instanceof HTMLElement && $target.getAttribute('role') === 'button') {
485
+ if ($target instanceof HTMLElement && $target.getAttribute("role") === "button") {
513
486
  event.preventDefault();
514
487
  $target.click();
515
488
  }
@@ -524,29 +497,20 @@ class Button extends ConfigurableComponent {
524
497
  }
525
498
  this.debounceFormSubmitTimer = window.setTimeout(() => {
526
499
  this.debounceFormSubmitTimer = null;
527
- }, DEBOUNCE_TIMEOUT_IN_SECONDS * 1000);
500
+ }, DEBOUNCE_TIMEOUT_IN_SECONDS * 1e3);
528
501
  }
529
502
  }
530
503
 
531
- /**
532
- * Button config
533
- *
534
- * @typedef {object} ButtonConfig
535
- * @property {boolean} [preventDoubleClick=false] - Prevent accidental double
536
- * clicks on submit buttons from submitting forms multiple times.
537
- */
504
+ Button.moduleName = "govuk-button";
538
505
 
539
- /**
540
- * @import { Schema } from '../../common/configuration.mjs'
541
- */
542
- Button.moduleName = 'govuk-button';
543
506
  Button.defaults = Object.freeze({
544
507
  preventDoubleClick: false
545
508
  });
509
+
546
510
  Button.schema = Object.freeze({
547
511
  properties: {
548
512
  preventDoubleClick: {
549
- type: 'boolean'
513
+ type: "boolean"
550
514
  }
551
515
  }
552
516
  });
@@ -568,11 +532,10 @@ function closestAttributeValue($element, attributeName) {
568
532
  *
569
533
  * @preserve
570
534
  * @augments ConfigurableComponent<CharacterCountConfig>
571
- */
572
- class CharacterCount extends ConfigurableComponent {
535
+ */ class CharacterCount extends ConfigurableComponent {
573
536
  [configOverride](datasetConfig) {
574
537
  let configOverrides = {};
575
- if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
538
+ if ("maxwords" in datasetConfig || "maxlength" in datasetConfig) {
576
539
  configOverrides = {
577
540
  maxlength: undefined,
578
541
  maxwords: undefined
@@ -580,11 +543,6 @@ class CharacterCount extends ConfigurableComponent {
580
543
  }
581
544
  return configOverrides;
582
545
  }
583
-
584
- /**
585
- * @param {Element | null} $root - HTML element to use for character count
586
- * @param {CharacterCountConfig} [config] - Character count config
587
- */
588
546
  constructor($root, config = {}) {
589
547
  var _ref, _this$config$maxwords;
590
548
  super($root, config);
@@ -592,17 +550,17 @@ class CharacterCount extends ConfigurableComponent {
592
550
  this.$visibleCountMessage = void 0;
593
551
  this.$screenReaderCountMessage = void 0;
594
552
  this.lastInputTimestamp = null;
595
- this.lastInputValue = '';
553
+ this.lastInputValue = "";
596
554
  this.valueChecker = null;
597
555
  this.i18n = void 0;
598
556
  this.maxLength = void 0;
599
- const $textarea = this.$root.querySelector('.govuk-js-character-count');
557
+ const $textarea = this.$root.querySelector(".govuk-js-character-count");
600
558
  if (!($textarea instanceof HTMLTextAreaElement || $textarea instanceof HTMLInputElement)) {
601
559
  throw new ElementError({
602
560
  component: CharacterCount,
603
561
  element: $textarea,
604
- expectedType: 'HTMLTextareaElement or HTMLInputElement',
605
- identifier: 'Form field (`.govuk-js-character-count`)'
562
+ expectedType: "HTMLTextareaElement or HTMLInputElement",
563
+ identifier: "Form field (`.govuk-js-character-count`)"
606
564
  });
607
565
  }
608
566
  const errors = validateConfig(CharacterCount.schema, this.config);
@@ -610,7 +568,7 @@ class CharacterCount extends ConfigurableComponent {
610
568
  throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
611
569
  }
612
570
  this.i18n = new I18n(this.config.i18n, {
613
- locale: closestAttributeValue(this.$root, 'lang')
571
+ locale: closestAttributeValue(this.$root, "lang")
614
572
  });
615
573
  this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
616
574
  this.$textarea = $textarea;
@@ -623,34 +581,34 @@ class CharacterCount extends ConfigurableComponent {
623
581
  identifier: `Count message (\`id="${textareaDescriptionId}"\`)`
624
582
  });
625
583
  }
626
- this.$errorMessage = this.$root.querySelector('.govuk-error-message');
584
+ this.$errorMessage = this.$root.querySelector(".govuk-error-message");
627
585
  if (`${$textareaDescription.textContent}`.match(/^\s*$/)) {
628
- $textareaDescription.textContent = this.i18n.t('textareaDescription', {
586
+ $textareaDescription.textContent = this.i18n.t("textareaDescription", {
629
587
  count: this.maxLength
630
588
  });
631
589
  }
632
- this.$textarea.insertAdjacentElement('afterend', $textareaDescription);
633
- const $screenReaderCountMessage = document.createElement('div');
634
- $screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden';
635
- $screenReaderCountMessage.setAttribute('aria-live', 'polite');
590
+ this.$textarea.insertAdjacentElement("afterend", $textareaDescription);
591
+ const $screenReaderCountMessage = document.createElement("div");
592
+ $screenReaderCountMessage.className = "govuk-character-count__sr-status govuk-visually-hidden";
593
+ $screenReaderCountMessage.setAttribute("aria-live", "polite");
636
594
  this.$screenReaderCountMessage = $screenReaderCountMessage;
637
- $textareaDescription.insertAdjacentElement('afterend', $screenReaderCountMessage);
638
- const $visibleCountMessage = document.createElement('div');
595
+ $textareaDescription.insertAdjacentElement("afterend", $screenReaderCountMessage);
596
+ const $visibleCountMessage = document.createElement("div");
639
597
  $visibleCountMessage.className = $textareaDescription.className;
640
- $visibleCountMessage.classList.add('govuk-character-count__status');
641
- $visibleCountMessage.setAttribute('aria-hidden', 'true');
598
+ $visibleCountMessage.classList.add("govuk-character-count__status");
599
+ $visibleCountMessage.setAttribute("aria-hidden", "true");
642
600
  this.$visibleCountMessage = $visibleCountMessage;
643
- $textareaDescription.insertAdjacentElement('afterend', $visibleCountMessage);
644
- $textareaDescription.classList.add('govuk-visually-hidden');
645
- this.$textarea.removeAttribute('maxlength');
601
+ $textareaDescription.insertAdjacentElement("afterend", $visibleCountMessage);
602
+ $textareaDescription.classList.add("govuk-visually-hidden");
603
+ this.$textarea.removeAttribute("maxlength");
646
604
  this.bindChangeEvents();
647
- window.addEventListener('pageshow', () => this.updateCountMessage());
605
+ window.addEventListener("pageshow", () => this.updateCountMessage());
648
606
  this.updateCountMessage();
649
607
  }
650
608
  bindChangeEvents() {
651
- this.$textarea.addEventListener('keyup', () => this.handleKeyUp());
652
- this.$textarea.addEventListener('focus', () => this.handleFocus());
653
- this.$textarea.addEventListener('blur', () => this.handleBlur());
609
+ this.$textarea.addEventListener("keyup", () => this.handleKeyUp());
610
+ this.$textarea.addEventListener("focus", () => this.handleFocus());
611
+ this.$textarea.addEventListener("blur", () => this.handleBlur());
654
612
  }
655
613
  handleKeyUp() {
656
614
  this.updateVisibleCountMessage();
@@ -661,7 +619,7 @@ class CharacterCount extends ConfigurableComponent {
661
619
  if (!this.lastInputTimestamp || Date.now() - 500 >= this.lastInputTimestamp) {
662
620
  this.updateIfValueChanged();
663
621
  }
664
- }, 1000);
622
+ }, 1e3);
665
623
  }
666
624
  handleBlur() {
667
625
  if (this.valueChecker) {
@@ -681,19 +639,19 @@ class CharacterCount extends ConfigurableComponent {
681
639
  updateVisibleCountMessage() {
682
640
  const remainingNumber = this.maxLength - this.count(this.$textarea.value);
683
641
  const isError = remainingNumber < 0;
684
- this.$visibleCountMessage.classList.toggle('govuk-character-count__message--disabled', !this.isOverThreshold());
642
+ this.$visibleCountMessage.classList.toggle("govuk-character-count__message--disabled", !this.isOverThreshold());
685
643
  if (!this.$errorMessage) {
686
- this.$textarea.classList.toggle('govuk-textarea--error', isError);
644
+ this.$textarea.classList.toggle("govuk-textarea--error", isError);
687
645
  }
688
- this.$visibleCountMessage.classList.toggle('govuk-error-message', isError);
689
- this.$visibleCountMessage.classList.toggle('govuk-hint', !isError);
646
+ this.$visibleCountMessage.classList.toggle("govuk-error-message", isError);
647
+ this.$visibleCountMessage.classList.toggle("govuk-hint", !isError);
690
648
  this.$visibleCountMessage.textContent = this.getCountMessage();
691
649
  }
692
650
  updateScreenReaderCountMessage() {
693
651
  if (this.isOverThreshold()) {
694
- this.$screenReaderCountMessage.removeAttribute('aria-hidden');
652
+ this.$screenReaderCountMessage.removeAttribute("aria-hidden");
695
653
  } else {
696
- this.$screenReaderCountMessage.setAttribute('aria-hidden', 'true');
654
+ this.$screenReaderCountMessage.setAttribute("aria-hidden", "true");
697
655
  }
698
656
  this.$screenReaderCountMessage.textContent = this.getCountMessage();
699
657
  }
@@ -707,14 +665,14 @@ class CharacterCount extends ConfigurableComponent {
707
665
  }
708
666
  getCountMessage() {
709
667
  const remainingNumber = this.maxLength - this.count(this.$textarea.value);
710
- const countType = this.config.maxwords ? 'words' : 'characters';
668
+ const countType = this.config.maxwords ? "words" : "characters";
711
669
  return this.formatCountMessage(remainingNumber, countType);
712
670
  }
713
671
  formatCountMessage(remainingNumber, countType) {
714
672
  if (remainingNumber === 0) {
715
673
  return this.i18n.t(`${countType}AtLimit`);
716
674
  }
717
- const translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit';
675
+ const translationKeySuffix = remainingNumber < 0 ? "OverLimit" : "UnderLimit";
718
676
  return this.i18n.t(`${countType}${translationKeySuffix}`, {
719
677
  count: Math.abs(remainingNumber)
720
678
  });
@@ -730,143 +688,64 @@ class CharacterCount extends ConfigurableComponent {
730
688
  }
731
689
  }
732
690
 
733
- /**
734
- * Character count config
735
- *
736
- * @see {@link CharacterCount.defaults}
737
- * @typedef {object} CharacterCountConfig
738
- * @property {number} [maxlength] - The maximum number of characters.
739
- * If maxwords is provided, the maxlength option will be ignored.
740
- * @property {number} [maxwords] - The maximum number of words. If maxwords is
741
- * provided, the maxlength option will be ignored.
742
- * @property {number} [threshold=0] - The percentage value of the limit at
743
- * which point the count message is displayed. If this attribute is set, the
744
- * count message will be hidden by default.
745
- * @property {CharacterCountTranslations} [i18n=CharacterCount.defaults.i18n] - Character count translations
746
- */
747
-
748
- /**
749
- * Character count translations
750
- *
751
- * @see {@link CharacterCount.defaults.i18n}
752
- * @typedef {object} CharacterCountTranslations
753
- *
754
- * Messages shown to users as they type. It provides feedback on how many words
755
- * or characters they have remaining or if they are over the limit. This also
756
- * includes a message used as an accessible description for the textarea.
757
- * @property {TranslationPluralForms} [charactersUnderLimit] - Message displayed
758
- * when the number of characters is under the configured maximum, `maxlength`.
759
- * This message is displayed visually and through assistive technologies. The
760
- * component will replace the `%{count}` placeholder with the number of
761
- * remaining characters. This is a [pluralised list of
762
- * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
763
- * @property {string} [charactersAtLimit] - Message displayed when the number of
764
- * characters reaches the configured maximum, `maxlength`. This message is
765
- * displayed visually and through assistive technologies.
766
- * @property {TranslationPluralForms} [charactersOverLimit] - Message displayed
767
- * when the number of characters is over the configured maximum, `maxlength`.
768
- * This message is displayed visually and through assistive technologies. The
769
- * component will replace the `%{count}` placeholder with the number of
770
- * remaining characters. This is a [pluralised list of
771
- * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
772
- * @property {TranslationPluralForms} [wordsUnderLimit] - Message displayed when
773
- * the number of words is under the configured maximum, `maxlength`. This
774
- * message is displayed visually and through assistive technologies. The
775
- * component will replace the `%{count}` placeholder with the number of
776
- * remaining words. This is a [pluralised list of
777
- * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
778
- * @property {string} [wordsAtLimit] - Message displayed when the number of
779
- * words reaches the configured maximum, `maxlength`. This message is
780
- * displayed visually and through assistive technologies.
781
- * @property {TranslationPluralForms} [wordsOverLimit] - Message displayed when
782
- * the number of words is over the configured maximum, `maxlength`. This
783
- * message is displayed visually and through assistive technologies. The
784
- * component will replace the `%{count}` placeholder with the number of
785
- * remaining words. This is a [pluralised list of
786
- * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
787
- * @property {TranslationPluralForms} [textareaDescription] - Message made
788
- * available to assistive technologies, if none is already present in the
789
- * HTML, to describe that the component accepts only a limited amount of
790
- * content. It is visible on the page when JavaScript is unavailable. The
791
- * component will replace the `%{count}` placeholder with the value of the
792
- * `maxlength` or `maxwords` parameter.
793
- */
691
+ CharacterCount.moduleName = "govuk-character-count";
794
692
 
795
- /**
796
- * @import { Schema } from '../../common/configuration.mjs'
797
- * @import { TranslationPluralForms } from '../../i18n.mjs'
798
- */
799
- CharacterCount.moduleName = 'govuk-character-count';
800
693
  CharacterCount.defaults = Object.freeze({
801
694
  threshold: 0,
802
695
  i18n: {
803
696
  charactersUnderLimit: {
804
- one: 'You have %{count} character remaining',
805
- other: 'You have %{count} characters remaining'
697
+ one: "You have %{count} character remaining",
698
+ other: "You have %{count} characters remaining"
806
699
  },
807
- charactersAtLimit: 'You have 0 characters remaining',
700
+ charactersAtLimit: "You have 0 characters remaining",
808
701
  charactersOverLimit: {
809
- one: 'You have %{count} character too many',
810
- other: 'You have %{count} characters too many'
702
+ one: "You have %{count} character too many",
703
+ other: "You have %{count} characters too many"
811
704
  },
812
705
  wordsUnderLimit: {
813
- one: 'You have %{count} word remaining',
814
- other: 'You have %{count} words remaining'
706
+ one: "You have %{count} word remaining",
707
+ other: "You have %{count} words remaining"
815
708
  },
816
- wordsAtLimit: 'You have 0 words remaining',
709
+ wordsAtLimit: "You have 0 words remaining",
817
710
  wordsOverLimit: {
818
- one: 'You have %{count} word too many',
819
- other: 'You have %{count} words too many'
711
+ one: "You have %{count} word too many",
712
+ other: "You have %{count} words too many"
820
713
  },
821
714
  textareaDescription: {
822
- other: ''
715
+ other: ""
823
716
  }
824
717
  }
825
718
  });
719
+
826
720
  CharacterCount.schema = Object.freeze({
827
721
  properties: {
828
722
  i18n: {
829
- type: 'object'
723
+ type: "object"
830
724
  },
831
725
  maxwords: {
832
- type: 'number'
726
+ type: "number"
833
727
  },
834
728
  maxlength: {
835
- type: 'number'
729
+ type: "number"
836
730
  },
837
731
  threshold: {
838
- type: 'number'
732
+ type: "number"
839
733
  }
840
734
  },
841
- anyOf: [{
842
- required: ['maxwords'],
735
+ anyOf: [ {
736
+ required: [ "maxwords" ],
843
737
  errorMessage: 'Either "maxlength" or "maxwords" must be provided'
844
738
  }, {
845
- required: ['maxlength'],
739
+ required: [ "maxlength" ],
846
740
  errorMessage: 'Either "maxlength" or "maxwords" must be provided'
847
- }]
741
+ } ]
848
742
  });
849
743
 
850
744
  /**
851
745
  * Checkboxes component
852
746
  *
853
747
  * @preserve
854
- */
855
- class Checkboxes extends Component {
856
- /**
857
- * Checkboxes can be associated with a 'conditionally revealed' content block
858
- * – for example, a checkbox for 'Phone' could reveal an additional form field
859
- * for the user to enter their phone number.
860
- *
861
- * These associations are made using a `data-aria-controls` attribute, which
862
- * is promoted to an aria-controls attribute during initialisation.
863
- *
864
- * We also need to restore the state of any conditional reveals on the page
865
- * (for example if the user has navigated back), and set up event handlers to
866
- * keep the reveal in sync with the checkbox state.
867
- *
868
- * @param {Element | null} $root - HTML element to use for checkboxes
869
- */
748
+ */ class Checkboxes extends Component {
870
749
  constructor($root) {
871
750
  super($root);
872
751
  this.$inputs = void 0;
@@ -879,7 +758,7 @@ class Checkboxes extends Component {
879
758
  }
880
759
  this.$inputs = $inputs;
881
760
  this.$inputs.forEach($input => {
882
- const targetId = $input.getAttribute('data-aria-controls');
761
+ const targetId = $input.getAttribute("data-aria-controls");
883
762
  if (!targetId) {
884
763
  return;
885
764
  }
@@ -889,26 +768,26 @@ class Checkboxes extends Component {
889
768
  identifier: `Conditional reveal (\`id="${targetId}"\`)`
890
769
  });
891
770
  }
892
- $input.setAttribute('aria-controls', targetId);
893
- $input.removeAttribute('data-aria-controls');
771
+ $input.setAttribute("aria-controls", targetId);
772
+ $input.removeAttribute("data-aria-controls");
894
773
  });
895
- window.addEventListener('pageshow', () => this.syncAllConditionalReveals());
774
+ window.addEventListener("pageshow", () => this.syncAllConditionalReveals());
896
775
  this.syncAllConditionalReveals();
897
- this.$root.addEventListener('click', event => this.handleClick(event));
776
+ this.$root.addEventListener("click", event => this.handleClick(event));
898
777
  }
899
778
  syncAllConditionalReveals() {
900
779
  this.$inputs.forEach($input => this.syncConditionalRevealWithInputState($input));
901
780
  }
902
781
  syncConditionalRevealWithInputState($input) {
903
- const targetId = $input.getAttribute('aria-controls');
782
+ const targetId = $input.getAttribute("aria-controls");
904
783
  if (!targetId) {
905
784
  return;
906
785
  }
907
786
  const $target = document.getElementById(targetId);
908
- if ($target != null && $target.classList.contains('govuk-checkboxes__conditional')) {
787
+ if ($target != null && $target.classList.contains("govuk-checkboxes__conditional")) {
909
788
  const inputIsChecked = $input.checked;
910
- $input.setAttribute('aria-expanded', inputIsChecked.toString());
911
- $target.classList.toggle('govuk-checkboxes__conditional--hidden', !inputIsChecked);
789
+ $input.setAttribute("aria-expanded", inputIsChecked.toString());
790
+ $target.classList.toggle("govuk-checkboxes__conditional--hidden", !inputIsChecked);
912
791
  }
913
792
  }
914
793
  unCheckAllInputsExcept($input) {
@@ -933,17 +812,17 @@ class Checkboxes extends Component {
933
812
  }
934
813
  handleClick(event) {
935
814
  const $clickedInput = event.target;
936
- if (!($clickedInput instanceof HTMLInputElement) || $clickedInput.type !== 'checkbox') {
815
+ if (!($clickedInput instanceof HTMLInputElement) || $clickedInput.type !== "checkbox") {
937
816
  return;
938
817
  }
939
- const hasAriaControls = $clickedInput.getAttribute('aria-controls');
818
+ const hasAriaControls = $clickedInput.getAttribute("aria-controls");
940
819
  if (hasAriaControls) {
941
820
  this.syncConditionalRevealWithInputState($clickedInput);
942
821
  }
943
822
  if (!$clickedInput.checked) {
944
823
  return;
945
824
  }
946
- const hasBehaviourExclusive = $clickedInput.getAttribute('data-behaviour') === 'exclusive';
825
+ const hasBehaviourExclusive = $clickedInput.getAttribute("data-behaviour") === "exclusive";
947
826
  if (hasBehaviourExclusive) {
948
827
  this.unCheckAllInputsExcept($clickedInput);
949
828
  } else {
@@ -951,7 +830,8 @@ class Checkboxes extends Component {
951
830
  }
952
831
  }
953
832
  }
954
- Checkboxes.moduleName = 'govuk-checkboxes';
833
+
834
+ Checkboxes.moduleName = "govuk-checkboxes";
955
835
 
956
836
  /**
957
837
  * Error summary component
@@ -961,18 +841,13 @@ Checkboxes.moduleName = 'govuk-checkboxes';
961
841
  *
962
842
  * @preserve
963
843
  * @augments ConfigurableComponent<ErrorSummaryConfig>
964
- */
965
- class ErrorSummary extends ConfigurableComponent {
966
- /**
967
- * @param {Element | null} $root - HTML element to use for error summary
968
- * @param {ErrorSummaryConfig} [config] - Error summary config
969
- */
844
+ */ class ErrorSummary extends ConfigurableComponent {
970
845
  constructor($root, config = {}) {
971
846
  super($root, config);
972
847
  if (!this.config.disableAutoFocus) {
973
848
  setFocus(this.$root);
974
849
  }
975
- this.$root.addEventListener('click', event => this.handleClick(event));
850
+ this.$root.addEventListener("click", event => this.handleClick(event));
976
851
  }
977
852
  handleClick(event) {
978
853
  const $target = event.target;
@@ -1004,12 +879,12 @@ class ErrorSummary extends ConfigurableComponent {
1004
879
  }
1005
880
  getAssociatedLegendOrLabel($input) {
1006
881
  var _document$querySelect;
1007
- const $fieldset = $input.closest('fieldset');
882
+ const $fieldset = $input.closest("fieldset");
1008
883
  if ($fieldset) {
1009
- const $legends = $fieldset.getElementsByTagName('legend');
884
+ const $legends = $fieldset.getElementsByTagName("legend");
1010
885
  if ($legends.length) {
1011
886
  const $candidateLegend = $legends[0];
1012
- if ($input instanceof HTMLInputElement && ($input.type === 'checkbox' || $input.type === 'radio')) {
887
+ if ($input instanceof HTMLInputElement && ($input.type === "checkbox" || $input.type === "radio")) {
1013
888
  return $candidateLegend;
1014
889
  }
1015
890
  const legendTop = $candidateLegend.getBoundingClientRect().top;
@@ -1022,29 +897,20 @@ class ErrorSummary extends ConfigurableComponent {
1022
897
  }
1023
898
  }
1024
899
  }
1025
- return (_document$querySelect = document.querySelector(`label[for='${$input.getAttribute('id')}']`)) != null ? _document$querySelect : $input.closest('label');
900
+ return (_document$querySelect = document.querySelector(`label[for='${$input.getAttribute("id")}']`)) != null ? _document$querySelect : $input.closest("label");
1026
901
  }
1027
902
  }
1028
903
 
1029
- /**
1030
- * Error summary config
1031
- *
1032
- * @typedef {object} ErrorSummaryConfig
1033
- * @property {boolean} [disableAutoFocus=false] - If set to `true` the error
1034
- * summary will not be focussed when the page loads.
1035
- */
904
+ ErrorSummary.moduleName = "govuk-error-summary";
1036
905
 
1037
- /**
1038
- * @import { Schema } from '../../common/configuration.mjs'
1039
- */
1040
- ErrorSummary.moduleName = 'govuk-error-summary';
1041
906
  ErrorSummary.defaults = Object.freeze({
1042
907
  disableAutoFocus: false
1043
908
  });
909
+
1044
910
  ErrorSummary.schema = Object.freeze({
1045
911
  properties: {
1046
912
  disableAutoFocus: {
1047
- type: 'boolean'
913
+ type: "boolean"
1048
914
  }
1049
915
  }
1050
916
  });
@@ -1054,59 +920,54 @@ ErrorSummary.schema = Object.freeze({
1054
920
  *
1055
921
  * @preserve
1056
922
  * @augments ConfigurableComponent<PasswordInputConfig>
1057
- */
1058
- class PasswordInput extends ConfigurableComponent {
1059
- /**
1060
- * @param {Element | null} $root - HTML element to use for password input
1061
- * @param {PasswordInputConfig} [config] - Password input config
1062
- */
923
+ */ class PasswordInput extends ConfigurableComponent {
1063
924
  constructor($root, config = {}) {
1064
925
  super($root, config);
1065
926
  this.i18n = void 0;
1066
927
  this.$input = void 0;
1067
928
  this.$showHideButton = void 0;
1068
929
  this.$screenReaderStatusMessage = void 0;
1069
- const $input = this.$root.querySelector('.govuk-js-password-input-input');
930
+ const $input = this.$root.querySelector(".govuk-js-password-input-input");
1070
931
  if (!($input instanceof HTMLInputElement)) {
1071
932
  throw new ElementError({
1072
933
  component: PasswordInput,
1073
934
  element: $input,
1074
- expectedType: 'HTMLInputElement',
1075
- identifier: 'Form field (`.govuk-js-password-input-input`)'
935
+ expectedType: "HTMLInputElement",
936
+ identifier: "Form field (`.govuk-js-password-input-input`)"
1076
937
  });
1077
938
  }
1078
- if ($input.type !== 'password') {
1079
- throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
939
+ if ($input.type !== "password") {
940
+ throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
1080
941
  }
1081
- const $showHideButton = this.$root.querySelector('.govuk-js-password-input-toggle');
942
+ const $showHideButton = this.$root.querySelector(".govuk-js-password-input-toggle");
1082
943
  if (!($showHideButton instanceof HTMLButtonElement)) {
1083
944
  throw new ElementError({
1084
945
  component: PasswordInput,
1085
946
  element: $showHideButton,
1086
- expectedType: 'HTMLButtonElement',
1087
- identifier: 'Button (`.govuk-js-password-input-toggle`)'
947
+ expectedType: "HTMLButtonElement",
948
+ identifier: "Button (`.govuk-js-password-input-toggle`)"
1088
949
  });
1089
950
  }
1090
- if ($showHideButton.type !== 'button') {
1091
- throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
951
+ if ($showHideButton.type !== "button") {
952
+ throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
1092
953
  }
1093
954
  this.$input = $input;
1094
955
  this.$showHideButton = $showHideButton;
1095
956
  this.i18n = new I18n(this.config.i18n, {
1096
- locale: closestAttributeValue(this.$root, 'lang')
957
+ locale: closestAttributeValue(this.$root, "lang")
1097
958
  });
1098
- this.$showHideButton.removeAttribute('hidden');
1099
- const $screenReaderStatusMessage = document.createElement('div');
1100
- $screenReaderStatusMessage.className = 'govuk-password-input__sr-status govuk-visually-hidden';
1101
- $screenReaderStatusMessage.setAttribute('aria-live', 'polite');
959
+ this.$showHideButton.removeAttribute("hidden");
960
+ const $screenReaderStatusMessage = document.createElement("div");
961
+ $screenReaderStatusMessage.className = "govuk-password-input__sr-status govuk-visually-hidden";
962
+ $screenReaderStatusMessage.setAttribute("aria-live", "polite");
1102
963
  this.$screenReaderStatusMessage = $screenReaderStatusMessage;
1103
- this.$input.insertAdjacentElement('afterend', $screenReaderStatusMessage);
1104
- this.$showHideButton.addEventListener('click', this.toggle.bind(this));
964
+ this.$input.insertAdjacentElement("afterend", $screenReaderStatusMessage);
965
+ this.$showHideButton.addEventListener("click", this.toggle.bind(this));
1105
966
  if (this.$input.form) {
1106
- this.$input.form.addEventListener('submit', () => this.hide());
967
+ this.$input.form.addEventListener("submit", () => this.hide());
1107
968
  }
1108
- window.addEventListener('pageshow', event => {
1109
- if (event.persisted && this.$input.type !== 'password') {
969
+ window.addEventListener("pageshow", event => {
970
+ if (event.persisted && this.$input.type !== "password") {
1110
971
  this.hide();
1111
972
  }
1112
973
  });
@@ -1114,80 +975,49 @@ class PasswordInput extends ConfigurableComponent {
1114
975
  }
1115
976
  toggle(event) {
1116
977
  event.preventDefault();
1117
- if (this.$input.type === 'password') {
978
+ if (this.$input.type === "password") {
1118
979
  this.show();
1119
980
  return;
1120
981
  }
1121
982
  this.hide();
1122
983
  }
1123
984
  show() {
1124
- this.setType('text');
985
+ this.setType("text");
1125
986
  }
1126
987
  hide() {
1127
- this.setType('password');
988
+ this.setType("password");
1128
989
  }
1129
990
  setType(type) {
1130
991
  if (type === this.$input.type) {
1131
992
  return;
1132
993
  }
1133
- this.$input.setAttribute('type', type);
1134
- const isHidden = type === 'password';
1135
- const prefixButton = isHidden ? 'show' : 'hide';
1136
- const prefixStatus = isHidden ? 'passwordHidden' : 'passwordShown';
994
+ this.$input.setAttribute("type", type);
995
+ const isHidden = type === "password";
996
+ const prefixButton = isHidden ? "show" : "hide";
997
+ const prefixStatus = isHidden ? "passwordHidden" : "passwordShown";
1137
998
  this.$showHideButton.innerText = this.i18n.t(`${prefixButton}Password`);
1138
- this.$showHideButton.setAttribute('aria-label', this.i18n.t(`${prefixButton}PasswordAriaLabel`));
999
+ this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${prefixButton}PasswordAriaLabel`));
1139
1000
  this.$screenReaderStatusMessage.innerText = this.i18n.t(`${prefixStatus}Announcement`);
1140
1001
  }
1141
1002
  }
1142
1003
 
1143
- /**
1144
- * Password input config
1145
- *
1146
- * @typedef {object} PasswordInputConfig
1147
- * @property {PasswordInputTranslations} [i18n=PasswordInput.defaults.i18n] - Password input translations
1148
- */
1149
-
1150
- /**
1151
- * Password input translations
1152
- *
1153
- * @see {@link PasswordInput.defaults.i18n}
1154
- * @typedef {object} PasswordInputTranslations
1155
- *
1156
- * Messages displayed to the user indicating the state of the show/hide toggle.
1157
- * @property {string} [showPassword] - Visible text of the button when the
1158
- * password is currently hidden. Plain text only.
1159
- * @property {string} [hidePassword] - Visible text of the button when the
1160
- * password is currently visible. Plain text only.
1161
- * @property {string} [showPasswordAriaLabel] - aria-label of the button when
1162
- * the password is currently hidden. Plain text only.
1163
- * @property {string} [hidePasswordAriaLabel] - aria-label of the button when
1164
- * the password is currently visible. Plain text only.
1165
- * @property {string} [passwordShownAnnouncement] - Screen reader
1166
- * announcement to make when the password has just become visible.
1167
- * Plain text only.
1168
- * @property {string} [passwordHiddenAnnouncement] - Screen reader
1169
- * announcement to make when the password has just been hidden.
1170
- * Plain text only.
1171
- */
1004
+ PasswordInput.moduleName = "govuk-password-input";
1172
1005
 
1173
- /**
1174
- * @import { Schema } from '../../common/configuration.mjs'
1175
- */
1176
- PasswordInput.moduleName = 'govuk-password-input';
1177
1006
  PasswordInput.defaults = Object.freeze({
1178
1007
  i18n: {
1179
- showPassword: 'Show',
1180
- hidePassword: 'Hide',
1181
- showPasswordAriaLabel: 'Show password',
1182
- hidePasswordAriaLabel: 'Hide password',
1183
- passwordShownAnnouncement: 'Your password is visible',
1184
- passwordHiddenAnnouncement: 'Your password is hidden'
1008
+ showPassword: "Show",
1009
+ hidePassword: "Hide",
1010
+ showPasswordAriaLabel: "Show password",
1011
+ hidePasswordAriaLabel: "Hide password",
1012
+ passwordShownAnnouncement: "Your password is visible",
1013
+ passwordHiddenAnnouncement: "Your password is hidden"
1185
1014
  }
1186
1015
  });
1016
+
1187
1017
  PasswordInput.schema = Object.freeze({
1188
1018
  properties: {
1189
1019
  i18n: {
1190
- type: 'object'
1020
+ type: "object"
1191
1021
  }
1192
1022
  }
1193
1023
  });
@@ -1196,22 +1026,7 @@ PasswordInput.schema = Object.freeze({
1196
1026
  * Radios component
1197
1027
  *
1198
1028
  * @preserve
1199
- */
1200
- class Radios extends Component {
1201
- /**
1202
- * Radios can be associated with a 'conditionally revealed' content block –
1203
- * for example, a radio for 'Phone' could reveal an additional form field for
1204
- * the user to enter their phone number.
1205
- *
1206
- * These associations are made using a `data-aria-controls` attribute, which
1207
- * is promoted to an aria-controls attribute during initialisation.
1208
- *
1209
- * We also need to restore the state of any conditional reveals on the page
1210
- * (for example if the user has navigated back), and set up event handlers to
1211
- * keep the reveal in sync with the radio state.
1212
- *
1213
- * @param {Element | null} $root - HTML element to use for radios
1214
- */
1029
+ */ class Radios extends Component {
1215
1030
  constructor($root) {
1216
1031
  super($root);
1217
1032
  this.$inputs = void 0;
@@ -1224,7 +1039,7 @@ class Radios extends Component {
1224
1039
  }
1225
1040
  this.$inputs = $inputs;
1226
1041
  this.$inputs.forEach($input => {
1227
- const targetId = $input.getAttribute('data-aria-controls');
1042
+ const targetId = $input.getAttribute("data-aria-controls");
1228
1043
  if (!targetId) {
1229
1044
  return;
1230
1045
  }
@@ -1234,31 +1049,31 @@ class Radios extends Component {
1234
1049
  identifier: `Conditional reveal (\`id="${targetId}"\`)`
1235
1050
  });
1236
1051
  }
1237
- $input.setAttribute('aria-controls', targetId);
1238
- $input.removeAttribute('data-aria-controls');
1052
+ $input.setAttribute("aria-controls", targetId);
1053
+ $input.removeAttribute("data-aria-controls");
1239
1054
  });
1240
- window.addEventListener('pageshow', () => this.syncAllConditionalReveals());
1055
+ window.addEventListener("pageshow", () => this.syncAllConditionalReveals());
1241
1056
  this.syncAllConditionalReveals();
1242
- this.$root.addEventListener('click', event => this.handleClick(event));
1057
+ this.$root.addEventListener("click", event => this.handleClick(event));
1243
1058
  }
1244
1059
  syncAllConditionalReveals() {
1245
1060
  this.$inputs.forEach($input => this.syncConditionalRevealWithInputState($input));
1246
1061
  }
1247
1062
  syncConditionalRevealWithInputState($input) {
1248
- const targetId = $input.getAttribute('aria-controls');
1063
+ const targetId = $input.getAttribute("aria-controls");
1249
1064
  if (!targetId) {
1250
1065
  return;
1251
1066
  }
1252
1067
  const $target = document.getElementById(targetId);
1253
- if ($target != null && $target.classList.contains('govuk-radios__conditional')) {
1068
+ if ($target != null && $target.classList.contains("govuk-radios__conditional")) {
1254
1069
  const inputIsChecked = $input.checked;
1255
- $input.setAttribute('aria-expanded', inputIsChecked.toString());
1256
- $target.classList.toggle('govuk-radios__conditional--hidden', !inputIsChecked);
1070
+ $input.setAttribute("aria-expanded", inputIsChecked.toString());
1071
+ $target.classList.toggle("govuk-radios__conditional--hidden", !inputIsChecked);
1257
1072
  }
1258
1073
  }
1259
1074
  handleClick(event) {
1260
1075
  const $clickedInput = event.target;
1261
- if (!($clickedInput instanceof HTMLInputElement) || $clickedInput.type !== 'radio') {
1076
+ if (!($clickedInput instanceof HTMLInputElement) || $clickedInput.type !== "radio") {
1262
1077
  return;
1263
1078
  }
1264
1079
  const $allInputs = document.querySelectorAll('input[type="radio"][aria-controls]');
@@ -1273,22 +1088,184 @@ class Radios extends Component {
1273
1088
  });
1274
1089
  }
1275
1090
  }
1276
- Radios.moduleName = 'govuk-radios';
1091
+
1092
+ Radios.moduleName = "govuk-radios";
1093
+
1094
+ class FileFieldController extends Controller {
1095
+ static targets=[ "preview", "destroy" ];
1096
+ static values={
1097
+ mimeTypes: Array
1098
+ };
1099
+ connect() {
1100
+ this.counter = 0;
1101
+ this.initialPreviewContent = null;
1102
+ this.onUploadFlag = false;
1103
+ }
1104
+ onUpload(event) {
1105
+ this.onUploadFlag = true;
1106
+ if (this.hasDestroyTarget) {
1107
+ this.destroyTarget.value = false;
1108
+ }
1109
+ this.previewTarget.removeAttribute("hidden");
1110
+ if (this.hasPreviewTarget) {
1111
+ if (event.currentTarget.files.length > 0) {
1112
+ this.showPreview(event.currentTarget.files[0]);
1113
+ } else {
1114
+ this.setPreviewContent(this.initialPreviewContent);
1115
+ }
1116
+ }
1117
+ }
1118
+ setDestroy(event) {
1119
+ event.preventDefault();
1120
+ if (this.initialPreviewContent && this.onUploadFlag) {
1121
+ this.onUploadFlag = false;
1122
+ this.setPreviewContent(this.initialPreviewContent);
1123
+ } else {
1124
+ if (this.hasDestroyTarget) {
1125
+ this.destroyTarget.value = true;
1126
+ }
1127
+ if (this.hasPreviewTarget) {
1128
+ this.previewTarget.setAttribute("hidden", "");
1129
+ this.setPreviewContent("");
1130
+ }
1131
+ if (this.previousInput) {
1132
+ this.previousInput.toggleAttribute("disabled", true);
1133
+ }
1134
+ }
1135
+ this.fileInput.value = "";
1136
+ }
1137
+ setPreviewContent(content) {
1138
+ if (this.filenameTag) {
1139
+ this.filenameTag.innerText = text;
1140
+ }
1141
+ }
1142
+ drop(event) {
1143
+ event.preventDefault();
1144
+ const file = this.fileForEvent(event, this.mimeTypesValue);
1145
+ if (file) {
1146
+ const dT = new DataTransfer;
1147
+ dT.items.add(file);
1148
+ this.fileInput.files = dT.files;
1149
+ this.fileInput.dispatchEvent(new Event("change"));
1150
+ }
1151
+ this.counter = 0;
1152
+ this.element.classList.remove("droppable");
1153
+ }
1154
+ dragover(event) {
1155
+ event.preventDefault();
1156
+ }
1157
+ dragenter(event) {
1158
+ event.preventDefault();
1159
+ if (this.counter === 0) {
1160
+ this.element.classList.add("droppable");
1161
+ }
1162
+ this.counter++;
1163
+ }
1164
+ dragleave(event) {
1165
+ event.preventDefault();
1166
+ this.counter--;
1167
+ if (this.counter === 0) {
1168
+ this.element.classList.remove("droppable");
1169
+ }
1170
+ }
1171
+ get fileInput() {
1172
+ return this.element.querySelector("input[type='file']");
1173
+ }
1174
+ get previousInput() {
1175
+ return this.element.querySelector(`input[type='hidden'][name='${this.fileInput.name}']`);
1176
+ }
1177
+ get filenameTag() {
1178
+ if (!this.hasPreviewTarget) return null;
1179
+ return this.previewTarget.querySelector("p.preview-filename");
1180
+ }
1181
+ showPreview(file) {
1182
+ const reader = new FileReader;
1183
+ reader.onload = e => {
1184
+ if (this.filenameTag) {
1185
+ this.filenameTag.innerText = file.name;
1186
+ }
1187
+ };
1188
+ reader.readAsDataURL(file);
1189
+ }
1190
+ fileForEvent(event, mimeTypes) {
1191
+ const accept = file => mimeTypes.indexOf(file.type) > -1;
1192
+ let file;
1193
+ if (event.dataTransfer.items) {
1194
+ const item = [ ...event.dataTransfer.items ].find(accept);
1195
+ if (item) {
1196
+ file = item.getAsFile();
1197
+ }
1198
+ } else {
1199
+ file = [ ...event.dataTransfer.files ].find(accept);
1200
+ }
1201
+ return file;
1202
+ }
1203
+ }
1204
+
1205
+ class DocumentFieldController extends FileFieldController {
1206
+ connect() {
1207
+ super.connect();
1208
+ this.initialPreviewContent = this.filenameTag.text;
1209
+ }
1210
+ setPreviewContent(content) {
1211
+ this.filenameTag.innerText = content;
1212
+ }
1213
+ showPreview(file) {
1214
+ const reader = new FileReader;
1215
+ reader.onload = e => {
1216
+ if (this.filenameTag) {
1217
+ this.filenameTag.innerText = file.name;
1218
+ }
1219
+ };
1220
+ reader.readAsDataURL(file);
1221
+ }
1222
+ get filenameTag() {
1223
+ return this.previewTarget.querySelector("p.preview-filename");
1224
+ }
1225
+ }
1226
+
1227
+ class ImageFieldController extends FileFieldController {
1228
+ connect() {
1229
+ super.connect();
1230
+ this.initialPreviewContent = this.imageTag.getAttribute("src");
1231
+ }
1232
+ setPreviewContent(content) {
1233
+ this.imageTag.src = content;
1234
+ }
1235
+ showPreview(file) {
1236
+ const reader = new FileReader;
1237
+ reader.onload = e => {
1238
+ this.imageTag.src = e.target.result;
1239
+ };
1240
+ reader.readAsDataURL(file);
1241
+ }
1242
+ get imageTag() {
1243
+ return this.previewTarget.querySelector("img");
1244
+ }
1245
+ }
1246
+
1247
+ const Definitions = [ {
1248
+ identifier: "govuk-document-field",
1249
+ controllerConstructor: DocumentFieldController
1250
+ }, {
1251
+ identifier: "govuk-image-field",
1252
+ controllerConstructor: ImageFieldController
1253
+ } ];
1277
1254
 
1278
1255
  function initAll(config) {
1279
1256
  let _config$scope;
1280
- config = typeof config !== 'undefined' ? config : {};
1257
+ config = typeof config !== "undefined" ? config : {};
1281
1258
  if (!isSupported()) {
1282
- console.log(new SupportError());
1259
+ console.log(new SupportError);
1283
1260
  return;
1284
1261
  }
1285
- const components = [[Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [Radios], [PasswordInput, config.passwordInput]];
1262
+ const components = [ [ Button, config.button ], [ CharacterCount, config.characterCount ], [ Checkboxes ], [ ErrorSummary, config.errorSummary ], [ Radios ], [ PasswordInput, config.passwordInput ] ];
1286
1263
  const $scope = (_config$scope = config.scope) != null ? _config$scope : document;
1287
1264
  components.forEach(([Component, config]) => {
1288
1265
  const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
1289
1266
  $elements.forEach($element => {
1290
1267
  try {
1291
- 'defaults' in Component ? new Component($element, config) : new Component($element);
1268
+ "defaults" in Component ? new Component($element, config) : new Component($element);
1292
1269
  } catch (error) {
1293
1270
  console.log(error);
1294
1271
  }
@@ -1296,4 +1273,4 @@ function initAll(config) {
1296
1273
  });
1297
1274
  }
1298
1275
 
1299
- export { Button, CharacterCount, Checkboxes, ErrorSummary, PasswordInput, Radios, initAll };
1276
+ export { Button, CharacterCount, Checkboxes, ErrorSummary, PasswordInput, Radios, Definitions as default, initAll };