Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow CharacterCount to receive configuration via JavaScript #2883

Merged
merged 8 commits into from
Sep 29, 2022
Merged
2 changes: 1 addition & 1 deletion src/govuk/all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function initAll (config) {

var $characterCounts = $scope.querySelectorAll('[data-module="govuk-character-count"]')
nodeListForEach($characterCounts, function ($characterCount) {
new CharacterCount($characterCount).init()
new CharacterCount($characterCount, config.characterCount).init()
romaricpascal marked this conversation as resolved.
Show resolved Hide resolved
})

var $checkboxes = $scope.querySelectorAll('[data-module="govuk-checkboxes"]')
Expand Down
124 changes: 71 additions & 53 deletions src/govuk/components/character-count/character-count.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,66 @@
import '../../vendor/polyfills/Function/prototype/bind.mjs'
import '../../vendor/polyfills/Event.mjs' // addEventListener and event.target normalisation
import '../../vendor/polyfills/Element/prototype/classList.mjs'
import { mergeConfigs, normaliseDataset } from '../../common.mjs'

/**
* JavaScript enhancements for the CharacterCount component
*
* Tracks the number of characters or words in the `.govuk-js-character-count`
* `<textarea>` inside the element. Displays a message when the
romaricpascal marked this conversation as resolved.
Show resolved Hide resolved
*
* @class
* @param {HTMLElement} $module - The element this component controls
* @param {Object} config
* @param {Number} config.maxlength - If `maxwords` is set, this is not required.
* The maximum number of characters. If `maxwords` is provided, it will be ignored.
* @param {Number} config.maxwords - If `maxlength` is set, this is not required.
* The maximum number of words. If `maxwords` is provided, `maxlength` will be ignored.
* @param {Number} [config.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.
romaricpascal marked this conversation as resolved.
Show resolved Hide resolved
*/
function CharacterCount ($module, config) {
if (!$module) {
return this
}

var defaultConfig = {
threshold: 0
}

// Read config set using dataset ('data-' values)
var datasetConfig = normaliseDataset($module.dataset)

// To ensure data-attributes take complete precedence,
// even if they change the type of count, we need to reset
// the `maxlength` and `maxwords` from the JavaScript config
// We can't mutate `config`, though, as it may be shared across
// multiple components inside `initAll`
romaricpascal marked this conversation as resolved.
Show resolved Hide resolved
var configOverrides = {}
if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
configOverrides = {
maxlength: false,
maxwords: false
}
}

this.config = mergeConfigs(
defaultConfig,
config || {},
configOverrides,
datasetConfig
)

// Determine the limit attribute (characters or words)
if (this.config.maxwords) {
this.maxLength = this.config.maxwords
} else if (this.config.maxlength) {
this.maxLength = this.config.maxlength
} else {
return
}

function CharacterCount ($module) {
this.$module = $module
this.$textarea = $module.querySelector('.govuk-js-character-count')
this.$visibleCountMessage = null
Expand All @@ -19,8 +77,6 @@ CharacterCount.prototype.init = function () {
return
}

// Check for module
var $module = this.$module
var $textarea = this.$textarea
var $fallbackLimitMessage = document.getElementById($textarea.id + '-info')

Expand Down Expand Up @@ -49,18 +105,6 @@ CharacterCount.prototype.init = function () {
// Hide the fallback limit message
$fallbackLimitMessage.classList.add('govuk-visually-hidden')

// Read options set using dataset ('data-' values)
this.options = this.getDataset($module)

// Determine the limit attribute (characters or words)
if (this.options.maxwords) {
this.maxLength = this.options.maxwords
} else if (this.options.maxlength) {
this.maxLength = this.options.maxlength
} else {
return
}

// Remove hard limit if set
$textarea.removeAttribute('maxlength')

Expand Down Expand Up @@ -207,14 +251,14 @@ CharacterCount.prototype.updateScreenReaderCountMessage = function () {
}

/**
* Count the number of characters (or words, if `options.maxwords` is set)
* Count the number of characters (or words, if `config.maxwords` is set)
* in the given text
*
* @param {String} text - The text to count the characters of
* @returns {Number} the number of characters (or words) in the text
*/
CharacterCount.prototype.count = function (text) {
if (this.options.maxwords) {
if (this.config.maxwords) {
var tokens = text.match(/\S+/g) || [] // Matches consecutive non-whitespace chars
return tokens.length
} else {
Expand All @@ -229,13 +273,13 @@ CharacterCount.prototype.count = function (text) {
*/
CharacterCount.prototype.getCountMessage = function () {
var $textarea = this.$textarea
var options = this.options
var config = this.config
var remainingNumber = this.maxLength - this.count($textarea.value)

var charVerb = 'remaining'
var charNoun = 'character'
var displayNumber = remainingNumber
if (options.maxwords) {
if (config.maxwords) {
romaricpascal marked this conversation as resolved.
Show resolved Hide resolved
charNoun = 'word'
}
charNoun = charNoun + ((remainingNumber === -1 || remainingNumber === 1) ? '' : 's')
Expand All @@ -253,51 +297,25 @@ CharacterCount.prototype.getCountMessage = function () {
* If there is no configured threshold, it is set to 0 and this function will
* always return true.
*
* @returns {Boolean} true if the current count is over the options.threshold
* @returns {Boolean} true if the current count is over the config.threshold
* (or no threshold is set)
*/
CharacterCount.prototype.isOverThreshold = function () {
// No threshold means we're always above threshold
// so we can save some computation
romaricpascal marked this conversation as resolved.
Show resolved Hide resolved
if (!this.config.threshold) {
return true
}

var $textarea = this.$textarea
var options = this.options

// Determine the remaining number of characters/words
var currentLength = this.count($textarea.value)
var maxLength = this.maxLength

// Set threshold if presented in options
var thresholdPercent = options.threshold ? options.threshold : 0
var thresholdValue = maxLength * thresholdPercent / 100
var thresholdValue = maxLength * this.config.threshold / 100

return (thresholdValue <= currentLength)
}

/**
* Get dataset
romaricpascal marked this conversation as resolved.
Show resolved Hide resolved
*
* Get all of the data-* attributes from a given $element as map of key-value
* pairs, with the data- prefix removed from the keys.
*
* This is a bit like HTMLElement.dataset, but it does not convert the keys to
* camel case (and it works in browsers that do not support HTMLElement.dataset)
*
* @todo Replace with HTMLElement.dataset
*
* @param {HTMLElement} $element - The element to read data attributes from
* @returns {Object} Object of key-value pairs representing the data attributes
*/
CharacterCount.prototype.getDataset = function ($element) {
var dataset = {}
var attributes = $element.attributes
if (attributes) {
for (var i = 0; i < attributes.length; i++) {
var attribute = attributes[i]
var match = attribute.name.match(/^data-(.+)/)
if (match) {
dataset[match[1]] = attribute.value
}
}
}
return dataset
}

export default CharacterCount
Loading