UNPKG

@financial-times/o-cookie-message

Version:

The cookie message and behaviour approved by the FT's legal team. All FT websites must have a cookie message. Using o-cookie-message will ensure your site is compliant according to EU regulatory law.

288 lines (252 loc) 8.88 kB
class CookieMessage { constructor(cookieMessageElement, options) { if (cookieMessageElement === null || cookieMessageElement === undefined) { cookieMessageElement = document.createElement('div'); document.body.append(cookieMessageElement); } this.cookieMessageElement = cookieMessageElement; /** * @typedef EventListener * @type {Function} * @param {Event} event - The event object */ /** * Keep track of event listeners to remove within the destroy method * * @type {Array<{target: EventTarget, type: string, listener: EventListener}>} * @access private */ this._eventListeners = []; // Get cookie message options options = options || CookieMessage.getOptionsFromDom(cookieMessageElement); // Set cookie message options this.options = Object.assign({}, options); this.options.theme = this.options.theme ? 'alternative' : null; this.cookieInfo = this.getCookieInfo(); if (this.shouldShowCookieMessage()) { this.createCookieMessage(); this.showCookieMessage(); } else { this.removeCookieMessage(); } const pageshowListener = event => { if ( event.persisted === true && this.shouldShowCookieMessage() === false ) { return this.removeCookieMessage(); } }; window.addEventListener('pageshow', pageshowListener); this._eventListeners.push({ target: window, type: 'pageshow', listener: pageshowListener }); } getCookieInfo() { let domain = 'ft.com'; let manageCookiesPath = 'preferences/manage-cookies'; if (!/\.ft\.com$/i.test(window.location.hostname)) { // replace www or subdomain domain = window.location.hostname.replace(/^(.*?)\./, ''); manageCookiesPath = 'preferences/cookies'; } if (typeof this.options.manageCookiesPath === 'string') { manageCookiesPath = this.options.manageCookiesPath; } const redirect = window.location.href; return { acceptUrl: `https://consent.${domain}/__consent/consent-record-cookie?cookieDomain=.${domain}`, acceptUrlFallback: `https://consent.${domain}/__consent/consent-record-cookie?redirect=${redirect}&cookieDomain=.${domain}`, manageCookiesUrl: `https://cookies.${domain}/${manageCookiesPath}?redirect=${redirect}&cookieDomain=.${domain}`, consentCookieName: 'FTCookieConsentGDPR', }; } createCookieMessage() { const wrapContent = content => ` <div class="o-cookie-message__outer" data-nosnippet="true"> <div class="o-cookie-message__inner"> <div class="o-cookie-message__content"> ${content} </div> <div class="o-cookie-message__actions"> <div class="o-cookie-message__action o-cookie-message__action--secondary"> <a href="${this.cookieInfo.manageCookiesUrl}" class="o-cookie-message__link">Manage cookies</a> </div> <div class="o-cookie-message__action"> <a href="${this.cookieInfo.acceptUrlFallback}" class="o-cookie-message__button"> Accept cookies </a> </div> </div> </div> </div>`; const labelId = 'o-cookie-message-label'; const descriptionId = 'o-cookie-message-description'; const defaultContent = ` <div class="o-cookie-message__heading"> <h2 id="${labelId}">Cookies on the FT</h2> </div> <p id="${descriptionId}"> We use <a href="https://help.ft.com/help/legal-privacy/cookies/" class="o-cookie-message__link" target="_blank" rel="noopener">cookies</a> for a number of reasons, such as keeping FT Sites reliable and secure, personalising content and ads, providing social media features and to analyse how our Sites are used. </p>`; const child = this.cookieMessageElement.firstElementChild; const html = this.cookieMessageElement.innerHTML; if (child && child.classList.contains('o-cookie-message__outer')) { // full custom html, leave it alone } else if (html.trim() === '') { // empty, provide default content this.cookieMessageElement.innerHTML = wrapContent(defaultContent); // with default content ids we can setup a labeled dialog role this.cookieMessageElement.setAttribute('role', 'dialog'); this.cookieMessageElement.setAttribute('aria-labelledby', labelId); this.cookieMessageElement.setAttribute('aria-describedby', descriptionId); } else { // some custom html, wrap it up this.cookieMessageElement.innerHTML = wrapContent(html); } } /** * Enables cookie setting behaviour from the FT consent service * https://github.com/Financial-Times/next-consent-proxy/tree/master/src * * @returns {void} */ updateConsent() { const button = document.querySelector(`.o-cookie-message__button`); if (button) { const clickListener = e => { e.preventDefault(); this.dispatchEvent('oCookieMessage.act'); this.removeCookieMessage(); return fetch(this.cookieInfo.acceptUrl, { method: 'get', credentials: 'include', }); }; button.addEventListener('click', clickListener); this._eventListeners.push({ target: button, type: 'click', listener: clickListener }); } } /** * Checks whether cookie is set * * @returns {boolean} - should the cookie message be shown */ shouldShowCookieMessage() { return !document.cookie.includes(`${this.cookieInfo.consentCookieName}`); } /** * Displays cookie message banner, based on existing cookies. * * @returns {void} */ showCookieMessage() { this.cookieMessageElement.classList.add( 'o-cookie-message', 'o-cookie-message--active' ); if (this.options.theme) { this.cookieMessageElement.classList.add( `o-cookie-message--${this.options.theme}` ); } this.dispatchEvent('oCookieMessage.view'); this.updateConsent(); } /** * Removes cookie message banner. * * @returns {void} */ removeCookieMessage() { this.dispatchEvent('oCookieMessage.close'); try { this.cookieMessageElement.parentNode.removeChild( this.cookieMessageElement ); } catch (err) { // cookieMessageElement or its parentNode has already been removed } } /** * Undo theme and event listeners set on init. * * @returns {void} */ destroy() { this.cookieMessageElement.classList.remove( `o-cookie-message--${this.options.theme}` ); this._eventListeners.forEach(eventListener => { const {target, type, listener} = eventListener; target.removeEventListener(type, listener); }); } dispatchEvent(eventName) { const e = new CustomEvent(eventName, {bubbles: true}); this.cookieMessageElement.dispatchEvent(e); } /** * Get the data attributes from the cookieMessageElement. If the cookie message is being set up * declaratively, this method is used to extract the data attributes from the DOM. * * @param {HTMLElement} cookieMessageElement - The cookie message element in the DOM * @returns {Object.<string, any>} - The options */ static getOptionsFromDom(cookieMessageElement) { if (!(cookieMessageElement instanceof HTMLElement)) { return {}; } return Object.keys(cookieMessageElement.dataset).reduce((options, key) => { // Ignore data-o-component if (key === 'oComponent') { return options; } // Build a concise key and get the option value const shortKey = key.replace( /^oCookieMessage(\w)(\w+)$/, (m, m1, m2) => m1.toLowerCase() + m2 ); const value = cookieMessageElement.dataset[key]; // Try parsing the value as JSON, otherwise just set it as a string try { options[shortKey] = JSON.parse(value.replace(/\'/g, '"')); } catch (error) { options[shortKey] = value; } return options; }, {}); } /** * Initialise cookie message components. * * @param {(HTMLElement | string)} rootElement - The root element to intialise cookie messages in, or a CSS selector for the root element * @param {object} [options={}] - An options object for configuring the cookie messages * @returns {CookieMessage | CookieMessage[]} - The newly instantiated CookieMessage (or CookieMessages, if rootElement was not a banner) */ static init(rootElement, options) { if (!rootElement) { rootElement = document.body; } // If the rootElement isnt an HTMLElement, treat it as a selector if (!(rootElement instanceof HTMLElement)) { rootElement = document.querySelector(rootElement); } // If the rootElement is an HTMLElement (ie it was found in the document anywhere) // AND the rootElement has the data-o-component=o-cookie-message then initialise just 1 cookie message (this one) if ( rootElement instanceof HTMLElement && /\bo-cookie-message\b/.test(rootElement.getAttribute('data-o-component')) ) { return new CookieMessage(rootElement, options); } // If the rootElement wasn't itself a cookie message, then find the first child that has the data-o-component=o-cookie-message set const cookieMessageElement = rootElement.querySelector( '[data-o-component="o-cookie-message"]' ); return new CookieMessage(cookieMessageElement, options); } } export default CookieMessage;