Skip to content

Commit

Permalink
Move common.mjs implementation into the common folder
Browse files Browse the repository at this point in the history
Leaves the `common.mjs` file to re-export it so that code importing it from the current release still find it
  • Loading branch information
romaricpascal committed Nov 7, 2022
1 parent 7dd00b1 commit 0a87bbf
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 162 deletions.
2 changes: 1 addition & 1 deletion src/govuk/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 Down
157 changes: 6 additions & 151 deletions src/govuk/common.mjs
Original file line number Diff line number Diff line change
@@ -1,151 +1,6 @@
import './vendor/polyfills/Element/prototype/dataset.mjs'

/**
* 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}
*/
// 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'
159 changes: 159 additions & 0 deletions src/govuk/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}
*/
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mergeConfigs, extractConfigByNamespace } from './common.mjs'
import { mergeConfigs, extractConfigByNamespace } from './index.mjs'

// TODO: Write unit tests for `nodeListForEach` and `generateUniqueID`

Expand Down
2 changes: 1 addition & 1 deletion src/govuk/components/accordion/accordion.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { nodeListForEach, mergeConfigs, extractConfigByNamespace } from '../../common.mjs'
import { nodeListForEach, mergeConfigs, extractConfigByNamespace } from '../../common/index.mjs'
import { I18n } from '../../i18n.mjs'
import '../../vendor/polyfills/Function/prototype/bind.mjs'
import '../../vendor/polyfills/Element/prototype/classList.mjs'
Expand Down
2 changes: 1 addition & 1 deletion src/govuk/components/button/button.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mergeConfigs } from '../../common.mjs'
import { mergeConfigs } from '../../common/index.mjs'
import { normaliseDataset } from '../../common/normalise-dataset.mjs'
import '../../vendor/polyfills/Event.mjs' // addEventListener and event.target normaliziation
import '../../vendor/polyfills/Function/prototype/bind.mjs'
Expand Down
2 changes: 1 addition & 1 deletion src/govuk/components/character-count/character-count.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import '../../vendor/polyfills/Date/now.mjs'
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 { extractConfigByNamespace, mergeConfigs } from '../../common.mjs'
import { extractConfigByNamespace, mergeConfigs } from '../../common/index.mjs'
import { I18n } from '../../i18n.mjs'
import { normaliseDataset } from '../../common/normalise-dataset.mjs'
import { closestAttributeValue } from '../../common/closest-attribute-value.mjs'
Expand Down
2 changes: 1 addition & 1 deletion src/govuk/components/checkboxes/checkboxes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import '../../vendor/polyfills/Function/prototype/bind.mjs'
// addEventListener, event.target normalization and DOMContentLoaded
import '../../vendor/polyfills/Event.mjs'
import '../../vendor/polyfills/Element/prototype/classList.mjs'
import { nodeListForEach } from '../../common.mjs'
import { nodeListForEach } from '../../common/index.mjs'

/**
* Checkboxes component
Expand Down
2 changes: 1 addition & 1 deletion src/govuk/components/details/details.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import '../../vendor/polyfills/Function/prototype/bind.mjs'
import '../../vendor/polyfills/Event.mjs' // addEventListener and event.target normaliziation
import { generateUniqueID } from '../../common.mjs'
import { generateUniqueID } from '../../common/index.mjs'

var KEY_ENTER = 13
var KEY_SPACE = 32
Expand Down
2 changes: 1 addition & 1 deletion src/govuk/components/error-summary/error-summary.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import '../../vendor/polyfills/Function/prototype/bind.mjs'
import '../../vendor/polyfills/Event.mjs' // addEventListener
import '../../vendor/polyfills/Element/prototype/closest.mjs'

import { mergeConfigs } from '../../common.mjs'
import { mergeConfigs } from '../../common/index.mjs'
import { normaliseDataset } from '../../common/normalise-dataset.mjs'

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import '../../vendor/polyfills/Event.mjs' // addEventListener

import { mergeConfigs } from '../../common.mjs'
import { mergeConfigs } from '../../common/index.mjs'
import { normaliseDataset } from '../../common/normalise-dataset.mjs'

/**
Expand Down
2 changes: 1 addition & 1 deletion src/govuk/components/radios/radios.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import '../../vendor/polyfills/Function/prototype/bind.mjs'
// addEventListener, event.target normalization and DOMContentLoaded
import '../../vendor/polyfills/Event.mjs'
import '../../vendor/polyfills/Element/prototype/classList.mjs'
import { nodeListForEach } from '../../common.mjs'
import { nodeListForEach } from '../../common/index.mjs'

/**
* Radios component
Expand Down
Loading

0 comments on commit 0a87bbf

Please sign in to comment.