govuk_publishing_components 32.0.0 → 33.0.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/app/assets/javascripts/component_guide/accessibility-test.js +0 -1
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-core.js +175 -0
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-ecommerce-tracker.js +1 -1
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-event-tracker.js +5 -13
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-link-tracker.js +80 -309
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-page-views.js +2 -2
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-specialist-link-tracker.js +140 -0
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/init-ga4.js +3 -0
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4.js +1 -0
- data/app/assets/javascripts/govuk_publishing_components/components/accordion.js +12 -1
- data/app/assets/javascripts/govuk_publishing_components/components/layout-super-navigation-header.js +13 -4
- data/app/assets/javascripts/govuk_publishing_components/components/single-page-notification-button.js +24 -8
- data/app/assets/javascripts/govuk_publishing_components/vendor/lux/lux-reporter.js +83 -86
- data/app/assets/stylesheets/govuk_publishing_components/components/_big-number.scss +2 -5
- data/app/assets/stylesheets/govuk_publishing_components/components/_image-card.scss +1 -5
- data/app/assets/stylesheets/govuk_publishing_components/components/_input.scss +3 -5
- data/app/assets/stylesheets/govuk_publishing_components/components/_layout-super-navigation-header.scss +10 -30
- data/app/assets/stylesheets/govuk_publishing_components/components/_search.scss +0 -7
- data/app/assets/stylesheets/govuk_publishing_components/components/_share-links.scss +0 -6
- data/app/views/govuk_publishing_components/components/_accordion.html.erb +14 -1
- data/app/views/govuk_publishing_components/components/_error_summary.html.erb +27 -26
- data/app/views/govuk_publishing_components/components/_layout_super_navigation_header.html.erb +2 -2
- data/app/views/govuk_publishing_components/components/_phase_banner.html.erb +1 -1
- data/app/views/govuk_publishing_components/components/_share_links.html.erb +18 -15
- data/app/views/govuk_publishing_components/components/_single_page_notification_button.html.erb +1 -1
- data/app/views/govuk_publishing_components/components/docs/accordion.yml +15 -3
- data/app/views/govuk_publishing_components/components/docs/button.yml +10 -0
- data/app/views/govuk_publishing_components/components/docs/share_links.yml +59 -30
- data/app/views/govuk_publishing_components/components/docs/single_page_notification_button.yml +10 -1
- data/app/views/govuk_publishing_components/components/feedback/_yes_no_banner.html.erb +3 -3
- data/config/locales/ar.yml +4 -1
- data/config/locales/az.yml +4 -1
- data/config/locales/be.yml +4 -1
- data/config/locales/bg.yml +4 -1
- data/config/locales/bn.yml +4 -1
- data/config/locales/cs.yml +4 -1
- data/config/locales/cy.yml +4 -1
- data/config/locales/da.yml +4 -1
- data/config/locales/de.yml +4 -1
- data/config/locales/dr.yml +4 -1
- data/config/locales/el.yml +4 -1
- data/config/locales/en.yml +20 -17
- data/config/locales/es-419.yml +4 -1
- data/config/locales/es.yml +4 -1
- data/config/locales/et.yml +4 -1
- data/config/locales/fa.yml +4 -1
- data/config/locales/fi.yml +4 -1
- data/config/locales/fr.yml +4 -1
- data/config/locales/gd.yml +4 -1
- data/config/locales/gu.yml +4 -1
- data/config/locales/he.yml +4 -1
- data/config/locales/hi.yml +4 -1
- data/config/locales/hr.yml +4 -1
- data/config/locales/hu.yml +4 -1
- data/config/locales/hy.yml +4 -1
- data/config/locales/id.yml +4 -1
- data/config/locales/is.yml +4 -1
- data/config/locales/it.yml +4 -1
- data/config/locales/ja.yml +4 -1
- data/config/locales/ka.yml +4 -1
- data/config/locales/kk.yml +4 -1
- data/config/locales/ko.yml +4 -1
- data/config/locales/lt.yml +4 -1
- data/config/locales/lv.yml +4 -1
- data/config/locales/ms.yml +4 -1
- data/config/locales/mt.yml +4 -1
- data/config/locales/nl.yml +4 -1
- data/config/locales/no.yml +4 -1
- data/config/locales/pa-pk.yml +4 -1
- data/config/locales/pa.yml +4 -1
- data/config/locales/pl.yml +4 -1
- data/config/locales/ps.yml +4 -1
- data/config/locales/pt.yml +4 -1
- data/config/locales/ro.yml +4 -1
- data/config/locales/ru.yml +4 -1
- data/config/locales/si.yml +4 -1
- data/config/locales/sk.yml +4 -1
- data/config/locales/sl.yml +4 -1
- data/config/locales/so.yml +4 -1
- data/config/locales/sq.yml +4 -1
- data/config/locales/sr.yml +4 -1
- data/config/locales/sv.yml +4 -1
- data/config/locales/sw.yml +4 -1
- data/config/locales/ta.yml +4 -1
- data/config/locales/th.yml +4 -1
- data/config/locales/tk.yml +4 -1
- data/config/locales/tr.yml +4 -1
- data/config/locales/uk.yml +4 -1
- data/config/locales/ur.yml +4 -1
- data/config/locales/uz.yml +4 -1
- data/config/locales/vi.yml +4 -1
- data/config/locales/zh-hk.yml +4 -1
- data/config/locales/zh-tw.yml +4 -1
- data/config/locales/zh.yml +4 -1
- data/lib/govuk_publishing_components/presenters/button_helper.rb +7 -1
- data/lib/govuk_publishing_components/presenters/single_page_notification_button_helper.rb +25 -1
- data/lib/govuk_publishing_components/version.rb +1 -1
- data/node_modules/axe-core/axe.js +4567 -4678
- data/node_modules/axe-core/axe.min.js +2 -2
- data/node_modules/axe-core/package.json +2 -2
- data/node_modules/axe-core/sri-history.json +8 -0
- data/node_modules/govuk-frontend/README.md +1 -2
- data/node_modules/govuk-frontend/govuk/all.js +1398 -273
- 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/accordion/accordion.js +753 -25
- data/node_modules/govuk-frontend/govuk/components/accordion/fixtures.json +54 -22
- data/node_modules/govuk-frontend/govuk/components/accordion/macro-options.json +36 -0
- data/node_modules/govuk-frontend/govuk/components/accordion/template.njk +7 -1
- data/node_modules/govuk-frontend/govuk/components/back-link/fixtures.json +12 -12
- data/node_modules/govuk-frontend/govuk/components/breadcrumbs/fixtures.json +22 -22
- data/node_modules/govuk-frontend/govuk/components/button/_index.scss +23 -5
- data/node_modules/govuk-frontend/govuk/components/button/button.js +365 -107
- data/node_modules/govuk-frontend/govuk/components/button/fixtures.json +85 -66
- data/node_modules/govuk-frontend/govuk/components/button/template.njk +1 -1
- 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 +1033 -121
- data/node_modules/govuk-frontend/govuk/components/character-count/fixtures.json +112 -36
- data/node_modules/govuk-frontend/govuk/components/character-count/macro-options.json +42 -0
- data/node_modules/govuk-frontend/govuk/components/character-count/template.njk +27 -3
- data/node_modules/govuk-frontend/govuk/components/checkboxes/checkboxes.js +30 -2
- data/node_modules/govuk-frontend/govuk/components/checkboxes/fixtures.json +96 -93
- data/node_modules/govuk-frontend/govuk/components/cookie-banner/fixtures.json +46 -46
- data/node_modules/govuk-frontend/govuk/components/date-input/fixtures.json +50 -50
- data/node_modules/govuk-frontend/govuk/components/details/details.js +43 -13
- data/node_modules/govuk-frontend/govuk/components/details/fixtures.json +20 -20
- data/node_modules/govuk-frontend/govuk/components/error-message/fixtures.json +20 -20
- data/node_modules/govuk-frontend/govuk/components/error-summary/error-summary.js +268 -6
- data/node_modules/govuk-frontend/govuk/components/error-summary/fixtures.json +44 -35
- data/node_modules/govuk-frontend/govuk/components/error-summary/template.njk +25 -21
- data/node_modules/govuk-frontend/govuk/components/fieldset/fixtures.json +51 -39
- data/node_modules/govuk-frontend/govuk/components/file-upload/fixtures.json +26 -26
- data/node_modules/govuk-frontend/govuk/components/footer/_index.scss +1 -1
- data/node_modules/govuk-frontend/govuk/components/footer/fixtures.json +46 -46
- data/node_modules/govuk-frontend/govuk/components/footer/macro-options.json +2 -2
- data/node_modules/govuk-frontend/govuk/components/header/fixtures.json +93 -38
- data/node_modules/govuk-frontend/govuk/components/header/header.js +6 -0
- data/node_modules/govuk-frontend/govuk/components/header/macro-options.json +8 -2
- data/node_modules/govuk-frontend/govuk/components/header/template.njk +4 -2
- data/node_modules/govuk-frontend/govuk/components/hint/fixtures.json +12 -12
- data/node_modules/govuk-frontend/govuk/components/input/fixtures.json +80 -80
- data/node_modules/govuk-frontend/govuk/components/inset-text/fixtures.json +12 -12
- data/node_modules/govuk-frontend/govuk/components/label/fixtures.json +34 -34
- data/node_modules/govuk-frontend/govuk/components/notification-banner/fixtures.json +56 -46
- data/node_modules/govuk-frontend/govuk/components/notification-banner/notification-banner.js +252 -2
- data/node_modules/govuk-frontend/govuk/components/notification-banner/template.njk +1 -1
- data/node_modules/govuk-frontend/govuk/components/pagination/_index.scss +10 -7
- data/node_modules/govuk-frontend/govuk/components/pagination/fixtures.json +33 -26
- data/node_modules/govuk-frontend/govuk/components/panel/fixtures.json +18 -18
- data/node_modules/govuk-frontend/govuk/components/phase-banner/fixtures.json +14 -14
- data/node_modules/govuk-frontend/govuk/components/radios/fixtures.json +94 -91
- data/node_modules/govuk-frontend/govuk/components/radios/radios.js +30 -2
- data/node_modules/govuk-frontend/govuk/components/select/fixtures.json +32 -32
- data/node_modules/govuk-frontend/govuk/components/skip-link/fixtures.json +22 -20
- data/node_modules/govuk-frontend/govuk/components/skip-link/skip-link.js +10 -4
- data/node_modules/govuk-frontend/govuk/components/summary-list/fixtures.json +50 -50
- data/node_modules/govuk-frontend/govuk/components/table/_index.scss +1 -1
- data/node_modules/govuk-frontend/govuk/components/table/fixtures.json +40 -40
- data/node_modules/govuk-frontend/govuk/components/tabs/fixtures.json +29 -29
- data/node_modules/govuk-frontend/govuk/components/tabs/tabs.js +28 -0
- data/node_modules/govuk-frontend/govuk/components/tag/fixtures.json +28 -28
- data/node_modules/govuk-frontend/govuk/components/textarea/fixtures.json +34 -34
- data/node_modules/govuk-frontend/govuk/components/warning-text/fixtures.json +14 -14
- data/node_modules/govuk-frontend/govuk/core/_section-break.scss +1 -1
- data/node_modules/govuk-frontend/govuk/helpers/_colour.scss +2 -2
- data/node_modules/govuk-frontend/govuk/helpers/_links.scss +6 -6
- data/node_modules/govuk-frontend/govuk/i18n.js +390 -0
- data/node_modules/govuk-frontend/govuk/macros/i18n.njk +15 -0
- 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/_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/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-esm/all.mjs +50 -27
- data/node_modules/govuk-frontend/govuk-esm/common/closest-attribute-value.mjs +15 -0
- data/node_modules/govuk-frontend/govuk-esm/common/index.mjs +159 -0
- data/node_modules/govuk-frontend/govuk-esm/common/normalise-dataset.mjs +58 -0
- data/node_modules/govuk-frontend/govuk-esm/common.mjs +6 -28
- data/node_modules/govuk-frontend/govuk-esm/components/accordion/accordion.mjs +113 -43
- data/node_modules/govuk-frontend/govuk-esm/components/button/button.mjs +67 -30
- data/node_modules/govuk-frontend/govuk-esm/components/character-count/character-count.mjs +325 -123
- data/node_modules/govuk-frontend/govuk-esm/components/checkboxes/checkboxes.mjs +9 -3
- data/node_modules/govuk-frontend/govuk-esm/components/details/details.mjs +22 -8
- data/node_modules/govuk-frontend/govuk-esm/components/error-summary/error-summary.mjs +48 -6
- data/node_modules/govuk-frontend/govuk-esm/components/header/header.mjs +6 -0
- data/node_modules/govuk-frontend/govuk-esm/components/notification-banner/notification-banner.mjs +32 -2
- data/node_modules/govuk-frontend/govuk-esm/components/radios/radios.mjs +9 -3
- data/node_modules/govuk-frontend/govuk-esm/components/skip-link/skip-link.mjs +10 -4
- data/node_modules/govuk-frontend/govuk-esm/components/tabs/tabs.mjs +8 -2
- data/node_modules/govuk-frontend/govuk-esm/i18n.mjs +380 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Date/now.mjs +13 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/Element/prototype/dataset.mjs +68 -0
- data/node_modules/govuk-frontend/govuk-esm/vendor/polyfills/String/prototype/trim.mjs +13 -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/node_modules/govuk-frontend/govuk-prototype-kit.config.json +138 -7
- data/node_modules/govuk-frontend/package.json +1 -1
- metadata +22 -3
@@ -4,6 +4,20 @@
|
|
4
4
|
(global.GOVUKFrontend = global.GOVUKFrontend || {}, global.GOVUKFrontend.CharacterCount = factory());
|
5
5
|
}(this, (function () { 'use strict';
|
6
6
|
|
7
|
+
(function(undefined) {
|
8
|
+
|
9
|
+
// Detection from https://github.com/Financial-Times/polyfill-library/blob/v3.111.0/polyfills/Date/now/detect.js
|
10
|
+
var detect = ('Date' in self && 'now' in self.Date && 'getTime' in self.Date.prototype);
|
11
|
+
|
12
|
+
if (detect) return
|
13
|
+
|
14
|
+
// Polyfill from https://polyfill.io/v3/polyfill.js?version=3.111.0&features=Date.now&flags=always
|
15
|
+
Date.now = function () {
|
16
|
+
return new Date().getTime();
|
17
|
+
};
|
18
|
+
|
19
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
20
|
+
|
7
21
|
(function(undefined) {
|
8
22
|
|
9
23
|
// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Object/defineProperty/detect.js
|
@@ -1014,7 +1028,801 @@ if (detect) return
|
|
1014
1028
|
|
1015
1029
|
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
1016
1030
|
|
1017
|
-
|
1031
|
+
/**
|
1032
|
+
* Common helpers which do not require polyfill.
|
1033
|
+
*
|
1034
|
+
* IMPORTANT: If a helper require a polyfill, please isolate it in its own module
|
1035
|
+
* so that the polyfill can be properly tree-shaken and does not burden
|
1036
|
+
* the components that do not need that helper
|
1037
|
+
*
|
1038
|
+
* @module common/index
|
1039
|
+
*/
|
1040
|
+
|
1041
|
+
/**
|
1042
|
+
* Config flattening function
|
1043
|
+
*
|
1044
|
+
* Takes any number of objects, flattens them into namespaced key-value pairs,
|
1045
|
+
* (e.g. {'i18n.showSection': 'Show section'}) and combines them together, with
|
1046
|
+
* greatest priority on the LAST item passed in.
|
1047
|
+
*
|
1048
|
+
* @returns {object} A flattened object of key-value pairs.
|
1049
|
+
*/
|
1050
|
+
function mergeConfigs (/* configObject1, configObject2, ...configObjects */) {
|
1051
|
+
/**
|
1052
|
+
* Function to take nested objects and flatten them to a dot-separated keyed
|
1053
|
+
* object. Doing this means we don't need to do any deep/recursive merging of
|
1054
|
+
* each of our objects, nor transform our dataset from a flat list into a
|
1055
|
+
* nested object.
|
1056
|
+
*
|
1057
|
+
* @param {object} configObject - Deeply nested object
|
1058
|
+
* @returns {object} Flattened object with dot-separated keys
|
1059
|
+
*/
|
1060
|
+
var flattenObject = function (configObject) {
|
1061
|
+
// Prepare an empty return object
|
1062
|
+
var flattenedObject = {};
|
1063
|
+
|
1064
|
+
// Our flattening function, this is called recursively for each level of
|
1065
|
+
// depth in the object. At each level we prepend the previous level names to
|
1066
|
+
// the key using `prefix`.
|
1067
|
+
var flattenLoop = function (obj, prefix) {
|
1068
|
+
// Loop through keys...
|
1069
|
+
for (var key in obj) {
|
1070
|
+
// Check to see if this is a prototypical key/value,
|
1071
|
+
// if it is, skip it.
|
1072
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
|
1073
|
+
continue
|
1074
|
+
}
|
1075
|
+
var value = obj[key];
|
1076
|
+
var prefixedKey = prefix ? prefix + '.' + key : key;
|
1077
|
+
if (typeof value === 'object') {
|
1078
|
+
// If the value is a nested object, recurse over that too
|
1079
|
+
flattenLoop(value, prefixedKey);
|
1080
|
+
} else {
|
1081
|
+
// Otherwise, add this value to our return object
|
1082
|
+
flattenedObject[prefixedKey] = value;
|
1083
|
+
}
|
1084
|
+
}
|
1085
|
+
};
|
1086
|
+
|
1087
|
+
// Kick off the recursive loop
|
1088
|
+
flattenLoop(configObject);
|
1089
|
+
return flattenedObject
|
1090
|
+
};
|
1091
|
+
|
1092
|
+
// Start with an empty object as our base
|
1093
|
+
var formattedConfigObject = {};
|
1094
|
+
|
1095
|
+
// Loop through each of the remaining passed objects and push their keys
|
1096
|
+
// one-by-one into configObject. Any duplicate keys will override the existing
|
1097
|
+
// key with the new value.
|
1098
|
+
for (var i = 0; i < arguments.length; i++) {
|
1099
|
+
var obj = flattenObject(arguments[i]);
|
1100
|
+
for (var key in obj) {
|
1101
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
1102
|
+
formattedConfigObject[key] = obj[key];
|
1103
|
+
}
|
1104
|
+
}
|
1105
|
+
}
|
1106
|
+
|
1107
|
+
return formattedConfigObject
|
1108
|
+
}
|
1109
|
+
|
1110
|
+
/**
|
1111
|
+
* Extracts keys starting with a particular namespace from a flattened config
|
1112
|
+
* object, removing the namespace in the process.
|
1113
|
+
*
|
1114
|
+
* @param {object} configObject - The object to extract key-value pairs from.
|
1115
|
+
* @param {string} namespace - The namespace to filter keys with.
|
1116
|
+
* @returns {object} Flattened object with dot-separated key namespace removed
|
1117
|
+
*/
|
1118
|
+
function extractConfigByNamespace (configObject, namespace) {
|
1119
|
+
// Check we have what we need
|
1120
|
+
if (!configObject || typeof configObject !== 'object') {
|
1121
|
+
throw new Error('Provide a `configObject` of type "object".')
|
1122
|
+
}
|
1123
|
+
if (!namespace || typeof namespace !== 'string') {
|
1124
|
+
throw new Error('Provide a `namespace` of type "string" to filter the `configObject` by.')
|
1125
|
+
}
|
1126
|
+
var newObject = {};
|
1127
|
+
for (var key in configObject) {
|
1128
|
+
// Split the key into parts, using . as our namespace separator
|
1129
|
+
var keyParts = key.split('.');
|
1130
|
+
// Check if the first namespace matches the configured namespace
|
1131
|
+
if (Object.prototype.hasOwnProperty.call(configObject, key) && keyParts[0] === namespace) {
|
1132
|
+
// Remove the first item (the namespace) from the parts array,
|
1133
|
+
// but only if there is more than one part (we don't want blank keys!)
|
1134
|
+
if (keyParts.length > 1) {
|
1135
|
+
keyParts.shift();
|
1136
|
+
}
|
1137
|
+
// Join the remaining parts back together
|
1138
|
+
var newKey = keyParts.join('.');
|
1139
|
+
// Add them to our new object
|
1140
|
+
newObject[newKey] = configObject[key];
|
1141
|
+
}
|
1142
|
+
}
|
1143
|
+
return newObject
|
1144
|
+
}
|
1145
|
+
|
1146
|
+
/**
|
1147
|
+
* @callback nodeListIterator
|
1148
|
+
* @param {Element} value - The current node being iterated on
|
1149
|
+
* @param {number} index - The current index in the iteration
|
1150
|
+
* @param {NodeListOf<Element>} nodes - NodeList from querySelectorAll()
|
1151
|
+
* @returns {undefined}
|
1152
|
+
*/
|
1153
|
+
|
1154
|
+
/**
|
1155
|
+
* Internal support for selecting messages to render, with placeholder
|
1156
|
+
* interpolation and locale-aware number formatting and pluralisation
|
1157
|
+
*
|
1158
|
+
* @class
|
1159
|
+
* @private
|
1160
|
+
* @param {TranslationsFlattened} translations - Key-value pairs of the translation strings to use.
|
1161
|
+
* @param {object} [config] - Configuration options for the function.
|
1162
|
+
* @param {string} config.locale - An overriding locale for the PluralRules functionality.
|
1163
|
+
*/
|
1164
|
+
function I18n (translations, config) {
|
1165
|
+
// Make list of translations available throughout function
|
1166
|
+
this.translations = translations || {};
|
1167
|
+
|
1168
|
+
// The locale to use for PluralRules and NumberFormat
|
1169
|
+
this.locale = (config && config.locale) || document.documentElement.lang || 'en';
|
1170
|
+
}
|
1171
|
+
|
1172
|
+
/**
|
1173
|
+
* The most used function - takes the key for a given piece of UI text and
|
1174
|
+
* returns the appropriate string.
|
1175
|
+
*
|
1176
|
+
* @param {string} lookupKey - The lookup key of the string to use.
|
1177
|
+
* @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
|
1178
|
+
* @returns {string} The appropriate translation string.
|
1179
|
+
*/
|
1180
|
+
I18n.prototype.t = function (lookupKey, options) {
|
1181
|
+
if (!lookupKey) {
|
1182
|
+
// Print a console error if no lookup key has been provided
|
1183
|
+
throw new Error('i18n: lookup key missing')
|
1184
|
+
}
|
1185
|
+
|
1186
|
+
// If the `count` option is set, determine which plural suffix is needed and
|
1187
|
+
// change the lookupKey to match. We check to see if it's undefined instead of
|
1188
|
+
// falsy, as this could legitimately be 0.
|
1189
|
+
if (options && typeof options.count !== 'undefined') {
|
1190
|
+
// Get the plural suffix
|
1191
|
+
lookupKey = lookupKey + '.' + this.getPluralSuffix(lookupKey, options.count);
|
1192
|
+
}
|
1193
|
+
|
1194
|
+
if (lookupKey in this.translations) {
|
1195
|
+
// Fetch the translation string for that lookup key
|
1196
|
+
var translationString = this.translations[lookupKey];
|
1197
|
+
|
1198
|
+
// Check for ${} placeholders in the translation string
|
1199
|
+
if (translationString.match(/%{(.\S+)}/)) {
|
1200
|
+
if (!options) {
|
1201
|
+
throw new Error('i18n: cannot replace placeholders in string if no option data provided')
|
1202
|
+
}
|
1203
|
+
|
1204
|
+
return this.replacePlaceholders(translationString, options)
|
1205
|
+
} else {
|
1206
|
+
return translationString
|
1207
|
+
}
|
1208
|
+
} else {
|
1209
|
+
// If the key wasn't found in our translations object,
|
1210
|
+
// return the lookup key itself as the fallback
|
1211
|
+
return lookupKey
|
1212
|
+
}
|
1213
|
+
};
|
1214
|
+
|
1215
|
+
/**
|
1216
|
+
* Takes a translation string with placeholders, and replaces the placeholders
|
1217
|
+
* with the provided data
|
1218
|
+
*
|
1219
|
+
* @param {string} translationString - The translation string
|
1220
|
+
* @param {object} options - Any options passed with the translation string, e.g: for string interpolation.
|
1221
|
+
* @returns {string} The translation string to output, with ${} placeholders replaced
|
1222
|
+
*/
|
1223
|
+
I18n.prototype.replacePlaceholders = function (translationString, options) {
|
1224
|
+
var formatter;
|
1225
|
+
|
1226
|
+
if (this.hasIntlNumberFormatSupport()) {
|
1227
|
+
formatter = new Intl.NumberFormat(this.locale);
|
1228
|
+
}
|
1229
|
+
|
1230
|
+
return translationString.replace(/%{(.\S+)}/g, function (placeholderWithBraces, placeholderKey) {
|
1231
|
+
if (Object.prototype.hasOwnProperty.call(options, placeholderKey)) {
|
1232
|
+
var placeholderValue = options[placeholderKey];
|
1233
|
+
|
1234
|
+
// If a user has passed `false` as the value for the placeholder
|
1235
|
+
// treat it as though the value should not be displayed
|
1236
|
+
if (placeholderValue === false) {
|
1237
|
+
return ''
|
1238
|
+
}
|
1239
|
+
|
1240
|
+
// If the placeholder's value is a number, localise the number formatting
|
1241
|
+
if (typeof placeholderValue === 'number' && formatter) {
|
1242
|
+
return formatter.format(placeholderValue)
|
1243
|
+
}
|
1244
|
+
|
1245
|
+
return placeholderValue
|
1246
|
+
} else {
|
1247
|
+
throw new Error('i18n: no data found to replace ' + placeholderWithBraces + ' placeholder in string')
|
1248
|
+
}
|
1249
|
+
})
|
1250
|
+
};
|
1251
|
+
|
1252
|
+
/**
|
1253
|
+
* Check to see if the browser supports Intl and Intl.PluralRules.
|
1254
|
+
*
|
1255
|
+
* It requires all conditions to be met in order to be supported:
|
1256
|
+
* - The browser supports the Intl class (true in IE11)
|
1257
|
+
* - The implementation of Intl supports PluralRules (NOT true in IE11)
|
1258
|
+
* - The browser/OS has plural rules for the current locale (browser dependent)
|
1259
|
+
*
|
1260
|
+
* @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
|
1261
|
+
*/
|
1262
|
+
I18n.prototype.hasIntlPluralRulesSupport = function () {
|
1263
|
+
return Boolean(window.Intl && ('PluralRules' in window.Intl && Intl.PluralRules.supportedLocalesOf(this.locale).length))
|
1264
|
+
};
|
1265
|
+
|
1266
|
+
/**
|
1267
|
+
* Check to see if the browser supports Intl and Intl.NumberFormat.
|
1268
|
+
*
|
1269
|
+
* It requires all conditions to be met in order to be supported:
|
1270
|
+
* - The browser supports the Intl class (true in IE11)
|
1271
|
+
* - The implementation of Intl supports NumberFormat (also true in IE11)
|
1272
|
+
* - The browser/OS has number formatting rules for the current locale (browser dependent)
|
1273
|
+
*
|
1274
|
+
* @returns {boolean} Returns true if all conditions are met. Returns false otherwise.
|
1275
|
+
*/
|
1276
|
+
I18n.prototype.hasIntlNumberFormatSupport = function () {
|
1277
|
+
return Boolean(window.Intl && ('NumberFormat' in window.Intl && Intl.NumberFormat.supportedLocalesOf(this.locale).length))
|
1278
|
+
};
|
1279
|
+
|
1280
|
+
/**
|
1281
|
+
* Get the appropriate suffix for the plural form.
|
1282
|
+
*
|
1283
|
+
* Uses Intl.PluralRules (or our own fallback implementation) to get the
|
1284
|
+
* 'preferred' form to use for the given count.
|
1285
|
+
*
|
1286
|
+
* Checks that a translation has been provided for that plural form – if it
|
1287
|
+
* hasn't, it'll fall back to the 'other' plural form (unless that doesn't exist
|
1288
|
+
* either, in which case an error will be thrown)
|
1289
|
+
*
|
1290
|
+
* @param {string} lookupKey - The lookup key of the string to use.
|
1291
|
+
* @param {number} count - Number used to determine which pluralisation to use.
|
1292
|
+
* @returns {PluralRule} The suffix associated with the correct pluralisation for this locale.
|
1293
|
+
*/
|
1294
|
+
I18n.prototype.getPluralSuffix = function (lookupKey, count) {
|
1295
|
+
// Validate that the number is actually a number.
|
1296
|
+
//
|
1297
|
+
// Number(count) will turn anything that can't be converted to a Number type
|
1298
|
+
// into 'NaN'. isFinite filters out NaN, as it isn't a finite number.
|
1299
|
+
count = Number(count);
|
1300
|
+
if (!isFinite(count)) { return 'other' }
|
1301
|
+
|
1302
|
+
var preferredForm;
|
1303
|
+
|
1304
|
+
// Check to verify that all the requirements for Intl.PluralRules are met.
|
1305
|
+
// If so, we can use that instead of our custom implementation. Otherwise,
|
1306
|
+
// use the hardcoded fallback.
|
1307
|
+
if (this.hasIntlPluralRulesSupport()) {
|
1308
|
+
preferredForm = new Intl.PluralRules(this.locale).select(count);
|
1309
|
+
} else {
|
1310
|
+
preferredForm = this.selectPluralFormUsingFallbackRules(count);
|
1311
|
+
}
|
1312
|
+
|
1313
|
+
// Use the correct plural form if provided
|
1314
|
+
if (lookupKey + '.' + preferredForm in this.translations) {
|
1315
|
+
return preferredForm
|
1316
|
+
// Fall back to `other` if the plural form is missing, but log a warning
|
1317
|
+
// to the console
|
1318
|
+
} else if (lookupKey + '.other' in this.translations) {
|
1319
|
+
if (console && 'warn' in console) {
|
1320
|
+
console.warn('i18n: Missing plural form ".' + preferredForm + '" for "' +
|
1321
|
+
this.locale + '" locale. Falling back to ".other".');
|
1322
|
+
}
|
1323
|
+
|
1324
|
+
return 'other'
|
1325
|
+
// If the required `other` plural form is missing, all we can do is error
|
1326
|
+
} else {
|
1327
|
+
throw new Error(
|
1328
|
+
'i18n: Plural form ".other" is required for "' + this.locale + '" locale'
|
1329
|
+
)
|
1330
|
+
}
|
1331
|
+
};
|
1332
|
+
|
1333
|
+
/**
|
1334
|
+
* Get the plural form using our fallback implementation
|
1335
|
+
*
|
1336
|
+
* This is split out into a separate function to make it easier to test the
|
1337
|
+
* fallback behaviour in an environment where Intl.PluralRules exists.
|
1338
|
+
*
|
1339
|
+
* @param {number} count - Number used to determine which pluralisation to use.
|
1340
|
+
* @returns {PluralRule} The pluralisation form for count in this locale.
|
1341
|
+
*/
|
1342
|
+
I18n.prototype.selectPluralFormUsingFallbackRules = function (count) {
|
1343
|
+
// Currently our custom code can only handle positive integers, so let's
|
1344
|
+
// make sure our number is one of those.
|
1345
|
+
count = Math.abs(Math.floor(count));
|
1346
|
+
|
1347
|
+
var ruleset = this.getPluralRulesForLocale();
|
1348
|
+
|
1349
|
+
if (ruleset) {
|
1350
|
+
return I18n.pluralRules[ruleset](count)
|
1351
|
+
}
|
1352
|
+
|
1353
|
+
return 'other'
|
1354
|
+
};
|
1355
|
+
|
1356
|
+
/**
|
1357
|
+
* Work out which pluralisation rules to use for the current locale
|
1358
|
+
*
|
1359
|
+
* The locale may include a regional indicator (such as en-GB), but we don't
|
1360
|
+
* usually care about this part, as pluralisation rules are usually the same
|
1361
|
+
* regardless of region. There are exceptions, however, (e.g. Portuguese) so
|
1362
|
+
* this searches by both the full and shortened locale codes, just to be sure.
|
1363
|
+
*
|
1364
|
+
* @returns {PluralRuleName | undefined} The name of the pluralisation rule to use (a key for one
|
1365
|
+
* of the functions in this.pluralRules)
|
1366
|
+
*/
|
1367
|
+
I18n.prototype.getPluralRulesForLocale = function () {
|
1368
|
+
var locale = this.locale;
|
1369
|
+
var localeShort = locale.split('-')[0];
|
1370
|
+
|
1371
|
+
// Look through the plural rules map to find which `pluralRule` is
|
1372
|
+
// appropriate for our current `locale`.
|
1373
|
+
for (var pluralRule in I18n.pluralRulesMap) {
|
1374
|
+
if (Object.prototype.hasOwnProperty.call(I18n.pluralRulesMap, pluralRule)) {
|
1375
|
+
var languages = I18n.pluralRulesMap[pluralRule];
|
1376
|
+
for (var i = 0; i < languages.length; i++) {
|
1377
|
+
if (languages[i] === locale || languages[i] === localeShort) {
|
1378
|
+
return pluralRule
|
1379
|
+
}
|
1380
|
+
}
|
1381
|
+
}
|
1382
|
+
}
|
1383
|
+
};
|
1384
|
+
|
1385
|
+
/**
|
1386
|
+
* Map of plural rules to languages where those rules apply.
|
1387
|
+
*
|
1388
|
+
* Note: These groups are named for the most dominant or recognisable language
|
1389
|
+
* that uses each system. The groupings do not imply that the languages are
|
1390
|
+
* related to one another. Many languages have evolved the same systems
|
1391
|
+
* independently of one another.
|
1392
|
+
*
|
1393
|
+
* Code to support more languages can be found in the i18n spike:
|
1394
|
+
* {@link https://github.com/alphagov/govuk-frontend/blob/spike-i18n-support/src/govuk/i18n.mjs}
|
1395
|
+
*
|
1396
|
+
* Languages currently supported:
|
1397
|
+
*
|
1398
|
+
* Arabic: Arabic (ar)
|
1399
|
+
* Chinese: Burmese (my), Chinese (zh), Indonesian (id), Japanese (ja),
|
1400
|
+
* Javanese (jv), Korean (ko), Malay (ms), Thai (th), Vietnamese (vi)
|
1401
|
+
* French: Armenian (hy), Bangla (bn), French (fr), Gujarati (gu), Hindi (hi),
|
1402
|
+
* Persian Farsi (fa), Punjabi (pa), Zulu (zu)
|
1403
|
+
* German: Afrikaans (af), Albanian (sq), Azerbaijani (az), Basque (eu),
|
1404
|
+
* Bulgarian (bg), Catalan (ca), Danish (da), Dutch (nl), English (en),
|
1405
|
+
* Estonian (et), Finnish (fi), Georgian (ka), German (de), Greek (el),
|
1406
|
+
* Hungarian (hu), Luxembourgish (lb), Norwegian (no), Somali (so),
|
1407
|
+
* Swahili (sw), Swedish (sv), Tamil (ta), Telugu (te), Turkish (tr),
|
1408
|
+
* Urdu (ur)
|
1409
|
+
* Irish: Irish Gaelic (ga)
|
1410
|
+
* Russian: Russian (ru), Ukrainian (uk)
|
1411
|
+
* Scottish: Scottish Gaelic (gd)
|
1412
|
+
* Spanish: European Portuguese (pt-PT), Italian (it), Spanish (es)
|
1413
|
+
* Welsh: Welsh (cy)
|
1414
|
+
*
|
1415
|
+
* @type {Object<PluralRuleName, string[]>}
|
1416
|
+
*/
|
1417
|
+
I18n.pluralRulesMap = {
|
1418
|
+
arabic: ['ar'],
|
1419
|
+
chinese: ['my', 'zh', 'id', 'ja', 'jv', 'ko', 'ms', 'th', 'vi'],
|
1420
|
+
french: ['hy', 'bn', 'fr', 'gu', 'hi', 'fa', 'pa', 'zu'],
|
1421
|
+
german: [
|
1422
|
+
'af', 'sq', 'az', 'eu', 'bg', 'ca', 'da', 'nl', 'en', 'et', 'fi', 'ka',
|
1423
|
+
'de', 'el', 'hu', 'lb', 'no', 'so', 'sw', 'sv', 'ta', 'te', 'tr', 'ur'
|
1424
|
+
],
|
1425
|
+
irish: ['ga'],
|
1426
|
+
russian: ['ru', 'uk'],
|
1427
|
+
scottish: ['gd'],
|
1428
|
+
spanish: ['pt-PT', 'it', 'es'],
|
1429
|
+
welsh: ['cy']
|
1430
|
+
};
|
1431
|
+
|
1432
|
+
/**
|
1433
|
+
* Different pluralisation rule sets
|
1434
|
+
*
|
1435
|
+
* Returns the appropriate suffix for the plural form associated with `n`.
|
1436
|
+
* Possible suffixes: 'zero', 'one', 'two', 'few', 'many', 'other' (the actual
|
1437
|
+
* meaning of each differs per locale). 'other' should always exist, even in
|
1438
|
+
* languages without plurals, such as Chinese.
|
1439
|
+
* {@link https://cldr.unicode.org/index/cldr-spec/plural-rules}
|
1440
|
+
*
|
1441
|
+
* The count must be a positive integer. Negative numbers and decimals aren't accounted for
|
1442
|
+
*
|
1443
|
+
* @type {Object<string, function(number): PluralRule>}
|
1444
|
+
*/
|
1445
|
+
I18n.pluralRules = {
|
1446
|
+
arabic: function (n) {
|
1447
|
+
if (n === 0) { return 'zero' }
|
1448
|
+
if (n === 1) { return 'one' }
|
1449
|
+
if (n === 2) { return 'two' }
|
1450
|
+
if (n % 100 >= 3 && n % 100 <= 10) { return 'few' }
|
1451
|
+
if (n % 100 >= 11 && n % 100 <= 99) { return 'many' }
|
1452
|
+
return 'other'
|
1453
|
+
},
|
1454
|
+
chinese: function () {
|
1455
|
+
return 'other'
|
1456
|
+
},
|
1457
|
+
french: function (n) {
|
1458
|
+
return n === 0 || n === 1 ? 'one' : 'other'
|
1459
|
+
},
|
1460
|
+
german: function (n) {
|
1461
|
+
return n === 1 ? 'one' : 'other'
|
1462
|
+
},
|
1463
|
+
irish: function (n) {
|
1464
|
+
if (n === 1) { return 'one' }
|
1465
|
+
if (n === 2) { return 'two' }
|
1466
|
+
if (n >= 3 && n <= 6) { return 'few' }
|
1467
|
+
if (n >= 7 && n <= 10) { return 'many' }
|
1468
|
+
return 'other'
|
1469
|
+
},
|
1470
|
+
russian: function (n) {
|
1471
|
+
var lastTwo = n % 100;
|
1472
|
+
var last = lastTwo % 10;
|
1473
|
+
if (last === 1 && lastTwo !== 11) { return 'one' }
|
1474
|
+
if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) { return 'few' }
|
1475
|
+
if (last === 0 || (last >= 5 && last <= 9) || (lastTwo >= 11 && lastTwo <= 14)) { return 'many' }
|
1476
|
+
// Note: The 'other' suffix is only used by decimal numbers in Russian.
|
1477
|
+
// We don't anticipate it being used, but it's here for consistency.
|
1478
|
+
return 'other'
|
1479
|
+
},
|
1480
|
+
scottish: function (n) {
|
1481
|
+
if (n === 1 || n === 11) { return 'one' }
|
1482
|
+
if (n === 2 || n === 12) { return 'two' }
|
1483
|
+
if ((n >= 3 && n <= 10) || (n >= 13 && n <= 19)) { return 'few' }
|
1484
|
+
return 'other'
|
1485
|
+
},
|
1486
|
+
spanish: function (n) {
|
1487
|
+
if (n === 1) { return 'one' }
|
1488
|
+
if (n % 1000000 === 0 && n !== 0) { return 'many' }
|
1489
|
+
return 'other'
|
1490
|
+
},
|
1491
|
+
welsh: function (n) {
|
1492
|
+
if (n === 0) { return 'zero' }
|
1493
|
+
if (n === 1) { return 'one' }
|
1494
|
+
if (n === 2) { return 'two' }
|
1495
|
+
if (n === 3) { return 'few' }
|
1496
|
+
if (n === 6) { return 'many' }
|
1497
|
+
return 'other'
|
1498
|
+
}
|
1499
|
+
};
|
1500
|
+
|
1501
|
+
/**
|
1502
|
+
* Supported languages for plural rules
|
1503
|
+
*
|
1504
|
+
* @typedef {'arabic' | 'chinese' | 'french' | 'german' | 'irish' | 'russian' | 'scottish' | 'spanish' | 'welsh'} PluralRuleName
|
1505
|
+
*/
|
1506
|
+
|
1507
|
+
/**
|
1508
|
+
* Plural rule category mnemonic tags
|
1509
|
+
*
|
1510
|
+
* @typedef {'zero' | 'one' | 'two' | 'few' | 'many' | 'other'} PluralRule
|
1511
|
+
*/
|
1512
|
+
|
1513
|
+
/**
|
1514
|
+
* Translated message by plural rule they correspond to.
|
1515
|
+
*
|
1516
|
+
* Allows to group pluralised messages under a single key when passing
|
1517
|
+
* translations to a component's constructor
|
1518
|
+
*
|
1519
|
+
* @typedef {object} TranslationPluralForms
|
1520
|
+
* @property {string} [other] - General plural form
|
1521
|
+
* @property {string} [zero] - Plural form used with 0
|
1522
|
+
* @property {string} [one] - Plural form used with 1
|
1523
|
+
* @property {string} [two] - Plural form used with 2
|
1524
|
+
* @property {string} [few] - Plural form used for a few
|
1525
|
+
* @property {string} [many] - Plural form used for many
|
1526
|
+
*/
|
1527
|
+
|
1528
|
+
/**
|
1529
|
+
* Translated messages (flattened)
|
1530
|
+
*
|
1531
|
+
* @private
|
1532
|
+
* @typedef {Object<string, string> | {}} TranslationsFlattened
|
1533
|
+
*/
|
1534
|
+
|
1535
|
+
(function(undefined) {
|
1536
|
+
|
1537
|
+
// Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/detect.js
|
1538
|
+
var detect = (function(){
|
1539
|
+
if (!document.documentElement.dataset) {
|
1540
|
+
return false;
|
1541
|
+
}
|
1542
|
+
var el = document.createElement('div');
|
1543
|
+
el.setAttribute("data-a-b", "c");
|
1544
|
+
return el.dataset && el.dataset.aB == "c";
|
1545
|
+
}());
|
1546
|
+
|
1547
|
+
if (detect) return
|
1548
|
+
|
1549
|
+
// Polyfill derived from https://raw.githubusercontent.com/Financial-Times/polyfill-library/13cf7c340974d128d557580b5e2dafcd1b1192d1/polyfills/Element/prototype/dataset/polyfill.js
|
1550
|
+
Object.defineProperty(Element.prototype, 'dataset', {
|
1551
|
+
get: function() {
|
1552
|
+
var element = this;
|
1553
|
+
var attributes = this.attributes;
|
1554
|
+
var map = {};
|
1555
|
+
|
1556
|
+
for (var i = 0; i < attributes.length; i++) {
|
1557
|
+
var attribute = attributes[i];
|
1558
|
+
|
1559
|
+
// This regex has been edited from the original polyfill, to add
|
1560
|
+
// support for period (.) separators in data-* attribute names. These
|
1561
|
+
// are allowed in the HTML spec, but were not covered by the original
|
1562
|
+
// polyfill's regex. We use periods in our i18n implementation.
|
1563
|
+
if (attribute && attribute.name && (/^data-\w[.\w-]*$/).test(attribute.name)) {
|
1564
|
+
var name = attribute.name;
|
1565
|
+
var value = attribute.value;
|
1566
|
+
|
1567
|
+
var propName = name.substr(5).replace(/-./g, function (prop) {
|
1568
|
+
return prop.charAt(1).toUpperCase();
|
1569
|
+
});
|
1570
|
+
|
1571
|
+
// If this browser supports __defineGetter__ and __defineSetter__,
|
1572
|
+
// continue using defineProperty. If not (like IE 8 and below), we use
|
1573
|
+
// a hacky fallback which at least gives an object in the right format
|
1574
|
+
if ('__defineGetter__' in Object.prototype && '__defineSetter__' in Object.prototype) {
|
1575
|
+
Object.defineProperty(map, propName, {
|
1576
|
+
enumerable: true,
|
1577
|
+
get: function() {
|
1578
|
+
return this.value;
|
1579
|
+
}.bind({value: value || ''}),
|
1580
|
+
set: function setter(name, value) {
|
1581
|
+
if (typeof value !== 'undefined') {
|
1582
|
+
this.setAttribute(name, value);
|
1583
|
+
} else {
|
1584
|
+
this.removeAttribute(name);
|
1585
|
+
}
|
1586
|
+
}.bind(element, name)
|
1587
|
+
});
|
1588
|
+
} else {
|
1589
|
+
map[propName] = value;
|
1590
|
+
}
|
1591
|
+
|
1592
|
+
}
|
1593
|
+
}
|
1594
|
+
|
1595
|
+
return map;
|
1596
|
+
}
|
1597
|
+
});
|
1598
|
+
|
1599
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
1600
|
+
|
1601
|
+
(function(undefined) {
|
1602
|
+
|
1603
|
+
// Detection from https://github.com/mdn/content/blob/cf607d68522cd35ee7670782d3ee3a361eaef2e4/files/en-us/web/javascript/reference/global_objects/string/trim/index.md#polyfill
|
1604
|
+
var detect = ('trim' in String.prototype);
|
1605
|
+
|
1606
|
+
if (detect) return
|
1607
|
+
|
1608
|
+
// Polyfill from https://github.com/mdn/content/blob/cf607d68522cd35ee7670782d3ee3a361eaef2e4/files/en-us/web/javascript/reference/global_objects/string/trim/index.md#polyfill
|
1609
|
+
String.prototype.trim = function () {
|
1610
|
+
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
1611
|
+
};
|
1612
|
+
|
1613
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
1614
|
+
|
1615
|
+
/**
|
1616
|
+
* Normalise string
|
1617
|
+
*
|
1618
|
+
* 'If it looks like a duck, and it quacks like a duck…' 🦆
|
1619
|
+
*
|
1620
|
+
* If the passed value looks like a boolean or a number, convert it to a boolean
|
1621
|
+
* or number.
|
1622
|
+
*
|
1623
|
+
* Designed to be used to convert config passed via data attributes (which are
|
1624
|
+
* always strings) into something sensible.
|
1625
|
+
*
|
1626
|
+
* @param {string} value - The value to normalise
|
1627
|
+
* @returns {string | boolean | number | undefined} Normalised data
|
1628
|
+
*/
|
1629
|
+
function normaliseString (value) {
|
1630
|
+
if (typeof value !== 'string') {
|
1631
|
+
return value
|
1632
|
+
}
|
1633
|
+
|
1634
|
+
var trimmedValue = value.trim();
|
1635
|
+
|
1636
|
+
if (trimmedValue === 'true') {
|
1637
|
+
return true
|
1638
|
+
}
|
1639
|
+
|
1640
|
+
if (trimmedValue === 'false') {
|
1641
|
+
return false
|
1642
|
+
}
|
1643
|
+
|
1644
|
+
// Empty / whitespace-only strings are considered finite so we need to check
|
1645
|
+
// the length of the trimmed string as well
|
1646
|
+
if (trimmedValue.length > 0 && isFinite(trimmedValue)) {
|
1647
|
+
return Number(trimmedValue)
|
1648
|
+
}
|
1649
|
+
|
1650
|
+
return value
|
1651
|
+
}
|
1652
|
+
|
1653
|
+
/**
|
1654
|
+
* Normalise dataset
|
1655
|
+
*
|
1656
|
+
* Loop over an object and normalise each value using normaliseData function
|
1657
|
+
*
|
1658
|
+
* @param {DOMStringMap} dataset - HTML element dataset
|
1659
|
+
* @returns {Object<string, string | boolean | number | undefined>} Normalised dataset
|
1660
|
+
*/
|
1661
|
+
function normaliseDataset (dataset) {
|
1662
|
+
var out = {};
|
1663
|
+
|
1664
|
+
for (var key in dataset) {
|
1665
|
+
out[key] = normaliseString(dataset[key]);
|
1666
|
+
}
|
1667
|
+
|
1668
|
+
return out
|
1669
|
+
}
|
1670
|
+
|
1671
|
+
(function(undefined) {
|
1672
|
+
|
1673
|
+
// Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/detect.js
|
1674
|
+
var detect = (
|
1675
|
+
'document' in this && "matches" in document.documentElement
|
1676
|
+
);
|
1677
|
+
|
1678
|
+
if (detect) return
|
1679
|
+
|
1680
|
+
// Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/polyfill.js
|
1681
|
+
Element.prototype.matches = Element.prototype.webkitMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || function matches(selector) {
|
1682
|
+
var element = this;
|
1683
|
+
var elements = (element.document || element.ownerDocument).querySelectorAll(selector);
|
1684
|
+
var index = 0;
|
1685
|
+
|
1686
|
+
while (elements[index] && elements[index] !== element) {
|
1687
|
+
++index;
|
1688
|
+
}
|
1689
|
+
|
1690
|
+
return !!elements[index];
|
1691
|
+
};
|
1692
|
+
|
1693
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
1694
|
+
|
1695
|
+
(function(undefined) {
|
1696
|
+
|
1697
|
+
// Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/detect.js
|
1698
|
+
var detect = (
|
1699
|
+
'document' in this && "closest" in document.documentElement
|
1700
|
+
);
|
1701
|
+
|
1702
|
+
if (detect) return
|
1703
|
+
|
1704
|
+
// Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/polyfill.js
|
1705
|
+
Element.prototype.closest = function closest(selector) {
|
1706
|
+
var node = this;
|
1707
|
+
|
1708
|
+
while (node) {
|
1709
|
+
if (node.matches(selector)) return node;
|
1710
|
+
else node = 'SVGElement' in window && node instanceof SVGElement ? node.parentNode : node.parentElement;
|
1711
|
+
}
|
1712
|
+
|
1713
|
+
return null;
|
1714
|
+
};
|
1715
|
+
|
1716
|
+
}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});
|
1717
|
+
|
1718
|
+
/**
|
1719
|
+
* Returns the value of the given attribute closest to the given element (including itself)
|
1720
|
+
*
|
1721
|
+
* @param {HTMLElement} $element - The element to start walking the DOM tree up
|
1722
|
+
* @param {string} attributeName - The name of the attribute
|
1723
|
+
* @returns {string | undefined} Attribute value
|
1724
|
+
*/
|
1725
|
+
function closestAttributeValue ($element, attributeName) {
|
1726
|
+
var closestElementWithAttribute = $element.closest('[' + attributeName + ']');
|
1727
|
+
if (closestElementWithAttribute) {
|
1728
|
+
return closestElementWithAttribute.getAttribute(attributeName)
|
1729
|
+
}
|
1730
|
+
}
|
1731
|
+
|
1732
|
+
/**
|
1733
|
+
* @constant
|
1734
|
+
* @type {CharacterCountTranslations}
|
1735
|
+
* @see Default value for {@link CharacterCountConfig.i18n}
|
1736
|
+
* @default
|
1737
|
+
*/
|
1738
|
+
var CHARACTER_COUNT_TRANSLATIONS = {
|
1739
|
+
// Characters
|
1740
|
+
charactersUnderLimit: {
|
1741
|
+
one: 'You have %{count} character remaining',
|
1742
|
+
other: 'You have %{count} characters remaining'
|
1743
|
+
},
|
1744
|
+
charactersAtLimit: 'You have 0 characters remaining',
|
1745
|
+
charactersOverLimit: {
|
1746
|
+
one: 'You have %{count} character too many',
|
1747
|
+
other: 'You have %{count} characters too many'
|
1748
|
+
},
|
1749
|
+
// Words
|
1750
|
+
wordsUnderLimit: {
|
1751
|
+
one: 'You have %{count} word remaining',
|
1752
|
+
other: 'You have %{count} words remaining'
|
1753
|
+
},
|
1754
|
+
wordsAtLimit: 'You have 0 words remaining',
|
1755
|
+
wordsOverLimit: {
|
1756
|
+
one: 'You have %{count} word too many',
|
1757
|
+
other: 'You have %{count} words too many'
|
1758
|
+
},
|
1759
|
+
textareaDescription: {
|
1760
|
+
other: ''
|
1761
|
+
}
|
1762
|
+
};
|
1763
|
+
|
1764
|
+
/**
|
1765
|
+
* JavaScript enhancements for the CharacterCount component
|
1766
|
+
*
|
1767
|
+
* Tracks the number of characters or words in the `.govuk-js-character-count`
|
1768
|
+
* `<textarea>` inside the element. Displays a message with the remaining number
|
1769
|
+
* of characters/words available, or the number of characters/words in excess.
|
1770
|
+
*
|
1771
|
+
* You can configure the message to only appear after a certain percentage
|
1772
|
+
* of the available characters/words has been entered.
|
1773
|
+
*
|
1774
|
+
* @class
|
1775
|
+
* @param {HTMLElement} $module - The element this component controls
|
1776
|
+
* @param {CharacterCountConfig} [config] - Character count config
|
1777
|
+
*/
|
1778
|
+
function CharacterCount ($module, config) {
|
1779
|
+
if (!$module) {
|
1780
|
+
return this
|
1781
|
+
}
|
1782
|
+
|
1783
|
+
var defaultConfig = {
|
1784
|
+
threshold: 0,
|
1785
|
+
i18n: CHARACTER_COUNT_TRANSLATIONS
|
1786
|
+
};
|
1787
|
+
|
1788
|
+
// Read config set using dataset ('data-' values)
|
1789
|
+
var datasetConfig = normaliseDataset($module.dataset);
|
1790
|
+
|
1791
|
+
// To ensure data-attributes take complete precedence, even if they change the
|
1792
|
+
// type of count, we need to reset the `maxlength` and `maxwords` from the
|
1793
|
+
// JavaScript config.
|
1794
|
+
//
|
1795
|
+
// We can't mutate `config`, though, as it may be shared across multiple
|
1796
|
+
// components inside `initAll`.
|
1797
|
+
var configOverrides = {};
|
1798
|
+
if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
|
1799
|
+
configOverrides = {
|
1800
|
+
maxlength: false,
|
1801
|
+
maxwords: false
|
1802
|
+
};
|
1803
|
+
}
|
1804
|
+
|
1805
|
+
this.config = mergeConfigs(
|
1806
|
+
defaultConfig,
|
1807
|
+
config || {},
|
1808
|
+
configOverrides,
|
1809
|
+
datasetConfig
|
1810
|
+
);
|
1811
|
+
|
1812
|
+
this.i18n = new I18n(extractConfigByNamespace(this.config, 'i18n'), {
|
1813
|
+
// Read the fallback if necessary rather than have it set in the defaults
|
1814
|
+
locale: closestAttributeValue($module, 'lang')
|
1815
|
+
});
|
1816
|
+
|
1817
|
+
// Determine the limit attribute (characters or words)
|
1818
|
+
if (this.config.maxwords) {
|
1819
|
+
this.maxLength = this.config.maxwords;
|
1820
|
+
} else if (this.config.maxlength) {
|
1821
|
+
this.maxLength = this.config.maxlength;
|
1822
|
+
} else {
|
1823
|
+
return
|
1824
|
+
}
|
1825
|
+
|
1018
1826
|
this.$module = $module;
|
1019
1827
|
this.$textarea = $module.querySelector('.govuk-js-character-count');
|
1020
1828
|
this.$visibleCountMessage = null;
|
@@ -1022,26 +1830,28 @@ function CharacterCount ($module) {
|
|
1022
1830
|
this.lastInputTimestamp = null;
|
1023
1831
|
}
|
1024
1832
|
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
};
|
1029
|
-
|
1030
|
-
// Initialize component
|
1833
|
+
/**
|
1834
|
+
* Initialise component
|
1835
|
+
*/
|
1031
1836
|
CharacterCount.prototype.init = function () {
|
1032
1837
|
// Check that required elements are present
|
1033
1838
|
if (!this.$textarea) {
|
1034
1839
|
return
|
1035
1840
|
}
|
1036
1841
|
|
1037
|
-
// Check for module
|
1038
|
-
var $module = this.$module;
|
1039
1842
|
var $textarea = this.$textarea;
|
1040
|
-
var $
|
1843
|
+
var $textareaDescription = document.getElementById($textarea.id + '-info');
|
1041
1844
|
|
1042
|
-
//
|
1845
|
+
// Inject a decription for the textarea if none is present already
|
1846
|
+
// for when the component was rendered with no maxlength, maxwords
|
1847
|
+
// nor custom textareaDescriptionText
|
1848
|
+
if ($textareaDescription.innerText.match(/^\s*$/)) {
|
1849
|
+
$textareaDescription.innerText = this.i18n.t('textareaDescription', { count: this.maxLength });
|
1850
|
+
}
|
1851
|
+
|
1852
|
+
// Move the textarea description to be immediately after the textarea
|
1043
1853
|
// Kept for backwards compatibility
|
1044
|
-
$textarea.insertAdjacentElement('afterend', $
|
1854
|
+
$textarea.insertAdjacentElement('afterend', $textareaDescription);
|
1045
1855
|
|
1046
1856
|
// Create the *screen reader* specific live-updating counter
|
1047
1857
|
// This doesn't need any styling classes, as it is never visible
|
@@ -1049,36 +1859,20 @@ CharacterCount.prototype.init = function () {
|
|
1049
1859
|
$screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden';
|
1050
1860
|
$screenReaderCountMessage.setAttribute('aria-live', 'polite');
|
1051
1861
|
this.$screenReaderCountMessage = $screenReaderCountMessage;
|
1052
|
-
$
|
1862
|
+
$textareaDescription.insertAdjacentElement('afterend', $screenReaderCountMessage);
|
1053
1863
|
|
1054
1864
|
// Create our live-updating counter element, copying the classes from the
|
1055
|
-
//
|
1865
|
+
// textarea description for backwards compatibility as these may have been
|
1866
|
+
// configured
|
1056
1867
|
var $visibleCountMessage = document.createElement('div');
|
1057
|
-
$visibleCountMessage.className = $
|
1868
|
+
$visibleCountMessage.className = $textareaDescription.className;
|
1058
1869
|
$visibleCountMessage.classList.add('govuk-character-count__status');
|
1059
1870
|
$visibleCountMessage.setAttribute('aria-hidden', 'true');
|
1060
1871
|
this.$visibleCountMessage = $visibleCountMessage;
|
1061
|
-
$
|
1062
|
-
|
1063
|
-
// Hide the fallback limit message
|
1064
|
-
$fallbackLimitMessage.classList.add('govuk-visually-hidden');
|
1065
|
-
|
1066
|
-
// Read options set using dataset ('data-' values)
|
1067
|
-
this.options = this.getDataset($module);
|
1872
|
+
$textareaDescription.insertAdjacentElement('afterend', $visibleCountMessage);
|
1068
1873
|
|
1069
|
-
//
|
1070
|
-
|
1071
|
-
if (this.options.maxwords) {
|
1072
|
-
countAttribute = this.defaults.wordCountAttribute;
|
1073
|
-
}
|
1074
|
-
|
1075
|
-
// Save the element limit
|
1076
|
-
this.maxLength = $module.getAttribute(countAttribute);
|
1077
|
-
|
1078
|
-
// Check for limit
|
1079
|
-
if (!this.maxLength) {
|
1080
|
-
return
|
1081
|
-
}
|
1874
|
+
// Hide the textarea description
|
1875
|
+
$textareaDescription.classList.add('govuk-visually-hidden');
|
1082
1876
|
|
1083
1877
|
// Remove hard limit if set
|
1084
1878
|
$textarea.removeAttribute('maxlength');
|
@@ -1086,9 +1880,9 @@ CharacterCount.prototype.init = function () {
|
|
1086
1880
|
this.bindChangeEvents();
|
1087
1881
|
|
1088
1882
|
// When the page is restored after navigating 'back' in some browsers the
|
1089
|
-
// state of the character count is not restored until *after* the
|
1090
|
-
// event is fired, so we need to manually update it after the
|
1091
|
-
// in browsers that support it.
|
1883
|
+
// state of the character count is not restored until *after* the
|
1884
|
+
// DOMContentLoaded event is fired, so we need to manually update it after the
|
1885
|
+
// pageshow event in browsers that support it.
|
1092
1886
|
if ('onpageshow' in window) {
|
1093
1887
|
window.addEventListener('pageshow', this.updateCountMessage.bind(this));
|
1094
1888
|
} else {
|
@@ -1097,35 +1891,12 @@ CharacterCount.prototype.init = function () {
|
|
1097
1891
|
this.updateCountMessage();
|
1098
1892
|
};
|
1099
1893
|
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
var attribute = attributes[i];
|
1107
|
-
var match = attribute.name.match(/^data-(.+)/);
|
1108
|
-
if (match) {
|
1109
|
-
dataset[match[1]] = attribute.value;
|
1110
|
-
}
|
1111
|
-
}
|
1112
|
-
}
|
1113
|
-
return dataset
|
1114
|
-
};
|
1115
|
-
|
1116
|
-
// Counts characters or words in text
|
1117
|
-
CharacterCount.prototype.count = function (text) {
|
1118
|
-
var length;
|
1119
|
-
if (this.options.maxwords) {
|
1120
|
-
var tokens = text.match(/\S+/g) || []; // Matches consecutive non-whitespace chars
|
1121
|
-
length = tokens.length;
|
1122
|
-
} else {
|
1123
|
-
length = text.length;
|
1124
|
-
}
|
1125
|
-
return length
|
1126
|
-
};
|
1127
|
-
|
1128
|
-
// Bind input propertychange to the elements and update based on the change
|
1894
|
+
/**
|
1895
|
+
* Bind change events
|
1896
|
+
*
|
1897
|
+
* Set up event listeners on the $textarea so that the count messages update
|
1898
|
+
* when the user types.
|
1899
|
+
*/
|
1129
1900
|
CharacterCount.prototype.bindChangeEvents = function () {
|
1130
1901
|
var $textarea = this.$textarea;
|
1131
1902
|
$textarea.addEventListener('keyup', this.handleKeyUp.bind(this));
|
@@ -1135,10 +1906,52 @@ CharacterCount.prototype.bindChangeEvents = function () {
|
|
1135
1906
|
$textarea.addEventListener('blur', this.handleBlur.bind(this));
|
1136
1907
|
};
|
1137
1908
|
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1909
|
+
/**
|
1910
|
+
* Handle key up event
|
1911
|
+
*
|
1912
|
+
* Update the visible character counter and keep track of when the last update
|
1913
|
+
* happened for each keypress
|
1914
|
+
*/
|
1915
|
+
CharacterCount.prototype.handleKeyUp = function () {
|
1916
|
+
this.updateVisibleCountMessage();
|
1917
|
+
this.lastInputTimestamp = Date.now();
|
1918
|
+
};
|
1919
|
+
|
1920
|
+
/**
|
1921
|
+
* Handle focus event
|
1922
|
+
*
|
1923
|
+
* Speech recognition software such as Dragon NaturallySpeaking will modify the
|
1924
|
+
* fields by directly changing its `value`. These changes don't trigger events
|
1925
|
+
* in JavaScript, so we need to poll to handle when and if they occur.
|
1926
|
+
*
|
1927
|
+
* Once the keyup event hasn't been detected for at least 1000 ms (1s), check if
|
1928
|
+
* the textarea value has changed and update the count message if it has.
|
1929
|
+
*
|
1930
|
+
* This is so that the update triggered by the manual comparison doesn't
|
1931
|
+
* conflict with debounced KeyboardEvent updates.
|
1932
|
+
*/
|
1933
|
+
CharacterCount.prototype.handleFocus = function () {
|
1934
|
+
this.valueChecker = setInterval(function () {
|
1935
|
+
if (!this.lastInputTimestamp || (Date.now() - 500) >= this.lastInputTimestamp) {
|
1936
|
+
this.updateIfValueChanged();
|
1937
|
+
}
|
1938
|
+
}.bind(this), 1000);
|
1939
|
+
};
|
1940
|
+
|
1941
|
+
/**
|
1942
|
+
* Handle blur event
|
1943
|
+
*
|
1944
|
+
* Stop checking the textarea value once the textarea no longer has focus
|
1945
|
+
*/
|
1946
|
+
CharacterCount.prototype.handleBlur = function () {
|
1947
|
+
// Cancel value checking on blur
|
1948
|
+
clearInterval(this.valueChecker);
|
1949
|
+
};
|
1950
|
+
|
1951
|
+
/**
|
1952
|
+
* Update count message if textarea value has changed
|
1953
|
+
*/
|
1954
|
+
CharacterCount.prototype.updateIfValueChanged = function () {
|
1142
1955
|
if (!this.$textarea.oldValue) this.$textarea.oldValue = '';
|
1143
1956
|
if (this.$textarea.value !== this.$textarea.oldValue) {
|
1144
1957
|
this.$textarea.oldValue = this.$textarea.value;
|
@@ -1146,14 +1959,20 @@ CharacterCount.prototype.checkIfValueChanged = function () {
|
|
1146
1959
|
}
|
1147
1960
|
};
|
1148
1961
|
|
1149
|
-
|
1150
|
-
|
1962
|
+
/**
|
1963
|
+
* Update count message
|
1964
|
+
*
|
1965
|
+
* Helper function to update both the visible and screen reader-specific
|
1966
|
+
* counters simultaneously (e.g. on init)
|
1967
|
+
*/
|
1151
1968
|
CharacterCount.prototype.updateCountMessage = function () {
|
1152
1969
|
this.updateVisibleCountMessage();
|
1153
1970
|
this.updateScreenReaderCountMessage();
|
1154
1971
|
};
|
1155
1972
|
|
1156
|
-
|
1973
|
+
/**
|
1974
|
+
* Update visible count message
|
1975
|
+
*/
|
1157
1976
|
CharacterCount.prototype.updateVisibleCountMessage = function () {
|
1158
1977
|
var $textarea = this.$textarea;
|
1159
1978
|
var $visibleCountMessage = this.$visibleCountMessage;
|
@@ -1179,10 +1998,12 @@ CharacterCount.prototype.updateVisibleCountMessage = function () {
|
|
1179
1998
|
}
|
1180
1999
|
|
1181
2000
|
// Update message
|
1182
|
-
$visibleCountMessage.
|
2001
|
+
$visibleCountMessage.innerText = this.getCountMessage();
|
1183
2002
|
};
|
1184
2003
|
|
1185
|
-
|
2004
|
+
/**
|
2005
|
+
* Update screen reader count message
|
2006
|
+
*/
|
1186
2007
|
CharacterCount.prototype.updateScreenReaderCountMessage = function () {
|
1187
2008
|
var $screenReaderCountMessage = this.$screenReaderCountMessage;
|
1188
2009
|
|
@@ -1195,70 +2016,161 @@ CharacterCount.prototype.updateScreenReaderCountMessage = function () {
|
|
1195
2016
|
}
|
1196
2017
|
|
1197
2018
|
// Update message
|
1198
|
-
$screenReaderCountMessage.
|
2019
|
+
$screenReaderCountMessage.innerText = this.getCountMessage();
|
1199
2020
|
};
|
1200
2021
|
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
2022
|
+
/**
|
2023
|
+
* Count the number of characters (or words, if `config.maxwords` is set)
|
2024
|
+
* in the given text
|
2025
|
+
*
|
2026
|
+
* @param {string} text - The text to count the characters of
|
2027
|
+
* @returns {number} the number of characters (or words) in the text
|
2028
|
+
*/
|
2029
|
+
CharacterCount.prototype.count = function (text) {
|
2030
|
+
if (this.config.maxwords) {
|
2031
|
+
var tokens = text.match(/\S+/g) || []; // Matches consecutive non-whitespace chars
|
2032
|
+
return tokens.length
|
2033
|
+
} else {
|
2034
|
+
return text.length
|
2035
|
+
}
|
2036
|
+
};
|
2037
|
+
|
2038
|
+
/**
|
2039
|
+
* Get count message
|
2040
|
+
*
|
2041
|
+
* @returns {string} Status message
|
2042
|
+
*/
|
2043
|
+
CharacterCount.prototype.getCountMessage = function () {
|
2044
|
+
var remainingNumber = this.maxLength - this.count(this.$textarea.value);
|
1206
2045
|
|
1207
|
-
var
|
1208
|
-
|
1209
|
-
|
1210
|
-
|
1211
|
-
|
2046
|
+
var countType = this.config.maxwords ? 'words' : 'characters';
|
2047
|
+
return this.formatCountMessage(remainingNumber, countType)
|
2048
|
+
};
|
2049
|
+
|
2050
|
+
/**
|
2051
|
+
* Formats the message shown to users according to what's counted
|
2052
|
+
* and how many remain
|
2053
|
+
*
|
2054
|
+
* @param {number} remainingNumber - The number of words/characaters remaining
|
2055
|
+
* @param {string} countType - "words" or "characters"
|
2056
|
+
* @returns {string} Status message
|
2057
|
+
*/
|
2058
|
+
CharacterCount.prototype.formatCountMessage = function (remainingNumber, countType) {
|
2059
|
+
if (remainingNumber === 0) {
|
2060
|
+
return this.i18n.t(countType + 'AtLimit')
|
1212
2061
|
}
|
1213
|
-
charNoun = charNoun + ((remainingNumber === -1 || remainingNumber === 1) ? '' : 's');
|
1214
2062
|
|
1215
|
-
|
1216
|
-
displayNumber = Math.abs(remainingNumber);
|
2063
|
+
var translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit';
|
1217
2064
|
|
1218
|
-
return
|
2065
|
+
return this.i18n.t(countType + translationKeySuffix, { count: Math.abs(remainingNumber) })
|
1219
2066
|
};
|
1220
2067
|
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
2068
|
+
/**
|
2069
|
+
* Check if count is over threshold
|
2070
|
+
*
|
2071
|
+
* Checks whether the value is over the configured threshold for the input.
|
2072
|
+
* If there is no configured threshold, it is set to 0 and this function will
|
2073
|
+
* always return true.
|
2074
|
+
*
|
2075
|
+
* @returns {boolean} true if the current count is over the config.threshold
|
2076
|
+
* (or no threshold is set)
|
2077
|
+
*/
|
1224
2078
|
CharacterCount.prototype.isOverThreshold = function () {
|
2079
|
+
// No threshold means we're always above threshold so save some computation
|
2080
|
+
if (!this.config.threshold) {
|
2081
|
+
return true
|
2082
|
+
}
|
2083
|
+
|
1225
2084
|
var $textarea = this.$textarea;
|
1226
|
-
var options = this.options;
|
1227
2085
|
|
1228
2086
|
// Determine the remaining number of characters/words
|
1229
2087
|
var currentLength = this.count($textarea.value);
|
1230
2088
|
var maxLength = this.maxLength;
|
1231
2089
|
|
1232
|
-
|
1233
|
-
var thresholdPercent = options.threshold ? options.threshold : 0;
|
1234
|
-
var thresholdValue = maxLength * thresholdPercent / 100;
|
2090
|
+
var thresholdValue = maxLength * this.config.threshold / 100;
|
1235
2091
|
|
1236
2092
|
return (thresholdValue <= currentLength)
|
1237
2093
|
};
|
1238
2094
|
|
1239
|
-
|
1240
|
-
|
1241
|
-
|
1242
|
-
|
1243
|
-
|
1244
|
-
|
1245
|
-
|
1246
|
-
|
1247
|
-
|
1248
|
-
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1260
|
-
|
1261
|
-
}
|
2095
|
+
/**
|
2096
|
+
* Character count config
|
2097
|
+
*
|
2098
|
+
* @typedef {CharacterCountConfigWithMaxLength | CharacterCountConfigWithMaxWords} CharacterCountConfig
|
2099
|
+
*/
|
2100
|
+
|
2101
|
+
/**
|
2102
|
+
* Character count config (with maximum number of characters)
|
2103
|
+
*
|
2104
|
+
* @typedef {object} CharacterCountConfigWithMaxLength
|
2105
|
+
* @property {number} [maxlength] - The maximum number of characters.
|
2106
|
+
* If maxwords is provided, the maxlength option will be ignored.
|
2107
|
+
* @property {number} [threshold = 0] - The percentage value of the limit at
|
2108
|
+
* which point the count message is displayed. If this attribute is set, the
|
2109
|
+
* count message will be hidden by default.
|
2110
|
+
* @property {CharacterCountTranslations} [i18n = CHARACTER_COUNT_TRANSLATIONS] - See constant {@link CHARACTER_COUNT_TRANSLATIONS}
|
2111
|
+
*/
|
2112
|
+
|
2113
|
+
/**
|
2114
|
+
* Character count config (with maximum number of words)
|
2115
|
+
*
|
2116
|
+
* @typedef {object} CharacterCountConfigWithMaxWords
|
2117
|
+
* @property {number} [maxwords] - The maximum number of words. If maxwords is
|
2118
|
+
* provided, the maxlength option will be ignored.
|
2119
|
+
* @property {number} [threshold = 0] - The percentage value of the limit at
|
2120
|
+
* which point the count message is displayed. If this attribute is set, the
|
2121
|
+
* count message will be hidden by default.
|
2122
|
+
* @property {CharacterCountTranslations} [i18n = CHARACTER_COUNT_TRANSLATIONS] - See constant {@link CHARACTER_COUNT_TRANSLATIONS}
|
2123
|
+
*/
|
2124
|
+
|
2125
|
+
/**
|
2126
|
+
* Character count translations
|
2127
|
+
*
|
2128
|
+
* @typedef {object} CharacterCountTranslations
|
2129
|
+
*
|
2130
|
+
* Messages shown to users as they type. It provides feedback on how many words
|
2131
|
+
* or characters they have remaining or if they are over the limit. This also
|
2132
|
+
* includes a message used as an accessible description for the textarea.
|
2133
|
+
* @property {TranslationPluralForms} [charactersUnderLimit] - Message displayed
|
2134
|
+
* when the number of characters is under the configured maximum, `maxlength`.
|
2135
|
+
* This message is displayed visually and through assistive technologies. The
|
2136
|
+
* component will replace the `%{count}` placeholder with the number of
|
2137
|
+
* remaining characters. This is a [pluralised list of
|
2138
|
+
* messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
|
2139
|
+
* @property {string} [charactersAtLimit] - Message displayed when the number of
|
2140
|
+
* characters reaches the configured maximum, `maxlength`. This message is
|
2141
|
+
* displayed visually and through assistive technologies.
|
2142
|
+
* @property {TranslationPluralForms} [charactersOverLimit] - Message displayed
|
2143
|
+
* when the number of characters is over the configured maximum, `maxlength`.
|
2144
|
+
* This message is displayed visually and through assistive technologies. The
|
2145
|
+
* component will replace the `%{count}` placeholder with the number of
|
2146
|
+
* remaining characters. This is a [pluralised list of
|
2147
|
+
* messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
|
2148
|
+
* @property {TranslationPluralForms} [wordsUnderLimit] - Message displayed when
|
2149
|
+
* the number of words is under the configured maximum, `maxlength`. This
|
2150
|
+
* message is displayed visually and through assistive technologies. The
|
2151
|
+
* component will replace the `%{count}` placeholder with the number of
|
2152
|
+
* remaining words. This is a [pluralised list of
|
2153
|
+
* messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
|
2154
|
+
* @property {string} [wordsAtLimit] - Message displayed when the number of
|
2155
|
+
* words reaches the configured maximum, `maxlength`. This message is
|
2156
|
+
* displayed visually and through assistive technologies.
|
2157
|
+
* @property {TranslationPluralForms} [wordsOverLimit] - Message displayed when
|
2158
|
+
* the number of words is over the configured maximum, `maxlength`. This
|
2159
|
+
* message is displayed visually and through assistive technologies. The
|
2160
|
+
* component will replace the `%{count}` placeholder with the number of
|
2161
|
+
* remaining words. This is a [pluralised list of
|
2162
|
+
* messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend).
|
2163
|
+
* @property {TranslationPluralForms} [textareaDescription] - Message made
|
2164
|
+
* available to assistive technologies, if none is already present in the
|
2165
|
+
* HTML, to describe that the component accepts only a limited amount of
|
2166
|
+
* content. It is visible on the page when JavaScript is unavailable. The
|
2167
|
+
* component will replace the `%{count}` placeholder with the value of the
|
2168
|
+
* `maxlength` or `maxwords` parameter.
|
2169
|
+
*/
|
2170
|
+
|
2171
|
+
/**
|
2172
|
+
* @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms
|
2173
|
+
*/
|
1262
2174
|
|
1263
2175
|
return CharacterCount;
|
1264
2176
|
|