UNPKG

govuk-frontend

Version:

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

254 lines (244 loc) 9.25 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = global.GOVUKFrontend || {})); })(this, (function (exports) { 'use strict'; function isInitialised($root, moduleName) { return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`); } /** * Checks if GOV.UK Frontend is supported on this page * * Some browsers will load and run our JavaScript but GOV.UK Frontend * won't be supported. * * @param {HTMLElement | null} [$scope] - (internal) `<body>` HTML element checked for browser support * @returns {boolean} Whether GOV.UK Frontend is supported on this page */ function isSupported($scope = document.body) { if (!$scope) { return false; } return $scope.classList.contains('govuk-frontend-supported'); } function isArray(option) { return Array.isArray(option); } function isObject(option) { return !!option && typeof option === 'object' && !isArray(option); } function formatErrorMessage(Component, message) { return `${Component.moduleName}: ${message}`; } /** * @typedef ComponentWithModuleName * @property {string} moduleName - Name of the component */ class GOVUKFrontendError extends Error { constructor(...args) { super(...args); this.name = 'GOVUKFrontendError'; } } class SupportError extends GOVUKFrontendError { /** * Checks if GOV.UK Frontend is supported on this page * * @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support */ constructor($scope = document.body) { const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser'; super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`'); this.name = 'SupportError'; } } class ElementError extends GOVUKFrontendError { constructor(messageOrOptions) { let message = typeof messageOrOptions === 'string' ? messageOrOptions : ''; if (isObject(messageOrOptions)) { const { component, identifier, element, expectedType } = messageOrOptions; message = identifier; message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found'; if (component) { message = formatErrorMessage(component, message); } } super(message); this.name = 'ElementError'; } } class InitError extends GOVUKFrontendError { constructor(componentOrMessage) { const message = typeof componentOrMessage === 'string' ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\`$root\`) already initialised`); super(message); this.name = 'InitError'; } } /** * @import { ComponentWithModuleName } from '../common/index.mjs' */ class Component { /** * Returns the root element of the component * * @protected * @returns {RootElementType} - the root element of component */ get $root() { return this._$root; } constructor($root) { this._$root = void 0; const childConstructor = this.constructor; if (typeof childConstructor.moduleName !== 'string') { throw new InitError(`\`moduleName\` not defined in component`); } if (!($root instanceof childConstructor.elementType)) { throw new ElementError({ element: $root, component: childConstructor, identifier: 'Root element (`$root`)', expectedType: childConstructor.elementType.name }); } else { this._$root = $root; } childConstructor.checkSupport(); this.checkInitialised(); const moduleName = childConstructor.moduleName; this.$root.setAttribute(`data-${moduleName}-init`, ''); } checkInitialised() { const constructor = this.constructor; const moduleName = constructor.moduleName; if (moduleName && isInitialised(this.$root, moduleName)) { throw new InitError(constructor); } } static checkSupport() { if (!isSupported()) { throw new SupportError(); } } } /** * @typedef ChildClass * @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component */ /** * @typedef {typeof Component & ChildClass} ChildClassConstructor */ Component.elementType = HTMLElement; /** * Checkboxes component * * @preserve */ class Checkboxes extends Component { /** * Checkboxes can be associated with a 'conditionally revealed' content block * – for example, a checkbox for 'Phone' could reveal an additional form field * for the user to enter their phone number. * * These associations are made using a `data-aria-controls` attribute, which * is promoted to an aria-controls attribute during initialisation. * * We also need to restore the state of any conditional reveals on the page * (for example if the user has navigated back), and set up event handlers to * keep the reveal in sync with the checkbox state. * * @param {Element | null} $root - HTML element to use for checkboxes */ constructor($root) { super($root); this.$inputs = void 0; const $inputs = this.$root.querySelectorAll('input[type="checkbox"]'); if (!$inputs.length) { throw new ElementError({ component: Checkboxes, identifier: 'Form inputs (`<input type="checkbox">`)' }); } this.$inputs = $inputs; this.$inputs.forEach($input => { const targetId = $input.getAttribute('data-aria-controls'); if (!targetId) { return; } if (!document.getElementById(targetId)) { throw new ElementError({ component: Checkboxes, identifier: `Conditional reveal (\`id="${targetId}"\`)` }); } $input.setAttribute('aria-controls', targetId); $input.removeAttribute('data-aria-controls'); }); window.addEventListener('pageshow', () => this.syncAllConditionalReveals()); this.syncAllConditionalReveals(); this.$root.addEventListener('click', event => this.handleClick(event)); } syncAllConditionalReveals() { this.$inputs.forEach($input => this.syncConditionalRevealWithInputState($input)); } syncConditionalRevealWithInputState($input) { const targetId = $input.getAttribute('aria-controls'); if (!targetId) { return; } const $target = document.getElementById(targetId); if ($target != null && $target.classList.contains('govuk-checkboxes__conditional')) { const inputIsChecked = $input.checked; $input.setAttribute('aria-expanded', inputIsChecked.toString()); $target.classList.toggle('govuk-checkboxes__conditional--hidden', !inputIsChecked); } } unCheckAllInputsExcept($input) { const allInputsWithSameName = document.querySelectorAll(`input[type="checkbox"][name="${$input.name}"]`); allInputsWithSameName.forEach($inputWithSameName => { const hasSameFormOwner = $input.form === $inputWithSameName.form; if (hasSameFormOwner && $inputWithSameName !== $input) { $inputWithSameName.checked = false; this.syncConditionalRevealWithInputState($inputWithSameName); } }); } unCheckExclusiveInputs($input) { const allInputsWithSameNameAndExclusiveBehaviour = document.querySelectorAll(`input[data-behaviour="exclusive"][type="checkbox"][name="${$input.name}"]`); allInputsWithSameNameAndExclusiveBehaviour.forEach($exclusiveInput => { const hasSameFormOwner = $input.form === $exclusiveInput.form; if (hasSameFormOwner) { $exclusiveInput.checked = false; this.syncConditionalRevealWithInputState($exclusiveInput); } }); } handleClick(event) { const $clickedInput = event.target; if (!($clickedInput instanceof HTMLInputElement) || $clickedInput.type !== 'checkbox') { return; } const hasAriaControls = $clickedInput.getAttribute('aria-controls'); if (hasAriaControls) { this.syncConditionalRevealWithInputState($clickedInput); } if (!$clickedInput.checked) { return; } const hasBehaviourExclusive = $clickedInput.getAttribute('data-behaviour') === 'exclusive'; if (hasBehaviourExclusive) { this.unCheckAllInputsExcept($clickedInput); } else { this.unCheckExclusiveInputs($clickedInput); } } } Checkboxes.moduleName = 'govuk-checkboxes'; exports.Checkboxes = Checkboxes; })); //# sourceMappingURL=checkboxes.bundle.js.map