/*! * CookieConsent v2.8.2 * https://www.github.com/orestbida/cookieconsent * Author Orest Bida * Released under the MIT License */ (function(){ 'use strict'; /** * @param {HTMLElement} [root] - [optional] element where the cookieconsent will be appended * @returns {Object} cookieconsent object with API */ var CookieConsent = function(root){ /** * CHANGE THIS FLAG FALSE TO DISABLE console.log() */ var ENABLE_LOGS = true; var _config = { 'mode': 'opt-in', // 'opt-in', 'opt-out' 'current_lang': 'en', 'auto_language': null, 'autorun': true, // run as soon as loaded 'page_scripts': true, 'hide_from_bots': true, 'cookie_name': 'cc_cookie', 'cookie_expiration': 182, // default: 6 months (in days) 'cookie_domain': window.location.hostname, // default: current domain 'cookie_path': '/', 'cookie_same_site': 'Lax', 'use_rfc_cookie': false, 'autoclear_cookies': true, 'revision': 0, 'script_selector': 'data-cookiecategory' }; var /** * Object which holds the main methods/API (.show, .run, ...) */ _cookieconsent = {}, /** * Global user configuration object */ user_config, /** * Internal state variables */ saved_cookie_content = {}, cookie_data = null, /** * @type {Date} */ consent_date, /** * @type {Date} */ last_consent_update, /** * @type {string} */ consent_uuid, /** * @type {boolean} */ invalid_consent = true, consent_modal_exists = false, consent_modal_visible = false, settings_modal_visible = false, clicked_inside_modal = false, current_modal_focusable, all_table_headers, all_blocks, // Helper callback functions // (avoid calling "user_config['onAccept']" all the time) onAccept, onChange, onFirstAction, revision_enabled = false, valid_revision = true, revision_message = '', // State variables for the autoclearCookies function changed_settings = [], reload_page = false; /** * Accept type: * - "all" * - "necessary" * - "custom" * @type {string} */ var accept_type; /** * Contains all accepted categories * @type {string[]} */ var accepted_categories = []; /** * Contains all non-accepted (rejected) categories * @type {string[]} */ var rejected_categories = []; /** * Contains all categories enabled by default * @type {string[]} */ var default_enabled_categories = []; // Don't run plugin (to avoid indexing its text content) if bot detected var is_bot = false; /** * Save reference to the last focused element on the page * (used later to restore focus when both modals are closed) */ var last_elem_before_modal; var last_consent_modal_btn_focus; /** * Both of the arrays below have the same structure: * [0] => holds reference to the FIRST focusable element inside modal * [1] => holds reference to the LAST focusable element inside modal */ var consent_modal_focusable = []; var settings_modal_focusable = []; /** * Keep track of enabled/disabled categories * @type {boolean[]} */ var toggle_states = []; /** * Stores all available categories * @type {string[]} */ var all_categories = []; /** * Keep track of readonly toggles * @type {boolean[]} */ var readonly_categories = []; /** * Pointers to main dom elements (to avoid retrieving them later using document.getElementById) */ var /** @type {HTMLElement} */ html_dom = document.documentElement, /** @type {HTMLElement} */ main_container, /** @type {HTMLElement} */ all_modals_container, /** @type {HTMLElement} */ consent_modal, /** @type {HTMLElement} */ consent_modal_title, /** @type {HTMLElement} */ consent_modal_description, /** @type {HTMLElement} */ consent_primary_btn, /** @type {HTMLElement} */ consent_secondary_btn, /** @type {HTMLElement} */ consent_buttons, /** @type {HTMLElement} */ consent_modal_inner, /** @type {HTMLElement} */ settings_container, /** @type {HTMLElement} */ settings_inner, /** @type {HTMLElement} */ settings_title, /** @type {HTMLElement} */ settings_close_btn, /** @type {HTMLElement} */ settings_blocks, /** @type {HTMLElement} */ new_settings_blocks, /** @type {HTMLElement} */ settings_buttons, /** @type {HTMLElement} */ settings_save_btn, /** @type {HTMLElement} */ settings_accept_all_btn, /** @type {HTMLElement} */ settings_reject_all_btn; /** * Update config settings * @param {Object} user_config */ var _setConfig = function(_user_config){ /** * Make user configuration globally available */ user_config = _user_config; _log("CookieConsent [CONFIG]: received_config_settings ", user_config); if(typeof user_config['cookie_expiration'] === "number") _config.cookie_expiration = user_config['cookie_expiration']; if(typeof user_config['cookie_necessary_only_expiration'] === "number") _config.cookie_necessary_only_expiration = user_config['cookie_necessary_only_expiration']; if(typeof user_config['autorun'] === "boolean") _config.autorun = user_config['autorun']; if(typeof user_config['cookie_domain'] === "string") _config.cookie_domain = user_config['cookie_domain']; if(typeof user_config['cookie_same_site'] === "string") _config.cookie_same_site = user_config['cookie_same_site']; if(typeof user_config['cookie_path'] === "string") _config.cookie_path = user_config['cookie_path']; if(typeof user_config['cookie_name'] === "string") _config.cookie_name = user_config['cookie_name']; if(typeof user_config['onAccept'] === "function") onAccept = user_config['onAccept']; if(typeof user_config['onFirstAction'] === "function") onFirstAction = user_config['onFirstAction']; if(typeof user_config['onChange'] === "function") onChange = user_config['onChange']; if(user_config['mode'] === 'opt-out') _config.mode = 'opt-out'; if(typeof user_config['revision'] === "number"){ user_config['revision'] > -1 && (_config.revision = user_config['revision']); revision_enabled = true; } if(typeof user_config['autoclear_cookies'] === "boolean") _config.autoclear_cookies = user_config['autoclear_cookies']; if(user_config['use_rfc_cookie'] === true) _config.use_rfc_cookie = true; if(typeof user_config['hide_from_bots'] === "boolean"){ _config.hide_from_bots = user_config['hide_from_bots']; } if(_config.hide_from_bots){ is_bot = navigator && ((navigator.userAgent && /bot|crawl|spider|slurp|teoma/i.test(navigator.userAgent)) || navigator.webdriver); } _config.page_scripts = user_config['page_scripts'] === true; if (user_config['auto_language'] === 'browser' || user_config['auto_language'] === true) { _config.auto_language = 'browser'; } else if (user_config['auto_language'] === 'document') { _config.auto_language = 'document'; } _log("CookieConsent [LANG]: auto_language strategy is '" + _config.auto_language + "'"); _config.current_lang = _resolveCurrentLang(user_config.languages, user_config['current_lang']); } /** * Add an onClick listeners to all html elements with data-cc attribute */ var _addDataButtonListeners = function(elem){ var _a = 'accept-'; var show_settings = _getElements('c-settings'); var accept_all = _getElements(_a + 'all'); var accept_necessary = _getElements(_a + 'necessary'); var accept_custom_selection = _getElements(_a + 'custom'); for(var i=0; i} */ function _getElements(data_role){ return (elem || document).querySelectorAll('a[data-cc="' + data_role + '"], button[data-cc="' + data_role + '"]'); } /** * Helper function: accept and then hide modals * @param {PointerEvent} e source event * @param {string} [accept_type] */ function _acceptAction(e, accept_type){ e.preventDefault(); _cookieconsent.accept(accept_type); _cookieconsent.hideSettings(); _cookieconsent.hide(); } } /** * Get a valid language (at least 1 must be defined) * @param {string} lang - desired language * @param {Object} all_languages - all defined languages * @returns {string} validated language */ var _getValidatedLanguage = function(lang, all_languages){ if(Object.prototype.hasOwnProperty.call(all_languages, lang)){ return lang; }else if(_getKeys(all_languages).length > 0){ if(Object.prototype.hasOwnProperty.call(all_languages, _config.current_lang)){ return _config.current_lang ; }else{ return _getKeys(all_languages)[0]; } } } /** * Save reference to first and last focusable elements inside each modal * to prevent losing focus while navigating with TAB */ var _getModalFocusableData = function(){ /** * Note: any of the below focusable elements, which has the attribute tabindex="-1" AND is either * the first or last element of the modal, won't receive focus during "open/close" modal */ var allowed_focusable_types = ['[href]', 'button', 'input', 'details', '[tabindex="0"]']; function _getAllFocusableElements(modal, _array){ var focus_later=false, focus_first=false; // ie might throw exception due to complex unsupported selector => a:not([tabindex="-1"]) try{ var focusable_elems = modal.querySelectorAll(allowed_focusable_types.join(':not([tabindex="-1"]), ')); var attr, len=focusable_elems.length, i=0; while(i < len){ attr = focusable_elems[i].getAttribute('data-focus'); if(!focus_first && attr === "1"){ focus_first = focusable_elems[i]; }else if(attr === "0"){ focus_later = focusable_elems[i]; if(!focus_first && focusable_elems[i+1].getAttribute('data-focus') !== "0"){ focus_first = focusable_elems[i+1]; } } i++; } }catch(e){ return modal.querySelectorAll(allowed_focusable_types.join(', ')); } /** * Save first and last elements (used to lock/trap focus inside modal) */ _array[0] = focusable_elems[0]; _array[1] = focusable_elems[focusable_elems.length - 1]; _array[2] = focus_later; _array[3] = focus_first; } /** * Get settings modal'S all focusable elements * Save first and last elements (used to lock/trap focus inside modal) */ _getAllFocusableElements(settings_inner, settings_modal_focusable); /** * If consent modal exists, do the same */ if(consent_modal_exists){ _getAllFocusableElements(consent_modal, consent_modal_focusable); } } var _createConsentModal = function(lang){ if(user_config['force_consent'] === true) _addClass(html_dom, 'force--consent'); // Create modal if it doesn't exist if(!consent_modal){ consent_modal = _createNode('div'); var consent_modal_inner_inner = _createNode('div'); var overlay = _createNode('div'); consent_modal.id = 'cm'; consent_modal_inner_inner.id = 'c-inr-i'; overlay.id = 'cm-ov'; consent_modal.setAttribute('role', 'dialog'); consent_modal.setAttribute('aria-modal', 'true'); consent_modal.setAttribute('aria-hidden', 'false'); consent_modal.setAttribute('aria-labelledby', 'c-ttl'); consent_modal.setAttribute('aria-describedby', 'c-txt'); // Append consent modal to main container all_modals_container.appendChild(consent_modal); all_modals_container.appendChild(overlay); /** * Make modal by default hidden to prevent weird page jumps/flashes (shown only once css is loaded) */ consent_modal.style.visibility = overlay.style.visibility = "hidden"; overlay.style.opacity = 0; } // Use insertAdjacentHTML instead of innerHTML var consent_modal_title_value = user_config.languages[lang]['consent_modal']['title']; // Add title (if valid) if(consent_modal_title_value){ if(!consent_modal_title){ consent_modal_title = _createNode('div'); consent_modal_title.id = 'c-ttl'; consent_modal_title.setAttribute('role', 'heading'); consent_modal_title.setAttribute('aria-level', '2'); consent_modal_inner_inner.appendChild(consent_modal_title); } consent_modal_title.innerHTML = consent_modal_title_value; } var description = user_config.languages[lang]['consent_modal']['description']; if(revision_enabled){ if(!valid_revision){ description = description.replace("{{revision_message}}", revision_message || user_config.languages[lang]['consent_modal']['revision_message'] || ""); }else{ description = description.replace("{{revision_message}}", ""); } } if(!consent_modal_description){ consent_modal_description = _createNode('div'); consent_modal_description.id = 'c-txt'; consent_modal_inner_inner.appendChild(consent_modal_description); } // Set description content consent_modal_description.innerHTML = description; var primary_btn_data = user_config.languages[lang]['consent_modal']['primary_btn'], // accept current selection secondary_btn_data = user_config.languages[lang]['consent_modal']['secondary_btn']; // Add primary button if not falsy if(primary_btn_data){ if(!consent_primary_btn){ consent_primary_btn = _createNode('button'); consent_primary_btn.id = 'c-p-bn'; consent_primary_btn.className = "c-bn"; var _accept_type; if(primary_btn_data['role'] === 'accept_all') _accept_type = 'all' _addEvent(consent_primary_btn, "click", function(){ _cookieconsent.hide(); _log("CookieConsent [ACCEPT]: cookie_consent was accepted!"); _cookieconsent.accept(_accept_type); }); } consent_primary_btn.innerHTML = user_config.languages[lang]['consent_modal']['primary_btn']['text']; } // Add secondary button if not falsy if(secondary_btn_data){ if(!consent_secondary_btn){ consent_secondary_btn = _createNode('button'); consent_secondary_btn.id = 'c-s-bn'; consent_secondary_btn.className = "c-bn c_link"; if(secondary_btn_data['role'] === 'accept_necessary'){ _addEvent(consent_secondary_btn, 'click', function(){ _cookieconsent.hide(); _cookieconsent.accept([]); // accept necessary only }); }else{ _addEvent(consent_secondary_btn, 'click', function(){ _cookieconsent.showSettings(0); }); } } consent_secondary_btn.innerHTML = user_config.languages[lang]['consent_modal']['secondary_btn']['text']; } // Swap buttons var gui_options_data = user_config['gui_options']; if(!consent_modal_inner){ consent_modal_inner = _createNode('div'); consent_modal_inner.id = 'c-inr'; consent_modal_inner.appendChild(consent_modal_inner_inner); } if(!consent_buttons){ consent_buttons = _createNode('div'); consent_buttons.id = "c-bns"; if(gui_options_data && gui_options_data['consent_modal'] && gui_options_data['consent_modal']['swap_buttons'] === true){ secondary_btn_data && consent_buttons.appendChild(consent_secondary_btn); primary_btn_data && consent_buttons.appendChild(consent_primary_btn); consent_buttons.className = 'swap'; }else{ primary_btn_data && consent_buttons.appendChild(consent_primary_btn); secondary_btn_data && consent_buttons.appendChild(consent_secondary_btn); } (primary_btn_data || secondary_btn_data ) && consent_modal_inner.appendChild(consent_buttons); consent_modal.appendChild(consent_modal_inner); } consent_modal_exists = true; } var _createSettingsModal = function(lang){ /** * Create all consent_modal elements */ if(!settings_container){ settings_container = _createNode('div'); var settings_container_valign = _createNode('div'); var settings = _createNode('div'); var settings_container_inner = _createNode('div'); settings_inner = _createNode('div'); settings_title = _createNode('div'); var settings_header = _createNode('div'); settings_close_btn = _createNode('button'); var settings_close_btn_container = _createNode('div'); settings_blocks = _createNode('div'); var overlay = _createNode('div'); /** * Set ids */ settings_container.id = 's-cnt'; settings_container_valign.id = "c-vln"; settings_container_inner.id = "c-s-in"; settings.id = "cs"; settings_title.id = 's-ttl'; settings_inner.id = 's-inr'; settings_header.id = "s-hdr"; settings_blocks.id = 's-bl'; settings_close_btn.id = 's-c-bn'; overlay.id = 'cs-ov'; settings_close_btn_container.id = 's-c-bnc'; settings_close_btn.className = 'c-bn'; settings_container.setAttribute('role', 'dialog'); settings_container.setAttribute('aria-modal', 'true'); settings_container.setAttribute('aria-hidden', 'true'); settings_container.setAttribute('aria-labelledby', 's-ttl'); settings_title.setAttribute('role', 'heading'); settings_container.style.visibility = overlay.style.visibility = "hidden"; overlay.style.opacity = 0; settings_close_btn_container.appendChild(settings_close_btn); // If 'esc' key is pressed inside settings_container div => hide settings _addEvent(settings_container_valign, 'keydown', function(evt){ evt = evt || window.event; if (evt.keyCode === 27) { _cookieconsent.hideSettings(0); } }, true); _addEvent(settings_close_btn, 'click', function(){ _cookieconsent.hideSettings(0); }); }else{ new_settings_blocks = _createNode('div'); new_settings_blocks.id = 's-bl'; } // Add label to close button settings_close_btn.setAttribute('aria-label', user_config.languages[lang]['settings_modal']['close_btn_label'] || 'Close'); all_blocks = user_config.languages[lang]['settings_modal']['blocks']; all_table_headers = user_config.languages[lang]['settings_modal']['cookie_table_headers']; var n_blocks = all_blocks.length; // Set settings modal title settings_title.innerHTML = user_config.languages[lang]['settings_modal']['title']; // Create settings modal content (blocks) for(var i=0; i retrieve category states from cookie * Otherwise use states defined in the user_config. object */ if(!invalid_consent){ if(_inArray(saved_cookie_content['categories'], cookie_category) > -1){ block_switch.checked = true; !new_settings_blocks && toggle_states.push(true); }else{ !new_settings_blocks && toggle_states.push(false); } }else if(toggle_data['enabled']){ block_switch.checked = true; !new_settings_blocks && toggle_states.push(true); /** * Keep track of categories enabled by default (useful when mode=='opt-out') */ if(toggle_data['enabled']) !new_settings_blocks && default_enabled_categories.push(cookie_category); }else{ !new_settings_blocks && toggle_states.push(false); } !new_settings_blocks && all_categories.push(cookie_category); /** * Set toggle as readonly if true (disable checkbox) */ if(toggle_data['readonly']){ block_switch.disabled = true; _addClass(block_switch_span, 'c-ro'); !new_settings_blocks && readonly_categories.push(true); }else{ !new_settings_blocks && readonly_categories.push(false); } _addClass(block_table_container, 'b-acc'); _addClass(block_title_container, 'b-bn'); _addClass(block_section, 'b-ex'); block_table_container.id = accordion_id; block_table_container.setAttribute('aria-hidden', 'true'); block_switch_label.appendChild(block_switch); block_switch_label.appendChild(block_switch_span); block_switch_label.appendChild(label_text_span); block_title_container.appendChild(block_switch_label); /** * On button click handle the following :=> aria-expanded, aria-hidden and act class for current block */ isExpandable && (function(accordion, block_section, btn){ _addEvent(block_title_btn, 'click', function(){ if(!_hasClass(block_section, 'act')){ _addClass(block_section, 'act'); btn.setAttribute('aria-expanded', 'true'); accordion.setAttribute('aria-hidden', 'false'); }else{ _removeClass(block_section, 'act'); btn.setAttribute('aria-expanded', 'false'); accordion.setAttribute('aria-hidden', 'true'); } }, false); })(block_table_container, block_section, block_title_btn); }else{ /** * If block is not a button (no toggle defined), * create a simple div instead */ if(title_data){ var block_title = _createNode('div'); block_title.className = 'b-tl'; block_title.setAttribute('role', 'heading'); block_title.setAttribute('aria-level', '3'); block_title.insertAdjacentHTML('beforeend', title_data); block_title_container.appendChild(block_title); } } title_data && block_section.appendChild(block_title_container); description_data && block_table_container.appendChild(block_desc); // if cookie table found, generate table for this block if(!remove_cookie_tables && typeof cookie_table_data !== 'undefined'){ var tr_tmp_fragment = document.createDocumentFragment(); /** * Use custom table headers */ for(var p=0; p ~faster value retrieval) var curr_block = all_blocks[i]; // If current block has a toggle for opt in/out if(Object.prototype.hasOwnProperty.call(curr_block, "toggle")){ // if current block has a cookie table, an off toggle, // and its preferences were just changed => delete cookies var category_just_disabled = _inArray(changed_settings, curr_block['toggle']['value']) > -1; if( !toggle_states[++count] && Object.prototype.hasOwnProperty.call(curr_block, "cookie_table") && (clearOnFirstAction || category_just_disabled) ){ var curr_cookie_table = curr_block['cookie_table']; // Get first property name var ckey = _getKeys(all_table_headers[0])[0]; // Get number of cookies defined in cookie_table var clen = curr_cookie_table.length; // set "reload_page" to true if reload=on_disable if(curr_block['toggle']['reload'] === 'on_disable') category_just_disabled && (reload_page = true); // for each row defined in the cookie table for(var j=0; j filter cookie array if(is_regex){ for(var n=0; n -1) found_cookies.push(all_cookies_array[found_index]); } _log("CookieConsent [AUTOCLEAR]: search cookie: '" + curr_cookie_name + "', found:", found_cookies); // If cookie exists -> delete it if(found_cookies.length > 0){ _eraseCookies(found_cookies, curr_cookie_path, domains); curr_block['toggle']['reload'] === 'on_clear' && (reload_page = true); } } } } } } /** * Set toggles/checkboxes based on accepted categories and save cookie * @param {string[]} accepted_categories - Array of categories to accept */ var _saveCookiePreferences = function(accepted_categories){ changed_settings = []; // Retrieve all toggle/checkbox values var category_toggles = document.querySelectorAll('.c-tgl') || []; // If there are opt in/out toggles ... if(category_toggles.length > 0){ for(var i=0; i 0) _autoclearCookies(); if(!consent_date) consent_date = new Date(); if(!consent_uuid) consent_uuid = _uuidv4(); saved_cookie_content = { "categories": accepted_categories, "revision": _config.revision, "data": cookie_data, "rfc_cookie": _config.use_rfc_cookie, "consent_date": consent_date.toISOString(), "consent_uuid": consent_uuid } // save cookie with preferences 'categories' (only if never accepted or settings were updated) if(invalid_consent || changed_settings.length > 0){ valid_revision = true; /** * Update "last_consent_update" only if it is invalid (after t) */ if(!last_consent_update) last_consent_update = consent_date; else last_consent_update = new Date(); saved_cookie_content['last_consent_update'] = last_consent_update.toISOString(); /** * Update accept type */ accept_type = _getAcceptType(_getCurrentCategoriesState()); _setCookie(_config.cookie_name, JSON.stringify(saved_cookie_content)); _manageExistingScripts(); } if(invalid_consent){ /** * Delete unused/"zombie" cookies if consent is not valid (not yet expressed or cookie has expired) */ if(_config.autoclear_cookies) _autoclearCookies(true); if(typeof onFirstAction === 'function') onFirstAction(_cookieconsent.getUserPreferences(), saved_cookie_content); if(typeof onAccept === 'function') onAccept(saved_cookie_content); /** * Set consent as valid */ invalid_consent = false; if(_config.mode === 'opt-in') return; } // fire onChange only if settings were changed if(typeof onChange === "function" && changed_settings.length > 0) onChange(saved_cookie_content, changed_settings); /** * reload page if needed */ if(reload_page) window.location.reload(); } /** * Returns index of found element inside array, otherwise -1 * @param {Array} arr * @param {Object} value * @returns {number} */ var _inArray = function(arr, value){ return arr.indexOf(value); } /** * Helper function which prints info (console.log()) * @param {Object} print_msg * @param {Object} [optional_param] */ var _log = function(print_msg, optional_param, error){ ENABLE_LOGS && (!error ? console.log(print_msg, optional_param !== undefined ? optional_param : ' ') : console.error(print_msg, optional_param || "")); } /** * Helper function which creates an HTMLElement object based on 'type' and returns it. * @param {string} type * @returns {HTMLElement} */ var _createNode = function(type){ var el = document.createElement(type); if(type === 'button'){ el.setAttribute('type', type); } return el; } /** * Generate RFC4122-compliant UUIDs. * https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid?page=1&tab=votes#tab-top * @returns {string} */ var _uuidv4 = function(){ return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c){ try{ return (c ^ (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) }catch(e){ return ''; } }); } /** * Resolve which language should be used. * * @param {Object} languages Object with language translations * @param {string} [requested_language] Language specified by given configuration parameters * @returns {string} */ var _resolveCurrentLang = function (languages, requested_language) { if (_config.auto_language === 'browser') { return _getValidatedLanguage(_getBrowserLang(), languages); } else if (_config.auto_language === 'document') { return _getValidatedLanguage(document.documentElement.lang, languages); } else { if (typeof requested_language === 'string') { return _config.current_lang = _getValidatedLanguage(requested_language, languages); } } _log("CookieConsent [LANG]: setting current_lang = '" + _config.current_lang + "'"); return _config.current_lang; // otherwise return default } /** * Get current client's browser language * @returns {string} */ var _getBrowserLang = function(){ var browser_lang = navigator.language || navigator.browserLanguage; browser_lang.length > 2 && (browser_lang = browser_lang[0]+browser_lang[1]); _log("CookieConsent [LANG]: detected_browser_lang = '"+ browser_lang + "'"); return browser_lang.toLowerCase() } /** * Trap focus inside modal and focus the first * focusable element of current active modal */ var _handleFocusTrap = function(){ var tabbedOutsideDiv = false; var tabbedInsideModal = false; _addEvent(document, 'keydown', function(e){ e = e || window.event; // If is tab key => ok if(e.key !== 'Tab') return; // If there is any modal to focus if(current_modal_focusable){ // If reached natural end of the tab sequence => restart if(e.shiftKey){ if (document.activeElement === current_modal_focusable[0]) { current_modal_focusable[1].focus(); e.preventDefault(); } }else{ if (document.activeElement === current_modal_focusable[1]) { current_modal_focusable[0].focus(); e.preventDefault(); } } // If have not yet used tab (or shift+tab) and modal is open ... // Focus the first focusable element if(!tabbedInsideModal && !clicked_inside_modal){ tabbedInsideModal = true; !tabbedOutsideDiv && e.preventDefault(); if(e.shiftKey){ if(current_modal_focusable[3]){ if(!current_modal_focusable[2]){ current_modal_focusable[0].focus(); }else{ current_modal_focusable[2].focus(); } }else{ current_modal_focusable[1].focus(); } }else{ if(current_modal_focusable[3]){ current_modal_focusable[3].focus(); }else{ current_modal_focusable[0].focus(); } } } } !tabbedInsideModal && (tabbedOutsideDiv = true); }); if(document.contains){ _addEvent(main_container, 'click', function(e){ e = e || window.event; /** * If click is on the foreground overlay (and not inside settings_modal), * hide settings modal * * Notice: click on div is not supported in IE */ if(settings_modal_visible){ if(!settings_inner.contains(e.target)){ _cookieconsent.hideSettings(0); clicked_inside_modal = false; }else{ clicked_inside_modal = true; } }else if(consent_modal_visible){ if(consent_modal.contains(e.target)){ clicked_inside_modal = true; } } }, true); } } /** * Manage each modal's layout * @param {Object} gui_options */ var _guiManager = function(gui_options, only_consent_modal){ // If gui_options is not object => exit if(typeof gui_options !== 'object') return; var consent_modal_options = gui_options['consent_modal']; var settings_modal_options = gui_options['settings_modal']; /** * Helper function which adds layout and * position classes to given modal * * @param {HTMLElement} modal * @param {string[]} allowed_layouts * @param {string[]} allowed_positions * @param {string} layout * @param {string[]} position */ function _setLayout(modal, allowed_layouts, allowed_positions, allowed_transitions, layout, position, transition){ position = (position && position.split(" ")) || []; // Check if specified layout is valid if(_inArray(allowed_layouts, layout) > -1){ // Add layout classes _addClass(modal, layout); // Add position class (if specified) if(!(layout === 'bar' && position[0] === 'middle') && _inArray(allowed_positions, position[0]) > -1){ for(var i=0; i -1) && _addClass(modal, transition); } if(consent_modal_exists && consent_modal_options){ _setLayout( consent_modal, ['box', 'bar', 'cloud'], ['top', 'middle', 'bottom'], ['zoom', 'slide'], consent_modal_options['layout'], consent_modal_options['position'], consent_modal_options['transition'] ); } if(!only_consent_modal && settings_modal_options){ _setLayout( settings_container, ['bar'], ['left', 'right'], ['zoom', 'slide'], settings_modal_options['layout'], settings_modal_options['position'], settings_modal_options['transition'] ); } } /** * Returns true if cookie category is accepted by the user * @param {string} cookie_category * @returns {boolean} */ _cookieconsent.allowedCategory = function(cookie_category){ if(!invalid_consent || _config.mode === 'opt-in') var allowed_categories = JSON.parse(_getCookie(_config.cookie_name, 'one', true) || '{}')['categories'] || [] else // mode is 'opt-out' var allowed_categories = default_enabled_categories; return _inArray(allowed_categories, cookie_category) > -1; } /** * "Init" method. Will run once and only if modals do not exist */ _cookieconsent.run = function(user_config){ if(!document.getElementById('cc_div')){ // configure all parameters _setConfig(user_config); // if is bot, don't run plugin if(is_bot) return; // Retrieve cookie value (if set) saved_cookie_content = JSON.parse(_getCookie(_config.cookie_name, 'one', true) || "{}"); // Retrieve "consent_uuid" consent_uuid = saved_cookie_content['consent_uuid']; // If "consent_uuid" is present => assume that consent was previously given var cookie_consent_accepted = consent_uuid !== undefined; // Retrieve "consent_date" consent_date = saved_cookie_content['consent_date']; consent_date && (consent_date = new Date(consent_date)); // Retrieve "last_consent_update" last_consent_update = saved_cookie_content['last_consent_update']; last_consent_update && (last_consent_update = new Date(last_consent_update)); // Retrieve "data" cookie_data = saved_cookie_content['data'] !== undefined ? saved_cookie_content['data'] : null; // If revision is enabled and current value !== saved value inside the cookie => revision is not valid if(revision_enabled && saved_cookie_content['revision'] !== _config.revision){ valid_revision = false; } // If consent is not valid => create consent modal consent_modal_exists = invalid_consent = (!cookie_consent_accepted || !valid_revision || !consent_date || !last_consent_update || !consent_uuid); // Generate cookie-settings dom (& consent modal) _createCookieConsentHTML(); _getModalFocusableData(); _guiManager(user_config['gui_options']); _addDataButtonListeners(); if(_config.autorun && consent_modal_exists){ _cookieconsent.show(user_config['delay'] || 0); } // Add class to enable animations/transitions setTimeout(function(){_addClass(main_container, 'c--anim');}, 30); // Accessibility :=> if tab pressed => trap focus inside modal setTimeout(function(){_handleFocusTrap();}, 100); // If consent is valid if(!invalid_consent){ var rfc_prop_exists = typeof saved_cookie_content['rfc_cookie'] === "boolean"; /* * Convert cookie to rfc format (if `use_rfc_cookie` is enabled) */ if(!rfc_prop_exists || (rfc_prop_exists && saved_cookie_content['rfc_cookie'] !== _config.use_rfc_cookie)){ saved_cookie_content['rfc_cookie'] = _config.use_rfc_cookie; _setCookie(_config.cookie_name, JSON.stringify(saved_cookie_content)); } /** * Update accept type */ accept_type = _getAcceptType(_getCurrentCategoriesState()); _manageExistingScripts(); if(typeof onAccept === 'function') onAccept(saved_cookie_content); _log("CookieConsent [NOTICE]: consent already given!", saved_cookie_content); }else{ if(_config.mode === 'opt-out'){ _log("CookieConsent [CONFIG] mode='" + _config.mode + "', default enabled categories:", default_enabled_categories); _manageExistingScripts(default_enabled_categories); } _log("CookieConsent [NOTICE]: ask for consent!"); } }else{ _log("CookieConsent [NOTICE]: cookie consent already attached to body!"); } } /** * Show settings modal (with optional delay) * @param {number} delay */ _cookieconsent.showSettings = function(delay){ setTimeout(function() { _addClass(html_dom, "show--settings"); settings_container.setAttribute('aria-hidden', 'false'); settings_modal_visible = true; /** * Set focus to the first focusable element inside settings modal */ setTimeout(function(){ // If there is no consent-modal, keep track of the last focused elem. if(!consent_modal_visible){ last_elem_before_modal = document.activeElement; }else{ last_consent_modal_btn_focus = document.activeElement; } if (settings_modal_focusable.length === 0) return; if(settings_modal_focusable[3]){ settings_modal_focusable[3].focus(); }else{ settings_modal_focusable[0].focus(); } current_modal_focusable = settings_modal_focusable; }, 200); _log("CookieConsent [SETTINGS]: show settings_modal"); }, delay > 0 ? delay : 0); } /** * This function handles the loading/activation logic of the already * existing scripts based on the current accepted cookie categories * * @param {string[]} [must_enable_categories] */ var _manageExistingScripts = function(must_enable_categories){ if(!_config.page_scripts) return; // get all the scripts with "cookie-category" attribute var scripts = document.querySelectorAll('script[' + _config.script_selector + ']'); var accepted_categories = must_enable_categories || saved_cookie_content['categories'] || []; /** * Load scripts (sequentially), using a recursive function * which loops through the scripts array * @param {Element[]} scripts scripts to load * @param {number} index current script to load */ var _loadScripts = function(scripts, index){ if(index < scripts.length){ var curr_script = scripts[index]; var curr_script_category = curr_script.getAttribute(_config.script_selector); /** * If current script's category is on the array of categories * accepted by the user => load script */ if(_inArray(accepted_categories, curr_script_category) > -1){ curr_script.type = 'text/javascript'; curr_script.removeAttribute(_config.script_selector); // get current script data-src var src = curr_script.getAttribute('data-src'); // some scripts (like ga) might throw warning if data-src is present src && curr_script.removeAttribute('data-src'); // create fresh script (with the same code) var fresh_script = _createNode('script'); fresh_script.textContent = curr_script.innerHTML; // Copy attributes over to the new "revived" script (function(destination, source){ var attributes = source.attributes; var len = attributes.length; for(var i=0; i the next script will not be loaded // until the current's script onload event triggers if(fresh_script.readyState) { // only required for IE <9 fresh_script.onreadystatechange = function() { if (fresh_script.readyState === "loaded" || fresh_script.readyState === "complete" ) { fresh_script.onreadystatechange = null; _loadScripts(scripts, ++index); } }; }else{ // others fresh_script.onload = function(){ fresh_script.onload = null; _loadScripts(scripts, ++index); }; } } // Replace current "sleeping" script with the new "revived" one curr_script.parentNode.replaceChild(fresh_script, curr_script); /** * If we managed to get here and scr is still set, it means that * the script is loading/loaded sequentially so don't go any further */ if(src) return; } // Go to next script right away _loadScripts(scripts, ++index); } } _loadScripts(scripts, 0); } /** * Save custom data inside cookie * @param {object|string} new_data * @param {string} [mode] * @returns {boolean} */ var _setCookieData = function(new_data, mode){ var set = false; /** * If mode is 'update': * add/update only the specified props. */ if(mode === 'update'){ cookie_data = _cookieconsent.get('data'); var same_type = typeof cookie_data === typeof new_data; if(same_type && typeof cookie_data === "object"){ !cookie_data && (cookie_data = {}); for(var prop in new_data){ if(cookie_data[prop] !== new_data[prop]){ cookie_data[prop] = new_data[prop] set = true; } } }else if((same_type || !cookie_data) && cookie_data !== new_data){ cookie_data = new_data; set = true; } }else{ cookie_data = new_data; set = true; } if(set){ saved_cookie_content['data'] = cookie_data; _setCookie(_config.cookie_name, JSON.stringify(saved_cookie_content)); } return set; } /** * Helper method to set a variety of fields * @param {string} field * @param {object} data * @returns {boolean} */ _cookieconsent.set = function(field, data){ switch(field){ case 'data': return _setCookieData(data['value'], data['mode']); default: return false; } } /** * Retrieve data from existing cookie * @param {string} field * @param {string} [cookie_name] * @returns {any} */ _cookieconsent.get = function(field, cookie_name){ var cookie = JSON.parse(_getCookie(cookie_name || _config.cookie_name, 'one', true) || "{}"); return cookie[field]; } /** * Read current configuration value * @returns {any} */ _cookieconsent.getConfig = function(field){ return _config[field] || user_config[field]; } /** * Obtain accepted and rejected categories * @returns {{accepted: string[], rejected: string[]}} */ var _getCurrentCategoriesState = function(){ // get accepted categories accepted_categories = saved_cookie_content['categories'] || []; // calculate rejected categories (all_categories - accepted_categories) rejected_categories = all_categories.filter(function(category){ return (_inArray(accepted_categories, category) === -1); }); return { accepted: accepted_categories, rejected: rejected_categories } } /** * Calculate "accept type" given current categories state * @param {{accepted: string[], rejected: string[]}} currentCategoriesState * @returns {string} */ var _getAcceptType = function(currentCategoriesState){ var type = 'custom'; // number of categories marked as necessary/readonly var necessary_categories_length = readonly_categories.filter(function(readonly){ return readonly === true; }).length; // calculate accept type based on accepted/rejected categories if(currentCategoriesState.accepted.length === all_categories.length) type = 'all'; else if(currentCategoriesState.accepted.length === necessary_categories_length) type = 'necessary' return type; } /** * @typedef {object} userPreferences * @property {string} accept_type * @property {string[]} accepted_categories * @property {string[]} rejected_categories */ /** * Retrieve current user preferences (summary) * @returns {userPreferences} */ _cookieconsent.getUserPreferences = function(){ var currentCategoriesState = _getCurrentCategoriesState(); var accept_type = _getAcceptType(currentCategoriesState); return { 'accept_type': accept_type, 'accepted_categories': currentCategoriesState.accepted, 'rejected_categories': currentCategoriesState.rejected } } /** * Function which will run after script load * @callback scriptLoaded */ /** * Dynamically load script (append to head) * @param {string} src * @param {scriptLoaded} callback * @param {object[]} [attrs] Custom attributes */ _cookieconsent.loadScript = function(src, callback, attrs){ var function_defined = typeof callback === 'function'; // Load script only if not already loaded if(!document.querySelector('script[src="' + src + '"]')){ var script = _createNode('script'); // if an array is provided => add custom attributes if(attrs && attrs.length > 0){ for(var i=0; i run callback onload if(function_defined){ script.onload = callback; } script.src = src; /** * Append script to head */ document.head.appendChild(script); }else{ function_defined && callback(); } } /** * Manage dynamically loaded scripts: https://github.com/orestbida/cookieconsent/issues/101 * If plugin has already run, call this method to enable * the newly added scripts based on currently selected preferences */ _cookieconsent.updateScripts = function(){ _manageExistingScripts(); } /** * Show cookie consent modal (with delay parameter) * @param {number} [delay] * @param {boolean} [create_modal] create modal if it doesn't exist */ _cookieconsent.show = function(delay, create_modal){ if(create_modal === true) _createConsentModal(_config.current_lang); if(consent_modal_exists){ setTimeout(function() { _addClass(html_dom, "show--consent"); /** * Update attributes/internal statuses */ consent_modal.setAttribute('aria-hidden', 'false'); consent_modal_visible = true; setTimeout(function(){ last_elem_before_modal = document.activeElement; current_modal_focusable = consent_modal_focusable; }, 200); _log("CookieConsent [MODAL]: show consent_modal"); }, delay > 0 ? delay : (create_modal ? 30 : 0)); } } /** * Hide consent modal */ _cookieconsent.hide = function(){ if(consent_modal_exists){ _removeClass(html_dom, "show--consent"); consent_modal.setAttribute('aria-hidden', 'true'); consent_modal_visible = false; setTimeout(function(){ //restore focus to the last page element which had focus before modal opening last_elem_before_modal.focus(); current_modal_focusable = null; }, 200); _log("CookieConsent [MODAL]: hide"); } } /** * Hide settings modal */ _cookieconsent.hideSettings = function(){ _removeClass(html_dom, "show--settings"); settings_modal_visible = false; settings_container.setAttribute('aria-hidden', 'true'); setTimeout(function(){ /** * If consent modal is visible, focus him (instead of page document) */ if(consent_modal_visible){ last_consent_modal_btn_focus && last_consent_modal_btn_focus.focus(); current_modal_focusable = consent_modal_focusable; }else{ /** * Restore focus to last page element which had focus before modal opening */ last_elem_before_modal && last_elem_before_modal.focus(); current_modal_focusable = null; } clicked_inside_modal = false; }, 200); _log("CookieConsent [SETTINGS]: hide settings_modal"); } /** * Accept cookieconsent function API * @param {string[]|string} _categories - Categories to accept * @param {string[]} [_exclusions] - Excluded categories [optional] */ _cookieconsent.accept = function(_categories, _exclusions){ var categories = _categories || undefined; var exclusions = _exclusions || []; var to_accept = []; /** * Get all accepted categories * @returns {string[]} */ var _getCurrentPreferences = function(){ var toggles = document.querySelectorAll('.c-tgl') || []; var states = []; for(var i=0; i= 1){ for(i=0; i 0){ for(var i=0; i<_cookies.length; i++){ this.validCookie(_cookies[i]) && cookies.push(_cookies[i]); } }else{ this.validCookie(_cookies) && cookies.push(_cookies); } _eraseCookies(cookies, _path, domains); } /** * Set cookie, by specifying name and value * @param {string} name * @param {string} value */ var _setCookie = function(name, value) { var cookie_expiration = _config.cookie_expiration; if(typeof _config.cookie_necessary_only_expiration === 'number' && accept_type === 'necessary') cookie_expiration = _config.cookie_necessary_only_expiration; value = _config.use_rfc_cookie ? encodeURIComponent(value) : value; var date = new Date(); date.setTime(date.getTime() + (1000 * (cookie_expiration * 24 * 60 * 60))); var expires = "; expires=" + date.toUTCString(); var cookieStr = name + "=" + (value || "") + expires + "; Path=" + _config.cookie_path + ";"; cookieStr += " SameSite=" + _config.cookie_same_site + ";"; // assures cookie works with localhost (=> don't specify domain if on localhost) if(window.location.hostname.indexOf(".") > -1){ cookieStr += " Domain=" + _config.cookie_domain + ";"; } if(window.location.protocol === "https:") { cookieStr += " Secure;"; } document.cookie = cookieStr; _log("CookieConsent [SET_COOKIE]: '" + name + "' expires after " + cookie_expiration + " day(s)"); } /** * Get cookie value by name, * returns the cookie value if found (or an array * of cookies if filter provided), otherwise empty string: "" * @param {string} name * @param {string} filter 'one' or 'all' * @param {boolean} [get_value] set to true to obtain its value * @returns {string|string[]} */ var _getCookie = function(name, filter, get_value) { var found; if(filter === 'one'){ found = document.cookie.match("(^|;)\\s*" + name + "\\s*=\\s*([^;]+)"); found = found ? (get_value ? found.pop() : name) : ""; if(found && name === _config.cookie_name){ try{ found = JSON.parse(found) }catch(e){ try { found = JSON.parse(decodeURIComponent(found)) } catch (e) { // if I got here => cookie value is not a valid json string found = {}; } } found = JSON.stringify(found); } }else if(filter === 'all'){ // array of names of all existing cookies var cookies = document.cookie.split(/;\s*/); found = []; for(var i=0; i