UNPKG

govuk-frontend

Version:

GOV.UK Frontend contains the code you need to start building a user interface for government platforms and services.

297 lines (290 loc) 12.4 kB
import { closestAttributeValue } from '../../common/closest-attribute-value.mjs'; import { ConfigurableComponent, configOverride, validateConfig } from '../../common/configuration.mjs'; import { formatErrorMessage } from '../../common/index.mjs'; import { ElementError, ConfigError } from '../../errors/index.mjs'; import { I18n } from '../../i18n.mjs'; /** * Character count component * * Tracks the number of characters or words in the `.govuk-js-character-count` * `<textarea>` inside the element. Displays a message with the remaining number * of characters/words available, or the number of characters/words in excess. * * You can configure the message to only appear after a certain percentage * of the available characters/words has been entered. * * @preserve * @augments ConfigurableComponent<CharacterCountConfig> */ class CharacterCount extends ConfigurableComponent { [configOverride](datasetConfig) { let configOverrides = {}; if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) { configOverrides = { maxlength: undefined, maxwords: undefined }; } return configOverrides; } /** * @param {Element | null} $root - HTML element to use for character count * @param {CharacterCountConfig} [config] - Character count config */ constructor($root, config = {}) { var _ref, _this$config$maxwords; super($root, config); this.$textarea = void 0; this.$visibleCountMessage = void 0; this.$screenReaderCountMessage = void 0; this.lastInputTimestamp = null; this.lastInputValue = ''; this.valueChecker = null; this.i18n = void 0; this.maxLength = void 0; const $textarea = this.$root.querySelector('.govuk-js-character-count'); if (!($textarea instanceof HTMLTextAreaElement || $textarea instanceof HTMLInputElement)) { throw new ElementError({ component: CharacterCount, element: $textarea, expectedType: 'HTMLTextareaElement or HTMLInputElement', identifier: 'Form field (`.govuk-js-character-count`)' }); } const errors = validateConfig(CharacterCount.schema, this.config); if (errors[0]) { throw new ConfigError(formatErrorMessage(CharacterCount, errors[0])); } this.i18n = new I18n(this.config.i18n, { locale: closestAttributeValue(this.$root, 'lang') }); this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity; this.$textarea = $textarea; const textareaDescriptionId = `${this.$textarea.id}-info`; const $textareaDescription = document.getElementById(textareaDescriptionId); if (!$textareaDescription) { throw new ElementError({ component: CharacterCount, element: $textareaDescription, identifier: `Count message (\`id="${textareaDescriptionId}"\`)` }); } if (`${$textareaDescription.textContent}`.match(/^\s*$/)) { $textareaDescription.textContent = this.i18n.t('textareaDescription', { count: this.maxLength }); } this.$textarea.insertAdjacentElement('afterend', $textareaDescription); const $screenReaderCountMessage = document.createElement('div'); $screenReaderCountMessage.className = 'govuk-character-count__sr-status govuk-visually-hidden'; $screenReaderCountMessage.setAttribute('aria-live', 'polite'); this.$screenReaderCountMessage = $screenReaderCountMessage; $textareaDescription.insertAdjacentElement('afterend', $screenReaderCountMessage); const $visibleCountMessage = document.createElement('div'); $visibleCountMessage.className = $textareaDescription.className; $visibleCountMessage.classList.add('govuk-character-count__status'); $visibleCountMessage.setAttribute('aria-hidden', 'true'); this.$visibleCountMessage = $visibleCountMessage; $textareaDescription.insertAdjacentElement('afterend', $visibleCountMessage); $textareaDescription.classList.add('govuk-visually-hidden'); this.$textarea.removeAttribute('maxlength'); this.bindChangeEvents(); window.addEventListener('pageshow', () => this.updateCountMessage()); this.updateCountMessage(); } bindChangeEvents() { this.$textarea.addEventListener('keyup', () => this.handleKeyUp()); this.$textarea.addEventListener('focus', () => this.handleFocus()); this.$textarea.addEventListener('blur', () => this.handleBlur()); } handleKeyUp() { this.updateVisibleCountMessage(); this.lastInputTimestamp = Date.now(); } handleFocus() { this.valueChecker = window.setInterval(() => { if (!this.lastInputTimestamp || Date.now() - 500 >= this.lastInputTimestamp) { this.updateIfValueChanged(); } }, 1000); } handleBlur() { if (this.valueChecker) { window.clearInterval(this.valueChecker); } } updateIfValueChanged() { if (this.$textarea.value !== this.lastInputValue) { this.lastInputValue = this.$textarea.value; this.updateCountMessage(); } } updateCountMessage() { this.updateVisibleCountMessage(); this.updateScreenReaderCountMessage(); } updateVisibleCountMessage() { const remainingNumber = this.maxLength - this.count(this.$textarea.value); const isError = remainingNumber < 0; this.$visibleCountMessage.classList.toggle('govuk-character-count__message--disabled', !this.isOverThreshold()); this.$textarea.classList.toggle('govuk-textarea--error', isError); this.$visibleCountMessage.classList.toggle('govuk-error-message', isError); this.$visibleCountMessage.classList.toggle('govuk-hint', !isError); this.$visibleCountMessage.textContent = this.getCountMessage(); } updateScreenReaderCountMessage() { if (this.isOverThreshold()) { this.$screenReaderCountMessage.removeAttribute('aria-hidden'); } else { this.$screenReaderCountMessage.setAttribute('aria-hidden', 'true'); } this.$screenReaderCountMessage.textContent = this.getCountMessage(); } count(text) { if (this.config.maxwords) { var _text$match; const tokens = (_text$match = text.match(/\S+/g)) != null ? _text$match : []; return tokens.length; } return text.length; } getCountMessage() { const remainingNumber = this.maxLength - this.count(this.$textarea.value); const countType = this.config.maxwords ? 'words' : 'characters'; return this.formatCountMessage(remainingNumber, countType); } formatCountMessage(remainingNumber, countType) { if (remainingNumber === 0) { return this.i18n.t(`${countType}AtLimit`); } const translationKeySuffix = remainingNumber < 0 ? 'OverLimit' : 'UnderLimit'; return this.i18n.t(`${countType}${translationKeySuffix}`, { count: Math.abs(remainingNumber) }); } isOverThreshold() { if (!this.config.threshold) { return true; } const currentLength = this.count(this.$textarea.value); const maxLength = this.maxLength; const thresholdValue = maxLength * this.config.threshold / 100; return thresholdValue <= currentLength; } } /** * Character count config * * @see {@link CharacterCount.defaults} * @typedef {object} CharacterCountConfig * @property {number} [maxlength] - The maximum number of characters. * If maxwords is provided, the maxlength option will be ignored. * @property {number} [maxwords] - The maximum number of words. If maxwords is * provided, the maxlength option will be ignored. * @property {number} [threshold=0] - The percentage value of the limit at * which point the count message is displayed. If this attribute is set, the * count message will be hidden by default. * @property {CharacterCountTranslations} [i18n=CharacterCount.defaults.i18n] - Character count translations */ /** * Character count translations * * @see {@link CharacterCount.defaults.i18n} * @typedef {object} CharacterCountTranslations * * Messages shown to users as they type. It provides feedback on how many words * or characters they have remaining or if they are over the limit. This also * includes a message used as an accessible description for the textarea. * @property {TranslationPluralForms} [charactersUnderLimit] - Message displayed * when the number of characters is under the configured maximum, `maxlength`. * This message is displayed visually and through assistive technologies. The * component will replace the `%{count}` placeholder with the number of * remaining characters. This is a [pluralised list of * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend). * @property {string} [charactersAtLimit] - Message displayed when the number of * characters reaches the configured maximum, `maxlength`. This message is * displayed visually and through assistive technologies. * @property {TranslationPluralForms} [charactersOverLimit] - Message displayed * when the number of characters is over the configured maximum, `maxlength`. * This message is displayed visually and through assistive technologies. The * component will replace the `%{count}` placeholder with the number of * remaining characters. This is a [pluralised list of * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend). * @property {TranslationPluralForms} [wordsUnderLimit] - Message displayed when * the number of words is under the configured maximum, `maxlength`. This * message is displayed visually and through assistive technologies. The * component will replace the `%{count}` placeholder with the number of * remaining words. This is a [pluralised list of * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend). * @property {string} [wordsAtLimit] - Message displayed when the number of * words reaches the configured maximum, `maxlength`. This message is * displayed visually and through assistive technologies. * @property {TranslationPluralForms} [wordsOverLimit] - Message displayed when * the number of words is over the configured maximum, `maxlength`. This * message is displayed visually and through assistive technologies. The * component will replace the `%{count}` placeholder with the number of * remaining words. This is a [pluralised list of * messages](https://frontend.design-system.service.gov.uk/localise-govuk-frontend). * @property {TranslationPluralForms} [textareaDescription] - Message made * available to assistive technologies, if none is already present in the * HTML, to describe that the component accepts only a limited amount of * content. It is visible on the page when JavaScript is unavailable. The * component will replace the `%{count}` placeholder with the value of the * `maxlength` or `maxwords` parameter. */ /** * @typedef {import('../../common/configuration.mjs').Schema} Schema * @typedef {import('../../i18n.mjs').TranslationPluralForms} TranslationPluralForms */ CharacterCount.moduleName = 'govuk-character-count'; CharacterCount.defaults = Object.freeze({ threshold: 0, i18n: { charactersUnderLimit: { one: 'You have %{count} character remaining', other: 'You have %{count} characters remaining' }, charactersAtLimit: 'You have 0 characters remaining', charactersOverLimit: { one: 'You have %{count} character too many', other: 'You have %{count} characters too many' }, wordsUnderLimit: { one: 'You have %{count} word remaining', other: 'You have %{count} words remaining' }, wordsAtLimit: 'You have 0 words remaining', wordsOverLimit: { one: 'You have %{count} word too many', other: 'You have %{count} words too many' }, textareaDescription: { other: '' } } }); CharacterCount.schema = Object.freeze({ properties: { i18n: { type: 'object' }, maxwords: { type: 'number' }, maxlength: { type: 'number' }, threshold: { type: 'number' } }, anyOf: [{ required: ['maxwords'], errorMessage: 'Either "maxlength" or "maxwords" must be provided' }, { required: ['maxlength'], errorMessage: 'Either "maxlength" or "maxwords" must be provided' }] }); export { CharacterCount }; //# sourceMappingURL=character-count.mjs.map