Cookie banner
-GDPR compliant cookie banner and consent form.
-Renders a cookie banner and a consent form based on configuration settings, and conditionally invokes cookie-reliant functionality based on user consent.
--
Usage
-Cookie consent is based on categorising cookies and the functions that initialise them, describing them in a configuration object passed into the module at initialisition.
-The cookie banner renders itself if no consent preferences are recorded in the browser.
-The consent form renders into a DOMElement with a particular className configurable options (classNames.formContainer).
-A page containing a cookie consent form should include a visually hidden live region (role=alert) with a particular className (classNames.formAnnouncement), default: ‘privacy-banner__form-announcement’.
-Optionally the banner also supports basic Google EU consent mode [https://developers.google.com/tag-platform/security/guides/consent?consentmode=basic], and can push user consent preferences to the dataLayer for Google libraries to use. All that is necessary to suport Google consent mode is to map Google consent categories to the cookie categories in the configuration.
-For example, to map the ad_storage, ad_user_data, and ad_personalisation to an ‘ads’ consent category defined in the banner config, add a euConsentTypes object to the configuration like this:
euConsentTypes: {
- ad_storage: 'test',
- ad_user_data: 'test',
- ad_personalization: 'test'
-}
-
-Install the package
-npm i -S @stormid/cookie-banner
-
-Create a container element for the consent form.
-<div class="privacy-banner__form-container"></div>
-
-Create a visually hidden live region for the screen reader announcement.
-<div class="visually-hidden privacy-banner__form-announcement" role="alert"></div>
-
-Initialise the module (example configuration shown below)
-import banner from '@stormid/cookie-banner';
-
-const cookieBanner = banner({
- types: {
- 'performance': {
- suggested: true, //set as pre-checked on consent form as a suggested response
- title: 'Performance preferences',
- description: 'Performance cookies are used to measure the performance of our website and make improvements. Your personal data is not identified.',
- labels: {
- yes: 'Pages you visit and actions you take will be measured and used to improve the service',
- no: 'Pages you visit and actions you take will not be measured and used to improve the service'
- },
- fns: [
- state => {
- //function that depends upon or creates a 'performance' cookie
- },
- state => state.utils.gtmSnippet(<UA-CODE>)
- ]
- },
- 'thirdParty': {
- title: 'Third party preferences',
- description: 'We work with third party partners to show you ads for our products and services across the web, and to serve video and audio content. You can choose whether we collect and share that data with our partners below. ',
- labels: {
- yes: 'Our partners might know you have visited our website',
- no: 'Our partners will will not know you have visited out website but you cannot video third party video and audio content'
- },
- fns: [
- model => {
- //function that depends upon or creates a 'performance' cookie
- },
- state => state.utils.renderIframe(),
- state => state.utils.gtmSnippet(<UA-CODE>)
- ]
- }
- }
-});
-
-Options
-{
- name: '.CookiePreferences', //name of the cookie set to record user consent
- path: '/', //path of the preferences cookie
- domain: window.location.hostname === 'localhost' ? '' : `.${removeSubdomain(window.location.hostname)}`, //domain of the preferences cookie, defaults to .<root-domain>
- secure: true, //preferences cookie secure
- samesite: 'lax', //preferences cookie samesite
- expiry: 365, //preferences cookie expiry in days
- types: {}, //types of cookie-dependent functionality
- euConsentTypes: {}, //map Google EU consent categories to types of cookie defined in 'types'
- necessary: [], //cookie-dependent functionality that will always execute, for convenience only
- policyURL: '/cookie-policy#preferences', //URL to cookie policy page (location of cookie consent form) rendered in the banner
- classNames: {
- banner: 'privacy-banner',
- acceptBtn: 'privacy-banner__accept',
- rejectBtn: 'privacy-banner__reject',
- submitBtn: 'privacy-banner__submit',
- field: 'privacy-banner__field',
- form: 'privacy-banner__form',
- fieldset: 'privacy-banner__fieldset',
- legend: 'privacy-banner__legend',
- formContainer: 'privacy-banner__form-container', //where the form is rendered
- formMessage: 'privacy-banner__form-msg',
- formAnnouncement: 'privacy-banner__form-announcement', //screen reader announcement
- title: 'privacy-banner__form-title',
- description: 'privacy-banner__form-description'
- },
- hideBannerOnFormPage: false, //don't show the banner when the user is on the same page as a consent form
- savedMessage: 'Your settings have been saved.', //displayed after consent form update,
- trapTab: false, //trap the user's keyboard tab within the banner when open
- bannerTemplate(model){
- return `<section role="dialog" aria-live="polite" aria-label="Your privacy" class="${model.classNames.banner}">
- <div class="privacy-content">
- <div class="wrap">
- <!--googleoff: all-->
- <div class="privacy-banner__title">Cookies</div>
- <p>We use cookies to improve your experience on our site and show you personalised advertising.</p>
- <p>Find out more from our <a class="privacy-banner__link" rel="noopener noreferrer nofollow" href="/privacy-policy">privacy policy</a> and <a class="privacy-banner__link" rel="noopener noreferrer nofollow" href="${model.policyURL}">cookie policy</a>.</p>
- <button class="btn btn--primary ${model.classNames.acceptBtn}">Accept and close</button>
- <a class="privacy-banner__link" rel="noopener noreferrer nofollow" href="${model.policyURL}">Your options</a>
- <!--googleon: all-->
- </div>
- </div>
- </section>`;
- },
- messageTemplate(model){
- return `<div class="${model.settings.classNames.formMessage}" aria-role="alert">${model.settings.savedMessage}</div>`
- },
- formTemplate(model){
- return `<form id="preferences" class="${model.settings.classNames.form}" novalidate>
- ${Object.keys(model.settings.types).map(type => `<fieldset class="${model.settings.classNames.fieldset}">
- <legend class="${model.settings.classNames.legend}">
- <span class="${model.settings.classNames.title}">${model.settings.types[type].title}</span>
- <span class="${model.settings.classNames.description}">${model.settings.types[type].description}</span>
- </legend>
- <div class="form-row">
- <div class="relative">
- <label class="privacy-banner__label">
- <input
- class="${model.settings.classNames.field}"
- type="radio"
- name="privacy-${type.split(' ')[0].replace(' ', '-')}"
- value="1"
- ${model.consent[type] === 1 ? ` checked` : ''}>
- <span class="privacy-banner__label-text">I am OK with this</span>
- <span class="privacy-banner__label-description">${model.settings.types[type].labels.yes}</span>
- </label>
- </div>
- </div>
- <div class="form-row">
- <div class="relative">
- <label class="privacy-banner__label">
- <input
- class="${model.settings.classNames.field}"
- type="radio"
- name="privacy-${type.split(' ')[0].replace(' ', '-')}"
- value="0"
- ${model.consent[type] === 0 ? ` checked` : ''}>
- <span class="privacy-banner__label-text">No thank you</span>
- <span class="privacy-banner__label-description">${model.settings.types[type].labels.no}</span>
- </label>
- </div>
- </div>
- </fieldset>`).join('')}
- <button class="${model.settings.classNames.submitBtn}"${Object.keys(model.consent).length === 0 ? ` disabled` : ''}>Save my settings</button>
- </form>`;
- }
-}
-
-Utility functions
-There are two utility functions provided by the library designed to be invoked following user consent.
-Render iframe
-state.utils.renderIframe
Renders an iframe from a placeholder element with specific data attributes:
-<div data-iframe-src="https://www.youtube.com/embed/qpLKTUQev30" data-iframe-title="Test video" data-iframe-height="1600px" data-iframe-width="900px">
- <p>Update your cookie preferences to view this content</p>
- <button class="js-preferences-update">Update</button>
-</div>
-
-In the cookie banner configuration:
-import cookieBanner from '@stormid/cookie-banner';
-
-cookieBanner({
- ...lots of other config
- type: {
- thirdParty: [
- state => state.utils.renderIframe()
- ]
- }
-})
-
-Google Tag Manager Snippet
-state.utils.gtmSnippet
Invokes a GTM snippet to load the GTM library via an script element, just pass the Tag Manager ID/UA number as an argument
-In the cookie banner configuration:
-import cookieBanner from '@stormid/cookie-banner';
-
-cookieBanner({
- ...lots of other config
- type: {
- thirdParty: [
- state => state.utils.gtmSnippet(`UA-1234-5678`)
- ]
- }
-})
-
-API
-The Object returned from initialisation exposes the interface
-{
- getState, Function that returns the current state Object
- showBanner, Function to show the banner, accepts a callback function
- renderForm, Function to render the consent form
-}
-
-Events
-There are three custom events that an instance of the cookie banner dispatches:
--
-
banner.showwhen the banner is displayed
-banner.hidewhen it is hidden
-banner.consentwhen consent is set or updated
-
The events are dispatched on the document. A reference to the getState function of the instance is contained in the custom event detail.
-const instance = banner(options);
-
-document.addEventListener('banner.show', e => {
- //e.g. initialise toggle for form-in-banner implementation
- const [ bannerToggle ] = toggle('.js-banner-toggle');
- const state = e.detail.getState();
- // do something with state if we want to
-});
-
-
-Tests
-npm t
-
-License
-MIT
- -Component Name
-This is a boilerplate for developing UI components by Storm Id.
--
Usage
-Create a container element in HTML
-<div class="js-boilerplate"></div>
-
-Install the package
-npm i -S @stormid/boilerplate
-
-Import the module
-import boilerplate from '@stormid/boilerplate';
-
-Initialise the module via selector string
-
-const [ instance ] = boilerplate(’.js-boilerplate’);
-
-Initialise with a DOM element
-
-const element = document.querySelector(’.js-boilerplate’); -const [ instance ] = boilerplate(element);
-
-Initialise with a Node list
-
-const elements = document.querySelectorAll(’.js-boilerplate’); -const [ instance ] = boilerplate(elements);
-
-Initialise with an Array of elements
-
-const elements = [].slice.call(document.querySelectorAll(’.js-boilerplate’)); -const [ instance ] = boilerplate(elements);
-
-## Options
-
-{ -callback: null -}
-
-For example
-
-boilerplate(’.js-selector’, { -callback(){ -console.log(this); -} -});
-
-## API
-boilerplate() returns an array of instances. Each instance exposes the interface
-
-{ -node, DOMNode augmented by initialisation -click, trigger the handleClick method -}
-
-## Tests
-
-npm t
-
-## Browser support
-
-## Dependencies
-
-## License
-MIT
-
- Modal Gallery
-Accessible modal image gallery
--
Usage
-Install the package
-npm i -S @stormid/modal-gallery
-
-A modal gallery can be created with DOM elements, or programmatically created from a JS Object.
-From HTML
-<ul>
- <li>
- <a class="js-modal-gallery" href="https://placehold.co/500x500" data-title="Image 1" data-description="Description 1" data-srcset="https://placehold.co/800x800 800w, https://placehold.co/500x500 320w">Image one</a>
- </li>
- <li>
- <a class="js-modal-gallery" href="https://placehold.co/300x800" data-title="Image 2" data-description="Description 2" data-srcset="https://placehold.co/500x800 800w, https://placehold.co/300x500 320w">Image two</a
- ></li>
-</ul>
-
-Initialise the module
-import modalGallery from '@stormid/modal-gallery';
-
-const [ gallery ] = modalGallery('.js-modal-gallery');
-
-Example MVP CSS
-.modal-gallery__outer {
- display: none;
- opacity: 0;
- position: fixed;
- overflow: hidden;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- z-index: 100;
- background-color: rgba(0,0,0,.9);
- transition: opacity 500ms ease;
-}
-.modal-gallery__outer.is--active {
- display: block;
- opacity: 1;
-}
-.modal-gallery__img-container {
- text-align:center;
-}
-.modal-gallery__img {
- margin:80px auto 0 auto;
- max-width:80%;
- max-height: 80vh;
-}
-.modal-gallery__item {
- position: fixed;
- top:0;
- left:0;
- right:0;
- bottom:0;
- opacity:0;
- visibility:hidden;
-}
-.modal-gallery__item.is--active {
- opacity:1;
- visibility:visible;
-}
-.modal-gallery__next {
- position: fixed;
- bottom:50%;
- right:25px;
-}
-.modal-gallery__previous {
- position: fixed;
- bottom:50%;
- left:25px;
-}
-.modal-gallery__close {
- position: fixed;
- top:15px;
- right:25px;
-}
-.modal-gallery__close:hover svg,
-.modal-gallery__previous:hover svg,
-.modal-gallery__next:hover svg{
- opacity:.8
-}
-.modal-gallery__total {
- position: absolute;
- bottom:25px;
- right:25px;
- color:#fff
-}
-.modal-gallery__details {
- position: fixed;
- bottom:0;
- left:120px;
- right:120px;
- padding:0 0 40px 0;
- color:#fff;
-}
-
-To create from a JavaScript Object
-import modalGallery from '@stormid/modal-gallery';
-
-const [ gallery ] = modalGallery([
- {
- src: 'https://placehold.co/500x500',
- srcset:'https://placehold.co/800x800 800w, https://placehold.co/500x500 320w',
- title: 'Image 1',
- description: 'Description 1'
- },
- {
- src: 'https://placehold.co/300x800',
- srcset:'https://placehold.co/500x800 800w, https://placehold.co/300x500 320w',
- title: 'Image 2',
- description: 'Description 2'
- }
-]);
-
-//e.g. Open the gallery at the second item (index 1) by clicking on a button with the className 'js-modal-gallery__trigger'
-document.querySelector('.js-modal-gallery__trigger').addEventListener('click', () => gallery.open(1));
-
-Options
-{
- fullscreen: false, //show gallery in fullscreen
- preload: false, //preload all images
- totals: true, //show totals
- scrollable: false, //modal is scrollable
- single: false, //single image or gallery
-}
-
-API
-modalGallery() returns an array of instances. Each instance exposes the interface
-{
- getState, a Function that returns the current state Object
- open, a Function that opens the modal gallery
-}
-
-Tests
-npm t
-
-License
-MIT
- -Modal
-Accessible modal dialog
--
Usage
-Create a modal dialog and button(s) to toggle in HTML
-<button class="js-modal-toggle">Open modal</button>
-<div id="modal-1" class="js-modal modal" data-modal-toggle="js-modal-toggle" hidden>
- <div class="modal__inner" role="dialog" aria-labelledby="modal-label">
- <h2 id="modal-label">Modal title</h2>
- ...
- <button class="modal__close-btn js-modal-toggle" aria-label="close">
- <svg focusable="false" fill="#fff" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
- <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
- <path d="M0 0h24v24H0z" fill="none"/>
- </svg>
- </button>
- </div>
-</div>
-
-Install the package
-npm i -S @stormid/modal
-
-Import the module
-import modal from '@stormid/modal';
-
-Initialise the module via selector string
-const [ instance ] = modal('.js-modal');
-
-Initialise with a DOM element
-const element = document.querySelector('.js-modal');
-const [ instance ] = modal(element);
-
-Initialise with a Node list
-const elements = document.querySelectorAll('.js-modal');
-const [ instance ] = modal(elements);
-
-Initialise with an Array of elements
-const elements = [].slice.call(document.querySelectorAll('.js-modal'));
-const [ instance ] = modal(elements);
-
-CSS -The className ‘is–modal’ added to the document.body when the modal is open. This can be used to prevent the body from scrolling
-.is--modal {
- overflow: hidden;
-}
-
-Options
-Options can be set during initialising in an Object passed as the second argument to the modal function, e.g. modal('.js-modal', { startOpen: true }), or as data-attributes on the modal element (the element passed to the modal function), e.g. data-start-open="true"
{
- onClassName: 'is--active', //className added to node when modal is open
- toggleSelectorAttribute: 'data-modal-toggle', //attribute on node to use as toggle selector
- callback: false, //optional function called after modal state change
- delay: 0, //ms delay before focus on first focuable element
- startOpen //boolean, to trigger modal to open when initialised
-}
-
-API
-modal() returns an array of instances. Each instance exposes the interface
-{
- getState, a Function that returns the current state Object
- open, a Function that opens the modal
- close a Function that closes the modal
-}
-
-Tests
-npm t
-
-License
-MIT
- -Outliner
-Adds a classNamw to the documentElement to be used to hide CSS outline on mouse interactions, show on keyboard interactions. Until :focus-visible has broader browser support.
--
Usage
-Install the package
-npm i -S @stormid/outliner
-
-Initialise the module
-import '@stormid/outliner';
-
-
-Add CSS
-.no-outline * {
- outline: 0 none !important;
- box-shadow: none !important;
-}
-
-Tests
-npm t
-
-License
-MIT
- -Scroll points
-Trigger className changes and callbacks based on element intersecting the viewport using IntersectionObservers.
--
Usage
-Add the selector to the DOMElement you wish to become a scroll-point
-<div class="js-scroll-point"></div>
-
-Install the package
-npm i -S @stormid/scroll-points
-
-Import the module
-import scrollPoints from '@stormid/scroll-points';
-
-Initialise the module via selector string
-const [ instance ] = scrollPoints('.js-scroll-points');
-
-Initialise with a DOM element
-const element = document.querySelector('.js-scroll-points');
-const [ instance ] = scrollPoints(element);
-
-Initialise with a Node list
-const elements = document.querySelectorAll('.js-scroll-points');
-const [ instance ] = scrollPoints(elements);
-
-Initialise with an Array of elements
-const elements = [].slice.call(document.querySelectorAll('.js-scroll-points'));
-const [ instance ] = scrollPoints(elements);
-
-Options
-{
- root: null, //element that is used as the viewport for checking visiblity of the target
- rootMargin: '0px 0px 0px 0px', //margin around the root, px or percentage values
- threshold: 0, //Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed
- callback: false, //function executed when scrolled into view
- className: 'is--scrolled-in', //className added when scrolled into view
- unload: true //only callback once
-};
-
-Tests
-npm t
-
-Browser support
-Depends on Object.assign and the IntersectionObserver API, IE11 will require polyfills.
-License
-MIT
- -Scroll Spy
-Use the IntersectionObserver API to check when a section of the document is in view and update an associated DOM node.
-Useful for scroll position-related navigation state management.
--
Example usage
-<header>
- <nav aria-label="mMain navigation">
- <a class="js-scroll-spy" href="#section1">Section 1</a>
- <a class="js-scroll-spy" href="#section2">Section 2</a>
- <a class="js-scroll-spy" href="#section3">Section 3</a>
- </nav>
-</header>
-<main>
- <section id="section1" aria-label="Section 1">
- ...
- </section>
- <section id="section2" aria-label="Section 2">
- ...
- </section>
- <section id="section3" aria-label="Section 3">
- ...
- </section>
-</main>
-
-Install the package
-npm i -S @stormid/scroll-spy
-
-Import the module
-import scrollSpy from '@stormid/scroll-spy';
-
-Initialise the module via selector string
-const instance = scrollSpy('.js-scroll-spy');
-
-Initialise with a DOM element
-const element = document.querySelector('.js-scroll-spy');
-const instance = scrollSpy(element);
-
-Initialise with a Node list
-const elements = document.querySelectorAll('.js-scroll-spy');
-const [ instance ] = scrollSpy(elements);
-
-Initialise with an Array of elements
-const elements = [].slice.call(document.querySelectorAll('.js-scroll-spy'));
-const instance = scrollSpy(elements);
-
-Options
-{
- root: null, //element that is used as the viewport for checking visiblity of the target
- rootMargin: '0px 0px 0px 0px', //margin around the root, px or percentage values
- threshold: 0, //Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed
- activeClassName: 'is--active', //className added when in view
- callback: null, //function executed when intersecting view
- single: true // boolean to indicate whether a single or multiple spies can be active at once
-}
-
-API
-scrollSpy() returns an array of instances. Each instance exposes the interface
-{
- getState, a Function that returns the current state Object
-}
-
-Tests
-npm t
-
-Browser support
-Depends on Object.assign and the IntersectionObserver API, IE11 will require polyfills.
-License
-MIT
- -Skip
-Ensure fragment identifier links (e.g. ‘skip to content’) focus on their target node
--
Usage
-JS
-npm i -S @stormid/skip
-
-import '@stormid/skip';
-
-Tests
-npm t
-
-License
-MIT
- -Tabs
-Accessible tabbed panelled content areas
--
Usage
-Create a tablist in HTML
-<div class="tabs js-tabs">
- <div class="tabs__tabslist" role="tablist">
- <a id="tab-1" class="tabs__tab js-tabs__link" href="#panel-1" role="tab">Tab 1</a>
- <a id="tab-2" class="tabs__tab js-tabs__link" href="#panel-2" role="tab">Tab 2</a>
- <a id="tab-3" class="tabs__tab js-tabs__link" href="#panel-3" role="tab">Tab 3</a>
- </div>
- <div id="panel-1" class="tabs__tabpanel" role="tabpanel">Panel 1</div>
- <div id="panel-2" class="tabs__tabpanel" role="tabpanel" hidden>Panel 2</div>
- <div id="panel-3" class="tabs__tabpanel" role="tabpanel" hidden>Panel 3</div>
-</div>
-
-Install the package
-npm i -S @stormid/tabs
-
-Import the module
-import tabs from '@stormid/tabs';
-
-Initialise the module via selector string
-const [ instance ] = tabs('.js-tabs');
-
-Initialise with a DOM element
-const element = document.querySelector('.js-tabs');
-const [ instance ] = tabs(element);
-
-Initialise with a Node list
-const elements = document.querySelectorAll('.js-tabs');
-const [ instance ] = tabs(elements);
-
-Initialise with an Array of elements
-const elements = [].slice.call(document.querySelectorAll('.js-tabs'));
-const [ instance ] = tabs(elements);
-
-Options
-{
- tabSelector: '[role=tab]', // selector for a tab link
- activeClass: 'is--active', //className added to active tab
- updateURL: true, //push tab fragment identifier to window location hash
- activeIndex: 0 //index of initially active tab
- focusOnLoad: true //a boolean to set whether the page should focus on the first tab after loading
-}
-
-Setting the active tab
-On page load the active tab will be set by (in order of precedence):
-1. The page hash. If the page hash in the address bar matches the ID of a panel, it will be activated on page load
-2. The data-active-index attribute. If the tabs node found to have a <pre>data-active-index</pre> attribute, that tab will be activated on page load. This is a zero-based index.
-3. The tab specified by the activeIndex in the settings. This is a zero-based index.
-4. The first tab in the set.
-
-## API
-
-tabs() returns an array of instances. Each instance exposes the interface
-
-{ -getState, a Function that returns the current state Object -}
-
-## Tests
-
-npm t
-
-## License
-MIT
-
- Textarea
-Auto-resizing textarea
--
Usage
-Install the package
-npm i -S @stormid/textarea
-
-Import the module
-import textarea from '@stormid/textarea';
-
-Initialise the module via selector string
-const [ instance ] = boilerplate('.js-boilerplate');
-
-Initialise with a DOM element
-const element = document.querySelector('textarea');
-const [ instance ] = textarea(element);
-
-Initialise with a Node list
-const elements = document.querySelectorAll('textarea');
-const [ instance ] = textarea(elements);
-
-Initialise with an Array of elements
-const elements = [].slice.call(document.querySelectorAll('textarea'));
-const [ instance ] = textarea(elements);
-
-Options
-{
- events: [
- 'input' //default textarea resize event
- ]
-}
-
-API
-textarea() returns an array of instances. Each instance exposes the interface
-{
- node, DOMElement, the text area
- resize, Function to trigger resize
-}
-
-Tests
-npm t
-
-License
-MIT
- -Toggle
-Accessible DOM state toggling for off-canvas and show/hide UI patterns using aria-expanded.
--
Usage
-For page-level state toggling (e.g. an off-canvas menu)
-Create a target and related button(s) in HTML
-<button class="js-toggle-btn">Menu</button>
-<nav id="primary-navigation" aria-label="Main navigation" class="js-toggle" data-toggle="js-toggle-btn">...</nav>
-
-Install the package
-npm i -S @stormid/toggle
-
-Import the module
-import toggle from '@stormid/toggle';
-
-Initialise the module via selector string
-const [ instance ] = toggle('.js-toggle');
-
-Initialise with a DOM element
-const element = document.querySelector('.js-toggle');
-const [ instance ] = toggle(element);
-
-Initialise with a Node list
-const elements = document.querySelectorAll('.js-toggle');
-const [ instance ] = toggle(elements);
-
-Initialise with an Array of elements
-const elements = [].slice.call(document.querySelectorAll('.js-toggle'));
-const [ instance ] = toggle(elements);
-
-Local toggle
-To localise a toggle state to part of the document (e.g. show/hide panel)
-Create a target and related button(s) in HTML
-<div class="parent">
- <button type="button" class="js-toggle__btn"></button>
- <div id="child" class="js-toggle__local child" data-toggle="js-toggle__btn"></div>
-</div>
-
-Example MVP CSS
-.child {
- display: none
-}
-.parent.is--active .child {
- display: static;
-}
-
-Options
-{
- delay: 0, //duration of animating out of toggled state
- startOpen: false, //initial toggle state
- local: false, // encapsulate in small part of document
- prehook: false, //function to fire before each toggle
- callback: false, //function to fire after each toggle
- focus: false, //focus on first focusable child node of the target element
- trapTab: false, //trap tab in the target element
- closeOnBlur: false, //close the target node on losing focus from the target node and any of the toggles
- closeOnClick: false, //close the target element when a non-child element is clicked
-}
-
-e.g.
-const [ instance ] = toggle('.js-toggle', {
- startOpen: true
-});
-
-Options can also be set on an instance by adding data-attributes to the toggle element, e.g.
-<div class="parent">
- <button type="button" class="js-toggle__btn"></button>
- <div class="js-toggle__local" data-toggle="js-toggle__btn" data-start-open="true"></div>
-</div>
-
-A toggle can also be started open usng the active className alone, e.g.
-<div class="parent is--active">
- <button type="button" class="js-toggle__btn"></button>
- <div class="js-toggle__local" data-toggle="js-toggle__btn"></div>
-</div>
-
-Developer note 06 Sep 2022: Use of closeOnBlur
-It should be noted that at the time of writing, the availaibility of the blur event was limited on mobile assistive tech, specifically iOS VoiceOver.
-When a user is swiping through content in VoiceOver, the focus/blur events will only fire if the focus is moving to or from a form input element or button. The focus/blur events will not fire when moving between links, headings or in-page content. Any use of the closeOnBlur setting should be carefully tested to make sure that the behaviour is as expected on these devices.
-API
-toggle() returns an array of instances. Each instance exposes the interface
-{
- node, DOMElement, the text area
- startToggle, a Function that starts the toggle lifecycle with prehook, toggle, and post-toggle callback
- toggle, a Function that just executes the toggle
- getState, a Function that returns the current state Object
-}
-
-Events
-There are two custom events that an instance of the toggle dispatches:
--
-
toggle.openwhen it opens
-toggle.closewhen closes
-
The events are dispatched on the same element used to initialise the toggle and bubble for event delegation. The a reference to the getState function of the instance is contained in the custom event detail.
-const [ instance ] = toggle('.js-toggle');
-
-//event bubbles so can delegate
-//could also add event listener to document.querySelector('.js-toggle')
-document.addEventListener('toggle.open', e => {
- const { node, toggles } = e.detail.getState();
- // do something
-});
-
-
-Tests
-npm t
-
-License
-MIT
- -Validate
-Client-side form validation library to support .NET validation using data-val attributes as a drop-in replacement for jQuery validate, and HTML5 attribute-based constraint validation.
--
Contents
- -Usage
-Install the package
-npm i -S @stormid/validate
-
-Import the module
-import validate from '@stormid/validate';
-
-Initialise the module via selector string
-const [ validator ] = validate('form:not([novalidate])');
-
-Initialise with a DOM element
-const element = document.querySelector('form:not([novalidate])');
-const [ validator ] = validate(element);
-
-Initialise with a Node list
-const elements = document.querySelectorAll('form:not([novalidate])');
-const [ validator ] = validate(elements);
-
-Initialise with an Array of elements
-const elements = [].slice.call(document.querySelectorAll('form:not([novalidate])'));
-const [ validator ] = validate(elements);
-
-Validators
-This library supports HTML5 attribute constraints and the data-val attributes generated by .Net Core model validation, .Net MVC DataAnnotation, or .Net Fluent validation libraries.
-Multiple validators can be used on a single field. Custom validators can be added via the addMethod API.
--
-
- Validate - - -
-
Required
-The field must have a value. Checkboxes are treated as non-nullable in required validation.
-HTML5
-<input name="field" id="field" required>
-
-Data attributes
-<input name="field" id="field" data-val="true" data-val-required="'field' is required">
-
--
Value is matched against the regular expression
-/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
-
-which is equivalent to the browser algorithm used in the constraint validation API for type=“email”, and is intentionally loose due to known issues related to international domain names and the validation of e-mail addresses in HTML. Stricter or more specific validation rules can be specified with a pattern attribute (or data-val-regex).
-HTML5
-<input type="email" name="field" id="field">
-
-Data attributes
-<input name="field" id="field" data-val="true" data-val-email="'field' must be a valid email address">
-
--
Url
-The value is matched against this regular expresion by Diego Perini
-/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
-
-Stricter or more specific validation rules can be specified with a pattern attribute (or data-val-regex).
-HTML5
-<input type="url" name="field" id="field">
-
-Data attributes
-<input name="field" id="field" data-val="true" data-val-url="'field' must be a valid url">
-
--
Pattern/Regex
-Value matches the supplied pattern or regular expression
-HTML5
-<input name="field" id="field" pattern="^http(s)?">
-
-Data attributes
-<input name="field" id="field" data-val="true" data-val-regex="'field' must start with http or https" data-val-regex-pattern="^http(s)?">
-
--
Digits
-Value must contain only characters in the range 0-9
-Data attributes
-<input name="field" id="field" data-val="true" data-val-digits="'field' must be a number">
-
--
Number
-Value is matched against the regular expresion
-/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;
-
-Allowing positive and negative numbers, decimals, and comma separated thousands.
-HTML5
-<input type="number" name="field" id="field">
-
-Data attributes
-<input name="field" id="field" data-val="true" data-val-number="'field' must be a number">
-
--
Min
-Value is a Number greater or equal to min
-HTML5
-<input type="number" name="field" id="field" min="0">
-
-Data attributes
-<input type="number" name="field" id="field" data-val="true" data-val-min="'field' must be >= 0" data-val-min-min="0">
-
--
Max
-Value is a Number less than or equal to max
-HTML5
-<input type="number" name="field" id="field" max="100">
-
-Data attributes
-<input type="number" name="field" id="field" data-val="true" data-val-max="'field' must be <= 100" data-val-max-max="100">
-
--
Range
-Value is a Number within the specified min and max
-Data attributes
-<input type="number" name="field" id="field" data-val="true" data-val-range="'field' must be between 0 and 100" data-val-range-min="0" data-val-range-max="100">
-
--
Length
-Value is a String with a length greater than or equal to min and/or a length less than or equal to max
-Data attributes
-<input name="field" id="field" data-val="true" data-val-length="'field' length must be between 2 and 4" data-val-length-min="2" data-val-length-max="4">
-
--
Stringlength
-Value is a String with a length less than or equal to max. Equivalent to the length/max validator, generated by .Net stringlength data notation.
-Data attributes
-<input name="field" id="field" data-val="true" data-val-stringlength="'field' length must be less than 4" data-val-stringlength-max="4">
-
--
Maxlength
-Value is a String with a length less than or equal to max. Equivalent to the length/max validator, generated by .Net maxlength data notation.
-.HTML5
-<input name="field" id="field" maxlength="4">
-
-Data attributes
-<input name="field" id="field" data-val="true" data-val-maxlength="'field' length must be less than 4" data-val-maxlength-max="4">
-
--
Minlength
-Value is a String with a length greater than or equal to min. Equivalent to the length/min validator, generated by .Net minlength data notation.
-.HTML5
-<input name="field" id="field" minlength="2">
-
-Data attributes
-<input name="field" id="field" data-val="true" data-val-minlength="'field' length must be greater than 2" data-val-minlength-min="2">
-
--
DateISO
-Value is a string in a format matching the date ISO standard, (YYYY-MM-DD), matching the regular expression
-/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/
-
-Data attributes
-<input name="field" id="field" data-val="true" data-val-dateISO="'field' must in the format YYYY-MM-DD">
-
--
Equalto
-Value must match the value of another field (or multiple fields). The data-val-equalto-other attribute is the name of another field, or a comma-separated list of names of multiple fields.
-Data attributes
-<input name="password" id="password">
-...
-<input name="passwordConfirmation" id="passwordConfirmation" data-val="true" data-val-equalto="'password confirmation' must match 'password'" data-val-equalto-other="password">
-
--
Remote
-Value is validated against via XHR against a remote resource. The resouce is defined by the data-val-remote-url attribute.
Defaults to a POST request, data-val-remote-type can be set to make GET requests.
The values of additional fields can be sent in the request by specifying a data-val-remote-additionalfields attribute, a comma separated list of field ids to be included.
Data attributes
-<input name="field" id="field" data-val="true" data-val-remote="'field' must pass remote validation" data-val-remote-url="/api/validate">
-
--
Errors
-Error message container
-The element to contain server-side error messages generated by .Net fluent and unobtrustive validation libraries is recycled by this library, it can also be manually added to HTML when you need more control over the position and markup of the error message container. The data-valmsg-for provides the association between error message and field/field group name, the id is used by the library to associate the error with the input using aria-describedby.
<span id="field-error-message" class="field-validation-valid" data-valmsg-for="field" />
-
-If this element is not present a span is appended to the label for the field with the className .error-message.
Error messages
-.Net error messages are extracted from data-val-[validator-type] data attributes, and apply to both HTML5 and .Net validators.
-Fields without data-val error messages will show the default messages for the failed validator (see options below).
-Including values in error messages
-To include the user input value in your error message, place a token “{{value}}” within the message string and the script will replace it at the time of validation. e.g. “{{value}} is not a valid email address” will become “test@test is not a valid email address”.
-If a validation group contains more than one field, the values of these will be returned as a comma seperated list within the message. For example: “{{value}} are not valid inputs” becomes “test1, test2 are not valid inputs”.
-Options
-{
- preSubmitHook: false, //function, called on validation pass, before submit
- submit: form.submit, // function, to support async form submissions, pass your own submit function
- messages: { //default HTML5 error messages
- required() { return 'This field is required'; } ,
- email() { return 'Please enter a valid email address'; },
- pattern() { return 'The value must match the pattern'; },
- url(){ return 'Please enter a valid URL'; },
- number() { return 'Please enter a valid number'; },
- digits() { return 'Please enter only digits'; },
- maxlength(props) { return `Please enter no more than ${props.max} characters`; },
- minlength(props) { return `Please enter at least ${props.min} characters`; },
- max(props){ return `Please enter a value less than or equal to ${props.max}`; },
- min(props){ return `Please enter a value greater than or equal to ${props.min}`}
- }
-}
-
-API
-validate() returns an array of instances. Each instance exposes the interface
-{
- validate
- addMethod
- validateGroup
- addGroup
- removeGroup
-}
-
-addMethod
-Add a custom validation method to a group:
-const [ validator ] = validate('.my-form');
-
-validator.addMethod(
- 'MyFieldName', //input/input group name/or validation group name if passing an array of fields
- (value, fields) => { //validation method
- //value is the value of the whole group of fields (grouped under the name attribute)
- return value === 'test'; //must return boolean
- },
- 'Value must equal "test"', //error message on validation failure
- fields // an optional array of inputs, if this isn't present the input/groupName is used as a name (or data-group) attribute selector
-);
-
-validate
-Manually trigger validation on the whole form, returns a promise:
-const [ validator ] = validate('.my-form');
-
-await validator.validate();
-
-addGroup
-Add a field or field group to the validator:
-const [ validator ] = validate('.my-form');
-const fieldsArray = Array.from(document.querySelector('.new-fields'))
-
-//add by passing an array of fields
-//if these fields span multiple groups they will be collected into the correct validation groups internally by the validator
-validator.addGroup(fieldsArray);
-
-validateGroup
-Immediately validates an individual group within the form:
-const [ validator ] = validate('.my-form');
-const validator.validateGroup('myInput');
-
-//pass in the name or data-val-group value that corresponds to the group you're looking to validate
-//returns a promise which resolves with the validity state of the group (true if valid, false if invalid)
-
-removeGroup
-Remove a validation group from the validator:
-const [ validator ] = validate('.my-form');
-const fieldsArray = Array.from(document.querySelectorAll([name=new-fields]))
-
-//add by passing an array of fields
-validator.addGroup(fieldsArray);
-
-//remove by passing the name of a group
-validator.removeGroup('new-fields');
-
-Plugins
-Plugins are a set of pre-built custom validators that are included in the package but not in the default build. They can be imported and used in addMethod you would your own custom validation method.
-isValidDate
-Validate three separate day/month/year fields (similar to the govuk design system date component) as a single valid date.
-The minimum accepted year value in the isValidDate plugin is 1000. To set a different (more recent) minimum value consider using the min validator on the year input.
-isFutureDate
-Validate three separate day/month/year fields (similar to the govuk design system date component) as a single date in the future.
-isPastDate
-Validate three separate day/month/year fields (similar to the govuk design system date component) as a single date in the past- today’s date is valid.
-HTML
-<fieldset>
- <legend>
- <span>Date</span>
- <span data-valmsg-for="date" id="date-error-message"></span>
- <span data-valmsg-for="dateDay" id="date-Day-error-message"></span>
- <span data-valmsg-for="dateMonth" id="date-Month-error-message"></span>
- <span data-valmsg-for="dateYear" id="date-Year-error-message"></span>
- </legend>
- <div class="flex">
- <input id="dateDay" name="dateDay" inputmode="numeric" data-val="true" data-val-required="Enter a day" aria-required="true"/>
- <input id="dateMonth" name="dateMonth" inputmode="numeric" data-val="true" data-val-required="Enter a month" aria-required="true"/>
- <input id="dateYear" name="dateYear" inputmode="numeric" data-val="true" data-val-required="Enter a year" aria-required="true" />
- </div>
-</fieldset>
-
-JS
-import validate from '@stormid/validate';
-import { isValidDate } from '@stormid/validate/src/lib/plugins/methods/date';
-
-const [ validator ] = validate('.my-form');
-validator.addMethod(
- 'date', //name of custom validation group
- isValidDate, // date validation method imported from the library
- 'Enter a valid date', // error message
- [ document.getElementById('dateDay'), document.getElementById('dateMonth'), document.getElementById('dateYear') ] //date fields array [day, month, year]
-);
-
-Tests
-npm t
-
-License
-MIT
- --
2. Local toggle
-Local toggles change the className of the target element's parentNode rather than the documentElement for isolating state to part of the DOM
- -Local toggles change the className of the target element's parentNode rather than the documentElement for isolating state to part of the DOM
- -