katalyst-govuk-formbuilder 1.19.0 → 1.21.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.
- checksums.yaml +4 -4
- data/README.md +12 -2
- data/app/assets/builds/katalyst/govuk/formbuilder.css +18 -15
- data/app/assets/builds/katalyst/govuk/formbuilder.js +639 -449
- data/app/assets/builds/katalyst/govuk/formbuilder.min.js +15 -8
- data/app/assets/builds/katalyst/govuk/formbuilder.min.js.map +1 -0
- data/config/importmap.rb +1 -2
- data/lib/katalyst/govuk/{formbuilder → form_builder}/builder.rb +67 -1
- data/lib/katalyst/govuk/form_builder/config.rb +23 -0
- data/lib/katalyst/govuk/{formbuilder → form_builder}/containers/fieldset_context.rb +1 -1
- data/lib/katalyst/govuk/{formbuilder → form_builder}/elements/combobox.rb +1 -1
- data/lib/katalyst/govuk/form_builder/elements/document.rb +68 -0
- data/lib/katalyst/govuk/form_builder/elements/image.rb +65 -0
- data/lib/katalyst/govuk/{formbuilder → form_builder}/elements/label.rb +1 -1
- data/lib/katalyst/govuk/{formbuilder → form_builder}/elements/legend.rb +1 -1
- data/lib/katalyst/govuk/{formbuilder → form_builder}/elements/rich_text_area.rb +1 -1
- data/lib/katalyst/govuk/{formbuilder → form_builder}/engine.rb +6 -3
- data/lib/katalyst/govuk/{formbuilder → form_builder}/extensions.rb +2 -1
- data/lib/katalyst/govuk/{formbuilder → form_builder}/frontend.rb +1 -1
- data/lib/katalyst/govuk/form_builder/traits/file.rb +104 -0
- data/lib/katalyst/govuk/{formbuilder → form_builder}/traits/label.rb +2 -2
- data/lib/katalyst/govuk/form_builder.rb +13 -0
- data/lib/katalyst/govuk/formbuilder.rb +3 -6
- data/lib/katalyst-govuk-formbuilder.rb +3 -0
- data/node_modules/govuk-frontend/dist/govuk/components/service-navigation/_index.scss +72 -5
- data/node_modules/govuk-frontend/dist/govuk/core/_govuk-frontend-properties.scss +5 -2
- data/node_modules/govuk-frontend/dist/govuk/helpers/_colour.scss +31 -2
- data/node_modules/govuk-frontend/dist/govuk/helpers/_visually-hidden.scss +4 -1
- data/node_modules/govuk-frontend/dist/govuk/settings/_colours-applied.scss +2 -2
- data/node_modules/govuk-frontend/dist/govuk/settings/_index.scss +13 -0
- metadata +19 -11
@@ -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(
|
7
|
+
return url.split("#").pop();
|
6
8
|
}
|
9
|
+
|
7
10
|
function setFocus($element, options = {}) {
|
8
11
|
var _options$onBeforeFocu;
|
9
|
-
const isFocusable = $element.getAttribute(
|
12
|
+
const isFocusable = $element.getAttribute("tabindex");
|
10
13
|
if (!isFocusable) {
|
11
|
-
$element.setAttribute(
|
14
|
+
$element.setAttribute("tabindex", "-1");
|
12
15
|
}
|
13
16
|
function onFocus() {
|
14
|
-
$element.addEventListener(
|
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(
|
25
|
+
$element.removeAttribute("tabindex");
|
23
26
|
}
|
24
27
|
}
|
25
|
-
$element.addEventListener(
|
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(
|
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 ===
|
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 =
|
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 =
|
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 =
|
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 =
|
76
|
+
this.name = "ConfigError";
|
82
77
|
}
|
83
78
|
}
|
79
|
+
|
84
80
|
class ElementError extends GOVUKFrontendError {
|
85
81
|
constructor(messageOrOptions) {
|
86
|
-
let message = typeof messageOrOptions ===
|
87
|
-
if (typeof messageOrOptions ===
|
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 :
|
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 =
|
90
|
+
this.name = "ElementError";
|
100
91
|
}
|
101
92
|
}
|
93
|
+
|
102
94
|
class InitError extends GOVUKFrontendError {
|
103
95
|
constructor(componentOrMessage) {
|
104
|
-
const message = typeof componentOrMessage ===
|
96
|
+
const message = typeof componentOrMessage === "string" ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\`$root\`) already initialised`);
|
105
97
|
super(message);
|
106
|
-
this.name =
|
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 !==
|
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:
|
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(
|
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,
|
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 ([
|
197
|
-
outputType =
|
169
|
+
if ([ "true", "false" ].includes(trimmedValue)) {
|
170
|
+
outputType = "boolean";
|
198
171
|
}
|
199
172
|
if (trimmedValue.length > 0 && isFinite(Number(trimmedValue))) {
|
200
|
-
outputType =
|
173
|
+
outputType = "number";
|
201
174
|
}
|
202
175
|
}
|
203
176
|
switch (outputType) {
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
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,
|
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) ===
|
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 ===
|
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) !==
|
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 ||
|
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(
|
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) ===
|
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 ===
|
290
|
+
if (typeof translation === "string") {
|
315
291
|
if (translation.match(/%{(.\S+)}/)) {
|
316
292
|
if (!options) {
|
317
|
-
throw new Error(
|
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
|
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 !==
|
331
|
-
return
|
306
|
+
if (placeholderValue === false || typeof placeholderValue !== "number" && typeof placeholderValue !== "string") {
|
307
|
+
return "";
|
332
308
|
}
|
333
|
-
if (typeof placeholderValue ===
|
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(
|
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
|
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 ===
|
327
|
+
if (typeof translation === "object") {
|
352
328
|
if (preferredForm in translation) {
|
353
329
|
return preferredForm;
|
354
|
-
} else if (
|
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
|
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
|
343
|
+
return "other";
|
368
344
|
}
|
369
345
|
getPluralRulesForLocale() {
|
370
|
-
const localeShort = this.locale.split(
|
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: [
|
381
|
-
chinese: [
|
382
|
-
french: [
|
383
|
-
german: [
|
384
|
-
irish: [
|
385
|
-
russian: [
|
386
|
-
scottish: [
|
387
|
-
spanish: [
|
388
|
-
welsh: [
|
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
|
371
|
+
return "zero";
|
394
372
|
}
|
395
373
|
if (n === 1) {
|
396
|
-
return
|
374
|
+
return "one";
|
397
375
|
}
|
398
376
|
if (n === 2) {
|
399
|
-
return
|
377
|
+
return "two";
|
400
378
|
}
|
401
379
|
if (n % 100 >= 3 && n % 100 <= 10) {
|
402
|
-
return
|
380
|
+
return "few";
|
403
381
|
}
|
404
382
|
if (n % 100 >= 11 && n % 100 <= 99) {
|
405
|
-
return
|
383
|
+
return "many";
|
406
384
|
}
|
407
|
-
return
|
385
|
+
return "other";
|
408
386
|
},
|
409
387
|
chinese() {
|
410
|
-
return
|
388
|
+
return "other";
|
411
389
|
},
|
412
390
|
french(n) {
|
413
|
-
return n === 0 || n === 1 ?
|
391
|
+
return n === 0 || n === 1 ? "one" : "other";
|
414
392
|
},
|
415
393
|
german(n) {
|
416
|
-
return n === 1 ?
|
394
|
+
return n === 1 ? "one" : "other";
|
417
395
|
},
|
418
396
|
irish(n) {
|
419
397
|
if (n === 1) {
|
420
|
-
return
|
398
|
+
return "one";
|
421
399
|
}
|
422
400
|
if (n === 2) {
|
423
|
-
return
|
401
|
+
return "two";
|
424
402
|
}
|
425
403
|
if (n >= 3 && n <= 6) {
|
426
|
-
return
|
404
|
+
return "few";
|
427
405
|
}
|
428
406
|
if (n >= 7 && n <= 10) {
|
429
|
-
return
|
407
|
+
return "many";
|
430
408
|
}
|
431
|
-
return
|
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
|
415
|
+
return "one";
|
438
416
|
}
|
439
417
|
if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) {
|
440
|
-
return
|
418
|
+
return "few";
|
441
419
|
}
|
442
420
|
if (last === 0 || last >= 5 && last <= 9 || lastTwo >= 11 && lastTwo <= 14) {
|
443
|
-
return
|
421
|
+
return "many";
|
444
422
|
}
|
445
|
-
return
|
423
|
+
return "other";
|
446
424
|
},
|
447
425
|
scottish(n) {
|
448
426
|
if (n === 1 || n === 11) {
|
449
|
-
return
|
427
|
+
return "one";
|
450
428
|
}
|
451
429
|
if (n === 2 || n === 12) {
|
452
|
-
return
|
430
|
+
return "two";
|
453
431
|
}
|
454
432
|
if (n >= 3 && n <= 10 || n >= 13 && n <= 19) {
|
455
|
-
return
|
433
|
+
return "few";
|
456
434
|
}
|
457
|
-
return
|
435
|
+
return "other";
|
458
436
|
},
|
459
437
|
spanish(n) {
|
460
438
|
if (n === 1) {
|
461
|
-
return
|
439
|
+
return "one";
|
462
440
|
}
|
463
|
-
if (n %
|
464
|
-
return
|
441
|
+
if (n % 1e6 === 0 && n !== 0) {
|
442
|
+
return "many";
|
465
443
|
}
|
466
|
-
return
|
444
|
+
return "other";
|
467
445
|
},
|
468
446
|
welsh(n) {
|
469
447
|
if (n === 0) {
|
470
|
-
return
|
448
|
+
return "zero";
|
471
449
|
}
|
472
450
|
if (n === 1) {
|
473
|
-
return
|
451
|
+
return "one";
|
474
452
|
}
|
475
453
|
if (n === 2) {
|
476
|
-
return
|
454
|
+
return "two";
|
477
455
|
}
|
478
456
|
if (n === 3) {
|
479
|
-
return
|
457
|
+
return "few";
|
480
458
|
}
|
481
459
|
if (n === 6) {
|
482
|
-
return
|
460
|
+
return "many";
|
483
461
|
}
|
484
|
-
return
|
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(
|
505
|
-
this.$root.addEventListener(
|
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(
|
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 *
|
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:
|
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 (
|
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(
|
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:
|
605
|
-
identifier:
|
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,
|
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(
|
584
|
+
this.$errorMessage = this.$root.querySelector(".govuk-error-message");
|
627
585
|
if (`${$textareaDescription.textContent}`.match(/^\s*$/)) {
|
628
|
-
$textareaDescription.textContent = this.i18n.t(
|
586
|
+
$textareaDescription.textContent = this.i18n.t("textareaDescription", {
|
629
587
|
count: this.maxLength
|
630
588
|
});
|
631
589
|
}
|
632
|
-
this.$textarea.insertAdjacentElement(
|
633
|
-
const $screenReaderCountMessage = document.createElement(
|
634
|
-
$screenReaderCountMessage.className =
|
635
|
-
$screenReaderCountMessage.setAttribute(
|
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(
|
638
|
-
const $visibleCountMessage = document.createElement(
|
595
|
+
$textareaDescription.insertAdjacentElement("afterend", $screenReaderCountMessage);
|
596
|
+
const $visibleCountMessage = document.createElement("div");
|
639
597
|
$visibleCountMessage.className = $textareaDescription.className;
|
640
|
-
$visibleCountMessage.classList.add(
|
641
|
-
$visibleCountMessage.setAttribute(
|
598
|
+
$visibleCountMessage.classList.add("govuk-character-count__status");
|
599
|
+
$visibleCountMessage.setAttribute("aria-hidden", "true");
|
642
600
|
this.$visibleCountMessage = $visibleCountMessage;
|
643
|
-
$textareaDescription.insertAdjacentElement(
|
644
|
-
$textareaDescription.classList.add(
|
645
|
-
this.$textarea.removeAttribute(
|
601
|
+
$textareaDescription.insertAdjacentElement("afterend", $visibleCountMessage);
|
602
|
+
$textareaDescription.classList.add("govuk-visually-hidden");
|
603
|
+
this.$textarea.removeAttribute("maxlength");
|
646
604
|
this.bindChangeEvents();
|
647
|
-
window.addEventListener(
|
605
|
+
window.addEventListener("pageshow", () => this.updateCountMessage());
|
648
606
|
this.updateCountMessage();
|
649
607
|
}
|
650
608
|
bindChangeEvents() {
|
651
|
-
this.$textarea.addEventListener(
|
652
|
-
this.$textarea.addEventListener(
|
653
|
-
this.$textarea.addEventListener(
|
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
|
-
},
|
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(
|
642
|
+
this.$visibleCountMessage.classList.toggle("govuk-character-count__message--disabled", !this.isOverThreshold());
|
685
643
|
if (!this.$errorMessage) {
|
686
|
-
this.$textarea.classList.toggle(
|
644
|
+
this.$textarea.classList.toggle("govuk-textarea--error", isError);
|
687
645
|
}
|
688
|
-
this.$visibleCountMessage.classList.toggle(
|
689
|
-
this.$visibleCountMessage.classList.toggle(
|
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(
|
652
|
+
this.$screenReaderCountMessage.removeAttribute("aria-hidden");
|
695
653
|
} else {
|
696
|
-
this.$screenReaderCountMessage.setAttribute(
|
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 ?
|
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 ?
|
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:
|
805
|
-
other:
|
697
|
+
one: "You have %{count} character remaining",
|
698
|
+
other: "You have %{count} characters remaining"
|
806
699
|
},
|
807
|
-
charactersAtLimit:
|
700
|
+
charactersAtLimit: "You have 0 characters remaining",
|
808
701
|
charactersOverLimit: {
|
809
|
-
one:
|
810
|
-
other:
|
702
|
+
one: "You have %{count} character too many",
|
703
|
+
other: "You have %{count} characters too many"
|
811
704
|
},
|
812
705
|
wordsUnderLimit: {
|
813
|
-
one:
|
814
|
-
other:
|
706
|
+
one: "You have %{count} word remaining",
|
707
|
+
other: "You have %{count} words remaining"
|
815
708
|
},
|
816
|
-
wordsAtLimit:
|
709
|
+
wordsAtLimit: "You have 0 words remaining",
|
817
710
|
wordsOverLimit: {
|
818
|
-
one:
|
819
|
-
other:
|
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:
|
723
|
+
type: "object"
|
830
724
|
},
|
831
725
|
maxwords: {
|
832
|
-
type:
|
726
|
+
type: "number"
|
833
727
|
},
|
834
728
|
maxlength: {
|
835
|
-
type:
|
729
|
+
type: "number"
|
836
730
|
},
|
837
731
|
threshold: {
|
838
|
-
type:
|
732
|
+
type: "number"
|
839
733
|
}
|
840
734
|
},
|
841
|
-
anyOf: [{
|
842
|
-
required: [
|
735
|
+
anyOf: [ {
|
736
|
+
required: [ "maxwords" ],
|
843
737
|
errorMessage: 'Either "maxlength" or "maxwords" must be provided'
|
844
738
|
}, {
|
845
|
-
required: [
|
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(
|
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(
|
893
|
-
$input.removeAttribute(
|
771
|
+
$input.setAttribute("aria-controls", targetId);
|
772
|
+
$input.removeAttribute("data-aria-controls");
|
894
773
|
});
|
895
|
-
window.addEventListener(
|
774
|
+
window.addEventListener("pageshow", () => this.syncAllConditionalReveals());
|
896
775
|
this.syncAllConditionalReveals();
|
897
|
-
this.$root.addEventListener(
|
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(
|
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(
|
787
|
+
if ($target != null && $target.classList.contains("govuk-checkboxes__conditional")) {
|
909
788
|
const inputIsChecked = $input.checked;
|
910
|
-
$input.setAttribute(
|
911
|
-
$target.classList.toggle(
|
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 !==
|
815
|
+
if (!($clickedInput instanceof HTMLInputElement) || $clickedInput.type !== "checkbox") {
|
937
816
|
return;
|
938
817
|
}
|
939
|
-
const hasAriaControls = $clickedInput.getAttribute(
|
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(
|
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
|
-
|
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(
|
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(
|
882
|
+
const $fieldset = $input.closest("fieldset");
|
1008
883
|
if ($fieldset) {
|
1009
|
-
const $legends = $fieldset.getElementsByTagName(
|
884
|
+
const $legends = $fieldset.getElementsByTagName("legend");
|
1010
885
|
if ($legends.length) {
|
1011
886
|
const $candidateLegend = $legends[0];
|
1012
|
-
if ($input instanceof HTMLInputElement && ($input.type ===
|
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,91 +897,290 @@ class ErrorSummary extends ConfigurableComponent {
|
|
1022
897
|
}
|
1023
898
|
}
|
1024
899
|
}
|
1025
|
-
return (_document$querySelect = document.querySelector(`label[for='${$input.getAttribute(
|
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:
|
913
|
+
type: "boolean"
|
1048
914
|
}
|
1049
915
|
}
|
1050
916
|
});
|
1051
917
|
|
918
|
+
/**
|
919
|
+
* File upload component
|
920
|
+
*
|
921
|
+
* @preserve
|
922
|
+
* @augments ConfigurableComponent<FileUploadConfig>
|
923
|
+
*/ class FileUpload extends ConfigurableComponent {
|
924
|
+
constructor($root, config = {}) {
|
925
|
+
super($root, config);
|
926
|
+
this.$input = void 0;
|
927
|
+
this.$button = void 0;
|
928
|
+
this.$status = void 0;
|
929
|
+
this.i18n = void 0;
|
930
|
+
this.id = void 0;
|
931
|
+
this.$announcements = void 0;
|
932
|
+
this.enteredAnotherElement = void 0;
|
933
|
+
const $input = this.$root.querySelector("input");
|
934
|
+
if ($input === null) {
|
935
|
+
throw new ElementError({
|
936
|
+
component: FileUpload,
|
937
|
+
identifier: 'File inputs (`<input type="file">`)'
|
938
|
+
});
|
939
|
+
}
|
940
|
+
if ($input.type !== "file") {
|
941
|
+
throw new ElementError(formatErrorMessage(FileUpload, 'File input (`<input type="file">`) attribute (`type`) is not `file`'));
|
942
|
+
}
|
943
|
+
this.$input = $input;
|
944
|
+
this.$input.setAttribute("hidden", "true");
|
945
|
+
if (!this.$input.id) {
|
946
|
+
throw new ElementError({
|
947
|
+
component: FileUpload,
|
948
|
+
identifier: 'File input (`<input type="file">`) attribute (`id`)'
|
949
|
+
});
|
950
|
+
}
|
951
|
+
this.id = this.$input.id;
|
952
|
+
this.i18n = new I18n(this.config.i18n, {
|
953
|
+
locale: closestAttributeValue(this.$root, "lang")
|
954
|
+
});
|
955
|
+
const $label = this.findLabel();
|
956
|
+
if (!$label.id) {
|
957
|
+
$label.id = `${this.id}-label`;
|
958
|
+
}
|
959
|
+
this.$input.id = `${this.id}-input`;
|
960
|
+
const $button = document.createElement("button");
|
961
|
+
$button.classList.add("govuk-file-upload-button");
|
962
|
+
$button.type = "button";
|
963
|
+
$button.id = this.id;
|
964
|
+
$button.classList.add("govuk-file-upload-button--empty");
|
965
|
+
const ariaDescribedBy = this.$input.getAttribute("aria-describedby");
|
966
|
+
if (ariaDescribedBy) {
|
967
|
+
$button.setAttribute("aria-describedby", ariaDescribedBy);
|
968
|
+
}
|
969
|
+
const $status = document.createElement("span");
|
970
|
+
$status.className = "govuk-body govuk-file-upload-button__status";
|
971
|
+
$status.setAttribute("aria-live", "polite");
|
972
|
+
$status.innerText = this.i18n.t("noFileChosen");
|
973
|
+
$button.appendChild($status);
|
974
|
+
const commaSpan = document.createElement("span");
|
975
|
+
commaSpan.className = "govuk-visually-hidden";
|
976
|
+
commaSpan.innerText = ", ";
|
977
|
+
commaSpan.id = `${this.id}-comma`;
|
978
|
+
$button.appendChild(commaSpan);
|
979
|
+
const containerSpan = document.createElement("span");
|
980
|
+
containerSpan.className = "govuk-file-upload-button__pseudo-button-container";
|
981
|
+
const buttonSpan = document.createElement("span");
|
982
|
+
buttonSpan.className = "govuk-button govuk-button--secondary govuk-file-upload-button__pseudo-button";
|
983
|
+
buttonSpan.innerText = this.i18n.t("chooseFilesButton");
|
984
|
+
containerSpan.appendChild(buttonSpan);
|
985
|
+
containerSpan.insertAdjacentText("beforeend", " ");
|
986
|
+
const instructionSpan = document.createElement("span");
|
987
|
+
instructionSpan.className = "govuk-body govuk-file-upload-button__instruction";
|
988
|
+
instructionSpan.innerText = this.i18n.t("dropInstruction");
|
989
|
+
containerSpan.appendChild(instructionSpan);
|
990
|
+
$button.appendChild(containerSpan);
|
991
|
+
$button.setAttribute("aria-labelledby", `${$label.id} ${commaSpan.id} ${$button.id}`);
|
992
|
+
$button.addEventListener("click", this.onClick.bind(this));
|
993
|
+
$button.addEventListener("dragover", event => {
|
994
|
+
event.preventDefault();
|
995
|
+
});
|
996
|
+
this.$root.insertAdjacentElement("afterbegin", $button);
|
997
|
+
this.$input.setAttribute("tabindex", "-1");
|
998
|
+
this.$input.setAttribute("aria-hidden", "true");
|
999
|
+
this.$button = $button;
|
1000
|
+
this.$status = $status;
|
1001
|
+
this.$input.addEventListener("change", this.onChange.bind(this));
|
1002
|
+
this.updateDisabledState();
|
1003
|
+
this.observeDisabledState();
|
1004
|
+
this.$announcements = document.createElement("span");
|
1005
|
+
this.$announcements.classList.add("govuk-file-upload-announcements");
|
1006
|
+
this.$announcements.classList.add("govuk-visually-hidden");
|
1007
|
+
this.$announcements.setAttribute("aria-live", "assertive");
|
1008
|
+
this.$root.insertAdjacentElement("afterend", this.$announcements);
|
1009
|
+
this.$button.addEventListener("drop", this.onDrop.bind(this));
|
1010
|
+
document.addEventListener("dragenter", this.updateDropzoneVisibility.bind(this));
|
1011
|
+
document.addEventListener("dragenter", () => {
|
1012
|
+
this.enteredAnotherElement = true;
|
1013
|
+
});
|
1014
|
+
document.addEventListener("dragleave", () => {
|
1015
|
+
if (!this.enteredAnotherElement && !this.$button.disabled) {
|
1016
|
+
this.hideDraggingState();
|
1017
|
+
this.$announcements.innerText = this.i18n.t("leftDropZone");
|
1018
|
+
}
|
1019
|
+
this.enteredAnotherElement = false;
|
1020
|
+
});
|
1021
|
+
}
|
1022
|
+
updateDropzoneVisibility(event) {
|
1023
|
+
if (this.$button.disabled) return;
|
1024
|
+
if (event.target instanceof Node) {
|
1025
|
+
if (this.$root.contains(event.target)) {
|
1026
|
+
if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {
|
1027
|
+
if (!this.$button.classList.contains("govuk-file-upload-button--dragging")) {
|
1028
|
+
this.showDraggingState();
|
1029
|
+
this.$announcements.innerText = this.i18n.t("enteredDropZone");
|
1030
|
+
}
|
1031
|
+
}
|
1032
|
+
} else {
|
1033
|
+
if (this.$button.classList.contains("govuk-file-upload-button--dragging")) {
|
1034
|
+
this.hideDraggingState();
|
1035
|
+
this.$announcements.innerText = this.i18n.t("leftDropZone");
|
1036
|
+
}
|
1037
|
+
}
|
1038
|
+
}
|
1039
|
+
}
|
1040
|
+
showDraggingState() {
|
1041
|
+
this.$button.classList.add("govuk-file-upload-button--dragging");
|
1042
|
+
}
|
1043
|
+
hideDraggingState() {
|
1044
|
+
this.$button.classList.remove("govuk-file-upload-button--dragging");
|
1045
|
+
}
|
1046
|
+
onDrop(event) {
|
1047
|
+
event.preventDefault();
|
1048
|
+
if (event.dataTransfer && isContainingFiles(event.dataTransfer)) {
|
1049
|
+
this.$input.files = event.dataTransfer.files;
|
1050
|
+
this.$input.dispatchEvent(new CustomEvent("change"));
|
1051
|
+
this.hideDraggingState();
|
1052
|
+
}
|
1053
|
+
}
|
1054
|
+
onChange() {
|
1055
|
+
const fileCount = this.$input.files.length;
|
1056
|
+
if (fileCount === 0) {
|
1057
|
+
this.$status.innerText = this.i18n.t("noFileChosen");
|
1058
|
+
this.$button.classList.add("govuk-file-upload-button--empty");
|
1059
|
+
} else {
|
1060
|
+
if (fileCount === 1) {
|
1061
|
+
this.$status.innerText = this.$input.files[0].name;
|
1062
|
+
} else {
|
1063
|
+
this.$status.innerText = this.i18n.t("multipleFilesChosen", {
|
1064
|
+
count: fileCount
|
1065
|
+
});
|
1066
|
+
}
|
1067
|
+
this.$button.classList.remove("govuk-file-upload-button--empty");
|
1068
|
+
}
|
1069
|
+
}
|
1070
|
+
findLabel() {
|
1071
|
+
const $label = document.querySelector(`label[for="${this.$input.id}"]`);
|
1072
|
+
if (!$label) {
|
1073
|
+
throw new ElementError({
|
1074
|
+
component: FileUpload,
|
1075
|
+
identifier: `Field label (\`<label for=${this.$input.id}>\`)`
|
1076
|
+
});
|
1077
|
+
}
|
1078
|
+
return $label;
|
1079
|
+
}
|
1080
|
+
onClick() {
|
1081
|
+
this.$input.click();
|
1082
|
+
}
|
1083
|
+
observeDisabledState() {
|
1084
|
+
const observer = new MutationObserver(mutationList => {
|
1085
|
+
for (const mutation of mutationList) {
|
1086
|
+
if (mutation.type === "attributes" && mutation.attributeName === "disabled") {
|
1087
|
+
this.updateDisabledState();
|
1088
|
+
}
|
1089
|
+
}
|
1090
|
+
});
|
1091
|
+
observer.observe(this.$input, {
|
1092
|
+
attributes: true
|
1093
|
+
});
|
1094
|
+
}
|
1095
|
+
updateDisabledState() {
|
1096
|
+
this.$button.disabled = this.$input.disabled;
|
1097
|
+
this.$root.classList.toggle("govuk-drop-zone--disabled", this.$button.disabled);
|
1098
|
+
}
|
1099
|
+
}
|
1100
|
+
|
1101
|
+
FileUpload.moduleName = "govuk-file-upload";
|
1102
|
+
|
1103
|
+
FileUpload.defaults = Object.freeze({
|
1104
|
+
i18n: {
|
1105
|
+
chooseFilesButton: "Choose file",
|
1106
|
+
dropInstruction: "or drop file",
|
1107
|
+
noFileChosen: "No file chosen",
|
1108
|
+
multipleFilesChosen: {
|
1109
|
+
one: "%{count} file chosen",
|
1110
|
+
other: "%{count} files chosen"
|
1111
|
+
},
|
1112
|
+
enteredDropZone: "Entered drop zone",
|
1113
|
+
leftDropZone: "Left drop zone"
|
1114
|
+
}
|
1115
|
+
});
|
1116
|
+
|
1117
|
+
FileUpload.schema = Object.freeze({
|
1118
|
+
properties: {
|
1119
|
+
i18n: {
|
1120
|
+
type: "object"
|
1121
|
+
}
|
1122
|
+
}
|
1123
|
+
});
|
1124
|
+
|
1125
|
+
function isContainingFiles(dataTransfer) {
|
1126
|
+
const hasNoTypesInfo = dataTransfer.types.length === 0;
|
1127
|
+
const isDraggingFiles = dataTransfer.types.some(type => type === "Files");
|
1128
|
+
return hasNoTypesInfo || isDraggingFiles;
|
1129
|
+
}
|
1130
|
+
|
1052
1131
|
/**
|
1053
1132
|
* Password input component
|
1054
1133
|
*
|
1055
1134
|
* @preserve
|
1056
1135
|
* @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
|
-
*/
|
1136
|
+
*/ class PasswordInput extends ConfigurableComponent {
|
1063
1137
|
constructor($root, config = {}) {
|
1064
1138
|
super($root, config);
|
1065
1139
|
this.i18n = void 0;
|
1066
1140
|
this.$input = void 0;
|
1067
1141
|
this.$showHideButton = void 0;
|
1068
1142
|
this.$screenReaderStatusMessage = void 0;
|
1069
|
-
const $input = this.$root.querySelector(
|
1143
|
+
const $input = this.$root.querySelector(".govuk-js-password-input-input");
|
1070
1144
|
if (!($input instanceof HTMLInputElement)) {
|
1071
1145
|
throw new ElementError({
|
1072
1146
|
component: PasswordInput,
|
1073
1147
|
element: $input,
|
1074
|
-
expectedType:
|
1075
|
-
identifier:
|
1148
|
+
expectedType: "HTMLInputElement",
|
1149
|
+
identifier: "Form field (`.govuk-js-password-input-input`)"
|
1076
1150
|
});
|
1077
1151
|
}
|
1078
|
-
if ($input.type !==
|
1079
|
-
throw new ElementError(
|
1152
|
+
if ($input.type !== "password") {
|
1153
|
+
throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
|
1080
1154
|
}
|
1081
|
-
const $showHideButton = this.$root.querySelector(
|
1155
|
+
const $showHideButton = this.$root.querySelector(".govuk-js-password-input-toggle");
|
1082
1156
|
if (!($showHideButton instanceof HTMLButtonElement)) {
|
1083
1157
|
throw new ElementError({
|
1084
1158
|
component: PasswordInput,
|
1085
1159
|
element: $showHideButton,
|
1086
|
-
expectedType:
|
1087
|
-
identifier:
|
1160
|
+
expectedType: "HTMLButtonElement",
|
1161
|
+
identifier: "Button (`.govuk-js-password-input-toggle`)"
|
1088
1162
|
});
|
1089
1163
|
}
|
1090
|
-
if ($showHideButton.type !==
|
1091
|
-
throw new ElementError(
|
1164
|
+
if ($showHideButton.type !== "button") {
|
1165
|
+
throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
|
1092
1166
|
}
|
1093
1167
|
this.$input = $input;
|
1094
1168
|
this.$showHideButton = $showHideButton;
|
1095
1169
|
this.i18n = new I18n(this.config.i18n, {
|
1096
|
-
locale: closestAttributeValue(this.$root,
|
1170
|
+
locale: closestAttributeValue(this.$root, "lang")
|
1097
1171
|
});
|
1098
|
-
this.$showHideButton.removeAttribute(
|
1099
|
-
const $screenReaderStatusMessage = document.createElement(
|
1100
|
-
$screenReaderStatusMessage.className =
|
1101
|
-
$screenReaderStatusMessage.setAttribute(
|
1172
|
+
this.$showHideButton.removeAttribute("hidden");
|
1173
|
+
const $screenReaderStatusMessage = document.createElement("div");
|
1174
|
+
$screenReaderStatusMessage.className = "govuk-password-input__sr-status govuk-visually-hidden";
|
1175
|
+
$screenReaderStatusMessage.setAttribute("aria-live", "polite");
|
1102
1176
|
this.$screenReaderStatusMessage = $screenReaderStatusMessage;
|
1103
|
-
this.$input.insertAdjacentElement(
|
1104
|
-
this.$showHideButton.addEventListener(
|
1177
|
+
this.$input.insertAdjacentElement("afterend", $screenReaderStatusMessage);
|
1178
|
+
this.$showHideButton.addEventListener("click", this.toggle.bind(this));
|
1105
1179
|
if (this.$input.form) {
|
1106
|
-
this.$input.form.addEventListener(
|
1180
|
+
this.$input.form.addEventListener("submit", () => this.hide());
|
1107
1181
|
}
|
1108
|
-
window.addEventListener(
|
1109
|
-
if (event.persisted && this.$input.type !==
|
1182
|
+
window.addEventListener("pageshow", event => {
|
1183
|
+
if (event.persisted && this.$input.type !== "password") {
|
1110
1184
|
this.hide();
|
1111
1185
|
}
|
1112
1186
|
});
|
@@ -1114,80 +1188,49 @@ class PasswordInput extends ConfigurableComponent {
|
|
1114
1188
|
}
|
1115
1189
|
toggle(event) {
|
1116
1190
|
event.preventDefault();
|
1117
|
-
if (this.$input.type ===
|
1191
|
+
if (this.$input.type === "password") {
|
1118
1192
|
this.show();
|
1119
1193
|
return;
|
1120
1194
|
}
|
1121
1195
|
this.hide();
|
1122
1196
|
}
|
1123
1197
|
show() {
|
1124
|
-
this.setType(
|
1198
|
+
this.setType("text");
|
1125
1199
|
}
|
1126
1200
|
hide() {
|
1127
|
-
this.setType(
|
1201
|
+
this.setType("password");
|
1128
1202
|
}
|
1129
1203
|
setType(type) {
|
1130
1204
|
if (type === this.$input.type) {
|
1131
1205
|
return;
|
1132
1206
|
}
|
1133
|
-
this.$input.setAttribute(
|
1134
|
-
const isHidden = type ===
|
1135
|
-
const prefixButton = isHidden ?
|
1136
|
-
const prefixStatus = isHidden ?
|
1207
|
+
this.$input.setAttribute("type", type);
|
1208
|
+
const isHidden = type === "password";
|
1209
|
+
const prefixButton = isHidden ? "show" : "hide";
|
1210
|
+
const prefixStatus = isHidden ? "passwordHidden" : "passwordShown";
|
1137
1211
|
this.$showHideButton.innerText = this.i18n.t(`${prefixButton}Password`);
|
1138
|
-
this.$showHideButton.setAttribute(
|
1212
|
+
this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${prefixButton}PasswordAriaLabel`));
|
1139
1213
|
this.$screenReaderStatusMessage.innerText = this.i18n.t(`${prefixStatus}Announcement`);
|
1140
1214
|
}
|
1141
1215
|
}
|
1142
1216
|
|
1143
|
-
|
1144
|
-
* Password input config
|
1145
|
-
*
|
1146
|
-
* @typedef {object} PasswordInputConfig
|
1147
|
-
* @property {PasswordInputTranslations} [i18n=PasswordInput.defaults.i18n] - Password input translations
|
1148
|
-
*/
|
1217
|
+
PasswordInput.moduleName = "govuk-password-input";
|
1149
1218
|
|
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
|
-
*/
|
1172
|
-
|
1173
|
-
/**
|
1174
|
-
* @import { Schema } from '../../common/configuration.mjs'
|
1175
|
-
*/
|
1176
|
-
PasswordInput.moduleName = 'govuk-password-input';
|
1177
1219
|
PasswordInput.defaults = Object.freeze({
|
1178
1220
|
i18n: {
|
1179
|
-
showPassword:
|
1180
|
-
hidePassword:
|
1181
|
-
showPasswordAriaLabel:
|
1182
|
-
hidePasswordAriaLabel:
|
1183
|
-
passwordShownAnnouncement:
|
1184
|
-
passwordHiddenAnnouncement:
|
1221
|
+
showPassword: "Show",
|
1222
|
+
hidePassword: "Hide",
|
1223
|
+
showPasswordAriaLabel: "Show password",
|
1224
|
+
hidePasswordAriaLabel: "Hide password",
|
1225
|
+
passwordShownAnnouncement: "Your password is visible",
|
1226
|
+
passwordHiddenAnnouncement: "Your password is hidden"
|
1185
1227
|
}
|
1186
1228
|
});
|
1229
|
+
|
1187
1230
|
PasswordInput.schema = Object.freeze({
|
1188
1231
|
properties: {
|
1189
1232
|
i18n: {
|
1190
|
-
type:
|
1233
|
+
type: "object"
|
1191
1234
|
}
|
1192
1235
|
}
|
1193
1236
|
});
|
@@ -1196,22 +1239,7 @@ PasswordInput.schema = Object.freeze({
|
|
1196
1239
|
* Radios component
|
1197
1240
|
*
|
1198
1241
|
* @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
|
-
*/
|
1242
|
+
*/ class Radios extends Component {
|
1215
1243
|
constructor($root) {
|
1216
1244
|
super($root);
|
1217
1245
|
this.$inputs = void 0;
|
@@ -1224,7 +1252,7 @@ class Radios extends Component {
|
|
1224
1252
|
}
|
1225
1253
|
this.$inputs = $inputs;
|
1226
1254
|
this.$inputs.forEach($input => {
|
1227
|
-
const targetId = $input.getAttribute(
|
1255
|
+
const targetId = $input.getAttribute("data-aria-controls");
|
1228
1256
|
if (!targetId) {
|
1229
1257
|
return;
|
1230
1258
|
}
|
@@ -1234,31 +1262,31 @@ class Radios extends Component {
|
|
1234
1262
|
identifier: `Conditional reveal (\`id="${targetId}"\`)`
|
1235
1263
|
});
|
1236
1264
|
}
|
1237
|
-
$input.setAttribute(
|
1238
|
-
$input.removeAttribute(
|
1265
|
+
$input.setAttribute("aria-controls", targetId);
|
1266
|
+
$input.removeAttribute("data-aria-controls");
|
1239
1267
|
});
|
1240
|
-
window.addEventListener(
|
1268
|
+
window.addEventListener("pageshow", () => this.syncAllConditionalReveals());
|
1241
1269
|
this.syncAllConditionalReveals();
|
1242
|
-
this.$root.addEventListener(
|
1270
|
+
this.$root.addEventListener("click", event => this.handleClick(event));
|
1243
1271
|
}
|
1244
1272
|
syncAllConditionalReveals() {
|
1245
1273
|
this.$inputs.forEach($input => this.syncConditionalRevealWithInputState($input));
|
1246
1274
|
}
|
1247
1275
|
syncConditionalRevealWithInputState($input) {
|
1248
|
-
const targetId = $input.getAttribute(
|
1276
|
+
const targetId = $input.getAttribute("aria-controls");
|
1249
1277
|
if (!targetId) {
|
1250
1278
|
return;
|
1251
1279
|
}
|
1252
1280
|
const $target = document.getElementById(targetId);
|
1253
|
-
if ($target != null && $target.classList.contains(
|
1281
|
+
if ($target != null && $target.classList.contains("govuk-radios__conditional")) {
|
1254
1282
|
const inputIsChecked = $input.checked;
|
1255
|
-
$input.setAttribute(
|
1256
|
-
$target.classList.toggle(
|
1283
|
+
$input.setAttribute("aria-expanded", inputIsChecked.toString());
|
1284
|
+
$target.classList.toggle("govuk-radios__conditional--hidden", !inputIsChecked);
|
1257
1285
|
}
|
1258
1286
|
}
|
1259
1287
|
handleClick(event) {
|
1260
1288
|
const $clickedInput = event.target;
|
1261
|
-
if (!($clickedInput instanceof HTMLInputElement) || $clickedInput.type !==
|
1289
|
+
if (!($clickedInput instanceof HTMLInputElement) || $clickedInput.type !== "radio") {
|
1262
1290
|
return;
|
1263
1291
|
}
|
1264
1292
|
const $allInputs = document.querySelectorAll('input[type="radio"][aria-controls]');
|
@@ -1273,22 +1301,184 @@ class Radios extends Component {
|
|
1273
1301
|
});
|
1274
1302
|
}
|
1275
1303
|
}
|
1276
|
-
|
1304
|
+
|
1305
|
+
Radios.moduleName = "govuk-radios";
|
1306
|
+
|
1307
|
+
class FileFieldController extends Controller {
|
1308
|
+
static targets=[ "preview", "destroy" ];
|
1309
|
+
static values={
|
1310
|
+
mimeTypes: Array
|
1311
|
+
};
|
1312
|
+
connect() {
|
1313
|
+
this.counter = 0;
|
1314
|
+
this.initialPreviewContent = null;
|
1315
|
+
this.onUploadFlag = false;
|
1316
|
+
}
|
1317
|
+
onUpload(event) {
|
1318
|
+
this.onUploadFlag = true;
|
1319
|
+
if (this.hasDestroyTarget) {
|
1320
|
+
this.destroyTarget.value = false;
|
1321
|
+
}
|
1322
|
+
this.previewTarget.removeAttribute("hidden");
|
1323
|
+
if (this.hasPreviewTarget) {
|
1324
|
+
if (event.currentTarget.files.length > 0) {
|
1325
|
+
this.showPreview(event.currentTarget.files[0]);
|
1326
|
+
} else {
|
1327
|
+
this.setPreviewContent(this.initialPreviewContent);
|
1328
|
+
}
|
1329
|
+
}
|
1330
|
+
}
|
1331
|
+
setDestroy(event) {
|
1332
|
+
event.preventDefault();
|
1333
|
+
if (this.initialPreviewContent && this.onUploadFlag) {
|
1334
|
+
this.onUploadFlag = false;
|
1335
|
+
this.setPreviewContent(this.initialPreviewContent);
|
1336
|
+
} else {
|
1337
|
+
if (this.hasDestroyTarget) {
|
1338
|
+
this.destroyTarget.value = true;
|
1339
|
+
}
|
1340
|
+
if (this.hasPreviewTarget) {
|
1341
|
+
this.previewTarget.setAttribute("hidden", "");
|
1342
|
+
this.setPreviewContent("");
|
1343
|
+
}
|
1344
|
+
if (this.previousInput) {
|
1345
|
+
this.previousInput.toggleAttribute("disabled", true);
|
1346
|
+
}
|
1347
|
+
}
|
1348
|
+
this.fileInput.value = "";
|
1349
|
+
}
|
1350
|
+
setPreviewContent(content) {
|
1351
|
+
if (this.filenameTag) {
|
1352
|
+
this.filenameTag.innerText = text;
|
1353
|
+
}
|
1354
|
+
}
|
1355
|
+
drop(event) {
|
1356
|
+
event.preventDefault();
|
1357
|
+
const file = this.fileForEvent(event, this.mimeTypesValue);
|
1358
|
+
if (file) {
|
1359
|
+
const dT = new DataTransfer;
|
1360
|
+
dT.items.add(file);
|
1361
|
+
this.fileInput.files = dT.files;
|
1362
|
+
this.fileInput.dispatchEvent(new Event("change"));
|
1363
|
+
}
|
1364
|
+
this.counter = 0;
|
1365
|
+
this.element.classList.remove("droppable");
|
1366
|
+
}
|
1367
|
+
dragover(event) {
|
1368
|
+
event.preventDefault();
|
1369
|
+
}
|
1370
|
+
dragenter(event) {
|
1371
|
+
event.preventDefault();
|
1372
|
+
if (this.counter === 0) {
|
1373
|
+
this.element.classList.add("droppable");
|
1374
|
+
}
|
1375
|
+
this.counter++;
|
1376
|
+
}
|
1377
|
+
dragleave(event) {
|
1378
|
+
event.preventDefault();
|
1379
|
+
this.counter--;
|
1380
|
+
if (this.counter === 0) {
|
1381
|
+
this.element.classList.remove("droppable");
|
1382
|
+
}
|
1383
|
+
}
|
1384
|
+
get fileInput() {
|
1385
|
+
return this.element.querySelector("input[type='file']");
|
1386
|
+
}
|
1387
|
+
get previousInput() {
|
1388
|
+
return this.element.querySelector(`input[type='hidden'][name='${this.fileInput.name}']`);
|
1389
|
+
}
|
1390
|
+
get filenameTag() {
|
1391
|
+
if (!this.hasPreviewTarget) return null;
|
1392
|
+
return this.previewTarget.querySelector("p.preview-filename");
|
1393
|
+
}
|
1394
|
+
showPreview(file) {
|
1395
|
+
const reader = new FileReader;
|
1396
|
+
reader.onload = e => {
|
1397
|
+
if (this.filenameTag) {
|
1398
|
+
this.filenameTag.innerText = file.name;
|
1399
|
+
}
|
1400
|
+
};
|
1401
|
+
reader.readAsDataURL(file);
|
1402
|
+
}
|
1403
|
+
fileForEvent(event, mimeTypes) {
|
1404
|
+
const accept = file => mimeTypes.indexOf(file.type) > -1;
|
1405
|
+
let file;
|
1406
|
+
if (event.dataTransfer.items) {
|
1407
|
+
const item = [ ...event.dataTransfer.items ].find(accept);
|
1408
|
+
if (item) {
|
1409
|
+
file = item.getAsFile();
|
1410
|
+
}
|
1411
|
+
} else {
|
1412
|
+
file = [ ...event.dataTransfer.files ].find(accept);
|
1413
|
+
}
|
1414
|
+
return file;
|
1415
|
+
}
|
1416
|
+
}
|
1417
|
+
|
1418
|
+
class DocumentFieldController extends FileFieldController {
|
1419
|
+
connect() {
|
1420
|
+
super.connect();
|
1421
|
+
this.initialPreviewContent = this.filenameTag.text;
|
1422
|
+
}
|
1423
|
+
setPreviewContent(content) {
|
1424
|
+
this.filenameTag.innerText = content;
|
1425
|
+
}
|
1426
|
+
showPreview(file) {
|
1427
|
+
const reader = new FileReader;
|
1428
|
+
reader.onload = e => {
|
1429
|
+
if (this.filenameTag) {
|
1430
|
+
this.filenameTag.innerText = file.name;
|
1431
|
+
}
|
1432
|
+
};
|
1433
|
+
reader.readAsDataURL(file);
|
1434
|
+
}
|
1435
|
+
get filenameTag() {
|
1436
|
+
return this.previewTarget.querySelector("p.preview-filename");
|
1437
|
+
}
|
1438
|
+
}
|
1439
|
+
|
1440
|
+
class ImageFieldController extends FileFieldController {
|
1441
|
+
connect() {
|
1442
|
+
super.connect();
|
1443
|
+
this.initialPreviewContent = this.imageTag.getAttribute("src");
|
1444
|
+
}
|
1445
|
+
setPreviewContent(content) {
|
1446
|
+
this.imageTag.src = content;
|
1447
|
+
}
|
1448
|
+
showPreview(file) {
|
1449
|
+
const reader = new FileReader;
|
1450
|
+
reader.onload = e => {
|
1451
|
+
this.imageTag.src = e.target.result;
|
1452
|
+
};
|
1453
|
+
reader.readAsDataURL(file);
|
1454
|
+
}
|
1455
|
+
get imageTag() {
|
1456
|
+
return this.previewTarget.querySelector("img");
|
1457
|
+
}
|
1458
|
+
}
|
1459
|
+
|
1460
|
+
const Definitions = [ {
|
1461
|
+
identifier: "govuk-document-field",
|
1462
|
+
controllerConstructor: DocumentFieldController
|
1463
|
+
}, {
|
1464
|
+
identifier: "govuk-image-field",
|
1465
|
+
controllerConstructor: ImageFieldController
|
1466
|
+
} ];
|
1277
1467
|
|
1278
1468
|
function initAll(config) {
|
1279
1469
|
let _config$scope;
|
1280
|
-
config = typeof config !==
|
1470
|
+
config = typeof config !== "undefined" ? config : {};
|
1281
1471
|
if (!isSupported()) {
|
1282
|
-
console.log(new SupportError
|
1472
|
+
console.log(new SupportError);
|
1283
1473
|
return;
|
1284
1474
|
}
|
1285
|
-
const components = [[Button, config.button], [CharacterCount, config.characterCount], [Checkboxes], [ErrorSummary, config.errorSummary], [Radios], [PasswordInput, config.passwordInput]];
|
1475
|
+
const components = [ [ Button, config.button ], [ CharacterCount, config.characterCount ], [ Checkboxes ], [ ErrorSummary, config.errorSummary ], [ FileUpload, config.fileUpload ], [ Radios ], [ PasswordInput, config.passwordInput ] ];
|
1286
1476
|
const $scope = (_config$scope = config.scope) != null ? _config$scope : document;
|
1287
1477
|
components.forEach(([Component, config]) => {
|
1288
1478
|
const $elements = $scope.querySelectorAll(`[data-module="${Component.moduleName}"]`);
|
1289
1479
|
$elements.forEach($element => {
|
1290
1480
|
try {
|
1291
|
-
|
1481
|
+
"defaults" in Component ? new Component($element, config) : new Component($element);
|
1292
1482
|
} catch (error) {
|
1293
1483
|
console.log(error);
|
1294
1484
|
}
|
@@ -1296,4 +1486,4 @@ function initAll(config) {
|
|
1296
1486
|
});
|
1297
1487
|
}
|
1298
1488
|
|
1299
|
-
export { Button, CharacterCount, Checkboxes, ErrorSummary, PasswordInput, Radios, initAll };
|
1489
|
+
export { Button, CharacterCount, Checkboxes, ErrorSummary, PasswordInput, Radios, Definitions as default, initAll };
|