govuk_tech_docs 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/publish.yaml +1 -1
- data/CHANGELOG.md +26 -7
- data/README.md +2 -2
- data/example/source/code.html.md +3 -26
- data/lib/govuk_tech_docs/contribution_banner.rb +1 -1
- data/lib/govuk_tech_docs/tech_docs_html_renderer.rb +3 -3
- data/lib/govuk_tech_docs/version.rb +1 -1
- data/lib/source/layouts/core.erb +1 -1
- data/node_modules/govuk-frontend/govuk/all.js +1548 -311
- data/node_modules/govuk-frontend/govuk/common/closest-attribute-value.js +70 -0
- data/node_modules/govuk-frontend/govuk/common/index.js +172 -0
- data/node_modules/govuk-frontend/govuk/common/normalise-dataset.js +373 -0
- data/node_modules/govuk-frontend/govuk/common.js +138 -3
- data/node_modules/govuk-frontend/govuk/components/_all.scss +1 -0
- data/node_modules/govuk-frontend/govuk/components/accordion/_index.scss +5 -6
- data/node_modules/govuk-frontend/govuk/components/accordion/accordion.js +754 -36
- data/node_modules/govuk-frontend/govuk/components/breadcrumbs/_index.scss +0 -2
- data/node_modules/govuk-frontend/govuk/components/button/_index.scss +29 -21
- data/node_modules/govuk-frontend/govuk/components/button/button.js +365 -107
- data/node_modules/govuk-frontend/govuk/components/character-count/_index.scss +9 -0
- data/node_modules/govuk-frontend/govuk/components/character-count/character-count.js +1092 -109
- data/node_modules/govuk-frontend/govuk/components/checkboxes/_index.scss +3 -2
- data/node_modules/govuk-frontend/govuk/components/checkboxes/checkboxes.js +30 -2
- data/node_modules/govuk-frontend/govuk/components/details/details.js +51 -33
- data/node_modules/govuk-frontend/govuk/components/error-summary/error-summary.js +289 -6
- data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +13 -23
- data/node_modules/govuk-frontend/govuk/components/header/_index.scss +30 -24
- data/node_modules/govuk-frontend/govuk/components/header/header.js +59 -11
- data/node_modules/govuk-frontend/govuk/components/input/_index.scss +13 -23
- data/node_modules/govuk-frontend/govuk/components/notification-banner/notification-banner.js +252 -2
- data/node_modules/govuk-frontend/govuk/components/pagination/_index.scss +247 -0
- data/node_modules/govuk-frontend/govuk/components/pagination/_pagination.scss +2 -0
- data/node_modules/govuk-frontend/govuk/components/panel/_index.scss +1 -1
- data/node_modules/govuk-frontend/govuk/components/radios/_index.scss +5 -12
- data/node_modules/govuk-frontend/govuk/components/radios/radios.js +30 -2
- data/node_modules/govuk-frontend/govuk/components/select/_index.scss +11 -0
- data/node_modules/govuk-frontend/govuk/components/skip-link/_index.scss +1 -3
- data/node_modules/govuk-frontend/govuk/components/skip-link/skip-link.js +10 -4
- data/node_modules/govuk-frontend/govuk/components/summary-list/_index.scss +45 -13
- data/node_modules/govuk-frontend/govuk/components/table/_index.scss +1 -1
- data/node_modules/govuk-frontend/govuk/components/tabs/tabs.js +28 -0
- data/node_modules/govuk-frontend/govuk/core/_section-break.scss +1 -1
- data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +5 -5
- data/node_modules/govuk-frontend/govuk/helpers/_focused.scss +5 -0
- data/node_modules/govuk-frontend/govuk/helpers/_links.scss +13 -11
- data/node_modules/govuk-frontend/govuk/helpers/_media-queries.scss +2 -2
- data/node_modules/govuk-frontend/govuk/helpers/_shape-arrow.scss +1 -1
- data/node_modules/govuk-frontend/govuk/helpers/_spacing.scss +3 -3
- data/node_modules/govuk-frontend/govuk/helpers/_typography.scss +16 -9
- data/node_modules/govuk-frontend/govuk/i18n.js +390 -0
- data/node_modules/govuk-frontend/govuk/objects/_button-group.scss +10 -26
- data/node_modules/govuk-frontend/govuk/objects/_template.scss +1 -1
- data/node_modules/govuk-frontend/govuk/objects/_width-container.scss +0 -4
- data/node_modules/govuk-frontend/govuk/overrides/_spacing.scss +56 -12
- data/node_modules/govuk-frontend/govuk/settings/_all.scss +1 -0
- data/node_modules/govuk-frontend/govuk/settings/_colours-palette.scss +12 -0
- data/node_modules/govuk-frontend/govuk/settings/_compatibility.scss +26 -0
- data/node_modules/govuk-frontend/govuk/settings/_spacing.scss +4 -8
- data/node_modules/govuk-frontend/govuk/settings/_typography-font.scss +23 -0
- data/node_modules/govuk-frontend/govuk/settings/_typography-responsive.scss +12 -0
- data/node_modules/govuk-frontend/govuk/settings/_warnings.scss +53 -0
- data/node_modules/govuk-frontend/govuk/tools/_compatibility.scss +20 -6
- data/node_modules/govuk-frontend/govuk/tools/_exports.scss +1 -1
- data/node_modules/govuk-frontend/govuk/tools/_font-url.scss +1 -1
- data/node_modules/govuk-frontend/govuk/tools/_image-url.scss +1 -1
- data/node_modules/govuk-frontend/govuk/tools/_px-to-em.scss +2 -2
- data/node_modules/govuk-frontend/govuk/tools/_px-to-rem.scss +1 -1
- data/node_modules/govuk-frontend/govuk/vendor/polyfills/Date/now.js +21 -0
- data/node_modules/govuk-frontend/govuk/vendor/polyfills/Element/prototype/dataset.js +300 -0
- data/node_modules/govuk-frontend/govuk/vendor/polyfills/String/prototype/trim.js +21 -0
- data/node_modules/govuk-frontend/govuk-prototype-kit/init.js +7 -0
- data/node_modules/govuk-frontend/govuk-prototype-kit/init.scss +12 -0
- data/package-lock.json +12 -12
- data/package.json +1 -1
- metadata +17 -5
|
@@ -4,10 +4,24 @@
|
|
|
4
4
|
(factory((global.GOVUKFrontend = {})));
|
|
5
5
|
}(this, (function (exports) { 'use strict';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Common helpers which do not require polyfill.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: If a helper require a polyfill, please isolate it in its own module
|
|
11
|
+
* so that the polyfill can be properly tree-shaken and does not burden
|
|
12
|
+
* the components that do not need that helper
|
|
13
|
+
*
|
|
14
|
+
* @module common/index
|
|
15
|
+
*/
|
|
16
|
+
|
|
7
17
|
/**
|
|
8
18
|
* TODO: Ideally this would be a NodeList.prototype.forEach polyfill
|
|
9
19
|
* This seems to fail in IE8, requires more investigation.
|
|
10
20
|
* See: https://github.com/imagitama/nodelist-foreach-polyfill
|
|
21
|
+
*
|
|
22
|
+
* @param {NodeListOf<Element>} nodes - NodeList from querySelectorAll()
|
|
23
|
+
* @param {nodeListIterator} callback - Callback function to run for each node
|
|
24
|
+
* @returns {undefined}
|
|
11
25
|
*/
|
|
12
26
|
function nodeListForEach (nodes, callback) {
|
|
13
27
|
if (window.NodeList.prototype.forEach) {
|
|
@@ -18,9 +32,13 @@ function nodeListForEach (nodes, callback) {
|
|
|
18
32
|
}
|
|
19
33
|
}
|
|
20
34
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Used to generate a unique string, allows multiple instances of the component
|
|
37
|
+
* without them conflicting with each other.
|
|
38
|
+
* https://stackoverflow.com/a/8809472
|
|
39
|
+
*
|
|
40
|
+
* @returns {string} Unique ID
|
|
41
|
+
*/
|
|
24
42
|
function generateUniqueID () {
|
|
25
43
|
var d = new Date().getTime();
|
|
26
44
|
if (typeof window.performance !== 'undefined' && typeof window.performance.now === 'function') {
|
|
@@ -33,6 +51,500 @@ function generateUniqueID () {
|
|
|
33
51
|
})
|
|
34
52
|
}
|
|
35
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Config flattening function
|
|
56
|
+
*
|
|
57
|
+
* Takes any number of objects, flattens them into namespaced key-value pairs,
|
|
58
|
+
* (e.g. {'i18n.showSection': 'Show section'}) and combines them together, with
|
|
59
|
+
* greatest priority on the LAST item passed in.
|
|
60
|
+
*
|
|
61
|
+
* @returns {object} A flattened object of key-value pairs.
|
|
62
|
+
*/
|
|
63
|
+
function mergeConfigs (/* configObject1, configObject2, ...configObjects */) {
|
|
64
|
+
/**
|
|
65
|
+
* Function to take nested objects and flatten them to a dot-separated keyed
|
|
66
|
+
* object. Doing this means we don't need to do any deep/recursive merging of
|
|
67
|
+
* each of our objects, nor transform our dataset from a flat list into a
|
|
68
|
+
* nested object.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} configObject - Deeply nested object
|
|
71
|
+
* @returns {object} Flattened object with dot-separated keys
|
|
72
|
+
*/
|
|
73
|
+
var flattenObject = function (configObject) {
|
|
74
|
+
// Prepare an empty return object
|
|
75
|
+
var flattenedObject = {};
|
|
76
|
+
|
|
77
|
+
// Our flattening function, this is called recursively for each level of
|
|
78
|
+
// depth in the object. At each level we prepend the previous level names to
|
|
79
|
+
// the key using `prefix`.
|
|
80
|
+
var flattenLoop = function (obj, prefix) {
|
|
81
|
+
// Loop through keys...
|
|
82
|
+
for (var key in obj) {
|
|
83
|
+
// Check to see if this is a prototypical key/value,
|
|
84
|
+
// if it is, skip it.
|
|
85
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
var value = obj[key];
|
|
89
|
+
var prefixedKey = prefix ? prefix + '.' + key : key;
|
|
90
|
+
if (typeof value === 'object') {
|
|
91
|
+
// If the value is a nested object, recurse over that too
|
|
92
|
+
flattenLoop(value, prefixedKey);
|
|
93
|
+
} else {
|
|
94
|
+
// Otherwise, add this value to our return object
|
|
95
|
+
flattenedObject[prefixedKey] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Kick off the recursive loop
|
|
101
|
+
flattenLoop(configObject);
|
|
102
|
+
return flattenedObject
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Start with an empty object as our base
|
|
106
|
+
var formattedConfigObject = {};
|
|
107
|
+
|
|
108
|
+
// Loop through each of the remaining passed objects and push their keys
|
|
109
|
+
// one-by-one into configObject. Any duplicate keys will override the existing
|
|
110
|
+
// key with the new value.
|
|
111
|
+
for (var i = 0; i < arguments.length; i++) {
|
|
112
|
+
var obj = flattenObject(arguments[i]);
|
|
113
|
+
for (var key in obj) {
|
|
114
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
115
|
+
formattedConfigObject[key] = obj[key];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return formattedConfigObject
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extracts keys starting with a particular namespace from a flattened config
|
|
125
|
+
* object, removing the namespace in the process.
|
|
126
|
+
*
|
|
127
|
+
* @param {object} configObject - The object to extract key-value pairs from.
|
|
128
|
+
* @param {string} namespace - The namespace to filter keys with.
|
|
129
|
+
* @returns {object} Flattened object with dot-separated key namespace removed
|
|
130
|
+
*/
|
|
131
|
+
function extractConfigByNamespace (configObject, namespace) {
|
|
132
|
+
// Check we have what we need
|
|
133
|
+
if (!configObject || typeof configObject !== 'object') {
|
|
134
|
+
throw new Error('Provide a `configObject` of type "object".')
|
|
135
|
+
}
|
|
136
|
+
if (!namespace || typeof namespace !== 'string') {
|
|
137
|
+
throw new Error('Provide a `namespace` of type "string" to filter the `configObject` by.')
|
|
138
|
+
}
|
|
139
|
+
var newObject = {};
|
|
140
|
+
for (var key in configObject) {
|
|
141
|
+
// Split the key into parts, using . as our namespace separator
|
|
142
|
+
var keyParts = key.split('.');
|
|
143
|
+
// Check if the first namespace matches the configured namespace
|
|
144
|
+
if (Object.prototype.hasOwnProperty.call(configObject, key) && keyParts[0] === namespace) {
|
|
145
|
+
// Remove the first item (the namespace) from the parts array,
|
|
146
|
+
// but only if there is more than one part (we don't want blank keys!)
|
|
147
|
+
if (keyParts.length > 1) {
|
|
148
|
+
keyParts.shift();
|
|
149
|
+
}
|
|
150
|
+
// Join the remaining parts back together
|
|
151
|
+
var newKey = keyParts.join('.');
|
|
152
|
+
// Add them to our new object
|
|
153
|
+
newObject[newKey] = configObject[key];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return newObject
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @callback nodeListIterator
|
|
161
|
+
* @param {Element} value - The current node being iterated on
|
|
162
|
+
* @param {number} index - The current index in the iteration
|
|
163
|
+
* @param {NodeListOf<Element>} nodes - NodeList from querySelectorAll()
|
|
164
|
+
* @returns {undefined}
|
|
165
|
+
*/
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Internal support for selecting messages to render, with placeholder
|
|
169
|
+
* interpolation and locale-aware number formatting and pluralisation
|
|
170
|
+
*
|
|
171
|
+
* @class
|
|
172
|
+
* @private
|
|
173
|
+
* @param {TranslationsFlattened} translations - Key-value pairs of the translation strings to use.
|
|
174
|
+
* @param {object} [config] - Configuration options for the function.
|
|
175
|
+
* @param {string} config.locale - An overriding locale for the PluralRules functionality.
|
|
176
|
+
*/
|
|
177
|
+
function I18n (translations, config) {
|
|
178
|
+
// Make list of translations available throughout function
|
|
179
|
+
this.translations = translations || {};
|
|
180
|
+
|
|
181
|
+
// The locale to use for PluralRules and NumberFormat
|
|
182
|
+
this.locale = (config && config.locale) || document.documentElement.lang || 'en';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The most used function - takes the key for a given piece of UI text and
|
|
187
|
+
* returns the appropriate string.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} lookupKey - The lookup key of the string to use.
|
|
190
|
+
* @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
|
|
191
|
+
* @returns {string} The appropriate translation string.
|
|
192
|
+
*/
|
|
193
|
+
I18n.prototype.t = function (lookupKey, options) {
|
|
194
|
+
if (!lookupKey) {
|
|
195
|
+
// Print a console error if no lookup key has been provided
|
|
196
|
+
throw new Error('i18n: lookup key missing')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// If the `count` option is set, determine which plural suffix is needed and
|
|
200
|
+
// change the lookupKey to match. We check to see if it's undefined instead of
|
|
201
|
+
// falsy, as this could legitimately be 0.
|
|
202
|
+
if (options && typeof options.count !== 'undefined') {
|
|
203
|
+
// Get the plural suffix
|
|
204
|
+
lookupKey = lookupKey + '.' + this.getPluralSuffix(lookupKey, options.count);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (lookupKey in this.translations) {
|
|
208
|
+
// Fetch the translation string for that lookup key
|
|
209
|
+
var translationString = this.translations[lookupKey];
|
|
210
|
+
|
|
211
|
+
// Check for ${} placeholders in the translation string
|
|
212
|
+
if (translationString.match(/%{(.\S+)}/)) {
|
|
213
|
+
if (!options) {
|
|
214
|
+
throw new Error('i18n: cannot replace placeholders in string if no option data provided')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return this.replacePlaceholders(translationString, options)
|
|
218
|
+
} else {
|
|
219
|
+
return translationString
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
// If the key wasn't found in our translations object,
|
|
223
|
+
// return the lookup key itself as the fallback
|
|
224
|
+
return lookupKey
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Takes a translation string with placeholders, and replaces the placeholders
|
|
230
|
+
* with the provided data
|
|
231
|
+
*
|
|
232
|
+
* @param {string} translationString - The translation string
|
|
233
|
+
* @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
|
|
234
|
+
* @returns {string} The translation string to output, with ${} placeholders replaced
|
|
235
|
+
*/
|
|
236
|
+
I18n.prototype.replacePlaceholders = function (translationString, options) {
|
|
237
|
+
var formatter;
|
|
238
|
+
|
|
239
|
+
if (this.hasIntlNumberFormatSupport()) {
|
|
240
|
+
formatter = new Intl.NumberFormat(this.locale);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return translationString.replace(/%{(.\S+)}/g, function (placeholderWithBraces, placeholderKey) {
|
|
244
|
+
if (Object.prototype.hasOwnProperty.call(options, placeholderKey)) {
|
|
245
|
+
var placeholderValue = options[placeholderKey];
|
|
246
|
+
|
|
247
|
+
// If a user has passed `false` as the value for the placeholder
|
|
248
|
+
// treat it as though the value should not be displayed
|
|
249
|
+
if (placeholderValue === false) {
|
|
250
|
+
return ''
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// If the placeholder's value is a number, localise the number formatting
|
|
254
|
+
if (typeof placeholderValue === 'number' && formatter) {
|
|
255
|
+
return formatter.format(placeholderValue)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return placeholderValue
|
|
259
|
+
} else {
|
|
260
|
+
throw new Error('i18n: no data found to replace ' + placeholderWithBraces + ' placeholder in string')
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check to see if the browser supports Intl and Intl.PluralRules.
|
|
267
|
+
*
|
|
268
|
+
* It requires all conditions to be met in order to be supported:
|
|
269
|
+
* - The browser supports the Intl class (true in IE11)
|
|
270
|
+
* - The implementation of Intl supports PluralRules (NOT true in IE11)
|
|
271
|
+
* - The browser/OS has plural rules for the current locale (browser dependent)
|
|
272
|
+
*
|
|
273
|
+
* @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
|
|
274
|
+
*/
|
|
275
|
+
I18n.prototype.hasIntlPluralRulesSupport = function () {
|
|
276
|
+
return Boolean(window.Intl && ('PluralRules' in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length))
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check to see if the browser supports Intl and Intl.NumberFormat.
|
|
281
|
+
*
|
|
282
|
+
* It requires all conditions to be met in order to be supported:
|
|
283
|
+
* - The browser supports the Intl class (true in IE11)
|
|
284
|
+
* - The implementation of Intl supports NumberFormat (also true in IE11)
|
|
285
|
+
* - The browser/OS has number formatting rules for the current locale (browser dependent)
|
|
286
|
+
*
|
|
287
|
+
* @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
|
|
288
|
+
*/
|
|
289
|
+
I18n.prototype.hasIntlNumberFormatSupport = function () {
|
|
290
|
+
return Boolean(window.Intl && ('NumberFormat' in window.Intl && Intl.NumberFormat.supportedLocalesOf(this.locale).length))
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get the appropriate suffix for the plural form.
|
|
295
|
+
*
|
|
296
|
+
* Uses Intl.PluralRules (or our own fallback implementation) to get the
|
|
297
|
+
* 'preferred' form to use for the given count.
|
|
298
|
+
*
|
|
299
|
+
* Checks that a translation has been provided for that plural form – if it
|
|
300
|
+
* hasn't, it'll fall back to the 'other' plural form (unless that doesn't exist
|
|
301
|
+
* either, in which case an error will be thrown)
|
|
302
|
+
*
|
|
303
|
+
* @param {string} lookupKey - The lookup key of the string to use.
|
|
304
|
+
* @param {number} count - Number used to determine which pluralisation to use.
|
|
305
|
+
* @returns {PluralRule} The suffix associated with the correct pluralisation for this locale.
|
|
306
|
+
*/
|
|
307
|
+
I18n.prototype.getPluralSuffix = function (lookupKey, count) {
|
|
308
|
+
// Validate that the number is actually a number.
|
|
309
|
+
//
|
|
310
|
+
// Number(count) will turn anything that can't be converted to a Number type
|
|
311
|
+
// into 'NaN'. isFinite filters out NaN, as it isn't a finite number.
|
|
312
|
+
count = Number(count);
|
|
313
|
+
if (!isFinite(count)) { return 'other' }
|
|
314
|
+
|
|
315
|
+
var preferredForm;
|
|
316
|
+
|
|
317
|
+
// Check to verify that all the requirements for Intl.PluralRules are met.
|
|
318
|
+
// If so, we can use that instead of our custom implementation. Otherwise,
|
|
319
|
+
// use the hardcoded fallback.
|
|
320
|
+
if (this.hasIntlPluralRulesSupport()) {
|
|
321
|
+
preferredForm = new Intl.PluralRules(this.locale).select(count);
|
|
322
|
+
} else {
|
|
323
|
+
preferredForm = this.selectPluralFormUsingFallbackRules(count);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Use the correct plural form if provided
|
|
327
|
+
if (lookupKey + '.' + preferredForm in this.translations) {
|
|
328
|
+
return preferredForm
|
|
329
|
+
// Fall back to `other` if the plural form is missing, but log a warning
|
|
330
|
+
// to the console
|
|
331
|
+
} else if (lookupKey + '.other' in this.translations) {
|
|
332
|
+
if (console && 'warn' in console) {
|
|
333
|
+
console.warn('i18n: Missing plural form ".' + preferredForm + '" for "' +
|
|
334
|
+
this.locale + '" locale. Falling back to ".other".');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return 'other'
|
|
338
|
+
// If the required `other` plural form is missing, all we can do is error
|
|
339
|
+
} else {
|
|
340
|
+
throw new Error(
|
|
341
|
+
'i18n: Plural form ".other" is required for "' + this.locale + '" locale'
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get the plural form using our fallback implementation
|
|
348
|
+
*
|
|
349
|
+
* This is split out into a separate function to make it easier to test the
|
|
350
|
+
* fallback behaviour in an environment where Intl.PluralRules exists.
|
|
351
|
+
*
|
|
352
|
+
* @param {number} count - Number used to determine which pluralisation to use.
|
|
353
|
+
* @returns {PluralRule} The pluralisation form for count in this locale.
|
|
354
|
+
*/
|
|
355
|
+
I18n.prototype.selectPluralFormUsingFallbackRules = function (count) {
|
|
356
|
+
// Currently our custom code can only handle positive integers, so let's
|
|
357
|
+
// make sure our number is one of those.
|
|
358
|
+
count = Math.abs(Math.floor(count));
|
|
359
|
+
|
|
360
|
+
var ruleset = this.getPluralRulesForLocale();
|
|
361
|
+
|
|
362
|
+
if (ruleset) {
|
|
363
|
+
return I18n.pluralRules[ruleset](count)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return 'other'
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Work out which pluralisation rules to use for the current locale
|
|
371
|
+
*
|
|
372
|
+
* The locale may include a regional indicator (such as en-GB), but we don't
|
|
373
|
+
* usually care about this part, as pluralisation rules are usually the same
|
|
374
|
+
* regardless of region. There are exceptions, however, (e.g. Portuguese) so
|
|
375
|
+
* this searches by both the full and shortened locale codes, just to be sure.
|
|
376
|
+
*
|
|
377
|
+
* @returns {PluralRuleName | undefined} The name of the pluralisation rule to use (a key for one
|
|
378
|
+
* of the functions in this.pluralRules)
|
|
379
|
+
*/
|
|
380
|
+
I18n.prototype.getPluralRulesForLocale = function () {
|
|
381
|
+
var locale = this.locale;
|
|
382
|
+
var localeShort = locale.split('-')[0];
|
|
383
|
+
|
|
384
|
+
// Look through the plural rules map to find which `pluralRule` is
|
|
385
|
+
// appropriate for our current `locale`.
|
|
386
|
+
for (var pluralRule in I18n.pluralRulesMap) {
|
|
387
|
+
if (Object.prototype.hasOwnProperty.call(I18n.pluralRulesMap, pluralRule)) {
|
|
388
|
+
var languages = I18n.pluralRulesMap[pluralRule];
|
|
389
|
+
for (var i = 0; i < languages.length; i++) {
|
|
390
|
+
if (languages[i] === locale || languages[i] === localeShort) {
|
|
391
|
+
return pluralRule
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Map of plural rules to languages where those rules apply.
|
|
400
|
+
*
|
|
401
|
+
* Note: These groups are named for the most dominant or recognisable language
|
|
402
|
+
* that uses each system. The groupings do not imply that the languages are
|
|
403
|
+
* related to one another. Many languages have evolved the same systems
|
|
404
|
+
* independently of one another.
|
|
405
|
+
*
|
|
406
|
+
* Code to support more languages can be found in the i18n spike:
|
|
407
|
+
* {@link https://github.com/alphagov/govuk-frontend/blob/spike-i18n-support/src/govuk/i18n.mjs}
|
|
408
|
+
*
|
|
409
|
+
* Languages currently supported:
|
|
410
|
+
*
|
|
411
|
+
* Arabic: Arabic (ar)
|
|
412
|
+
* Chinese: Burmese (my), Chinese (zh), Indonesian (id), Japanese (ja),
|
|
413
|
+
* Javanese (jv), Korean (ko), Malay (ms), Thai (th), Vietnamese (vi)
|
|
414
|
+
* French: Armenian (hy), Bangla (bn), French (fr), Gujarati (gu), Hindi (hi),
|
|
415
|
+
* Persian Farsi (fa), Punjabi (pa), Zulu (zu)
|
|
416
|
+
* German: Afrikaans (af), Albanian (sq), Azerbaijani (az), Basque (eu),
|
|
417
|
+
* Bulgarian (bg), Catalan (ca), Danish (da), Dutch (nl), English (en),
|
|
418
|
+
* Estonian (et), Finnish (fi), Georgian (ka), German (de), Greek (el),
|
|
419
|
+
* Hungarian (hu), Luxembourgish (lb), Norwegian (no), Somali (so),
|
|
420
|
+
* Swahili (sw), Swedish (sv), Tamil (ta), Telugu (te), Turkish (tr),
|
|
421
|
+
* Urdu (ur)
|
|
422
|
+
* Irish: Irish Gaelic (ga)
|
|
423
|
+
* Russian: Russian (ru), Ukrainian (uk)
|
|
424
|
+
* Scottish: Scottish Gaelic (gd)
|
|
425
|
+
* Spanish: European Portuguese (pt-PT), Italian (it), Spanish (es)
|
|
426
|
+
* Welsh: Welsh (cy)
|
|
427
|
+
*
|
|
428
|
+
* @type {Object<PluralRuleName, string[]>}
|
|
429
|
+
*/
|
|
430
|
+
I18n.pluralRulesMap = {
|
|
431
|
+
arabic: ['ar'],
|
|
432
|
+
chinese: ['my', 'zh', 'id', 'ja', 'jv', 'ko', 'ms', 'th', 'vi'],
|
|
433
|
+
french: ['hy', 'bn', 'fr', 'gu', 'hi', 'fa', 'pa', 'zu'],
|
|
434
|
+
german: [
|
|
435
|
+
'af', 'sq', 'az', 'eu', 'bg', 'ca', 'da', 'nl', 'en', 'et', 'fi', 'ka',
|
|
436
|
+
'de', 'el', 'hu', 'lb', 'no', 'so', 'sw', 'sv', 'ta', 'te', 'tr', 'ur'
|
|
437
|
+
],
|
|
438
|
+
irish: ['ga'],
|
|
439
|
+
russian: ['ru', 'uk'],
|
|
440
|
+
scottish: ['gd'],
|
|
441
|
+
spanish: ['pt-PT', 'it', 'es'],
|
|
442
|
+
welsh: ['cy']
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Different pluralisation rule sets
|
|
447
|
+
*
|
|
448
|
+
* Returns the appropriate suffix for the plural form associated with `n`.
|
|
449
|
+
* Possible suffixes: 'zero', 'one', 'two', 'few', 'many', 'other' (the actual
|
|
450
|
+
* meaning of each differs per locale). 'other' should always exist, even in
|
|
451
|
+
* languages without plurals, such as Chinese.
|
|
452
|
+
* {@link https://cldr.unicode.org/index/cldr-spec/plural-rules}
|
|
453
|
+
*
|
|
454
|
+
* The count must be a positive integer. Negative numbers and decimals aren't accounted for
|
|
455
|
+
*
|
|
456
|
+
* @type {Object<string, function(number): PluralRule>}
|
|
457
|
+
*/
|
|
458
|
+
I18n.pluralRules = {
|
|
459
|
+
arabic: function (n) {
|
|
460
|
+
if (n === 0) { return 'zero' }
|
|
461
|
+
if (n === 1) { return 'one' }
|
|
462
|
+
if (n === 2) { return 'two' }
|
|
463
|
+
if (n % 100 >= 3 && n % 100 <= 10) { return 'few' }
|
|
464
|
+
if (n % 100 >= 11 && n % 100 <= 99) { return 'many' }
|
|
465
|
+
return 'other'
|
|
466
|
+
},
|
|
467
|
+
chinese: function () {
|
|
468
|
+
return 'other'
|
|
469
|
+
},
|
|
470
|
+
french: function (n) {
|
|
471
|
+
return n === 0 || n === 1 ? 'one' : 'other'
|
|
472
|
+
},
|
|
473
|
+
german: function (n) {
|
|
474
|
+
return n === 1 ? 'one' : 'other'
|
|
475
|
+
},
|
|
476
|
+
irish: function (n) {
|
|
477
|
+
if (n === 1) { return 'one' }
|
|
478
|
+
if (n === 2) { return 'two' }
|
|
479
|
+
if (n >= 3 && n <= 6) { return 'few' }
|
|
480
|
+
if (n >= 7 && n <= 10) { return 'many' }
|
|
481
|
+
return 'other'
|
|
482
|
+
},
|
|
483
|
+
russian: function (n) {
|
|
484
|
+
var lastTwo = n % 100;
|
|
485
|
+
var last = lastTwo % 10;
|
|
486
|
+
if (last === 1 && lastTwo !== 11) { return 'one' }
|
|
487
|
+
if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) { return 'few' }
|
|
488
|
+
if (last === 0 || (last >= 5 && last <= 9) || (lastTwo >= 11 && lastTwo <= 14)) { return 'many' }
|
|
489
|
+
// Note: The 'other' suffix is only used by decimal numbers in Russian.
|
|
490
|
+
// We don't anticipate it being used, but it's here for consistency.
|
|
491
|
+
return 'other'
|
|
492
|
+
},
|
|
493
|
+
scottish: function (n) {
|
|
494
|
+
if (n === 1 || n === 11) { return 'one' }
|
|
495
|
+
if (n === 2 || n === 12) { return 'two' }
|
|
496
|
+
if ((n >= 3 && n <= 10) || (n >= 13 && n <= 19)) { return 'few' }
|
|
497
|
+
return 'other'
|
|
498
|
+
},
|
|
499
|
+
spanish: function (n) {
|
|
500
|
+
if (n === 1) { return 'one' }
|
|
501
|
+
if (n % 1000000 === 0 && n !== 0) { return 'many' }
|
|
502
|
+
return 'other'
|
|
503
|
+
},
|
|
504
|
+
welsh: function (n) {
|
|
505
|
+
if (n === 0) { return 'zero' }
|
|
506
|
+
if (n === 1) { return 'one' }
|
|
507
|
+
if (n === 2) { return 'two' }
|
|
508
|
+
if (n === 3) { return 'few' }
|
|
509
|
+
if (n === 6) { return 'many' }
|
|
510
|
+
return 'other'
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Supported languages for plural rules
|
|
516
|
+
*
|
|
517
|
+
* @typedef {'arabic' | 'chinese' | 'french' | 'german' | 'irish' | 'russian' | 'scottish' | 'spanish' | 'welsh'} PluralRuleName
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Plural rule category mnemonic tags
|
|
522
|
+
*
|
|
523
|
+
* @typedef {'zero' | 'one' | 'two' | 'few' | 'many' | 'other'} PluralRule
|
|
524
|
+
*/
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Translated message by plural rule they correspond to.
|
|
528
|
+
*
|
|
529
|
+
* Allows to group pluralised messages under a single key when passing
|
|
530
|
+
* translations to a component's constructor
|
|
531
|
+
*
|
|
532
|
+
* @typedef {object} TranslationPluralForms
|
|
533
|
+
* @property {string} [other] - General plural form
|
|
534
|
+
* @property {string} [zero] - Plural form used with 0
|
|
535
|
+
* @property {string} [one] - Plural form used with 1
|
|
536
|
+
* @property {string} [two] - Plural form used with 2
|
|
537
|
+
* @property {string} [few] - Plural form used for a few
|
|
538
|
+
* @property {string} [many] - Plural form used for many
|
|
539
|
+
*/
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Translated messages (flattened)
|
|
543
|
+
*
|
|
544
|
+
* @private
|
|
545
|
+
* @typedef {Object<string, string> | {}} TranslationsFlattened
|
|
546
|
+
*/
|
|
547
|
+
|
|
36
548
|
(function(undefined) {
|
|
37
549
|
|
|
38
550
|
// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Object/defineProperty/detect.js
|
|
@@ -773,13 +1285,188 @@ if (detect) return
|
|
|
773
1285
|
|
|
774
1286
|
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
|
775
1287
|
|
|
776
|
-
function
|
|
1288
|
+
(function(undefined) {
|
|
1289
|
+
|
|
1290
|
+
// Detection from https://github.com/mdn/content/blob/cf607d68522cd35ee7670782d3ee3a361eaef2e4/files/en-us/web/javascript/reference/global_objects/string/trim/index.md#polyfill
|
|
1291
|
+
var detect = ('trim' in String.prototype);
|
|
1292
|
+
|
|
1293
|
+
if (detect) return
|
|
1294
|
+
|
|
1295
|
+
// Polyfill from https://github.com/mdn/content/blob/cf607d68522cd35ee7670782d3ee3a361eaef2e4/files/en-us/web/javascript/reference/global_objects/string/trim/index.md#polyfill
|
|
1296
|
+
String.prototype.trim = function () {
|
|
1297
|
+
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
|
1298
|
+
};
|
|
1299
|
+
|
|
1300
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
|
1301
|
+
|
|
1302
|
+
(function(undefined) {
|
|
1303
|
+
|
|
1304
|
+
// Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/detect.js
|
|
1305
|
+
var detect = (function(){
|
|
1306
|
+
if (!document.documentElement.dataset) {
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
var el = document.createElement('div');
|
|
1310
|
+
el.setAttribute("data-a-b", "c");
|
|
1311
|
+
return el.dataset && el.dataset.aB == "c";
|
|
1312
|
+
}());
|
|
1313
|
+
|
|
1314
|
+
if (detect) return
|
|
1315
|
+
|
|
1316
|
+
// Polyfill derived from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/polyfill.js
|
|
1317
|
+
Object.defineProperty(Element.prototype, 'dataset', {
|
|
1318
|
+
get: function() {
|
|
1319
|
+
var element = this;
|
|
1320
|
+
var attributes = this.attributes;
|
|
1321
|
+
var map = {};
|
|
1322
|
+
|
|
1323
|
+
for (var i = 0; i < attributes.length; i++) {
|
|
1324
|
+
var attribute = attributes[i];
|
|
1325
|
+
|
|
1326
|
+
// This regex has been edited from the original polyfill, to add
|
|
1327
|
+
// support for period (.) separators in data-* attribute names. These
|
|
1328
|
+
// are allowed in the HTML spec, but were not covered by the original
|
|
1329
|
+
// polyfill's regex. We use periods in our i18n implementation.
|
|
1330
|
+
if (attribute && attribute.name && (/^data-\w[.\w-]*$/).test(attribute.name)) {
|
|
1331
|
+
var name = attribute.name;
|
|
1332
|
+
var value = attribute.value;
|
|
1333
|
+
|
|
1334
|
+
var propName = name.substr(5).replace(/-./g, function (prop) {
|
|
1335
|
+
return prop.charAt(1).toUpperCase();
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
// If this browser supports __defineGetter__ and __defineSetter__,
|
|
1339
|
+
// continue using defineProperty. If not (like IE 8 and below), we use
|
|
1340
|
+
// a hacky fallback which at least gives an object in the right format
|
|
1341
|
+
if ('__defineGetter__' in Object.prototype && '__defineSetter__' in Object.prototype) {
|
|
1342
|
+
Object.defineProperty(map, propName, {
|
|
1343
|
+
enumerable: true,
|
|
1344
|
+
get: function() {
|
|
1345
|
+
return this.value;
|
|
1346
|
+
}.bind({value: value || ''}),
|
|
1347
|
+
set: function setter(name, value) {
|
|
1348
|
+
if (typeof value !== 'undefined') {
|
|
1349
|
+
this.setAttribute(name, value);
|
|
1350
|
+
} else {
|
|
1351
|
+
this.removeAttribute(name);
|
|
1352
|
+
}
|
|
1353
|
+
}.bind(element, name)
|
|
1354
|
+
});
|
|
1355
|
+
} else {
|
|
1356
|
+
map[propName] = value;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
return map;
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
|
1367
|
+
|
|
1368
|
+
/**
|
|
1369
|
+
* Normalise string
|
|
1370
|
+
*
|
|
1371
|
+
* 'If it looks like a duck, and it quacks like a duck…' 🦆
|
|
1372
|
+
*
|
|
1373
|
+
* If the passed value looks like a boolean or a number, convert it to a boolean
|
|
1374
|
+
* or number.
|
|
1375
|
+
*
|
|
1376
|
+
* Designed to be used to convert config passed via data attributes (which are
|
|
1377
|
+
* always strings) into something sensible.
|
|
1378
|
+
*
|
|
1379
|
+
* @param {string} value - The value to normalise
|
|
1380
|
+
* @returns {string | boolean | number | undefined} Normalised data
|
|
1381
|
+
*/
|
|
1382
|
+
function normaliseString (value) {
|
|
1383
|
+
if (typeof value !== 'string') {
|
|
1384
|
+
return value
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
var trimmedValue = value.trim();
|
|
1388
|
+
|
|
1389
|
+
if (trimmedValue === 'true') {
|
|
1390
|
+
return true
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if (trimmedValue === 'false') {
|
|
1394
|
+
return false
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// Empty / whitespace-only strings are considered finite so we need to check
|
|
1398
|
+
// the length of the trimmed string as well
|
|
1399
|
+
if (trimmedValue.length > 0 && isFinite(trimmedValue)) {
|
|
1400
|
+
return Number(trimmedValue)
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
return value
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Normalise dataset
|
|
1408
|
+
*
|
|
1409
|
+
* Loop over an object and normalise each value using normaliseData function
|
|
1410
|
+
*
|
|
1411
|
+
* @param {DOMStringMap} dataset - HTML element dataset
|
|
1412
|
+
* @returns {Object<string, string | boolean | number | undefined>} Normalised dataset
|
|
1413
|
+
*/
|
|
1414
|
+
function normaliseDataset (dataset) {
|
|
1415
|
+
var out = {};
|
|
1416
|
+
|
|
1417
|
+
for (var key in dataset) {
|
|
1418
|
+
out[key] = normaliseString(dataset[key]);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
return out
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* @constant
|
|
1426
|
+
* @type {AccordionTranslations}
|
|
1427
|
+
* @see Default value for {@link AccordionConfig.i18n}
|
|
1428
|
+
* @default
|
|
1429
|
+
*/
|
|
1430
|
+
var ACCORDION_TRANSLATIONS = {
|
|
1431
|
+
hideAllSections: 'Hide all sections',
|
|
1432
|
+
hideSection: 'Hide',
|
|
1433
|
+
hideSectionAriaLabel: 'Hide this section',
|
|
1434
|
+
showAllSections: 'Show all sections',
|
|
1435
|
+
showSection: 'Show',
|
|
1436
|
+
showSectionAriaLabel: 'Show this section'
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Accordion component
|
|
1441
|
+
*
|
|
1442
|
+
* This allows a collection of sections to be collapsed by default, showing only
|
|
1443
|
+
* their headers. Sections can be expanded or collapsed individually by clicking
|
|
1444
|
+
* their headers. A "Show all sections" button is also added to the top of the
|
|
1445
|
+
* accordion, which switches to "Hide all sections" when all the sections are
|
|
1446
|
+
* expanded.
|
|
1447
|
+
*
|
|
1448
|
+
* The state of each section is saved to the DOM via the `aria-expanded`
|
|
1449
|
+
* attribute, which also provides accessibility.
|
|
1450
|
+
*
|
|
1451
|
+
* @class
|
|
1452
|
+
* @param {HTMLElement} $module - HTML element to use for accordion
|
|
1453
|
+
* @param {AccordionConfig} [config] - Accordion config
|
|
1454
|
+
*/
|
|
1455
|
+
function Accordion ($module, config) {
|
|
777
1456
|
this.$module = $module;
|
|
778
|
-
this.moduleId = $module.getAttribute('id');
|
|
779
1457
|
this.$sections = $module.querySelectorAll('.govuk-accordion__section');
|
|
780
|
-
this.$showAllButton = '';
|
|
781
1458
|
this.browserSupportsSessionStorage = helper.checkForSessionStorage();
|
|
782
1459
|
|
|
1460
|
+
var defaultConfig = {
|
|
1461
|
+
i18n: ACCORDION_TRANSLATIONS
|
|
1462
|
+
};
|
|
1463
|
+
this.config = mergeConfigs(
|
|
1464
|
+
defaultConfig,
|
|
1465
|
+
config || {},
|
|
1466
|
+
normaliseDataset($module.dataset)
|
|
1467
|
+
);
|
|
1468
|
+
this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'));
|
|
1469
|
+
|
|
783
1470
|
this.controlsClass = 'govuk-accordion__controls';
|
|
784
1471
|
this.showAllClass = 'govuk-accordion__show-all';
|
|
785
1472
|
this.showAllTextClass = 'govuk-accordion__show-all-text';
|
|
@@ -870,7 +1557,7 @@ Accordion.prototype.constructHeaderMarkup = function ($headerWrapper, index) {
|
|
|
870
1557
|
// Create a button element that will replace the '.govuk-accordion__section-button' span
|
|
871
1558
|
var $button = document.createElement('button');
|
|
872
1559
|
$button.setAttribute('type', 'button');
|
|
873
|
-
$button.setAttribute('aria-controls', this.
|
|
1560
|
+
$button.setAttribute('aria-controls', this.$module.id + '-content-' + (index + 1));
|
|
874
1561
|
|
|
875
1562
|
// Copy all attributes (https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes) from $span to $button
|
|
876
1563
|
for (var i = 0; i < $span.attributes.length; i++) {
|
|
@@ -987,17 +1674,34 @@ Accordion.prototype.setExpanded = function (expanded, $section) {
|
|
|
987
1674
|
var $icon = $section.querySelector('.' + this.upChevronIconClass);
|
|
988
1675
|
var $showHideText = $section.querySelector('.' + this.sectionShowHideTextClass);
|
|
989
1676
|
var $button = $section.querySelector('.' + this.sectionButtonClass);
|
|
990
|
-
var
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
var $visuallyHiddenText = document.createElement('span');
|
|
994
|
-
$visuallyHiddenText.classList.add('govuk-visually-hidden');
|
|
995
|
-
$visuallyHiddenText.innerHTML = ' this section';
|
|
1677
|
+
var newButtonText = expanded
|
|
1678
|
+
? this.i18n.t('hideSection')
|
|
1679
|
+
: this.i18n.t('showSection');
|
|
996
1680
|
|
|
997
|
-
$showHideText.
|
|
998
|
-
$showHideText.appendChild($visuallyHiddenText);
|
|
1681
|
+
$showHideText.innerText = newButtonText;
|
|
999
1682
|
$button.setAttribute('aria-expanded', expanded);
|
|
1000
1683
|
|
|
1684
|
+
// Update aria-label combining
|
|
1685
|
+
var $header = $section.querySelector('.' + this.sectionHeadingTextClass);
|
|
1686
|
+
var ariaLabelParts = [$header.innerText.trim()];
|
|
1687
|
+
|
|
1688
|
+
var $summary = $section.querySelector('.' + this.sectionSummaryClass);
|
|
1689
|
+
if ($summary) {
|
|
1690
|
+
ariaLabelParts.push($summary.innerText.trim());
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
var ariaLabelMessage = expanded
|
|
1694
|
+
? this.i18n.t('hideSectionAriaLabel')
|
|
1695
|
+
: this.i18n.t('showSectionAriaLabel');
|
|
1696
|
+
ariaLabelParts.push(ariaLabelMessage);
|
|
1697
|
+
|
|
1698
|
+
/*
|
|
1699
|
+
* Join with a comma to add pause for assistive technology.
|
|
1700
|
+
* Example: [heading]Section A ,[pause] Show this section.
|
|
1701
|
+
* https://accessibility.blog.gov.uk/2017/12/18/what-working-on-gov-uk-navigation-taught-us-about-accessibility/
|
|
1702
|
+
*/
|
|
1703
|
+
$button.setAttribute('aria-label', ariaLabelParts.join(' , '));
|
|
1704
|
+
|
|
1001
1705
|
// Swap icon, change class
|
|
1002
1706
|
if (expanded) {
|
|
1003
1707
|
$section.classList.add(this.sectionExpandedClass);
|
|
@@ -1032,9 +1736,11 @@ Accordion.prototype.checkIfAllSectionsOpen = function () {
|
|
|
1032
1736
|
Accordion.prototype.updateShowAllButton = function (expanded) {
|
|
1033
1737
|
var $showAllIcon = this.$showAllButton.querySelector('.' + this.upChevronIconClass);
|
|
1034
1738
|
var $showAllText = this.$showAllButton.querySelector('.' + this.showAllTextClass);
|
|
1035
|
-
var newButtonText = expanded
|
|
1739
|
+
var newButtonText = expanded
|
|
1740
|
+
? this.i18n.t('hideAllSections')
|
|
1741
|
+
: this.i18n.t('showAllSections');
|
|
1036
1742
|
this.$showAllButton.setAttribute('aria-expanded', expanded);
|
|
1037
|
-
$showAllText.
|
|
1743
|
+
$showAllText.innerText = newButtonText;
|
|
1038
1744
|
|
|
1039
1745
|
// Swap icon, toggle class
|
|
1040
1746
|
if (expanded) {
|
|
@@ -1055,9 +1761,7 @@ var helper = {
|
|
|
1055
1761
|
window.sessionStorage.removeItem(testString);
|
|
1056
1762
|
return result
|
|
1057
1763
|
} catch (exception) {
|
|
1058
|
-
|
|
1059
|
-
console.log('Notice: sessionStorage not available.');
|
|
1060
|
-
}
|
|
1764
|
+
return false
|
|
1061
1765
|
}
|
|
1062
1766
|
}
|
|
1063
1767
|
};
|
|
@@ -1074,14 +1778,6 @@ Accordion.prototype.storeState = function ($section) {
|
|
|
1074
1778
|
var contentId = $button.getAttribute('aria-controls');
|
|
1075
1779
|
var contentState = $button.getAttribute('aria-expanded');
|
|
1076
1780
|
|
|
1077
|
-
if (typeof contentId === 'undefined' && (typeof console === 'undefined' || typeof console.log === 'undefined')) {
|
|
1078
|
-
console.error(new Error('No aria controls present in accordion section heading.'));
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
if (typeof contentState === 'undefined' && (typeof console === 'undefined' || typeof console.log === 'undefined')) {
|
|
1082
|
-
console.error(new Error('No aria expanded present in accordion section heading.'));
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
1781
|
// Only set the state when both `contentId` and `contentState` are taken from the DOM.
|
|
1086
1782
|
if (contentId && contentState) {
|
|
1087
1783
|
window.sessionStorage.setItem(contentId, contentState);
|
|
@@ -1107,17 +1803,14 @@ Accordion.prototype.setInitialState = function ($section) {
|
|
|
1107
1803
|
};
|
|
1108
1804
|
|
|
1109
1805
|
/**
|
|
1110
|
-
* Create an element to improve semantics of the section button with punctuation
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
*
|
|
1116
|
-
*
|
|
1117
|
-
|
|
1118
|
-
* into thematic chunks.
|
|
1119
|
-
* See https://github.com/alphagov/govuk-frontend/issues/2327#issuecomment-922957442
|
|
1120
|
-
*/
|
|
1806
|
+
* Create an element to improve semantics of the section button with punctuation
|
|
1807
|
+
*
|
|
1808
|
+
* @returns {HTMLSpanElement} DOM element
|
|
1809
|
+
*
|
|
1810
|
+
* Adding punctuation to the button can also improve its general semantics by dividing its contents
|
|
1811
|
+
* into thematic chunks.
|
|
1812
|
+
* See https://github.com/alphagov/govuk-frontend/issues/2327#issuecomment-922957442
|
|
1813
|
+
*/
|
|
1121
1814
|
Accordion.prototype.getButtonPunctuationEl = function () {
|
|
1122
1815
|
var $punctuationEl = document.createElement('span');
|
|
1123
1816
|
$punctuationEl.classList.add('govuk-visually-hidden', 'govuk-accordion__section-heading-divider');
|
|
@@ -1125,6 +1818,35 @@ Accordion.prototype.getButtonPunctuationEl = function () {
|
|
|
1125
1818
|
return $punctuationEl
|
|
1126
1819
|
};
|
|
1127
1820
|
|
|
1821
|
+
/**
|
|
1822
|
+
* Accordion config
|
|
1823
|
+
*
|
|
1824
|
+
* @typedef {object} AccordionConfig
|
|
1825
|
+
* @property {AccordionTranslations} [i18n = ACCORDION_TRANSLATIONS] - See constant {@link ACCORDION_TRANSLATIONS}
|
|
1826
|
+
*/
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Accordion translations
|
|
1830
|
+
*
|
|
1831
|
+
* @typedef {object} AccordionTranslations
|
|
1832
|
+
*
|
|
1833
|
+
* Messages used by the component for the labels of its buttons. This includes
|
|
1834
|
+
* the visible text shown on screen, and text to help assistive technology users
|
|
1835
|
+
* for the buttons toggling each section.
|
|
1836
|
+
* @property {string} [hideAllSections] - The text content for the 'Hide all
|
|
1837
|
+
* sections' button, used when at least one section is expanded.
|
|
1838
|
+
* @property {string} [hideSection] - The text content for the 'Hide'
|
|
1839
|
+
* button, used when a section is expanded.
|
|
1840
|
+
* @property {string} [hideSectionAriaLabel] - The text content appended to the
|
|
1841
|
+
* 'Hide' button's accessible name when a section is expanded.
|
|
1842
|
+
* @property {string} [showAllSections] - The text content for the 'Show all
|
|
1843
|
+
* sections' button, used when all sections are collapsed.
|
|
1844
|
+
* @property {string} [showSection] - The text content for the 'Show'
|
|
1845
|
+
* button, used when a section is collapsed.
|
|
1846
|
+
* @property {string} [showSectionAriaLabel] - The text content appended to the
|
|
1847
|
+
* 'Show' button's accessible name when a section is expanded.
|
|
1848
|
+
*/
|
|
1849
|
+
|
|
1128
1850
|
(function(undefined) {
|
|
1129
1851
|
|
|
1130
1852
|
// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Window/detect.js
|
|
@@ -1398,44 +2120,79 @@ if (detect) return
|
|
|
1398
2120
|
var KEY_SPACE = 32;
|
|
1399
2121
|
var DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
|
|
1400
2122
|
|
|
1401
|
-
|
|
2123
|
+
/**
|
|
2124
|
+
* JavaScript enhancements for the Button component
|
|
2125
|
+
*
|
|
2126
|
+
* @class
|
|
2127
|
+
* @param {HTMLElement} $module - The element this component controls
|
|
2128
|
+
* @param {ButtonConfig} config - Button config
|
|
2129
|
+
*/
|
|
2130
|
+
function Button ($module, config) {
|
|
2131
|
+
if (!$module) {
|
|
2132
|
+
return this
|
|
2133
|
+
}
|
|
2134
|
+
|
|
1402
2135
|
this.$module = $module;
|
|
1403
2136
|
this.debounceFormSubmitTimer = null;
|
|
2137
|
+
|
|
2138
|
+
var defaultConfig = {
|
|
2139
|
+
preventDoubleClick: false
|
|
2140
|
+
};
|
|
2141
|
+
this.config = mergeConfigs(
|
|
2142
|
+
defaultConfig,
|
|
2143
|
+
config || {},
|
|
2144
|
+
normaliseDataset($module.dataset)
|
|
2145
|
+
);
|
|
1404
2146
|
}
|
|
1405
2147
|
|
|
1406
2148
|
/**
|
|
1407
|
-
*
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
var target = event.target;
|
|
1418
|
-
// if the element has a role='button' and the pressed key is a space, we'll simulate a click
|
|
1419
|
-
if (target.getAttribute('role') === 'button' && event.keyCode === KEY_SPACE) {
|
|
1420
|
-
event.preventDefault();
|
|
1421
|
-
// trigger the target's click event
|
|
1422
|
-
target.click();
|
|
1423
|
-
}
|
|
1424
|
-
};
|
|
2149
|
+
* Initialise component
|
|
2150
|
+
*/
|
|
2151
|
+
Button.prototype.init = function () {
|
|
2152
|
+
if (!this.$module) {
|
|
2153
|
+
return
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
this.$module.addEventListener('keydown', this.handleKeyDown);
|
|
2157
|
+
this.$module.addEventListener('click', this.debounce.bind(this));
|
|
2158
|
+
};
|
|
1425
2159
|
|
|
1426
2160
|
/**
|
|
1427
|
-
*
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1430
|
-
|
|
1431
|
-
|
|
2161
|
+
* Trigger a click event when the space key is pressed
|
|
2162
|
+
*
|
|
2163
|
+
* Some screen readers tell users they can activate things with the 'button'
|
|
2164
|
+
* role, so we need to match the functionality of native HTML buttons
|
|
2165
|
+
*
|
|
2166
|
+
* See https://github.com/alphagov/govuk_elements/pull/272#issuecomment-233028270
|
|
2167
|
+
*
|
|
2168
|
+
* @param {KeyboardEvent} event
|
|
2169
|
+
*/
|
|
2170
|
+
Button.prototype.handleKeyDown = function (event) {
|
|
1432
2171
|
var target = event.target;
|
|
1433
|
-
|
|
1434
|
-
if (target.getAttribute('
|
|
2172
|
+
|
|
2173
|
+
if (target.getAttribute('role') === 'button' && event.keyCode === KEY_SPACE) {
|
|
2174
|
+
event.preventDefault(); // prevent the page from scrolling
|
|
2175
|
+
target.click();
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
|
|
2179
|
+
/**
|
|
2180
|
+
* Debounce double-clicks
|
|
2181
|
+
*
|
|
2182
|
+
* If the click quickly succeeds a previous click then nothing will happen. This
|
|
2183
|
+
* stops people accidentally causing multiple form submissions by double
|
|
2184
|
+
* clicking buttons.
|
|
2185
|
+
*
|
|
2186
|
+
* @param {MouseEvent} event
|
|
2187
|
+
* @returns {undefined | false} - Returns undefined, or false when debounced
|
|
2188
|
+
*/
|
|
2189
|
+
Button.prototype.debounce = function (event) {
|
|
2190
|
+
// Check the button that was clicked has preventDoubleClick enabled
|
|
2191
|
+
if (!this.config.preventDoubleClick) {
|
|
1435
2192
|
return
|
|
1436
2193
|
}
|
|
1437
2194
|
|
|
1438
|
-
// If the timer is still running
|
|
2195
|
+
// If the timer is still running, prevent the click from submitting the form
|
|
1439
2196
|
if (this.debounceFormSubmitTimer) {
|
|
1440
2197
|
event.preventDefault();
|
|
1441
2198
|
return false
|
|
@@ -1447,13 +2204,13 @@ Button.prototype.debounce = function (event) {
|
|
|
1447
2204
|
};
|
|
1448
2205
|
|
|
1449
2206
|
/**
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
2207
|
+
* Button config
|
|
2208
|
+
*
|
|
2209
|
+
* @typedef {object} ButtonConfig
|
|
2210
|
+
* @property {boolean} [preventDoubleClick = false] -
|
|
2211
|
+
* Prevent accidental double clicks on submit buttons from submitting forms
|
|
2212
|
+
* multiple times.
|
|
2213
|
+
*/
|
|
1457
2214
|
|
|
1458
2215
|
/**
|
|
1459
2216
|
* JavaScript 'polyfill' for HTML5's <details> and <summary> elements
|
|
@@ -1465,6 +2222,12 @@ Button.prototype.init = function () {
|
|
|
1465
2222
|
var KEY_ENTER = 13;
|
|
1466
2223
|
var KEY_SPACE$1 = 32;
|
|
1467
2224
|
|
|
2225
|
+
/**
|
|
2226
|
+
* Details component
|
|
2227
|
+
*
|
|
2228
|
+
* @class
|
|
2229
|
+
* @param {HTMLElement} $module - HTML element to use for details
|
|
2230
|
+
*/
|
|
1468
2231
|
function Details ($module) {
|
|
1469
2232
|
this.$module = $module;
|
|
1470
2233
|
}
|
|
@@ -1519,13 +2282,10 @@ Details.prototype.polyfillDetails = function () {
|
|
|
1519
2282
|
$summary.tabIndex = 0;
|
|
1520
2283
|
|
|
1521
2284
|
// Detect initial open state
|
|
1522
|
-
|
|
1523
|
-
if (openAttr === true) {
|
|
2285
|
+
if (this.$module.hasAttribute('open')) {
|
|
1524
2286
|
$summary.setAttribute('aria-expanded', 'true');
|
|
1525
|
-
$content.setAttribute('aria-hidden', 'false');
|
|
1526
2287
|
} else {
|
|
1527
2288
|
$summary.setAttribute('aria-expanded', 'false');
|
|
1528
|
-
$content.setAttribute('aria-hidden', 'true');
|
|
1529
2289
|
$content.style.display = 'none';
|
|
1530
2290
|
}
|
|
1531
2291
|
|
|
@@ -1534,37 +2294,30 @@ Details.prototype.polyfillDetails = function () {
|
|
|
1534
2294
|
};
|
|
1535
2295
|
|
|
1536
2296
|
/**
|
|
1537
|
-
* Define a statechange function that updates aria-expanded and style.display
|
|
1538
|
-
*
|
|
1539
|
-
|
|
2297
|
+
* Define a statechange function that updates aria-expanded and style.display
|
|
2298
|
+
*
|
|
2299
|
+
* @returns {boolean} Returns true
|
|
2300
|
+
*/
|
|
1540
2301
|
Details.prototype.polyfillSetAttributes = function () {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
var expanded = $summary.getAttribute('aria-expanded') === 'true';
|
|
1546
|
-
var hidden = $content.getAttribute('aria-hidden') === 'true';
|
|
1547
|
-
|
|
1548
|
-
$summary.setAttribute('aria-expanded', (expanded ? 'false' : 'true'));
|
|
1549
|
-
$content.setAttribute('aria-hidden', (hidden ? 'false' : 'true'));
|
|
1550
|
-
|
|
1551
|
-
$content.style.display = (expanded ? 'none' : '');
|
|
1552
|
-
|
|
1553
|
-
var hasOpenAttr = $module.getAttribute('open') !== null;
|
|
1554
|
-
if (!hasOpenAttr) {
|
|
1555
|
-
$module.setAttribute('open', 'open');
|
|
2302
|
+
if (this.$module.hasAttribute('open')) {
|
|
2303
|
+
this.$module.removeAttribute('open');
|
|
2304
|
+
this.$summary.setAttribute('aria-expanded', 'false');
|
|
2305
|
+
this.$content.style.display = 'none';
|
|
1556
2306
|
} else {
|
|
1557
|
-
|
|
2307
|
+
this.$module.setAttribute('open', 'open');
|
|
2308
|
+
this.$summary.setAttribute('aria-expanded', 'true');
|
|
2309
|
+
this.$content.style.display = '';
|
|
1558
2310
|
}
|
|
1559
2311
|
|
|
1560
2312
|
return true
|
|
1561
2313
|
};
|
|
1562
2314
|
|
|
1563
2315
|
/**
|
|
1564
|
-
* Handle cross-modal click events
|
|
1565
|
-
*
|
|
1566
|
-
* @param {
|
|
1567
|
-
|
|
2316
|
+
* Handle cross-modal click events
|
|
2317
|
+
*
|
|
2318
|
+
* @param {object} node - element
|
|
2319
|
+
* @param {polyfillHandleInputsCallback} callback - function
|
|
2320
|
+
*/
|
|
1568
2321
|
Details.prototype.polyfillHandleInputs = function (node, callback) {
|
|
1569
2322
|
node.addEventListener('keypress', function (event) {
|
|
1570
2323
|
var target = event.target;
|
|
@@ -1598,114 +2351,310 @@ Details.prototype.polyfillHandleInputs = function (node, callback) {
|
|
|
1598
2351
|
node.addEventListener('click', callback);
|
|
1599
2352
|
};
|
|
1600
2353
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
2354
|
+
/**
|
|
2355
|
+
* @callback polyfillHandleInputsCallback
|
|
2356
|
+
* @param {KeyboardEvent} event - Keyboard event
|
|
2357
|
+
* @returns {undefined}
|
|
2358
|
+
*/
|
|
2359
|
+
|
|
2360
|
+
(function(undefined) {
|
|
2361
|
+
|
|
2362
|
+
// Detection from https://github.com/Financial-Times/polyfill-library/blob/v3.111.0/polyfills/Date/now/detect.js
|
|
2363
|
+
var detect = ('Date' in self && 'now' in self.Date && 'getTime' in self.Date.prototype);
|
|
2364
|
+
|
|
2365
|
+
if (detect) return
|
|
2366
|
+
|
|
2367
|
+
// Polyfill from https://polyfill.io/v3/polyfill.js?version=3.111.0&features=Date.now&flags=always
|
|
2368
|
+
Date.now = function () {
|
|
2369
|
+
return new Date().getTime();
|
|
2370
|
+
};
|
|
2371
|
+
|
|
2372
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
|
2373
|
+
|
|
2374
|
+
(function(undefined) {
|
|
2375
|
+
|
|
2376
|
+
// Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/detect.js
|
|
2377
|
+
var detect = (
|
|
2378
|
+
'document' in this && "matches" in document.documentElement
|
|
2379
|
+
);
|
|
2380
|
+
|
|
2381
|
+
if (detect) return
|
|
2382
|
+
|
|
2383
|
+
// Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/polyfill.js
|
|
2384
|
+
Element.prototype.matches = Element.prototype.webkitMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || function matches(selector) {
|
|
2385
|
+
var element = this;
|
|
2386
|
+
var elements = (element.document || element.ownerDocument).querySelectorAll(selector);
|
|
2387
|
+
var index = 0;
|
|
2388
|
+
|
|
2389
|
+
while (elements[index] && elements[index] !== element) {
|
|
2390
|
+
++index;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
return !!elements[index];
|
|
2394
|
+
};
|
|
2395
|
+
|
|
2396
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
|
2397
|
+
|
|
2398
|
+
(function(undefined) {
|
|
2399
|
+
|
|
2400
|
+
// Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/detect.js
|
|
2401
|
+
var detect = (
|
|
2402
|
+
'document' in this && "closest" in document.documentElement
|
|
2403
|
+
);
|
|
2404
|
+
|
|
2405
|
+
if (detect) return
|
|
2406
|
+
|
|
2407
|
+
// Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/polyfill.js
|
|
2408
|
+
Element.prototype.closest = function closest(selector) {
|
|
2409
|
+
var node = this;
|
|
2410
|
+
|
|
2411
|
+
while (node) {
|
|
2412
|
+
if (node.matches(selector)) return node;
|
|
2413
|
+
else node = 'SVGElement' in window && node instanceof SVGElement ? node.parentNode : node.parentElement;
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
return null;
|
|
2417
|
+
};
|
|
2418
|
+
|
|
2419
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
|
2420
|
+
|
|
2421
|
+
/**
|
|
2422
|
+
* Returns the value of the given attribute closest to the given element (including itself)
|
|
2423
|
+
*
|
|
2424
|
+
* @param {HTMLElement} $element - The element to start walking the DOM tree up
|
|
2425
|
+
* @param {string} attributeName - The name of the attribute
|
|
2426
|
+
* @returns {string | undefined} Attribute value
|
|
2427
|
+
*/
|
|
2428
|
+
function closestAttributeValue ($element, attributeName) {
|
|
2429
|
+
var closestElementWithAttribute = $element.closest('[' + attributeName + ']');
|
|
2430
|
+
if (closestElementWithAttribute) {
|
|
2431
|
+
return closestElementWithAttribute.getAttribute(attributeName)
|
|
1606
2432
|
}
|
|
1607
2433
|
}
|
|
1608
2434
|
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
2435
|
+
/**
|
|
2436
|
+
* @constant
|
|
2437
|
+
* @type {CharacterCountTranslations}
|
|
2438
|
+
* @see Default value for {@link CharacterCountConfig.i18n}
|
|
2439
|
+
* @default
|
|
2440
|
+
*/
|
|
2441
|
+
var CHARACTER_COUNT_TRANSLATIONS = {
|
|
2442
|
+
// Characters
|
|
2443
|
+
charactersUnderLimit: {
|
|
2444
|
+
one: 'You have %{count} character remaining',
|
|
2445
|
+
other: 'You have %{count} characters remaining'
|
|
2446
|
+
},
|
|
2447
|
+
charactersAtLimit: 'You have 0 characters remaining',
|
|
2448
|
+
charactersOverLimit: {
|
|
2449
|
+
one: 'You have %{count} character too many',
|
|
2450
|
+
other: 'You have %{count} characters too many'
|
|
2451
|
+
},
|
|
2452
|
+
// Words
|
|
2453
|
+
wordsUnderLimit: {
|
|
2454
|
+
one: 'You have %{count} word remaining',
|
|
2455
|
+
other: 'You have %{count} words remaining'
|
|
2456
|
+
},
|
|
2457
|
+
wordsAtLimit: 'You have 0 words remaining',
|
|
2458
|
+
wordsOverLimit: {
|
|
2459
|
+
one: 'You have %{count} word too many',
|
|
2460
|
+
other: 'You have %{count} words too many'
|
|
2461
|
+
},
|
|
2462
|
+
textareaDescription: {
|
|
2463
|
+
other: ''
|
|
2464
|
+
}
|
|
1612
2465
|
};
|
|
1613
2466
|
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
2467
|
+
/**
|
|
2468
|
+
* JavaScript enhancements for the CharacterCount component
|
|
2469
|
+
*
|
|
2470
|
+
* Tracks the number of characters or words in the `.govuk-js-character-count`
|
|
2471
|
+
* `<textarea>` inside the element. Displays a message with the remaining number
|
|
2472
|
+
* of characters/words available, or the number of characters/words in excess.
|
|
2473
|
+
*
|
|
2474
|
+
* You can configure the message to only appear after a certain percentage
|
|
2475
|
+
* of the available characters/words has been entered.
|
|
2476
|
+
*
|
|
2477
|
+
* @class
|
|
2478
|
+
* @param {HTMLElement} $module - The element this component controls
|
|
2479
|
+
* @param {CharacterCountConfig} [config] - Character count config
|
|
2480
|
+
*/
|
|
2481
|
+
function CharacterCount ($module, config) {
|
|
2482
|
+
if (!$module) {
|
|
2483
|
+
return this
|
|
1623
2484
|
}
|
|
1624
2485
|
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
2486
|
+
var defaultConfig = {
|
|
2487
|
+
threshold: 0,
|
|
2488
|
+
i18n: CHARACTER_COUNT_TRANSLATIONS
|
|
2489
|
+
};
|
|
1628
2490
|
|
|
1629
|
-
// Read
|
|
1630
|
-
|
|
2491
|
+
// Read config set using dataset ('data-' values)
|
|
2492
|
+
var datasetConfig = normaliseDataset($module.dataset);
|
|
2493
|
+
|
|
2494
|
+
// To ensure data-attributes take complete precedence, even if they change the
|
|
2495
|
+
// type of count, we need to reset the `maxlength` and `maxwords` from the
|
|
2496
|
+
// JavaScript config.
|
|
2497
|
+
//
|
|
2498
|
+
// We can't mutate `config`, though, as it may be shared across multiple
|
|
2499
|
+
// components inside `initAll`.
|
|
2500
|
+
var configOverrides = {};
|
|
2501
|
+
if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
|
|
2502
|
+
configOverrides = {
|
|
2503
|
+
maxlength: false,
|
|
2504
|
+
maxwords: false
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
this.config = mergeConfigs(
|
|
2509
|
+
defaultConfig,
|
|
2510
|
+
config || {},
|
|
2511
|
+
configOverrides,
|
|
2512
|
+
datasetConfig
|
|
2513
|
+
);
|
|
2514
|
+
|
|
2515
|
+
this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), {
|
|
2516
|
+
// Read the fallback if necessary rather than have it set in the defaults
|
|
2517
|
+
locale: closestAttributeValue($module, 'lang')
|
|
2518
|
+
});
|
|
1631
2519
|
|
|
1632
2520
|
// Determine the limit attribute (characters or words)
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
2521
|
+
if (this.config.maxwords) {
|
|
2522
|
+
this.maxLength = this.config.maxwords;
|
|
2523
|
+
} else if (this.config.maxlength) {
|
|
2524
|
+
this.maxLength = this.config.maxlength;
|
|
2525
|
+
} else {
|
|
2526
|
+
return
|
|
1636
2527
|
}
|
|
1637
2528
|
|
|
1638
|
-
|
|
1639
|
-
this
|
|
2529
|
+
this.$module = $module;
|
|
2530
|
+
this.$textarea = $module.querySelector('.govuk-js-character-count');
|
|
2531
|
+
this.$visibleCountMessage = null;
|
|
2532
|
+
this.$screenReaderCountMessage = null;
|
|
2533
|
+
this.lastInputTimestamp = null;
|
|
2534
|
+
}
|
|
1640
2535
|
|
|
1641
|
-
|
|
1642
|
-
|
|
2536
|
+
/**
|
|
2537
|
+
* Initialise component
|
|
2538
|
+
*/
|
|
2539
|
+
CharacterCount.prototype.init = function () {
|
|
2540
|
+
// Check that required elements are present
|
|
2541
|
+
if (!this.$textarea) {
|
|
1643
2542
|
return
|
|
1644
2543
|
}
|
|
1645
2544
|
|
|
1646
|
-
|
|
1647
|
-
$
|
|
2545
|
+
var $textarea = this.$textarea;
|
|
2546
|
+
var $textareaDescription = document.getElementById($textarea.id + '-info');
|
|
1648
2547
|
|
|
1649
|
-
//
|
|
1650
|
-
//
|
|
1651
|
-
//
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
window.addEventListener('pageshow', this.sync.bind(this));
|
|
1655
|
-
} else {
|
|
1656
|
-
window.addEventListener('DOMContentLoaded', this.sync.bind(this));
|
|
2548
|
+
// Inject a decription for the textarea if none is present already
|
|
2549
|
+
// for when the component was rendered with no maxlength, maxwords
|
|
2550
|
+
// nor custom textareaDescriptionText
|
|
2551
|
+
if ($textareaDescription.innerText.match(/^\s*$/)) {
|
|
2552
|
+
$textareaDescription.innerText = this.i18n.t('textareaDescription', { count: this.maxLength });
|
|
1657
2553
|
}
|
|
1658
2554
|
|
|
1659
|
-
|
|
1660
|
-
|
|
2555
|
+
// Move the textarea description to be immediately after the textarea
|
|
2556
|
+
// Kept for backwards compatibility
|
|
2557
|
+
$textarea.insertAdjacentElement('afterend', $textareaDescription);
|
|
2558
|
+
|
|
2559
|
+
// Create the *screen reader* specific live-updating counter
|
|
2560
|
+
// This doesn't need any styling classes, as it is never visible
|
|
2561
|
+
var $screenReaderCountMessage = document.createElement('div');
|
|
2562
|
+
$screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden';
|
|
2563
|
+
$screenReaderCountMessage.setAttribute('aria-live', 'polite');
|
|
2564
|
+
this.$screenReaderCountMessage = $screenReaderCountMessage;
|
|
2565
|
+
$textareaDescription.insertAdjacentElement('afterend', $screenReaderCountMessage);
|
|
2566
|
+
|
|
2567
|
+
// Create our live-updating counter element, copying the classes from the
|
|
2568
|
+
// textarea description for backwards compatibility as these may have been
|
|
2569
|
+
// configured
|
|
2570
|
+
var $visibleCountMessage = document.createElement('div');
|
|
2571
|
+
$visibleCountMessage.className = $textareaDescription.className;
|
|
2572
|
+
$visibleCountMessage.classList.add('govuk-character-count__status');
|
|
2573
|
+
$visibleCountMessage.setAttribute('aria-hidden', 'true');
|
|
2574
|
+
this.$visibleCountMessage = $visibleCountMessage;
|
|
2575
|
+
$textareaDescription.insertAdjacentElement('afterend', $visibleCountMessage);
|
|
2576
|
+
|
|
2577
|
+
// Hide the textarea description
|
|
2578
|
+
$textareaDescription.classList.add('govuk-visually-hidden');
|
|
1661
2579
|
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
this.updateCountMessage();
|
|
1665
|
-
};
|
|
2580
|
+
// Remove hard limit if set
|
|
2581
|
+
$textarea.removeAttribute('maxlength');
|
|
1666
2582
|
|
|
1667
|
-
|
|
1668
|
-
CharacterCount.prototype.getDataset = function (element) {
|
|
1669
|
-
var dataset = {};
|
|
1670
|
-
var attributes = element.attributes;
|
|
1671
|
-
if (attributes) {
|
|
1672
|
-
for (var i = 0; i < attributes.length; i++) {
|
|
1673
|
-
var attribute = attributes[i];
|
|
1674
|
-
var match = attribute.name.match(/^data-(.+)/);
|
|
1675
|
-
if (match) {
|
|
1676
|
-
dataset[match[1]] = attribute.value;
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
return dataset
|
|
1681
|
-
};
|
|
2583
|
+
this.bindChangeEvents();
|
|
1682
2584
|
|
|
1683
|
-
//
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
2585
|
+
// When the page is restored after navigating 'back' in some browsers the
|
|
2586
|
+
// state of the character count is not restored until *after* the
|
|
2587
|
+
// DOMContentLoaded event is fired, so we need to manually update it after the
|
|
2588
|
+
// pageshow event in browsers that support it.
|
|
2589
|
+
if ('onpageshow' in window) {
|
|
2590
|
+
window.addEventListener('pageshow', this.updateCountMessage.bind(this));
|
|
1689
2591
|
} else {
|
|
1690
|
-
|
|
2592
|
+
window.addEventListener('DOMContentLoaded', this.updateCountMessage.bind(this));
|
|
1691
2593
|
}
|
|
1692
|
-
|
|
2594
|
+
this.updateCountMessage();
|
|
1693
2595
|
};
|
|
1694
2596
|
|
|
1695
|
-
|
|
2597
|
+
/**
|
|
2598
|
+
* Bind change events
|
|
2599
|
+
*
|
|
2600
|
+
* Set up event listeners on the $textarea so that the count messages update
|
|
2601
|
+
* when the user types.
|
|
2602
|
+
*/
|
|
1696
2603
|
CharacterCount.prototype.bindChangeEvents = function () {
|
|
1697
2604
|
var $textarea = this.$textarea;
|
|
1698
|
-
$textarea.addEventListener('keyup', this.
|
|
2605
|
+
$textarea.addEventListener('keyup', this.handleKeyUp.bind(this));
|
|
1699
2606
|
|
|
1700
2607
|
// Bind focus/blur events to start/stop polling
|
|
1701
2608
|
$textarea.addEventListener('focus', this.handleFocus.bind(this));
|
|
1702
2609
|
$textarea.addEventListener('blur', this.handleBlur.bind(this));
|
|
1703
2610
|
};
|
|
1704
2611
|
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
2612
|
+
/**
|
|
2613
|
+
* Handle key up event
|
|
2614
|
+
*
|
|
2615
|
+
* Update the visible character counter and keep track of when the last update
|
|
2616
|
+
* happened for each keypress
|
|
2617
|
+
*/
|
|
2618
|
+
CharacterCount.prototype.handleKeyUp = function () {
|
|
2619
|
+
this.updateVisibleCountMessage();
|
|
2620
|
+
this.lastInputTimestamp = Date.now();
|
|
2621
|
+
};
|
|
2622
|
+
|
|
2623
|
+
/**
|
|
2624
|
+
* Handle focus event
|
|
2625
|
+
*
|
|
2626
|
+
* Speech recognition software such as Dragon NaturallySpeaking will modify the
|
|
2627
|
+
* fields by directly changing its `value`. These changes don't trigger events
|
|
2628
|
+
* in JavaScript, so we need to poll to handle when and if they occur.
|
|
2629
|
+
*
|
|
2630
|
+
* Once the keyup event hasn't been detected for at least 1000 ms (1s), check if
|
|
2631
|
+
* the textarea value has changed and update the count message if it has.
|
|
2632
|
+
*
|
|
2633
|
+
* This is so that the update triggered by the manual comparison doesn't
|
|
2634
|
+
* conflict with debounced KeyboardEvent updates.
|
|
2635
|
+
*/
|
|
2636
|
+
CharacterCount.prototype.handleFocus = function () {
|
|
2637
|
+
this.valueChecker = setInterval(function () {
|
|
2638
|
+
if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
|
|
2639
|
+
this.updateIfValueChanged();
|
|
2640
|
+
}
|
|
2641
|
+
}.bind(this), 1000);
|
|
2642
|
+
};
|
|
2643
|
+
|
|
2644
|
+
/**
|
|
2645
|
+
* Handle blur event
|
|
2646
|
+
*
|
|
2647
|
+
* Stop checking the textarea value once the textarea no longer has focus
|
|
2648
|
+
*/
|
|
2649
|
+
CharacterCount.prototype.handleBlur = function () {
|
|
2650
|
+
// Cancel value checking on blur
|
|
2651
|
+
clearInterval(this.valueChecker);
|
|
2652
|
+
};
|
|
2653
|
+
|
|
2654
|
+
/**
|
|
2655
|
+
* Update count message if textarea value has changed
|
|
2656
|
+
*/
|
|
2657
|
+
CharacterCount.prototype.updateIfValueChanged = function () {
|
|
1709
2658
|
if (!this.$textarea.oldValue) this.$textarea.oldValue = '';
|
|
1710
2659
|
if (this.$textarea.value !== this.$textarea.oldValue) {
|
|
1711
2660
|
this.$textarea.oldValue = this.$textarea.value;
|
|
@@ -1713,66 +2662,225 @@ CharacterCount.prototype.checkIfValueChanged = function () {
|
|
|
1713
2662
|
}
|
|
1714
2663
|
};
|
|
1715
2664
|
|
|
1716
|
-
|
|
2665
|
+
/**
|
|
2666
|
+
* Update count message
|
|
2667
|
+
*
|
|
2668
|
+
* Helper function to update both the visible and screen reader-specific
|
|
2669
|
+
* counters simultaneously (e.g. on init)
|
|
2670
|
+
*/
|
|
1717
2671
|
CharacterCount.prototype.updateCountMessage = function () {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
2672
|
+
this.updateVisibleCountMessage();
|
|
2673
|
+
this.updateScreenReaderCountMessage();
|
|
2674
|
+
};
|
|
1721
2675
|
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
var
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
2676
|
+
/**
|
|
2677
|
+
* Update visible count message
|
|
2678
|
+
*/
|
|
2679
|
+
CharacterCount.prototype.updateVisibleCountMessage = function () {
|
|
2680
|
+
var $textarea = this.$textarea;
|
|
2681
|
+
var $visibleCountMessage = this.$visibleCountMessage;
|
|
2682
|
+
var remainingNumber = this.maxLength - this.count($textarea.value);
|
|
2683
|
+
|
|
2684
|
+
// If input is over the threshold, remove the disabled class which renders the
|
|
2685
|
+
// counter invisible.
|
|
2686
|
+
if (this.isOverThreshold()) {
|
|
2687
|
+
$visibleCountMessage.classList.remove('govuk-character-count__message--disabled');
|
|
1734
2688
|
} else {
|
|
1735
|
-
|
|
1736
|
-
// Ensure threshold is visible for users of assistive technologies
|
|
1737
|
-
countMessage.removeAttribute('aria-hidden');
|
|
2689
|
+
$visibleCountMessage.classList.add('govuk-character-count__message--disabled');
|
|
1738
2690
|
}
|
|
1739
2691
|
|
|
1740
2692
|
// Update styles
|
|
1741
2693
|
if (remainingNumber < 0) {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2694
|
+
$textarea.classList.add('govuk-textarea--error');
|
|
2695
|
+
$visibleCountMessage.classList.remove('govuk-hint');
|
|
2696
|
+
$visibleCountMessage.classList.add('govuk-error-message');
|
|
1745
2697
|
} else {
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
2698
|
+
$textarea.classList.remove('govuk-textarea--error');
|
|
2699
|
+
$visibleCountMessage.classList.remove('govuk-error-message');
|
|
2700
|
+
$visibleCountMessage.classList.add('govuk-hint');
|
|
1749
2701
|
}
|
|
1750
2702
|
|
|
1751
2703
|
// Update message
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
2704
|
+
$visibleCountMessage.innerText = this.getCountMessage();
|
|
2705
|
+
};
|
|
2706
|
+
|
|
2707
|
+
/**
|
|
2708
|
+
* Update screen reader count message
|
|
2709
|
+
*/
|
|
2710
|
+
CharacterCount.prototype.updateScreenReaderCountMessage = function () {
|
|
2711
|
+
var $screenReaderCountMessage = this.$screenReaderCountMessage;
|
|
2712
|
+
|
|
2713
|
+
// If over the threshold, remove the aria-hidden attribute, allowing screen
|
|
2714
|
+
// readers to announce the content of the element.
|
|
2715
|
+
if (this.isOverThreshold()) {
|
|
2716
|
+
$screenReaderCountMessage.removeAttribute('aria-hidden');
|
|
2717
|
+
} else {
|
|
2718
|
+
$screenReaderCountMessage.setAttribute('aria-hidden', true);
|
|
1757
2719
|
}
|
|
1758
|
-
charNoun = charNoun + ((remainingNumber === -1 || remainingNumber === 1) ? '' : 's');
|
|
1759
2720
|
|
|
1760
|
-
|
|
1761
|
-
|
|
2721
|
+
// Update message
|
|
2722
|
+
$screenReaderCountMessage.innerText = this.getCountMessage();
|
|
2723
|
+
};
|
|
1762
2724
|
|
|
1763
|
-
|
|
2725
|
+
/**
|
|
2726
|
+
* Count the number of characters (or words, if `config.maxwords` is set)
|
|
2727
|
+
* in the given text
|
|
2728
|
+
*
|
|
2729
|
+
* @param {string} text - The text to count the characters of
|
|
2730
|
+
* @returns {number} the number of characters (or words) in the text
|
|
2731
|
+
*/
|
|
2732
|
+
CharacterCount.prototype.count = function (text) {
|
|
2733
|
+
if (this.config.maxwords) {
|
|
2734
|
+
var tokens = text.match(/\S+/g) || []; // Matches consecutive non-whitespace chars
|
|
2735
|
+
return tokens.length
|
|
2736
|
+
} else {
|
|
2737
|
+
return text.length
|
|
2738
|
+
}
|
|
1764
2739
|
};
|
|
1765
2740
|
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2741
|
+
/**
|
|
2742
|
+
* Get count message
|
|
2743
|
+
*
|
|
2744
|
+
* @returns {string} Status message
|
|
2745
|
+
*/
|
|
2746
|
+
CharacterCount.prototype.getCountMessage = function () {
|
|
2747
|
+
var remainingNumber = this.maxLength - this.count(this.$textarea.value);
|
|
2748
|
+
|
|
2749
|
+
var countType = this.config.maxwords ? 'words' : 'characters';
|
|
2750
|
+
return this.formatCountMessage(remainingNumber, countType)
|
|
1769
2751
|
};
|
|
1770
2752
|
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
2753
|
+
/**
|
|
2754
|
+
* Formats the message shown to users according to what's counted
|
|
2755
|
+
* and how many remain
|
|
2756
|
+
*
|
|
2757
|
+
* @param {number} remainingNumber - The number of words/characaters remaining
|
|
2758
|
+
* @param {string} countType - "words" or "characters"
|
|
2759
|
+
* @returns {string} Status message
|
|
2760
|
+
*/
|
|
2761
|
+
CharacterCount.prototype.formatCountMessage = function (remainingNumber, countType) {
|
|
2762
|
+
if (remainingNumber === 0) {
|
|
2763
|
+
return this.i18n.t(countType + 'AtLimit')
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
var translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit';
|
|
2767
|
+
|
|
2768
|
+
return this.i18n.t(countType + translationKeySuffix, { count: Math.abs(remainingNumber) })
|
|
2769
|
+
};
|
|
2770
|
+
|
|
2771
|
+
/**
|
|
2772
|
+
* Check if count is over threshold
|
|
2773
|
+
*
|
|
2774
|
+
* Checks whether the value is over the configured threshold for the input.
|
|
2775
|
+
* If there is no configured threshold, it is set to 0 and this function will
|
|
2776
|
+
* always return true.
|
|
2777
|
+
*
|
|
2778
|
+
* @returns {boolean} true if the current count is over the config.threshold
|
|
2779
|
+
* (or no threshold is set)
|
|
2780
|
+
*/
|
|
2781
|
+
CharacterCount.prototype.isOverThreshold = function () {
|
|
2782
|
+
// No threshold means we're always above threshold so save some computation
|
|
2783
|
+
if (!this.config.threshold) {
|
|
2784
|
+
return true
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
var $textarea = this.$textarea;
|
|
2788
|
+
|
|
2789
|
+
// Determine the remaining number of characters/words
|
|
2790
|
+
var currentLength = this.count($textarea.value);
|
|
2791
|
+
var maxLength = this.maxLength;
|
|
2792
|
+
|
|
2793
|
+
var thresholdValue = maxLength * this.config.threshold / 100;
|
|
2794
|
+
|
|
2795
|
+
return (thresholdValue <= currentLength)
|
|
1774
2796
|
};
|
|
1775
2797
|
|
|
2798
|
+
/**
|
|
2799
|
+
* Character count config
|
|
2800
|
+
*
|
|
2801
|
+
* @typedef {CharacterCountConfigWithMaxLength | CharacterCountConfigWithMaxWords} CharacterCountConfig
|
|
2802
|
+
*/
|
|
2803
|
+
|
|
2804
|
+
/**
|
|
2805
|
+
* Character count config (with maximum number of characters)
|
|
2806
|
+
*
|
|
2807
|
+
* @typedef {object} CharacterCountConfigWithMaxLength
|
|
2808
|
+
* @property {number} [maxlength] - The maximum number of characters.
|
|
2809
|
+
* If maxwords is provided, the maxlength option will be ignored.
|
|
2810
|
+
* @property {number} [threshold = 0] - The percentage value of the limit at
|
|
2811
|
+
* which point the count message is displayed. If this attribute is set, the
|
|
2812
|
+
* count message will be hidden by default.
|
|
2813
|
+
* @property {CharacterCountTranslations} [i18n = CHARACTER_COUNT_TRANSLATIONS] - See constant {@link CHARACTER_COUNT_TRANSLATIONS}
|
|
2814
|
+
*/
|
|
2815
|
+
|
|
2816
|
+
/**
|
|
2817
|
+
* Character count config (with maximum number of words)
|
|
2818
|
+
*
|
|
2819
|
+
* @typedef {object} CharacterCountConfigWithMaxWords
|
|
2820
|
+
* @property {number} [maxwords] - The maximum number of words. If maxwords is
|
|
2821
|
+
* provided, the maxlength option will be ignored.
|
|
2822
|
+
* @property {number} [threshold = 0] - The percentage value of the limit at
|
|
2823
|
+
* which point the count message is displayed. If this attribute is set, the
|
|
2824
|
+
* count message will be hidden by default.
|
|
2825
|
+
* @property {CharacterCountTranslations} [i18n = CHARACTER_COUNT_TRANSLATIONS] - See constant {@link CHARACTER_COUNT_TRANSLATIONS}
|
|
2826
|
+
*/
|
|
2827
|
+
|
|
2828
|
+
/**
|
|
2829
|
+
* Character count translations
|
|
2830
|
+
*
|
|
2831
|
+
* @typedef {object} CharacterCountTranslations
|
|
2832
|
+
*
|
|
2833
|
+
* Messages shown to users as they type. It provides feedback on how many words
|
|
2834
|
+
* or characters they have remaining or if they are over the limit. This also
|
|
2835
|
+
* includes a message used as an accessible description for the textarea.
|
|
2836
|
+
* @property {TranslationPluralForms} [charactersUnderLimit] - Message displayed
|
|
2837
|
+
* when the number of characters is under the configured maximum, `maxlength`.
|
|
2838
|
+
* This message is displayed visually and through assistive technologies. The
|
|
2839
|
+
* component will replace the `%{count}` placeholder with the number of
|
|
2840
|
+
* remaining characters. This is a [pluralised list of
|
|
2841
|
+
* messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
|
|
2842
|
+
* @property {string} [charactersAtLimit] - Message displayed when the number of
|
|
2843
|
+
* characters reaches the configured maximum, `maxlength`. This message is
|
|
2844
|
+
* displayed visually and through assistive technologies.
|
|
2845
|
+
* @property {TranslationPluralForms} [charactersOverLimit] - Message displayed
|
|
2846
|
+
* when the number of characters is over the configured maximum, `maxlength`.
|
|
2847
|
+
* This message is displayed visually and through assistive technologies. The
|
|
2848
|
+
* component will replace the `%{count}` placeholder with the number of
|
|
2849
|
+
* remaining characters. This is a [pluralised list of
|
|
2850
|
+
* messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
|
|
2851
|
+
* @property {TranslationPluralForms} [wordsUnderLimit] - Message displayed when
|
|
2852
|
+
* the number of words is under the configured maximum, `maxlength`. This
|
|
2853
|
+
* message is displayed visually and through assistive technologies. The
|
|
2854
|
+
* component will replace the `%{count}` placeholder with the number of
|
|
2855
|
+
* remaining words. This is a [pluralised list of
|
|
2856
|
+
* messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
|
|
2857
|
+
* @property {string} [wordsAtLimit] - Message displayed when the number of
|
|
2858
|
+
* words reaches the configured maximum, `maxlength`. This message is
|
|
2859
|
+
* displayed visually and through assistive technologies.
|
|
2860
|
+
* @property {TranslationPluralForms} [wordsOverLimit] - Message displayed when
|
|
2861
|
+
* the number of words is over the configured maximum, `maxlength`. This
|
|
2862
|
+
* message is displayed visually and through assistive technologies. The
|
|
2863
|
+
* component will replace the `%{count}` placeholder with the number of
|
|
2864
|
+
* remaining words. This is a [pluralised list of
|
|
2865
|
+
* messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
|
|
2866
|
+
* @property {TranslationPluralForms} [textareaDescription] - Message made
|
|
2867
|
+
* available to assistive technologies, if none is already present in the
|
|
2868
|
+
* HTML, to describe that the component accepts only a limited amount of
|
|
2869
|
+
* content. It is visible on the page when JavaScript is unavailable. The
|
|
2870
|
+
* component will replace the `%{count}` placeholder with the value of the
|
|
2871
|
+
* `maxlength` or `maxwords` parameter.
|
|
2872
|
+
*/
|
|
2873
|
+
|
|
2874
|
+
/**
|
|
2875
|
+
* @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
|
|
2876
|
+
*/
|
|
2877
|
+
|
|
2878
|
+
/**
|
|
2879
|
+
* Checkboxes component
|
|
2880
|
+
*
|
|
2881
|
+
* @class
|
|
2882
|
+
* @param {HTMLElement} $module - HTML element to use for checkboxes
|
|
2883
|
+
*/
|
|
1776
2884
|
function Checkboxes ($module) {
|
|
1777
2885
|
this.$module = $module;
|
|
1778
2886
|
this.$inputs = $module.querySelectorAll('input[type="checkbox"]');
|
|
@@ -1842,7 +2950,7 @@ Checkboxes.prototype.syncAllConditionalReveals = function () {
|
|
|
1842
2950
|
* Synchronise the visibility of the conditional reveal, and its accessible
|
|
1843
2951
|
* state, with the input's checked state.
|
|
1844
2952
|
*
|
|
1845
|
-
* @param {HTMLInputElement} $input Checkbox input
|
|
2953
|
+
* @param {HTMLInputElement} $input - Checkbox input
|
|
1846
2954
|
*/
|
|
1847
2955
|
Checkboxes.prototype.syncConditionalRevealWithInputState = function ($input) {
|
|
1848
2956
|
var $target = document.getElementById($input.getAttribute('aria-controls'));
|
|
@@ -1900,7 +3008,7 @@ Checkboxes.prototype.unCheckExclusiveInputs = function ($input) {
|
|
|
1900
3008
|
* Handle a click within the $module – if the click occurred on a checkbox, sync
|
|
1901
3009
|
* the state of any associated conditional reveal with the checkbox state.
|
|
1902
3010
|
*
|
|
1903
|
-
* @param {MouseEvent} event Click event
|
|
3011
|
+
* @param {MouseEvent} event - Click event
|
|
1904
3012
|
*/
|
|
1905
3013
|
Checkboxes.prototype.handleClick = function (event) {
|
|
1906
3014
|
var $target = event.target;
|
|
@@ -1930,55 +3038,39 @@ Checkboxes.prototype.handleClick = function (event) {
|
|
|
1930
3038
|
}
|
|
1931
3039
|
};
|
|
1932
3040
|
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
3041
|
+
/**
|
|
3042
|
+
* JavaScript enhancements for the ErrorSummary
|
|
3043
|
+
*
|
|
3044
|
+
* Takes focus on initialisation for accessible announcement, unless disabled in configuration.
|
|
3045
|
+
*
|
|
3046
|
+
* @class
|
|
3047
|
+
* @param {HTMLElement} $module - The element this component controls
|
|
3048
|
+
* @param {ErrorSummaryConfig} config - Error summary config
|
|
3049
|
+
*/
|
|
3050
|
+
function ErrorSummary ($module, config) {
|
|
3051
|
+
// Some consuming code may not be passing a module,
|
|
3052
|
+
// for example if they initialise the component
|
|
3053
|
+
// on their own by directly passing the result
|
|
3054
|
+
// of `document.querySelector`.
|
|
3055
|
+
// To avoid breaking further JavaScript initialisation
|
|
3056
|
+
// we need to safeguard against this so things keep
|
|
3057
|
+
// working the same now we read the elements data attributes
|
|
3058
|
+
if (!$module) {
|
|
3059
|
+
// Little safety in case code gets ported as-is
|
|
3060
|
+
// into and ES6 class constructor, where the return value matters
|
|
3061
|
+
return this
|
|
3062
|
+
}
|
|
1947
3063
|
|
|
1948
|
-
|
|
1949
|
-
++index;
|
|
1950
|
-
}
|
|
3064
|
+
this.$module = $module;
|
|
1951
3065
|
|
|
1952
|
-
|
|
3066
|
+
var defaultConfig = {
|
|
3067
|
+
disableAutoFocus: false
|
|
1953
3068
|
};
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
(
|
|
1958
|
-
|
|
1959
|
-
// Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/detect.js
|
|
1960
|
-
var detect = (
|
|
1961
|
-
'document' in this && "closest" in document.documentElement
|
|
3069
|
+
this.config = mergeConfigs(
|
|
3070
|
+
defaultConfig,
|
|
3071
|
+
config || {},
|
|
3072
|
+
normaliseDataset($module.dataset)
|
|
1962
3073
|
);
|
|
1963
|
-
|
|
1964
|
-
if (detect) return
|
|
1965
|
-
|
|
1966
|
-
// Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/polyfill.js
|
|
1967
|
-
Element.prototype.closest = function closest(selector) {
|
|
1968
|
-
var node = this;
|
|
1969
|
-
|
|
1970
|
-
while (node) {
|
|
1971
|
-
if (node.matches(selector)) return node;
|
|
1972
|
-
else node = 'SVGElement' in window && node instanceof SVGElement ? node.parentNode : node.parentElement;
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
return null;
|
|
1976
|
-
};
|
|
1977
|
-
|
|
1978
|
-
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
|
1979
|
-
|
|
1980
|
-
function ErrorSummary ($module) {
|
|
1981
|
-
this.$module = $module;
|
|
1982
3074
|
}
|
|
1983
3075
|
|
|
1984
3076
|
ErrorSummary.prototype.init = function () {
|
|
@@ -1986,16 +3078,37 @@ ErrorSummary.prototype.init = function () {
|
|
|
1986
3078
|
if (!$module) {
|
|
1987
3079
|
return
|
|
1988
3080
|
}
|
|
1989
|
-
$module.focus();
|
|
1990
3081
|
|
|
3082
|
+
this.setFocus();
|
|
1991
3083
|
$module.addEventListener('click', this.handleClick.bind(this));
|
|
1992
3084
|
};
|
|
1993
3085
|
|
|
1994
3086
|
/**
|
|
1995
|
-
*
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
3087
|
+
* Focus the error summary
|
|
3088
|
+
*/
|
|
3089
|
+
ErrorSummary.prototype.setFocus = function () {
|
|
3090
|
+
var $module = this.$module;
|
|
3091
|
+
|
|
3092
|
+
if (this.config.disableAutoFocus) {
|
|
3093
|
+
return
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
// Set tabindex to -1 to make the element programmatically focusable, but
|
|
3097
|
+
// remove it on blur as the error summary doesn't need to be focused again.
|
|
3098
|
+
$module.setAttribute('tabindex', '-1');
|
|
3099
|
+
|
|
3100
|
+
$module.addEventListener('blur', function () {
|
|
3101
|
+
$module.removeAttribute('tabindex');
|
|
3102
|
+
});
|
|
3103
|
+
|
|
3104
|
+
$module.focus();
|
|
3105
|
+
};
|
|
3106
|
+
|
|
3107
|
+
/**
|
|
3108
|
+
* Click event handler
|
|
3109
|
+
*
|
|
3110
|
+
* @param {MouseEvent} event - Click event
|
|
3111
|
+
*/
|
|
1999
3112
|
ErrorSummary.prototype.handleClick = function (event) {
|
|
2000
3113
|
var target = event.target;
|
|
2001
3114
|
if (this.focusTarget(target)) {
|
|
@@ -2119,8 +3232,32 @@ ErrorSummary.prototype.getAssociatedLegendOrLabel = function ($input) {
|
|
|
2119
3232
|
$input.closest('label')
|
|
2120
3233
|
};
|
|
2121
3234
|
|
|
2122
|
-
|
|
3235
|
+
/**
|
|
3236
|
+
* Error summary config
|
|
3237
|
+
*
|
|
3238
|
+
* @typedef {object} ErrorSummaryConfig
|
|
3239
|
+
* @property {boolean} [disableAutoFocus = false] -
|
|
3240
|
+
* If set to `true` the error summary will not be focussed when the page loads.
|
|
3241
|
+
*/
|
|
3242
|
+
|
|
3243
|
+
/**
|
|
3244
|
+
* Notification Banner component
|
|
3245
|
+
*
|
|
3246
|
+
* @class
|
|
3247
|
+
* @param {HTMLElement} $module - HTML element to use for notification banner
|
|
3248
|
+
* @param {NotificationBannerConfig} config - Notification banner config
|
|
3249
|
+
*/
|
|
3250
|
+
function NotificationBanner ($module, config) {
|
|
2123
3251
|
this.$module = $module;
|
|
3252
|
+
|
|
3253
|
+
var defaultConfig = {
|
|
3254
|
+
disableAutoFocus: false
|
|
3255
|
+
};
|
|
3256
|
+
this.config = mergeConfigs(
|
|
3257
|
+
defaultConfig,
|
|
3258
|
+
config || {},
|
|
3259
|
+
normaliseDataset($module.dataset)
|
|
3260
|
+
);
|
|
2124
3261
|
}
|
|
2125
3262
|
|
|
2126
3263
|
/**
|
|
@@ -2149,7 +3286,7 @@ NotificationBanner.prototype.init = function () {
|
|
|
2149
3286
|
NotificationBanner.prototype.setFocus = function () {
|
|
2150
3287
|
var $module = this.$module;
|
|
2151
3288
|
|
|
2152
|
-
if (
|
|
3289
|
+
if (this.config.disableAutoFocus) {
|
|
2153
3290
|
return
|
|
2154
3291
|
}
|
|
2155
3292
|
|
|
@@ -2171,12 +3308,40 @@ NotificationBanner.prototype.setFocus = function () {
|
|
|
2171
3308
|
$module.focus();
|
|
2172
3309
|
};
|
|
2173
3310
|
|
|
3311
|
+
/**
|
|
3312
|
+
* Notification banner config
|
|
3313
|
+
*
|
|
3314
|
+
* @typedef {object} NotificationBannerConfig
|
|
3315
|
+
* @property {boolean} [disableAutoFocus = false] -
|
|
3316
|
+
* If set to `true` the notification banner will not be focussed when the page
|
|
3317
|
+
* loads. This only applies if the component has a `role` of `alert` – in
|
|
3318
|
+
* other cases the component will not be focused on page load, regardless of
|
|
3319
|
+
* this option.
|
|
3320
|
+
*/
|
|
3321
|
+
|
|
3322
|
+
/**
|
|
3323
|
+
* Header component
|
|
3324
|
+
*
|
|
3325
|
+
* @class
|
|
3326
|
+
* @param {HTMLElement} $module - HTML element to use for header
|
|
3327
|
+
*/
|
|
2174
3328
|
function Header ($module) {
|
|
2175
3329
|
this.$module = $module;
|
|
2176
3330
|
this.$menuButton = $module && $module.querySelector('.govuk-js-header-toggle');
|
|
2177
3331
|
this.$menu = this.$menuButton && $module.querySelector(
|
|
2178
3332
|
'#' + this.$menuButton.getAttribute('aria-controls')
|
|
2179
3333
|
);
|
|
3334
|
+
|
|
3335
|
+
// Save the opened/closed state for the nav in memory so that we can
|
|
3336
|
+
// accurately maintain state when the screen is changed from small to
|
|
3337
|
+
// big and back to small
|
|
3338
|
+
this.menuIsOpen = false;
|
|
3339
|
+
|
|
3340
|
+
// A global const for storing a matchMedia instance which we'll use to
|
|
3341
|
+
// detect when a screen size change happens. We set this later during the
|
|
3342
|
+
// init function and rely on it being null if the feature isn't available
|
|
3343
|
+
// to initially apply hidden attributes
|
|
3344
|
+
this.mql = null;
|
|
2180
3345
|
}
|
|
2181
3346
|
|
|
2182
3347
|
/**
|
|
@@ -2184,27 +3349,58 @@ function Header ($module) {
|
|
|
2184
3349
|
*
|
|
2185
3350
|
* Check for the presence of the header, menu and menu button – if any are
|
|
2186
3351
|
* missing then there's nothing to do so return early.
|
|
3352
|
+
* Feature sniff for and apply a matchMedia for desktop which will
|
|
3353
|
+
* trigger a state sync if the browser viewport moves between states. If
|
|
3354
|
+
* matchMedia isn't available, hide the menu button and present the "no js"
|
|
3355
|
+
* version of the menu to the user.
|
|
2187
3356
|
*/
|
|
2188
3357
|
Header.prototype.init = function () {
|
|
2189
3358
|
if (!this.$module || !this.$menuButton || !this.$menu) {
|
|
2190
3359
|
return
|
|
2191
3360
|
}
|
|
2192
3361
|
|
|
2193
|
-
|
|
2194
|
-
|
|
3362
|
+
if ('matchMedia' in window) {
|
|
3363
|
+
// Set the matchMedia to the govuk-frontend desktop breakpoint
|
|
3364
|
+
this.mql = window.matchMedia('(min-width: 48.0625em)');
|
|
3365
|
+
|
|
3366
|
+
if ('addEventListener' in this.mql) {
|
|
3367
|
+
this.mql.addEventListener('change', this.syncState.bind(this));
|
|
3368
|
+
} else {
|
|
3369
|
+
// addListener is a deprecated function, however addEventListener
|
|
3370
|
+
// isn't supported by IE or Safari. We therefore add this in as
|
|
3371
|
+
// a fallback for those browsers
|
|
3372
|
+
this.mql.addListener(this.syncState.bind(this));
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
this.syncState();
|
|
3376
|
+
this.$menuButton.addEventListener('click', this.handleMenuButtonClick.bind(this));
|
|
3377
|
+
} else {
|
|
3378
|
+
this.$menuButton.setAttribute('hidden', '');
|
|
3379
|
+
}
|
|
2195
3380
|
};
|
|
2196
3381
|
|
|
2197
3382
|
/**
|
|
2198
3383
|
* Sync menu state
|
|
2199
3384
|
*
|
|
2200
|
-
*
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
3385
|
+
* Uses the global variable menuIsOpen to correctly set the accessible and
|
|
3386
|
+
* visual states of the menu and the menu button.
|
|
3387
|
+
* Additionally will force the menu to be visible and the menu button to be
|
|
3388
|
+
* hidden if the matchMedia is triggered to desktop.
|
|
2204
3389
|
*/
|
|
2205
|
-
Header.prototype.syncState = function (
|
|
2206
|
-
this
|
|
2207
|
-
|
|
3390
|
+
Header.prototype.syncState = function () {
|
|
3391
|
+
if (this.mql.matches) {
|
|
3392
|
+
this.$menu.removeAttribute('hidden');
|
|
3393
|
+
this.$menuButton.setAttribute('hidden', '');
|
|
3394
|
+
} else {
|
|
3395
|
+
this.$menuButton.removeAttribute('hidden');
|
|
3396
|
+
this.$menuButton.setAttribute('aria-expanded', this.menuIsOpen);
|
|
3397
|
+
|
|
3398
|
+
if (this.menuIsOpen) {
|
|
3399
|
+
this.$menu.removeAttribute('hidden');
|
|
3400
|
+
} else {
|
|
3401
|
+
this.$menu.setAttribute('hidden', '');
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
2208
3404
|
};
|
|
2209
3405
|
|
|
2210
3406
|
/**
|
|
@@ -2214,10 +3410,16 @@ Header.prototype.syncState = function (isVisible) {
|
|
|
2214
3410
|
* sync the accessibility state and menu button state
|
|
2215
3411
|
*/
|
|
2216
3412
|
Header.prototype.handleMenuButtonClick = function () {
|
|
2217
|
-
|
|
2218
|
-
this.syncState(
|
|
3413
|
+
this.menuIsOpen = !this.menuIsOpen;
|
|
3414
|
+
this.syncState();
|
|
2219
3415
|
};
|
|
2220
3416
|
|
|
3417
|
+
/**
|
|
3418
|
+
* Radios component
|
|
3419
|
+
*
|
|
3420
|
+
* @class
|
|
3421
|
+
* @param {HTMLElement} $module - HTML element to use for radios
|
|
3422
|
+
*/
|
|
2221
3423
|
function Radios ($module) {
|
|
2222
3424
|
this.$module = $module;
|
|
2223
3425
|
this.$inputs = $module.querySelectorAll('input[type="radio"]');
|
|
@@ -2288,7 +3490,7 @@ Radios.prototype.syncAllConditionalReveals = function () {
|
|
|
2288
3490
|
* Synchronise the visibility of the conditional reveal, and its accessible
|
|
2289
3491
|
* state, with the input's checked state.
|
|
2290
3492
|
*
|
|
2291
|
-
* @param {HTMLInputElement} $input Radio input
|
|
3493
|
+
* @param {HTMLInputElement} $input - Radio input
|
|
2292
3494
|
*/
|
|
2293
3495
|
Radios.prototype.syncConditionalRevealWithInputState = function ($input) {
|
|
2294
3496
|
var $target = document.getElementById($input.getAttribute('aria-controls'));
|
|
@@ -2309,7 +3511,7 @@ Radios.prototype.syncConditionalRevealWithInputState = function ($input) {
|
|
|
2309
3511
|
* with the same name (because checking one radio could have un-checked a radio
|
|
2310
3512
|
* in another $module)
|
|
2311
3513
|
*
|
|
2312
|
-
* @param {MouseEvent} event Click event
|
|
3514
|
+
* @param {MouseEvent} event - Click event
|
|
2313
3515
|
*/
|
|
2314
3516
|
Radios.prototype.handleClick = function (event) {
|
|
2315
3517
|
var $clickedInput = event.target;
|
|
@@ -2333,6 +3535,12 @@ Radios.prototype.handleClick = function (event) {
|
|
|
2333
3535
|
}.bind(this));
|
|
2334
3536
|
};
|
|
2335
3537
|
|
|
3538
|
+
/**
|
|
3539
|
+
* Skip link component
|
|
3540
|
+
*
|
|
3541
|
+
* @class
|
|
3542
|
+
* @param {HTMLElement} $module - HTML element to use for skip link
|
|
3543
|
+
*/
|
|
2336
3544
|
function SkipLink ($module) {
|
|
2337
3545
|
this.$module = $module;
|
|
2338
3546
|
this.$linkedElement = null;
|
|
@@ -2358,10 +3566,10 @@ SkipLink.prototype.init = function () {
|
|
|
2358
3566
|
};
|
|
2359
3567
|
|
|
2360
3568
|
/**
|
|
2361
|
-
* Get linked element
|
|
2362
|
-
*
|
|
2363
|
-
* @returns {HTMLElement} $linkedElement - DOM element linked to from the skip link
|
|
2364
|
-
*/
|
|
3569
|
+
* Get linked element
|
|
3570
|
+
*
|
|
3571
|
+
* @returns {HTMLElement} $linkedElement - DOM element linked to from the skip link
|
|
3572
|
+
*/
|
|
2365
3573
|
SkipLink.prototype.getLinkedElement = function () {
|
|
2366
3574
|
var linkedElementId = this.getFragmentFromUrl();
|
|
2367
3575
|
|
|
@@ -2462,6 +3670,12 @@ SkipLink.prototype.getFragmentFromUrl = function () {
|
|
|
2462
3670
|
|
|
2463
3671
|
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
|
2464
3672
|
|
|
3673
|
+
/**
|
|
3674
|
+
* Tabs component
|
|
3675
|
+
*
|
|
3676
|
+
* @class
|
|
3677
|
+
* @param {HTMLElement} $module - HTML element to use for tabs
|
|
3678
|
+
*/
|
|
2465
3679
|
function Tabs ($module) {
|
|
2466
3680
|
this.$module = $module;
|
|
2467
3681
|
this.$tabs = $module.querySelectorAll('.govuk-tabs__tab');
|
|
@@ -2736,67 +3950,90 @@ Tabs.prototype.getHref = function ($tab) {
|
|
|
2736
3950
|
return hash
|
|
2737
3951
|
};
|
|
2738
3952
|
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
3953
|
+
/**
|
|
3954
|
+
* Initialise all components
|
|
3955
|
+
*
|
|
3956
|
+
* Use the `data-module` attributes to find, instantiate and init all of the
|
|
3957
|
+
* components provided as part of GOV.UK Frontend.
|
|
3958
|
+
*
|
|
3959
|
+
* @param {Config} [config] - Config for all components
|
|
3960
|
+
*/
|
|
3961
|
+
function initAll (config) {
|
|
3962
|
+
config = typeof config !== 'undefined' ? config : {};
|
|
2742
3963
|
|
|
2743
3964
|
// Allow the user to initialise GOV.UK Frontend in only certain sections of the page
|
|
2744
3965
|
// Defaults to the entire document if nothing is set.
|
|
2745
|
-
var scope = typeof
|
|
2746
|
-
|
|
2747
|
-
var $buttons = scope.querySelectorAll('[data-module="govuk-button"]');
|
|
2748
|
-
nodeListForEach($buttons, function ($button) {
|
|
2749
|
-
new Button($button).init();
|
|
2750
|
-
});
|
|
3966
|
+
var $scope = typeof config.scope !== 'undefined' ? config.scope : document;
|
|
2751
3967
|
|
|
2752
|
-
var $accordions = scope.querySelectorAll('[data-module="govuk-accordion"]');
|
|
3968
|
+
var $accordions = $scope.querySelectorAll('[data-module="govuk-accordion"]');
|
|
2753
3969
|
nodeListForEach($accordions, function ($accordion) {
|
|
2754
|
-
new Accordion($accordion).init();
|
|
3970
|
+
new Accordion($accordion, config.accordion).init();
|
|
2755
3971
|
});
|
|
2756
3972
|
|
|
2757
|
-
var $
|
|
2758
|
-
nodeListForEach($
|
|
2759
|
-
new
|
|
3973
|
+
var $buttons = $scope.querySelectorAll('[data-module="govuk-button"]');
|
|
3974
|
+
nodeListForEach($buttons, function ($button) {
|
|
3975
|
+
new Button($button, config.button).init();
|
|
2760
3976
|
});
|
|
2761
3977
|
|
|
2762
|
-
var $characterCounts = scope.querySelectorAll('[data-module="govuk-character-count"]');
|
|
3978
|
+
var $characterCounts = $scope.querySelectorAll('[data-module="govuk-character-count"]');
|
|
2763
3979
|
nodeListForEach($characterCounts, function ($characterCount) {
|
|
2764
|
-
new CharacterCount($characterCount).init();
|
|
3980
|
+
new CharacterCount($characterCount, config.characterCount).init();
|
|
2765
3981
|
});
|
|
2766
3982
|
|
|
2767
|
-
var $checkboxes = scope.querySelectorAll('[data-module="govuk-checkboxes"]');
|
|
3983
|
+
var $checkboxes = $scope.querySelectorAll('[data-module="govuk-checkboxes"]');
|
|
2768
3984
|
nodeListForEach($checkboxes, function ($checkbox) {
|
|
2769
3985
|
new Checkboxes($checkbox).init();
|
|
2770
3986
|
});
|
|
2771
3987
|
|
|
3988
|
+
var $details = $scope.querySelectorAll('[data-module="govuk-details"]');
|
|
3989
|
+
nodeListForEach($details, function ($detail) {
|
|
3990
|
+
new Details($detail).init();
|
|
3991
|
+
});
|
|
3992
|
+
|
|
2772
3993
|
// Find first error summary module to enhance.
|
|
2773
|
-
var $errorSummary = scope.querySelector('[data-module="govuk-error-summary"]');
|
|
2774
|
-
|
|
3994
|
+
var $errorSummary = $scope.querySelector('[data-module="govuk-error-summary"]');
|
|
3995
|
+
if ($errorSummary) {
|
|
3996
|
+
new ErrorSummary($errorSummary, config.errorSummary).init();
|
|
3997
|
+
}
|
|
2775
3998
|
|
|
2776
3999
|
// Find first header module to enhance.
|
|
2777
|
-
var $
|
|
2778
|
-
|
|
4000
|
+
var $header = $scope.querySelector('[data-module="govuk-header"]');
|
|
4001
|
+
if ($header) {
|
|
4002
|
+
new Header($header).init();
|
|
4003
|
+
}
|
|
2779
4004
|
|
|
2780
|
-
var $notificationBanners = scope.querySelectorAll('[data-module="govuk-notification-banner"]');
|
|
4005
|
+
var $notificationBanners = $scope.querySelectorAll('[data-module="govuk-notification-banner"]');
|
|
2781
4006
|
nodeListForEach($notificationBanners, function ($notificationBanner) {
|
|
2782
|
-
new NotificationBanner($notificationBanner).init();
|
|
4007
|
+
new NotificationBanner($notificationBanner, config.notificationBanner).init();
|
|
2783
4008
|
});
|
|
2784
4009
|
|
|
2785
|
-
var $radios = scope.querySelectorAll('[data-module="govuk-radios"]');
|
|
4010
|
+
var $radios = $scope.querySelectorAll('[data-module="govuk-radios"]');
|
|
2786
4011
|
nodeListForEach($radios, function ($radio) {
|
|
2787
4012
|
new Radios($radio).init();
|
|
2788
4013
|
});
|
|
2789
4014
|
|
|
2790
4015
|
// Find first skip link module to enhance.
|
|
2791
|
-
var $skipLink = scope.querySelector('[data-module="govuk-skip-link"]');
|
|
4016
|
+
var $skipLink = $scope.querySelector('[data-module="govuk-skip-link"]');
|
|
2792
4017
|
new SkipLink($skipLink).init();
|
|
2793
4018
|
|
|
2794
|
-
var $tabs = scope.querySelectorAll('[data-module="govuk-tabs"]');
|
|
4019
|
+
var $tabs = $scope.querySelectorAll('[data-module="govuk-tabs"]');
|
|
2795
4020
|
nodeListForEach($tabs, function ($tabs) {
|
|
2796
4021
|
new Tabs($tabs).init();
|
|
2797
4022
|
});
|
|
2798
4023
|
}
|
|
2799
4024
|
|
|
4025
|
+
/**
|
|
4026
|
+
* Config for all components
|
|
4027
|
+
*
|
|
4028
|
+
* @typedef {object} Config
|
|
4029
|
+
* @property {HTMLElement} [scope=document] - Scope to query for components
|
|
4030
|
+
* @property {import('./components/accordion/accordion.mjs').AccordionConfig} [accordion] - Accordion config
|
|
4031
|
+
* @property {import('./components/button/button.mjs').ButtonConfig} [button] - Button config
|
|
4032
|
+
* @property {import('./components/character-count/character-count.mjs').CharacterCountConfig} [characterCount] - Character Count config
|
|
4033
|
+
* @property {import('./components/error-summary/error-summary.mjs').ErrorSummaryConfig} [errorSummary] - Error Summary config
|
|
4034
|
+
* @property {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} [notificationBanner] - Notification Banner config
|
|
4035
|
+
*/
|
|
4036
|
+
|
|
2800
4037
|
exports.initAll = initAll;
|
|
2801
4038
|
exports.Accordion = Accordion;
|
|
2802
4039
|
exports.Button = Button;
|