Skip to content

Commit

Permalink
Merge pull request #3000 from alphagov/release-v4.4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
romaricpascal committed Nov 14, 2022
2 parents 51f8dc8 + 4ec2ebd commit 65bf0ac
Show file tree
Hide file tree
Showing 110 changed files with 8,953 additions and 2,097 deletions.
76 changes: 44 additions & 32 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.3.1
4.4.0
3 changes: 0 additions & 3 deletions dist/govuk-frontend-4.3.1.min.css

This file was deleted.

1 change: 0 additions & 1 deletion dist/govuk-frontend-4.3.1.min.js

This file was deleted.

3 changes: 3 additions & 0 deletions dist/govuk-frontend-4.4.0.min.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/govuk-frontend-4.4.0.min.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion dist/govuk-frontend-ie8-4.3.1.min.css

This file was deleted.

1 change: 1 addition & 0 deletions dist/govuk-frontend-ie8-4.4.0.min.css

Large diffs are not rendered by default.

77 changes: 50 additions & 27 deletions package/govuk-esm/all.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { nodeListForEach } from './common.mjs'
import { nodeListForEach } from './common/index.mjs'
import Accordion from './components/accordion/accordion.mjs'
import Button from './components/button/button.mjs'
import Details from './components/details/details.mjs'
Expand All @@ -11,62 +11,73 @@ import Radios from './components/radios/radios.mjs'
import SkipLink from './components/skip-link/skip-link.mjs'
import Tabs from './components/tabs/tabs.mjs'

function initAll (options) {
// Set the options to an empty object by default if no options are passed.
options = typeof options !== 'undefined' ? options : {}
/**
* Initialise all components
*
* Use the `data-module` attributes to find, instantiate and init all of the
* components provided as part of GOV.UK Frontend.
*
* @param {Config} [config] - Config for all components
*/
function initAll (config) {
config = typeof config !== 'undefined' ? config : {}

// Allow the user to initialise GOV.UK Frontend in only certain sections of the page
// Defaults to the entire document if nothing is set.
var scope = typeof options.scope !== 'undefined' ? options.scope : document
var $scope = typeof config.scope !== 'undefined' ? config.scope : document

var $buttons = scope.querySelectorAll('[data-module="govuk-button"]')
nodeListForEach($buttons, function ($button) {
new Button($button).init()
})

var $accordions = scope.querySelectorAll('[data-module="govuk-accordion"]')
var $accordions = $scope.querySelectorAll('[data-module="govuk-accordion"]')
nodeListForEach($accordions, function ($accordion) {
new Accordion($accordion).init()
new Accordion($accordion, config.accordion).init()
})

var $details = scope.querySelectorAll('[data-module="govuk-details"]')
nodeListForEach($details, function ($detail) {
new Details($detail).init()
var $buttons = $scope.querySelectorAll('[data-module="govuk-button"]')
nodeListForEach($buttons, function ($button) {
new Button($button, config.button).init()
})

var $characterCounts = scope.querySelectorAll('[data-module="govuk-character-count"]')
var $characterCounts = $scope.querySelectorAll('[data-module="govuk-character-count"]')
nodeListForEach($characterCounts, function ($characterCount) {
new CharacterCount($characterCount).init()
new CharacterCount($characterCount, config.characterCount).init()
})

var $checkboxes = scope.querySelectorAll('[data-module="govuk-checkboxes"]')
var $checkboxes = $scope.querySelectorAll('[data-module="govuk-checkboxes"]')
nodeListForEach($checkboxes, function ($checkbox) {
new Checkboxes($checkbox).init()
})

var $details = $scope.querySelectorAll('[data-module="govuk-details"]')
nodeListForEach($details, function ($detail) {
new Details($detail).init()
})

// Find first error summary module to enhance.
var $errorSummary = scope.querySelector('[data-module="govuk-error-summary"]')
new ErrorSummary($errorSummary).init()
var $errorSummary = $scope.querySelector('[data-module="govuk-error-summary"]')
if ($errorSummary) {
new ErrorSummary($errorSummary, config.errorSummary).init()
}

// Find first header module to enhance.
var $toggleButton = scope.querySelector('[data-module="govuk-header"]')
new Header($toggleButton).init()
var $header = $scope.querySelector('[data-module="govuk-header"]')
if ($header) {
new Header($header).init()
}

var $notificationBanners = scope.querySelectorAll('[data-module="govuk-notification-banner"]')
var $notificationBanners = $scope.querySelectorAll('[data-module="govuk-notification-banner"]')
nodeListForEach($notificationBanners, function ($notificationBanner) {
new NotificationBanner($notificationBanner).init()
new NotificationBanner($notificationBanner, config.notificationBanner).init()
})

var $radios = scope.querySelectorAll('[data-module="govuk-radios"]')
var $radios = $scope.querySelectorAll('[data-module="govuk-radios"]')
nodeListForEach($radios, function ($radio) {
new Radios($radio).init()
})

// Find first skip link module to enhance.
var $skipLink = scope.querySelector('[data-module="govuk-skip-link"]')
var $skipLink = $scope.querySelector('[data-module="govuk-skip-link"]')
new SkipLink($skipLink).init()

var $tabs = scope.querySelectorAll('[data-module="govuk-tabs"]')
var $tabs = $scope.querySelectorAll('[data-module="govuk-tabs"]')
nodeListForEach($tabs, function ($tabs) {
new Tabs($tabs).init()
})
Expand All @@ -86,3 +97,15 @@ export {
SkipLink,
Tabs
}

/**
* Config for all components
*
* @typedef {object} Config
* @property {HTMLElement} [scope=document] - Scope to query for components
* @property {import('./components/accordion/accordion.mjs').AccordionConfig} [accordion] - Accordion config
* @property {import('./components/button/button.mjs').ButtonConfig} [button] - Button config
* @property {import('./components/character-count/character-count.mjs').CharacterCountConfig} [characterCount] - Character Count config
* @property {import('./components/error-summary/error-summary.mjs').ErrorSummaryConfig} [errorSummary] - Error Summary config
* @property {import('./components/notification-banner/notification-banner.mjs').NotificationBannerConfig} [notificationBanner] - Notification Banner config
*/
34 changes: 6 additions & 28 deletions package/govuk-esm/common.mjs
Original file line number Diff line number Diff line change
@@ -1,28 +1,6 @@
/**
* TODO: Ideally this would be a NodeList.prototype.forEach polyfill
* This seems to fail in IE8, requires more investigation.
* See: https://github.com/imagitama/nodelist-foreach-polyfill
*/
export function nodeListForEach (nodes, callback) {
if (window.NodeList.prototype.forEach) {
return nodes.forEach(callback)
}
for (var i = 0; i < nodes.length; i++) {
callback.call(window, nodes[i], i, nodes)
}
}

// Used to generate a unique string, allows multiple instances of the component without
// Them conflicting with each other.
// https://stackoverflow.com/a/8809472
export function generateUniqueID () {
var d = new Date().getTime()
if (typeof window.performance !== 'undefined' && typeof window.performance.now === 'function') {
d += window.performance.now() // use high-precision timer if available
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
})
}
// Implementation of common function is gathered in the `common` folder
// as some are split in their own modules to limit impacts of the polyfills
// they require.
// This module exports the non polyfilled methods as they used to be
// to avoid breaking changes
export * from './common/index.mjs'
15 changes: 15 additions & 0 deletions package/govuk-esm/common/closest-attribute-value.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import '../vendor/polyfills/Element/prototype/closest.mjs'

/**
* Returns the value of the given attribute closest to the given element (including itself)
*
* @param {HTMLElement} $element - The element to start walking the DOM tree up
* @param {string} attributeName - The name of the attribute
* @returns {string | undefined} Attribute value
*/
export function closestAttributeValue ($element, attributeName) {
var closestElementWithAttribute = $element.closest('[' + attributeName + ']')
if (closestElementWithAttribute) {
return closestElementWithAttribute.getAttribute(attributeName)
}
}
159 changes: 159 additions & 0 deletions package/govuk-esm/common/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* Common helpers which do not require polyfill.
*
* IMPORTANT: If a helper require a polyfill, please isolate it in its own module
* so that the polyfill can be properly tree-shaken and does not burden
* the components that do not need that helper
*
* @module common/index
*/

/**
* TODO: Ideally this would be a NodeList.prototype.forEach polyfill
* This seems to fail in IE8, requires more investigation.
* See: https://github.com/imagitama/nodelist-foreach-polyfill
*
* @param {NodeListOf<Element>} nodes - NodeList from querySelectorAll()
* @param {nodeListIterator} callback - Callback function to run for each node
* @returns {undefined}
*/
export function nodeListForEach (nodes, callback) {
if (window.NodeList.prototype.forEach) {
return nodes.forEach(callback)
}
for (var i = 0; i < nodes.length; i++) {
callback.call(window, nodes[i], i, nodes)
}
}

/**
* Used to generate a unique string, allows multiple instances of the component
* without them conflicting with each other.
* https://stackoverflow.com/a/8809472
*
* @returns {string} Unique ID
*/
export function generateUniqueID () {
var d = new Date().getTime()
if (typeof window.performance !== 'undefined' && typeof window.performance.now === 'function') {
d += window.performance.now() // use high-precision timer if available
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0
d = Math.floor(d / 16)
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
})
}

/**
* Config flattening function
*
* Takes any number of objects, flattens them into namespaced key-value pairs,
* (e.g. {'i18n.showSection': 'Show section'}) and combines them together, with
* greatest priority on the LAST item passed in.
*
* @returns {object} A flattened object of key-value pairs.
*/
export function mergeConfigs (/* configObject1, configObject2, ...configObjects */) {
/**
* Function to take nested objects and flatten them to a dot-separated keyed
* object. Doing this means we don't need to do any deep/recursive merging of
* each of our objects, nor transform our dataset from a flat list into a
* nested object.
*
* @param {object} configObject - Deeply nested object
* @returns {object} Flattened object with dot-separated keys
*/
var flattenObject = function (configObject) {
// Prepare an empty return object
var flattenedObject = {}

// Our flattening function, this is called recursively for each level of
// depth in the object. At each level we prepend the previous level names to
// the key using `prefix`.
var flattenLoop = function (obj, prefix) {
// Loop through keys...
for (var key in obj) {
// Check to see if this is a prototypical key/value,
// if it is, skip it.
if (!Object.prototype.hasOwnProperty.call(obj, key)) {
continue
}
var value = obj[key]
var prefixedKey = prefix ? prefix + '.' + key : key
if (typeof value === 'object') {
// If the value is a nested object, recurse over that too
flattenLoop(value, prefixedKey)
} else {
// Otherwise, add this value to our return object
flattenedObject[prefixedKey] = value
}
}
}

// Kick off the recursive loop
flattenLoop(configObject)
return flattenedObject
}

// Start with an empty object as our base
var formattedConfigObject = {}

// Loop through each of the remaining passed objects and push their keys
// one-by-one into configObject. Any duplicate keys will override the existing
// key with the new value.
for (var i = 0; i < arguments.length; i++) {
var obj = flattenObject(arguments[i])
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
formattedConfigObject[key] = obj[key]
}
}
}

return formattedConfigObject
}

/**
* Extracts keys starting with a particular namespace from a flattened config
* object, removing the namespace in the process.
*
* @param {object} configObject - The object to extract key-value pairs from.
* @param {string} namespace - The namespace to filter keys with.
* @returns {object} Flattened object with dot-separated key namespace removed
*/
export function extractConfigByNamespace (configObject, namespace) {
// Check we have what we need
if (!configObject || typeof configObject !== 'object') {
throw new Error('Provide a `configObject` of type "object".')
}
if (!namespace || typeof namespace !== 'string') {
throw new Error('Provide a `namespace` of type "string" to filter the `configObject` by.')
}
var newObject = {}
for (var key in configObject) {
// Split the key into parts, using . as our namespace separator
var keyParts = key.split('.')
// Check if the first namespace matches the configured namespace
if (Object.prototype.hasOwnProperty.call(configObject, key) && keyParts[0] === namespace) {
// Remove the first item (the namespace) from the parts array,
// but only if there is more than one part (we don't want blank keys!)
if (keyParts.length > 1) {
keyParts.shift()
}
// Join the remaining parts back together
var newKey = keyParts.join('.')
// Add them to our new object
newObject[newKey] = configObject[key]
}
}
return newObject
}

/**
* @callback nodeListIterator
* @param {Element} value - The current node being iterated on
* @param {number} index - The current index in the iteration
* @param {NodeListOf<Element>} nodes - NodeList from querySelectorAll()
* @returns {undefined}
*/
58 changes: 58 additions & 0 deletions package/govuk-esm/common/normalise-dataset.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import '../vendor/polyfills/Element/prototype/dataset.mjs'
import '../vendor/polyfills/String/prototype/trim.mjs'

/**
* Normalise string
*
* 'If it looks like a duck, and it quacks like a duck…' 🦆
*
* If the passed value looks like a boolean or a number, convert it to a boolean
* or number.
*
* Designed to be used to convert config passed via data attributes (which are
* always strings) into something sensible.
*
* @param {string} value - The value to normalise
* @returns {string | boolean | number | undefined} Normalised data
*/
export function normaliseString (value) {
if (typeof value !== 'string') {
return value
}

var trimmedValue = value.trim()

if (trimmedValue === 'true') {
return true
}

if (trimmedValue === 'false') {
return false
}

// Empty / whitespace-only strings are considered finite so we need to check
// the length of the trimmed string as well
if (trimmedValue.length > 0 && isFinite(trimmedValue)) {
return Number(trimmedValue)
}

return value
}

/**
* Normalise dataset
*
* Loop over an object and normalise each value using normaliseData function
*
* @param {DOMStringMap} dataset - HTML element dataset
* @returns {Object<string, string | boolean | number | undefined>} Normalised dataset
*/
export function normaliseDataset (dataset) {
var out = {}

for (var key in dataset) {
out[key] = normaliseString(dataset[key])
}

return out
}
Loading

0 comments on commit 65bf0ac

Please sign in to comment.