1/*! 2* CookieConsent 3.0.0 3* https://github.com/orestbida/cookieconsent/tree/v3 4* Author Orest Bida 5* Released under the MIT License 6*/ 7 8(function (global, factory) { 9 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 10 typeof define === 'function' && define.amd ? define(['exports'], factory) : 11 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.CookieConsent = {})); 12})(this, (function (exports) { 'use strict'; 13 14 const COOKIE_NAME = 'cc_cookie'; 15 16 const OPT_IN_MODE = 'opt-in'; 17 const OPT_OUT_MODE = 'opt-out'; 18 19 const TOGGLE_CONSENT_MODAL_CLASS = 'show--consent'; 20 const TOGGLE_PREFERENCES_MODAL_CLASS = 'show--preferences'; 21 const TOGGLE_DISABLE_INTERACTION_CLASS = 'disable--interaction'; 22 23 const SCRIPT_TAG_SELECTOR = 'data-category'; 24 25 const DIV_TAG = 'div'; 26 const BUTTON_TAG = 'button'; 27 28 const ARIA_HIDDEN = 'aria-hidden'; 29 const BTN_GROUP_CLASS = 'btn-group'; 30 const CLICK_EVENT = 'click'; 31 const DATA_ROLE = 'data-role'; 32 33 const CONSENT_MODAL_NAME = 'consentModal'; 34 const PREFERENCES_MODAL_NAME = 'preferencesModal'; 35 36 /** 37 * @typedef {import('../../types')} CookieConsent 38 * 39 * @typedef {CookieConsent} Api 40 * @typedef {CookieConsent.CookieConsentConfig} UserConfig 41 * @typedef {CookieConsent.Category} Category 42 * @typedef {CookieConsent.Service} Service 43 * @typedef {Object.<string, Service>} Services 44 * @typedef {CookieConsent.AutoClear} AutoClear 45 * @typedef {CookieConsent.GuiOptions} GuiOptions 46 * @typedef {GuiOptions['consentModal']} GuiModalOption 47 * @typedef {CookieConsent.CookieConsentConfig['language']} Language 48 * @typedef {CookieConsent.Translation} Translation 49 * @typedef {CookieConsent.ConsentModalOptions} ConsentModalOptions 50 * @typedef {CookieConsent.PreferencesModalOptions} PreferencesModalOptions 51 * @typedef {CookieConsent.CookieTable} CookieTable 52 * @typedef {CookieConsent.Section} Section 53 * @typedef {CookieConsent.CookieValue} CookieValue 54 * @typedef {CookieConsent.UserPreferences} UserPreferences 55 */ 56 57 /** 58 * Internal state for each script tag 59 * @typedef {Object} ScriptInfo 60 * @property {HTMLScriptElement} _script 61 * @property {string} _categoryName 62 * @property {string} [_serviceName] 63 * @property {boolean} _executed 64 * @property {boolean} _runOnDisable 65 */ 66 67 /** 68 * Pointers to main dom elements 69 * @typedef {Object} DomElements 70 * @property {Document} _document 71 * @property {HTMLElement} _htmlDom 72 * 73 * @property {HTMLElement} _ccMain 74 * @property {HTMLElement} _cmContainer 75 * @property {HTMLElement} _pmContainer 76 * 77 * @property {HTMLElement} _cm 78 * @property {HTMLElement} _cmBody 79 * @property {HTMLElement} _cmTexts 80 * @property {HTMLElement} _cmTitle 81 * @property {HTMLElement} _cmDescription 82 * @property {HTMLElement} _cmBtns 83 * @property {HTMLElement} _cmBtnGroup 84 * @property {HTMLElement} _cmBtnGroup2 85 * @property {HTMLElement} _cmAcceptAllBtn 86 * @property {HTMLElement} _cmAcceptNecessaryBtn 87 * @property {HTMLElement} _cmShowPreferencesBtn 88 * @property {HTMLElement} _cmFooterLinksGroup 89 * @property {HTMLElement} _cmCloseIconBtn 90 * 91 * @property {HTMLElement} _pm 92 * @property {HTMLElement} _pmHeader 93 * @property {HTMLElement} _pmTitle 94 * @property {HTMLElement} _pmCloseBtn 95 * @property {HTMLElement} _pmBody 96 * @property {HTMLElement} _pmNewBody 97 * @property {HTMLElement} _pmSections 98 * @property {HTMLElement} _pmFooter 99 * @property {HTMLElement} _pmAcceptAllBtn 100 * @property {HTMLElement} _pmAcceptNecessaryBtn 101 * @property {HTMLElement} _pmSavePreferencesBtn 102 * 103 * @property {Object.<string, HTMLInputElement>} _categoryCheckboxInputs 104 * @property {Object.<string, ServiceToggle>} _serviceCheckboxInputs 105 * 106 * // Used to properly restore focus when modal is closed 107 * @property {HTMLSpanElement} _focusSpan 108 * @property {HTMLSpanElement} _pmFocusSpan 109 */ 110 111 /** 112 * @typedef {Object} CustomCallbacks 113 * @property {Function} _onFirstConsent 114 * @property {Function} _onConsent 115 * @property {Function} _onChange 116 * @property {Function} _onModalShow 117 * @property {Function} _onModalHide 118 * @property {Function} _onModalReady 119 */ 120 121 /** 122 * Pointers to all services toggles relative to a category 123 * @typedef {Object.<string, HTMLInputElement>} ServiceToggle 124 */ 125 126 class GlobalState { 127 constructor() { 128 129 /** 130 * Default config. options 131 * @type {CookieConsent.CookieConsentConfig} 132 */ 133 this._config = { 134 mode: OPT_IN_MODE, 135 revision: 0, 136 137 //{{START: GUI}} 138 autoShow: true, 139 lazyHtmlGeneration: true, 140 //{{END: GUI}} 141 142 autoClearCookies: true, 143 manageScriptTags: true, 144 hideFromBots: true, 145 146 cookie: { 147 name: COOKIE_NAME, 148 expiresAfterDays: 182, 149 domain: '', 150 path: '/', 151 sameSite: 'Lax'
152 } 153 }; 154 155 this._state = { 156 /** 157 * @type {UserConfig} 158 */ 159 _userConfig: {}, 160 161 _currentLanguageCode: '', 162 163 /** 164 * @type {Object.<string, Translation>} 165 */ 166 _allTranslations: {}, 167 168 /** 169 * @type {Translation} 170 */ 171 _currentTranslation: {}, 172 173 /** 174 * Internal state variables 175 * @type {CookieValue} 176 */ 177 _savedCookieContent : {}, 178 179 /** 180 * Store all event data-cc event listeners 181 * (so that they can be removed on .reset()) 182 * 183 * @type {{ 184 * _element: HTMLElement, 185 * _event: string, 186 * _listener: Function 187 * }[]} 188 */ 189 _dataEventListeners: [], 190 191 _disablePageInteraction: false, 192 193 /** 194 * @type {any} 195 */ 196 _cookieData : null, 197 198 /** 199 * @type {Date} 200 */ 201 _consentTimestamp: null, 202 203 /** 204 * @type {Date} 205 */ 206 _lastConsentTimestamp: null, 207 208 /** 209 * @type {string} 210 */ 211 _consentId: '', 212 213 _invalidConsent : true, 214 215 //{{START: GUI}} 216 _consentModalExists : false, 217 _consentModalVisible : false, 218 219 _preferencesModalVisible : false, 220 _preferencesModalExists: false, 221 222 /** 223 * @type {HTMLElement[]} 224 */ 225 _currentModalFocusableElements: [], 226 //{{END: GUI}} 227 228 _revisionEnabled : false, 229 _validRevision : true, 230 231 /** 232 * Array containing the last changed categories (enabled/disabled) 233 * @type {string[]} 234 */ 235 _lastChangedCategoryNames : [], 236 237 _reloadPage : false, 238 239 /** 240 * @type {CookieConsent.AcceptType} 241 */ 242 _acceptType: '', 243 244 /** 245 * Object containing all user's defined categories 246 * @type {Object.<string, Category>} 247 */ 248 _allDefinedCategories: false, 249 250 /** 251 * Stores all available categories 252 * @type {string[]} 253 */ 254 _allCategoryNames: [], 255 256 /** 257 * Contains all accepted categories 258 * @type {string[]} 259 */ 260 _acceptedCategories : [], 261 262 /** 263 * Keep track of readonly toggles 264 * @type {string[]} 265 */ 266 _readOnlyCategories : [], 267 268 /** 269 * Contains all categories enabled by default 270 * @type {string[]} 271 */ 272 _defaultEnabledCategories : [], 273 274 /** 275 * Don't run plugin if bot detected 276 * (to avoid indexing its text content) 277 */ 278 _botAgentDetected : false, 279 280 /** 281 * Save reference to the last focused element on the page 282 * (used later to restore focus when both modals are closed) 283 */ 284 285 //{{START: GUI}} 286 287 /** @type {HTMLElement} **/_lastFocusedElemBeforeModal: false, 288 /** @type {HTMLElement} **/_lastFocusedModalElement: false, 289 290 /** 291 * Both of the arrays below have the same structure: 292 * [0]: first focusable element inside modal 293 * [1]: last focusable element inside modal 294 */ 295 296 /** @type {HTMLElement[]}
296 **/ _cmFocusableElements : [], 297 /** @type {HTMLElement[]} **/ _pmFocusableElements : [], 298 299 /** 300 * Keep track of enabled/disabled categories 301 * @type {boolean[]} 302 */ 303 _allToggleStates : [], 304 305 //{{END: GUI}} 306 307 /** 308 * @type {Object.<string, Services>} 309 */ 310 _allDefinedServices: {}, 311 312 /** 313 * @type {Object.<string, string[]>} 314 */ 315 _acceptedServices: {}, 316 317 /** 318 * Keep track of the current state of the services 319 * (may not be the same as enabledServices) 320 * 321 * @type {Object.<string, string[]>} 322 */ 323 _enabledServices: {}, 324 325 /** 326 * @type {Object.<string, string[]>} 327 */ 328 _lastChangedServices: {}, 329 330 /** 331 * @type {Object.<string, string[]>} 332 */ 333 _lastEnabledServices: {}, 334 335 /** 336 * @type {ScriptInfo[]} 337 */ 338 _allScriptTags: [] 339 }; 340 341 //{{START: GUI}} 342 343 /** 344 * Pointers to main dom elements 345 * @type {DomElements} 346 */ 347 this._dom = { 348 _categoryCheckboxInputs: {}, 349 _serviceCheckboxInputs: {} 350 }; 351 352 //{{END: GUI}} 353 354 /** 355 * Callback functions 356 * @type {CustomCallbacks} 357 */ 358 this._callbacks = {}; 359 360 this._customEvents = { 361 _onFirstConsent: 'cc:onFirstConsent', 362 _onConsent: 'cc:onConsent', 363 _onChange: 'cc:onChange', 364 //{{START: GUI}} 365 _onModalShow: 'cc:onModalShow', 366 _onModalHide: 'cc:onModalHide', 367 _onModalReady: 'cc:onModalReady' 368 //{{END: GUI}} 369 }; 370 } 371 } 372 373 const globalObj = new GlobalState(); 374 375 /** 376 * Helper console.log function 377 * @param {Object} printMsg 378 * @param {Object} [optionalParam] 379 */ 380 const _log = (printMsg, optionalParam) => { 381 console.log(printMsg, optionalParam !== undefined 382 ? optionalParam 383 : ' ' 384 ); 385 }; 386 387 /** 388 * Helper indexOf 389 * @param {any[]|string} el 390 * @param {any} value 391 */ 392 const indexOf = (el, value) => el.indexOf(value); 393 394 /** 395 * Returns true if el. (array or string) contains the specified value 396 * @param {any[]|string} el 397 */ 398 const elContains = (el, value) => indexOf(el, value) !== -1; 399 400 const isArray = el => Array.isArray(el); 401 402 const isString = el => typeof el === 'string'; 403 404 const isObject = el => !!el && typeof el === 'object' && !isArray(el); 405 406 const isFunction = el => typeof el === 'function'; 407 408 const getKeys = obj => Object.keys(obj); 409 410 /** 411 * Return array without duplicates 412 * @param {any[]} arr 413 */ 414 const unique = (arr) => Array.from(new Set(arr)); 415 416 const getActiveElement = () => document.activeElement; 417 418 /** 419 * @param {Event} e 420 */ 421 const preventDefault = (e) => e.preventDefault(); 422 423 /** 424 * @param {Element} el 425 * @param {string} selector 426 */ 427 const querySelectorAll = (el, selector) => el.querySelectorAll(selector); 428 429 /** 430 * @param {HTMLInputElement} input 431 */ 432 const dispatchInputChangeEvent = (input) => input.dispatchEvent(new Event('change')); 433 434 /** 435 * @param {keyof HTMLElementTagNameMap} type 436 */ 437 const createNode = (type) => { 438 const el = document.createElement(type); 439 440 if (type === BUTTON_TAG) { 441 el.type = type; 442 } 443 444 return el; 445 }; 446 447 /** 448 * @param {HTMLElement} el 449 * @param {string} attribute 450 * @param {string} value 451 */ 452 const setAttribute = (el, attribute, value) => el.setAttribute(attribute, value); 453 454 /** 455 * @param {HTMLElement} el 456 * @param {string} attribute 457 * @param {boolean}
457 [prependData] 458 */ 459 const removeAttribute = (el, attribute, prependData) => { 460 el.removeAttribute(prependData 461 ? 'data-' + attribute 462 : attribute 463 ); 464 }; 465 466 /** 467 * @param {HTMLElement} el 468 * @param {string} attribute 469 * @param {boolean} [prependData] 470 * @returns {string} 471 */ 472 const getAttribute = (el, attribute, prependData) => { 473 return el.getAttribute(prependData 474 ? 'data-' + attribute 475 : attribute 476 ); 477 }; 478 479 /** 480 * @param {Node} parent 481 * @param {Node} child 482 */ 483 const appendChild = (parent, child) => parent.appendChild(child); 484 485 /** 486 * @param {HTMLElement} elem 487 * @param {string} className 488 */ 489 const addClass = (elem, className) => elem.classList.add(className); 490 491 /** 492 * @param {HTMLElement} elem 493 * @param {string} className 494 */ 495 const addClassCm = (elem, className) => addClass(elem, 'cm__' + className); 496 /** 497 * @param {HTMLElement} elem 498 * @param {string} className 499 */ 500 const addClassPm = (elem, className) => addClass(elem, 'pm__' + className); 501 502 /** 503 * @param {HTMLElement} elem 504 * @param {string} className 505 */ 506 const removeClass = (el, className) => el.classList.remove(className); 507 508 /** 509 * @param {HTMLElement} el 510 * @param {string} className 511 */ 512 const hasClass = (el, className) => el.classList.contains(className); 513 514 const deepCopy = (el) => { 515 if (typeof el !== 'object' ) 516 return el; 517 518 if (el instanceof Date) 519 return new Date(el.getTime()); 520 521 let clone = Array.isArray(el) ? [] : {}; 522 523 for (let key in el) { 524 let value = el[key]; 525 clone[key] = deepCopy(value); 526 } 527 528 return clone; 529 }; 530 531 /** 532 * Store categories and services' config. details 533 * @param {string[]} allCategoryNames 534 */ 535 const fetchCategoriesAndServices = (allCategoryNames) => { 536 const { 537 _allDefinedCategories, 538 _allDefinedServices, 539 _acceptedServices, 540 _enabledServices, 541 _readOnlyCategories 542 } = globalObj._state; 543 544 for (let categoryName of allCategoryNames) { 545 546 const currCategory = _allDefinedCategories[categoryName]; 547 const services = currCategory.services || {}; 548 const serviceNames = isObject(services) && getKeys(services) || []; 549 550 _allDefinedServices[categoryName] = {}; 551 _acceptedServices[categoryName] = []; 552 _enabledServices[categoryName] = []; 553 554 /** 555 * Keep track of readOnly categories 556 */ 557 if (currCategory.readOnly) { 558 _readOnlyCategories.push(categoryName); 559 _acceptedServices[categoryName] = serviceNames; 560 } 561 562 globalObj._dom._serviceCheckboxInputs[categoryName] = {}; 563 564 for (let serviceName of serviceNames) { 565 const service = services[serviceName]; 566 service._enabled = false; 567 _allDefinedServices[categoryName][serviceName] = service; 568 } 569 } 570 }; 571 572 /** 573 * Retrieves all script elements with 'data-category' attribute 574 * and save the following attributes: category-name and service 575 */ 576 const retrieveScriptElements = () => { 577 if (!globalObj._config.manageScriptTags) 578 return; 579 580 const state = globalObj._state; 581 582 /** 583 * @type {NodeListOf<HTMLScriptElement>} 584 */ 585 const scripts = querySelectorAll(document, 'script[' + SCRIPT_TAG_SELECTOR +']'); 586 587 for (const scriptTag of scripts) { 588 let scriptCategoryName = getAttribute(scriptTag, SCRIPT_TAG_SELECTOR); 589 let scriptServiceName = scriptTag.dataset.service || ''; 590 let runOnDisable = false; 591 592 /** 593 * Remove the '!' char if it is present 594 */ 595 if (scriptCategoryName && scriptCategoryName.charAt(0) === '!') { 596 scriptCategoryName = scriptCategoryName.slice(1); 597 runOnDisable = true; 598 } 599 600 if (scriptServiceName.charAt(0) === '!') { 601 scriptServiceName = scriptServiceName.slice(1); 602 runOnDisable = true; 603 } 604 605 if (elContains(state._allCategoryNames, scriptCategoryName)) { 606 state._allScriptTags.push({ 607 _script: scriptTag, 608 _executed: false, 609 _runOnDisable: runOnDisable, 610 _categoryName: scriptCategoryName, 611 _serviceName: scriptServiceName 612 }); 613 614 if (scriptServiceName) { 615 const categoryServices = state._allDefinedServices[scriptCategoryName]; 616 if (!categoryServices[scriptServiceName]) { 617 categoryServices[scriptServiceName] = { 618 _enabled: false 619 }; 620 } 621 } 622 } 623 } 624 }; 625 626 /** 627 * Calculate rejected services (all services - enabled services) 628 * @returns {Object.<string, string[]>} 629 */ 630 const retrieveRejectedServices = () => { 631 const rejectedServices = {}; 632 633 const { 634 _allCategoryNames, 635 _allDefinedServices, 636 _acceptedServices 637 } = globalObj._state; 638 639 for (const categoryName of _allCategoryNames) { 640 rejectedServices[categoryName] = arrayDiff( 641 _acceptedServices[categoryName], 642 getKeys(_allDefinedServices[categoryName]) 643 ); 644 } 645 646 return rejectedServices; 647 }; 648 649 const retrieveCategoriesFromModal = () => { 650 const toggles = globalObj._dom._categoryCheckboxInputs; 651 652 if (!toggles) 653 return []; 654 655 let enabledCategories = []; 656 657 for (let categoryName in toggles) { 658 if (toggles[categoryName].checked) { 659 enabledCategories.push(categoryName); 660 } 661 } 662 663 return enabledCategories; 664 }; 665 666 /** 667 * @param {string[]|string} categories - Categories to accept 668 * @param {string[]} [excludedCategories] 669 */ 670 const resolveEnabledCategories = (categories, excludedCategories) => { 671 const { 672 _allCategoryNames, 673 _acceptedCategories, 674 _readOnlyCategories, 675 _preferencesModalExists, 676 _enabledServices, 677 _allDefinedServices 678 } = globalObj._state; 679 680 /** 681 * @type {string[]} 682 */ 683 let enabledCategories = []; 684 685 if (!categories) { 686 enabledCategories = _acceptedCategories; 687 //{{START: GUI}}
688 _preferencesModalExists && (enabledCategories = retrieveCategoriesFromModal()); 689 //{{END: GUI}} 690 } else { 691 692 if (isArray(categories)) { 693 enabledCategories.push(...categories); 694 }else if (isString(categories)) { 695 enabledCategories = categories === 'all' 696 ? _allCategoryNames 697 : [categories]; 698 } 699 700 /** 701 * If there are services, turn them all on or off 702 */ 703 for (const categoryName of _allCategoryNames) { 704 _enabledServices[categoryName] = elContains(enabledCategories, categoryName) 705 ? getKeys(_allDefinedServices[categoryName]) 706 : []; 707 } 708 } 709 710 // Remove invalid and excluded categories 711 enabledCategories = enabledCategories.filter(category => 712 !elContains(_allCategoryNames, category) || 713 !elContains(excludedCategories, category) 714 ); 715 716 // Add back all the categories set as "readonly/required" 717 enabledCategories.push(..._readOnlyCategories); 718 719 setAcceptedCategories(enabledCategories); 720 }; 721 722 /** 723 * @param {string} [relativeCategory] 724 */ 725 const resolveEnabledServices = (relativeCategory) => { 726 const state = globalObj._state; 727 728 const { 729 _enabledServices, 730 _readOnlyCategories, 731 _acceptedServices, 732 _allDefinedServices, 733 _allCategoryNames 734 } = state; 735 736 const categoriesToConsider = relativeCategory 737 ? [relativeCategory] 738 : _allCategoryNames; 739 740 /** 741 * Save previously enabled services to calculate later on which of them was changed 742 */ 743 state._lastEnabledServices = deepCopy(_acceptedServices); 744 745 for (const categoryName of categoriesToConsider) { 746 const services = _allDefinedServices[categoryName]; 747 const serviceNames = getKeys(services); 748 const customServicesSelection = _enabledServices[categoryName] && _enabledServices[categoryName].length > 0; 749 const readOnlyCategory = elContains(_readOnlyCategories, categoryName); 750 751 /** 752 * Stop here if there are no services 753 */ 754 if (serviceNames.length === 0) 755 continue; 756 757 // Empty (previously) enabled services 758 _acceptedServices[categoryName] = []; 759 760 // If category is marked as readOnly enable all its services 761 if (readOnlyCategory) { 762 _acceptedServices[categoryName].push(...serviceNames); 763 } else { 764 if (customServicesSelection) { 765 const selectedServices = _enabledServices[categoryName]; 766 _acceptedServices[categoryName].push(...selectedServices); 767 } else { 768 _acceptedServices[categoryName] = []; 769 } 770 } 771 772 /** 773 * Make sure there are no duplicates inside array 774 */ 775 _acceptedServices[categoryName] = unique(_acceptedServices[categoryName]); 776 } 777 }; 778 779 /** 780 * @param {string} eventName 781 */ 782 const dispatchPluginEvent = (eventName, data) => dispatchEvent(new CustomEvent(eventName, {detail: data})); 783 784 /** 785 * Update services state internally and tick/untick checkboxes 786 * @param {string|string[]} service 787 * @param {string} category 788 */ 789 const updateModalToggles = (service, category) => { 790 const state = globalObj._state; 791 const { 792 _allDefinedServices, 793 _enabledServices, 794 _preferencesModalExists 795 } = state; 796 797 const servicesInputs = globalObj._dom._serviceCheckboxInputs[category] || {}; 798 const categoryInput = globalObj._dom._categoryCheckboxInputs[category] || {}; 799 const allServiceNames = getKeys(_allDefinedServices[category]); 800 801 // Clear previously enabled services 802 _enabledServices[category] = []; 803 804 if (isString(service)) { 805 if (service === 'all') { 806 807 // Enable all services 808 _enabledServices[category].push(...allServiceNames); 809 810 if (_preferencesModalExists) { 811 for (let serviceName in servicesInputs) {
812 servicesInputs[serviceName].checked = true; 813 dispatchInputChangeEvent(servicesInputs[serviceName]); 814 } 815 } 816 817 } else { 818 819 // Enable only one service (if valid) and disable all the others 820 if (elContains(allServiceNames, service)) 821 _enabledServices[category].push(service); 822 823 if (_preferencesModalExists) { 824 for (let serviceName in servicesInputs) { 825 servicesInputs[serviceName].checked = service === serviceName; 826 dispatchInputChangeEvent(servicesInputs[serviceName]); 827 } 828 } 829 } 830 }else if (isArray(service)) { 831 for (let serviceName of allServiceNames) { 832 const validService = elContains(service, serviceName); 833 validService && _enabledServices[category].push(serviceName); 834 835 if (_preferencesModalExists) { 836 servicesInputs[serviceName].checked = validService; 837 dispatchInputChangeEvent(servicesInputs[serviceName]); 838 } 839 } 840 } 841 842 const uncheckCategory = _enabledServices[category].length === 0; 843 844 /** 845 * Remove/add the category from acceptedCategories 846 */ 847 state._acceptedCategories = uncheckCategory 848 ? state._acceptedCategories.filter(cat => cat !== category) 849 : unique([...state._acceptedCategories, category]); 850 851 /** 852 * If there are no services enabled in the 853 * current category, uncheck the category 854 */ 855 if (_preferencesModalExists) { 856 categoryInput.checked = !uncheckCategory; 857 dispatchInputChangeEvent(categoryInput); 858 } 859 }; 860 861 /** 862 * Generate RFC4122-compliant UUIDs. 863 * https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid?page=1&tab=votes#tab-top 864 * @returns {string} unique uuid string 865 */ 866 const uuidv4 = () => { 867 return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) => { 868 return (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16); 869 }); 870 }; 871 872 /** 873 * Add event listener to dom object (cross browser function) 874 * @param {Element} elem 875 * @param {keyof WindowEventMap} event 876 * @param {EventListener} fn 877 * @param {boolean} [saveListener] 878 */ 879 const addEvent = (elem, event, fn, saveListener) => { 880 elem.addEventListener(event, fn); 881 882 /** 883 * Keep track of specific event listeners 884 * that must be removed on `.reset()` 885 */ 886 if (saveListener) { 887 globalObj._state._dataEventListeners.push({ 888 _element: elem, 889 _event: event, 890 _listener: fn 891 }); 892 } 893 }; 894 895 /** 896 * Calculate the existing cookie's remaining time until expiration (in milliseconds) 897 */ 898 const getRemainingExpirationTimeMS = () => { 899 const lastTimestamp = globalObj._state._lastConsentTimestamp; 900 901 const elapsedTimeMilliseconds = lastTimestamp 902 ? new Date() - lastTimestamp 903 : 0; 904 905 return getExpiresAfterDaysValue()*86400000 - elapsedTimeMilliseconds; 906 }; 907 908 /** 909 * Used to fetch external language files (.json) 910 * @param {string} url 911 * @returns {Promise<import('../core/global').Translation | boolean>} 912 */ 913 const fetchJson = async (url) => { 914 try { 915 const response = await fetch(url); 916 return await response.json(); 917 } catch (e) { 918 console.error(e); 919 return false; 920 } 921 }; 922 923 /** 924 * Helper function to retrieve cookie duration 925 * @returns {number} 926 */ 927 const getExpiresAfterDaysValue = () => { 928 const expiresAfterDays = globalObj._config.cookie.expiresAfterDays; 929 930 return isFunction(expiresAfterDays) 931 ? expiresAfterDays(globalObj._state._acceptType) 932 : expiresAfterDays; 933 }; 934 935 /** 936 * Symmetric difference between 2 arrays 937 * @param {any[]} arr1 938 * @param {any[]} arr2 939 */ 940 const arrayDiff = (arr1, arr2) => { 941 const a = arr1 || []; 942 const b = arr2 || []; 943 944 return a 945 .filter(x => !elContains(b, x)) 946 .concat(b.filter(x => !elContains(a, x))); 947 }; 948 949 /** 950 * Calculate "accept type" 951 * @returns {'all'|'custom'|'necessary'} accept type 952 */ 953 const resolveAcceptType = () => { 954 let type = 'custom'; 955 956 const { _acceptedCategories, _allCategoryNames, _readOnlyCategories } = globalObj._state; 957 const nAcceptedCategories = _acceptedCategories.length; 958 959 if (nAcceptedCategories === _allCategoryNames.length) 960 type = 'all'; 961 else if (nAcceptedCategories === _readOnlyCategories.length) 962 type = 'necessary'; 963 964 return type; 965 }; 966 967 /** 968 * Note: getUserPreferences() depends on "acceptType" 969 * @param {string[]} acceptedCategories 970 */ 971 const setAcceptedCategories = (acceptedCategories) => { 972 globalObj._state._acceptedCategories = unique(acceptedCategories); 973 globalObj._state._acceptType = resolveAcceptType(); 974 }; 975 976 /** 977 * This callback type is called `requestCallback` and is displayed as a global symbol. 978 * 979 * @callback createModal 980 * @param {import('../core/global').Api} api 981 */ 982 983 /** 984 * Add an onClick listeners to all html elements with data-cc attribute 985 * @param {HTMLElement} [elem] 986 * @param {import('../core/global').Api} api 987 * @param {createModal} [createPreferencesModal] 988 */ 989 const addDataButtonListeners = (elem, api, createPreferencesModal, createMainContainer) => { 990 const ACCEPT_PREFIX = 'accept-'; 991 992 const { 993 show, 994 showPreferences, 995 hide, 996 hidePreferences, 997 acceptCategory 998 } = api; 999 1000 const rootEl = elem || document; 1001 const getElements = dataRole => querySelectorAll(rootEl, `[data-cc="${dataRole}"]`); 1002 1003 /** 1004 * Helper function: accept and then hide modals 1005 * @param {Event} event source event 1006 * @param {string|string[]} [acceptType] 1007 */ 1008 const acceptAction = (event, acceptType) => { 1009 preventDefault(event); 1010 acceptCategory(acceptType); 1011 hidePreferences(); 1012 hide(); 1013 }; 1014 1015 const 1016 showPreferencesModalElements = getElements('show-preferencesModal'), 1017 showConsentModalElements = getElements('show-consentModal'), 1018 acceptAllElements = getElements(ACCEPT_PREFIX + 'all'), 1019 acceptNecessaryElements = getElements(ACCEPT_PREFIX + 'necessary'), 1020 acceptCustomElements = getElements(ACCEPT_PREFIX + 'custom'), 1021 createPreferencesModalOnHover = globalObj._config.lazyHtmlGeneration; 1022 1023 //{{START: GUI}} 1024 for (const el of showPreferencesModalElements) { 1025 setAttribute(el, 'aria-haspopup', 'dialog'); 1026 addEvent(el, CLICK_EVENT, (event) => { 1027 preventDefault(event); 1028 showPreferences(); 1029 }); 1030 1031 if (createPreferencesModalOnHover) { 1032 addEvent(el, 'mouseenter', (event) => { 1033 preventDefault(event); 1034 if (!globalObj._state._preferencesModalExists) 1035 createPreferencesModal(api, createMainContainer); 1036 }, true); 1037 1038 addEvent(el, 'focus', () => { 1039 if (!globalObj._state._preferencesModalExists) 1040 createPreferencesModal(api, createMainContainer); 1041 }); 1042 } 1043 } 1044 1045 for (let el of showConsentModalElements) { 1046 setAttribute(el, 'aria-haspopup', 'dialog'); 1047 addEvent(el, CLICK_EVENT, (event) => { 1048 preventDefault(event); 1049 show(true); 1050 }, true); 1051 } 1052 //{{END: GUI}} 1053 1054 for (let el of acceptAllElements) { 1055 addEvent(el, CLICK_EVENT, (event) => { 1056 acceptAction(event, 'all'); 1057 }, true); 1058 } 1059 1060 for (let el of acceptCustomElements) { 1061 addEvent(el, CLICK_EVENT, (event) => { 1062 acceptAction(event); 1063 }, true); 1064 } 1065 1066 for (let el of acceptNecessaryElements) { 1067 addEvent(el, CLICK_EVENT, (event) => { 1068 acceptAction(event, []); 1069 }, true); 1070 } 1071 }; 1072 1073 /** 1074 * @param {HTMLElement} el 1075 * @param {boolean} [toggleTabIndex] 1076 */ 1077 const focus = (el, toggleTabIndex) => { 1078 if (!el) return; 1079 1080 /** 1081 * Momentarily add the `tabindex` attribute to fix 1082 * a bug with focus restoration in chrome 1083 */ 1084 toggleTabIndex && (el.tabIndex = -1); 1085 1086 el.focus(); 1087 1088 /** 1089 * Remove the `tabindex` attribute so 1090 * that the html markup is valid again 1091 */ 1092 toggleTabIndex && el.removeAttribute('tabindex'); 1093 }; 1094 1095 /** 1096 * @param {HTMLDivElement} element 1097 * @param {1 | 2} modalId 1098 */ 1099 const focusAfterTransition = (element, modalId) => { 1100 const getVisibleDiv = (modalId) => modalId === 1 1101 ? globalObj._dom._cmDivTabindex 1102 : globalObj._dom._pmDivTabindex; 1103 1104 const setFocus = (event) => { 1105 event.target.removeEventListener('transitionend', setFocus); 1106 if (event.propertyName === 'opacity' && getComputedStyle(element).opacity === '1') { 1107 focus(getVisibleDiv(modalId)); 1108 } 1109 }; 1110 1111 addEvent(element, 'transitionend', setFocus); 1112 }; 1113 1114 /** 1115 * Obtain accepted and rejected categories 1116 * @returns {{accepted: string[], rejected: string[]}} 1117 */ 1118 const getCurrentCategoriesState = () => { 1119 const { 1120 _invalidConsent, 1121 _acceptedCategories, 1122 _allCategoryNames 1123 } = globalObj._state; 1124 1125 return { 1126 accepted: _acceptedCategories, 1127 rejected: _invalidConsent 1128 ? [] 1129 : _allCategoryNames.filter(category => 1130 !elContains(_acceptedCategories, category) 1131 ) 1132 }; 1133 }; 1134 1135 let disableInteractionTimeout; 1136 1137 /** 1138 * @param {boolean} [enable] 1139 */ 1140 const toggleDisableInteraction = (enable) => { 1141 clearTimeout(disableInteractionTimeout); 1142 1143 if (enable) { 1144 addClass(globalObj._dom._htmlDom, TOGGLE_DISABLE_INTERACTION_CLASS); 1145 }else { 1146 disableInteractionTimeout = setTimeout(() => { 1147 removeClass(globalObj._dom._htmlDom, TOGGLE_DISABLE_INTERACTION_CLASS); 1148 }, 500); 1149 } 1150 }; 1151 1152 const iconStrokes = [ 1153 'M 19.5 4.5 L 4.5 19.5 M 4.5 4.501 L 19.5 19.5', // X 1154 'M 3.572 13.406 L 8.281 18.115 L 20.428 5.885', // TICK 1155 'M 21.999 6.94 L 11.639 17.18 L 2.001 6.82 ' // ARROW 1156 ]; 1157 1158 /** 1159 * [0: x, 1: tick, 2: arrow] 1160 * @param {0 | 1 | 2} [iconIndex] 1161 * @param {number} [strokeWidth] 1162 */ 1163 const getSvgIcon = (iconIndex = 0, strokeWidth = 1.5) => { 1164 return `<svg viewBox="0 0 24 24" stroke-width="${strokeWidth}"><path d="${iconStrokes[iconIndex]}"/></svg>`; 1165 }; 1166 1167 /** 1168 * Trap focus inside modal and focus the first 1169 * focusable element of current active modal 1170 * @param {HTMLDivElement} modal 1171 */ 1172 const handleFocusTrap = (modal) => { 1173 const dom = globalObj._dom; 1174 const state = globalObj._state; 1175 1176 /** 1177 * @param {HTMLDivElement} modal 1178 * @param {HTMLElement[]} focusableElements 1179 */ 1180 const trapFocus = (modal) => { 1181 const isConsentModal = modal === dom._cm; 1182 1183 const scope = state._userConfig.disablePageInteraction 1184 ? dom._htmlDom 1185 : isConsentModal 1186 ? dom._ccMain 1187 : dom._htmlDom;
1188 1189 const getFocusableElements = () => isConsentModal 1190 ? state._cmFocusableElements 1191 : state._pmFocusableElements; 1192 1193 const isModalVisible = () => isConsentModal 1194 ? state._consentModalVisible && !state._preferencesModalVisible 1195 : state._preferencesModalVisible; 1196 1197 addEvent(scope, 'keydown', (e) => { 1198 if (e.key !== 'Tab' || !isModalVisible()) 1199 return; 1200 1201 const currentActiveElement = getActiveElement(); 1202 const focusableElements = getFocusableElements(); 1203 1204 if (focusableElements.length === 0) 1205 return; 1206 1207 /** 1208 * If reached natural end of the tab sequence => restart 1209 * If current focused element is not inside modal => focus modal 1210 */ 1211 if (e.shiftKey) { 1212 if (currentActiveElement === focusableElements[0] || !modal.contains(currentActiveElement)) { 1213 preventDefault(e); 1214 focus(focusableElements[1]); 1215 } 1216 } else { 1217 if (currentActiveElement === focusableElements[1] || !modal.contains(currentActiveElement)) { 1218 preventDefault(e); 1219 focus(focusableElements[0]); 1220 } 1221 } 1222 }, true); 1223 }; 1224 1225 trapFocus(modal); 1226 }; 1227 1228 /** 1229 * Note: any of the below focusable elements, which has the attribute tabindex="-1" AND is either 1230 * the first or last element of the modal, won't receive focus during "open/close" modal 1231 */ 1232 const focusableTypesSelector = ['[href]', BUTTON_TAG, 'input', 'details', '[tabindex]'] 1233 .map(selector => selector+':not([tabindex="-1"])').join(','); 1234 1235 const getFocusableElements = (root) => querySelectorAll(root, focusableTypesSelector); 1236 1237 /** 1238 * Save reference to first and last focusable elements inside each modal 1239 * to prevent losing focus while navigating with TAB 1240 * @param {1 | 2} [modalId] 1241 */ 1242 const getModalFocusableData = (modalId) => { 1243 const { _state, _dom } = globalObj; 1244 1245 /** 1246 * Saves all focusable elements inside modal, into the array 1247 * @param {HTMLElement} modal 1248 * @param {Element[]} array 1249 */ 1250 const saveAllFocusableElements = (modal, array) => { 1251 const focusableElements = getFocusableElements(modal); 1252 1253 /** 1254 * Save first and last elements (trap focus inside modal) 1255 */ 1256 array[0] = focusableElements[0]; 1257 array[1] = focusableElements[focusableElements.length - 1]; 1258 }; 1259 1260 if (modalId === 1 && _state._consentModalExists) 1261 saveAllFocusableElements(_dom._cm, _state._cmFocusableElements); 1262 1263 if (modalId === 2 && _state._preferencesModalExists) 1264 saveAllFocusableElements(_dom._pm, _state._pmFocusableElements); 1265 }; 1266 1267 /** 1268 * Fire custom event 1269 * @param {string} eventName 1270 * @param {string} [modalName] 1271 * @param {HTMLElement} [modal] 1272 */ 1273 const fireEvent = (eventName, modalName, modal) => { 1274 const { 1275 _onChange, 1276 _onConsent, 1277 _onFirstConsent, 1278 _onModalHide, 1279 _onModalReady, 1280 _onModalShow 1281 } = globalObj._callbacks; 1282 1283 const events = globalObj._customEvents; 1284 1285 //{{START: GUI}} 1286 if (modalName) { 1287 const modalParams = { modalName }; 1288 1289 if (eventName === events._onModalShow) { 1290 isFunction(_onModalShow) && _onModalShow(modalParams); 1291 }else if (eventName === events._onModalHide) { 1292 isFunction(_onModalHide) && _onModalHide(modalParams); 1293 } else { 1294 modalParams.modal = modal; 1295 isFunction(_onModalReady) && _onModalReady(modalParams); 1296 } 1297 1298 return dispatchPluginEvent(eventName, modalParams); 1299 } 1300 //{{END: GUI}} 1301 1302 const params = { 1303 cookie: globalObj._state._savedCookieContent 1304 }; 1305 1306 if (eventName === events._onFirstConsent) { 1307 isFunction(_onFirstConsent) && _onFirstConsent(deepCopy(params)); 1308 }else if (eventName === events._onConsent) { 1309 isFunction(_onConsent) && _onConsent(deepCopy(params)); 1310 }else { 1311 params.changedCategories = globalObj._state._lastChangedCategoryNames; 1312 params.changedServices = globalObj._state._lastChangedServices; 1313 isFunction(_onChange) && _onChange(deepCopy(params)); 1314 } 1315 1316 dispatchPluginEvent(eventName, deepCopy(params)); 1317 }; 1318 1319 /** 1320 * @param {CallableFunction} fn 1321 */ 1322 const safeRun = (fn, hideError) => { 1323 try { 1324 return fn(); 1325 } catch (e) { 1326 !hideError && console.warn('CookieConsent:', e); 1327 return false;
1328 } 1329 }; 1330 1331 /** 1332 * @param {string} type 1333 */ 1334 const validMimeType = type => ['text/javascript', 'module'].includes(type); 1335 1336 /** 1337 * This function handles the loading/activation logic of the already 1338 * existing scripts based on the current accepted cookie categories 1339 * 1340 * @param {string[]} [defaultEnabledCategories] 1341 */ 1342 const manageExistingScripts = (defaultEnabledCategories) => { 1343 const { 1344 _acceptedServices, 1345 _lastChangedServices, 1346 _allCategoryNames, 1347 _allDefinedServices, 1348 _allScriptTags, 1349 _savedCookieContent, 1350 _lastChangedCategoryNames, 1351 } = globalObj._state; 1352 1353 /** 1354 * Automatically Enable/Disable internal services 1355 */ 1356 for (const categoryName of _allCategoryNames) { 1357 const lastChangedServices = _lastChangedServices[categoryName] 1358 || _acceptedServices[categoryName] 1359 || []; 1360 1361 for (const serviceName of lastChangedServices) { 1362 const service = _allDefinedServices[categoryName][serviceName]; 1363 1364 if (!service) 1365 continue; 1366 1367 const {onAccept, onReject} = service; 1368 1369 if ( 1370 !service._enabled 1371 && elContains(_acceptedServices[categoryName], serviceName) 1372 && isFunction(onAccept) 1373 ) { 1374 service._enabled = true; 1375 onAccept(); 1376 } 1377 1378 else if ( 1379 service._enabled 1380 && !elContains(_acceptedServices[categoryName], serviceName) 1381 && isFunction(onReject) 1382 ) { 1383 service._enabled = false; 1384 onReject(); 1385 } 1386 } 1387 } 1388 1389 if (!globalObj._config.manageScriptTags) 1390 return; 1391 1392 const scripts = _allScriptTags; 1393 const acceptedCategories = defaultEnabledCategories 1394 || _savedCookieContent.categories 1395 || []; 1396 1397 /** 1398 * Load scripts (sequentially), using a recursive function 1399 * which loops through the scripts array 1400 * @param {import('../core/global').ScriptInfo[]} scripts scripts to load 1401 * @param {number} index current script to load 1402 */ 1403 const loadScriptsHelper = (scripts, index) => { 1404 if (index >= scripts.length) 1405 return; 1406 1407 const currScriptInfo = _allScriptTags[index]; 1408 1409 /** 1410 * Skip script if it was already executed 1411 */ 1412 if (currScriptInfo._executed) 1413 return loadScriptsHelper(scripts, index+1); 1414 1415 const currScript = currScriptInfo._script; 1416 const currScriptCategory = currScriptInfo._categoryName; 1417 const currScriptService = currScriptInfo._serviceName; 1418 const categoryAccepted = elContains(acceptedCategories, currScriptCategory); 1419 const serviceAccepted = currScriptService 1420 ? elContains(_acceptedServices[currScriptCategory], currScriptService) 1421 : false; 1422 1423 const categoryWasJustEnabled = () => !currScriptService 1424 && !currScriptInfo._runOnDisable 1425 && categoryAccepted; 1426 1427 const serviceWasJustEnabled = () => currScriptService 1428 && !currScriptInfo._runOnDisable 1429 && serviceAccepted; 1430 1431 const categoryWasJustDisabled = () => !currScriptService 1432 && currScriptInfo._runOnDisable 1433 && !categoryAccepted 1434 && elContains(_lastChangedCategoryNames, currScriptCategory); 1435 1436 const serviceWasJustDisabled = () => currScriptService 1437 && currScriptInfo._runOnDisable 1438 && !serviceAccepted 1439 && elContains(_lastChangedServices[currScriptCategory] || [], currScriptService); 1440 1441 const shouldRunScript = 1442 categoryWasJustEnabled() 1443 || categoryWasJustDisabled() 1444 || serviceWasJustEnabled() 1445 || serviceWasJustDisabled(); 1446 1447 if (shouldRunScript) { 1448 currScriptInfo._executed = true; 1449 const dataType = getAttribute(currScript, 'type', true); 1450 1451 removeAttribute(currScript, 'type', !!dataType); 1452 removeAttribute(currScript, SCRIPT_TAG_SELECTOR); 1453 1454 // Get current script data-src (if there is one) 1455 let src = getAttribute(currScript, 'src', true); 1456 1457 // Some scripts (like ga) might throw warning if data-src is present 1458 src && removeAttribute(currScript, 'src', true); 1459 1460 /** 1461 * Fresh script 1462 * @type {HTMLScriptElement} 1463 */ 1464 const freshScript = createNode('script'); 1465 1466 freshScript.textContent = currScript.innerHTML; 1467 1468 //Copy attributes over to the new "revived" script 1469 for (const {nodeName} of currScript.attributes) { 1470 setAttribute( 1471 freshScript, 1472 nodeName,
1473 currScript[nodeName] || getAttribute(currScript, nodeName) 1474 ); 1475 } 1476 1477 /** 1478 * Set custom type 1479 */ 1480 dataType && (freshScript.type = dataType); 1481 1482 // Set src (if data-src found) 1483 src 1484 ? (freshScript.src = src) 1485 : (src = currScript.src); 1486 1487 const externalScript = !!src && (dataType ? validMimeType(dataType) : true); 1488 1489 // If script has valid "src" attribute 1490 // try loading it sequentially 1491 if (externalScript) { 1492 // load script sequentially => the next script will not be loaded 1493 // until the current's script onload event triggers 1494 freshScript.onload = freshScript.onerror = () => { 1495 loadScriptsHelper(scripts, ++index); 1496 }; 1497 } 1498 1499 // Replace current "sleeping" script with the new "revived" one 1500 currScript.replaceWith(freshScript); 1501 1502 /** 1503 * If we managed to get here and src is still set, it means that 1504 * the script is loading/loaded sequentially so don't go any further 1505 */ 1506 if (externalScript) 1507 return; 1508 } 1509 1510 1511 // Go to next script right away 1512 loadScriptsHelper(scripts, ++index); 1513 }; 1514 1515 loadScriptsHelper(scripts, 0); 1516 }; 1517 1518 /** 1519 * Keep track of categories enabled by default (useful when mode==OPT_OUT_MODE) 1520 */ 1521 const retrieveEnabledCategoriesAndServices = () => { 1522 const state = globalObj._state; 1523 1524 for (const categoryName of state._allCategoryNames) { 1525 const category = state._allDefinedCategories[categoryName]; 1526 1527 if (category.readOnly || (category.enabled && state._userConfig.mode === OPT_OUT_MODE)) { 1528 state._defaultEnabledCategories.push(categoryName); 1529 const services = state._allDefinedServices[categoryName] || {}; 1530 1531 for (let serviceName in services) { 1532 state._acceptedServices[categoryName].push(serviceName); 1533 } 1534 } 1535 } 1536 }; 1537 1538 /** 1539 * @typedef {Object} Layout 1540 * @property {string[]} _variants 1541 * @property {string[]} _alignV 1542 * @property {string[]} _alignH 1543 * @property {string} _defaultAlignV 1544 * @property {string} _defaultAlignH 1545 */ 1546 1547 /** 1548 * @typedef {Object.<string, Layout>}
1548 Layouts 1549 */ 1550 1551 const CLASS_CONSTANTS = { 1552 _top: 'top', 1553 _middle: 'middle', 1554 _bottom: 'bottom', 1555 _left: 'left', 1556 _center: 'center', 1557 _right: 'right', 1558 _inline: 'inline', 1559 _wide: 'wide', 1560 _pmPrefix: 'pm--', 1561 _cmPrefix: 'cm--', 1562 _box: 'box' 1563 }; 1564 1565 const alignV = [ 1566 CLASS_CONSTANTS._middle, 1567 CLASS_CONSTANTS._top, 1568 CLASS_CONSTANTS._bottom 1569 ]; 1570 1571 const alignH = [ 1572 CLASS_CONSTANTS._left, 1573 CLASS_CONSTANTS._center, 1574 CLASS_CONSTANTS._right 1575 ]; 1576 1577 const ALL_CM_LAYOUTS = { 1578 box: { 1579 _variants: [CLASS_CONSTANTS._wide, CLASS_CONSTANTS._inline], 1580 _alignV: alignV, 1581 _alignH: alignH, 1582 _defaultAlignV: CLASS_CONSTANTS._bottom, 1583 _defaultAlignH: CLASS_CONSTANTS._right 1584 }, 1585 cloud: { 1586 _variants: [CLASS_CONSTANTS._inline], 1587 _alignV: alignV, 1588 _alignH: alignH, 1589 _defaultAlignV: CLASS_CONSTANTS._bottom, 1590 _defaultAlignH: CLASS_CONSTANTS._center 1591 }, 1592 bar: { 1593 _variants: [CLASS_CONSTANTS._inline], 1594 _alignV: alignV.slice(1), //remove the first "middle" option 1595 _alignH: [], 1596 _defaultAlignV: CLASS_CONSTANTS._bottom, 1597 _defaultAlignH: '' 1598 } 1599 }; 1600 1601 const ALL_PM_LAYOUTS = { 1602 box: { 1603 _variants: [], 1604 _alignV: [], 1605 _alignH: [], 1606 _defaultAlignV: '', 1607 _defaultAlignH: '' 1608 }, 1609 bar: { 1610 _variants: [CLASS_CONSTANTS._wide], 1611 _alignV: [], 1612 _alignH: [CLASS_CONSTANTS._left, CLASS_CONSTANTS._right], 1613 _defaultAlignV: '', 1614 _defaultAlignH: CLASS_CONSTANTS._left 1615 } 1616 }; 1617 1618 /** 1619 * Add appropriate classes to modals and buttons 1620 * @param {0 | 1} applyToModal 1621 */ 1622 const guiManager = (applyToModal) => { 1623 const guiOptions = globalObj._state._userConfig.guiOptions; 1624 const consentModalOptions = guiOptions && guiOptions.consentModal; 1625 const preferencesModalOptions = guiOptions && guiOptions.preferencesModal; 1626 1627 if (applyToModal === 0) { 1628 setLayout( 1629 globalObj._dom._cm, 1630 ALL_CM_LAYOUTS, 1631 consentModalOptions, 1632 CLASS_CONSTANTS._cmPrefix, 1633 CLASS_CONSTANTS._box, 1634 'cm' 1635 ); 1636 } 1637 1638 if (applyToModal === 1) { 1639 setLayout( 1640 globalObj._dom._pm, 1641 ALL_PM_LAYOUTS, 1642 preferencesModalOptions, 1643 CLASS_CONSTANTS._pmPrefix, 1644 CLASS_CONSTANTS._box, 1645 'pm' 1646 ); 1647 } 1648 }; 1649 1650 /** 1651 * Helper function to set the proper layout classes 1652 * @param {HTMLElement} modal 1653 * @param {Layouts} allowedLayoutsObj 1654 * @param {import("../core/global").GuiModalOption} userGuiOptions 1655 * @param {'cm--' | 'pm--'} modalClassPrefix 1656 * @param {string} defaultLayoutName 1657 * @param {'cm' | 'pm'} modalClassName 1658 */ 1659 const setLayout = (modal, allowedLayoutsObj, userGuiOptions, modalClassPrefix, defaultLayoutName, modalClassName) => { 1660 /** 1661 * Reset modal classes to default 1662 */ 1663 modal.className = modalClassName; 1664 1665 const layout = userGuiOptions && userGuiOptions.layout; 1666 const position = userGuiOptions && userGuiOptions.position; 1667 const flipButtons = userGuiOptions && userGuiOptions.flipButtons; 1668 const equalWeightButtons = !userGuiOptions || userGuiOptions.equalWeightButtons !== false; 1669 1670 const layoutSplit = layout && layout.split(' ') || []; 1671 1672 const layoutName = layoutSplit[0]; 1673 const layoutVariant = layoutSplit[1]; 1674 1675 const currentLayoutName = layoutName in allowedLayoutsObj 1676 ? layoutName 1677 : defaultLayoutName; 1678 1679 const currentLayout = allowedLayoutsObj[currentLayoutName]; 1680 const currentLayoutVariant = elContains(currentLayout._variants, layoutVariant) && layoutVariant; 1681 1682 const positionSplit = position && position.split(' ') || []; 1683 const positionV = positionSplit[0]; 1684 1685 const positionH = modalClassPrefix === CLASS_CONSTANTS._pmPrefix 1686 ? positionSplit[0] 1687 : positionSplit[1]; 1688 1689 const currentPositionV = elContains(currentLayout._alignV, positionV) 1690 ? positionV 1691 : currentLayout._defaultAlignV; 1692 1693 const currentPositionH = elContains(currentLayout._alignH, positionH) 1694 ? positionH 1695 : currentLayout._defaultAlignH; 1696 1697 const addModalClass = className => { 1698 className && addClass(modal, modalClassPrefix + className); 1699 }; 1700 1701 addModalClass(currentLayoutName); 1702 addModalClass(currentLayoutVariant); 1703 addModalClass(currentPositionV); 1704 addModalClass(currentPositionH); 1705 flipButtons && addModalClass('flip'); 1706 1707 const secondaryBtnClass = 'btn--secondary'; 1708 const btnClassPrefix = modalClassName + '__'; 1709 const btnClass = btnClassPrefix + secondaryBtnClass; 1710 1711 /** 1712 * Add classes to buttons 1713 */ 1714 if (modalClassName === 'cm') { 1715 const {_cmAcceptNecessaryBtn, _cmCloseIconBtn} = globalObj._dom; 1716 1717 if (_cmAcceptNecessaryBtn) { 1718 equalWeightButtons 1719 ? removeClass(_cmAcceptNecessaryBtn, btnClass) 1720 : addClass(_cmAcceptNecessaryBtn, btnClass); 1721 } 1722 1723 if (_cmCloseIconBtn) { 1724 equalWeightButtons 1725 ? removeClass(_cmCloseIconBtn, btnClass) 1726 : addClass(_cmCloseIconBtn, btnClass); 1727 } 1728 } else { 1729 const { _pmAcceptNecessaryBtn } = globalObj._dom; 1730 1731 if (_pmAcceptNecessaryBtn) { 1732 equalWeightButtons 1733 ? removeClass(_pmAcceptNecessaryBtn, btnClass) 1734 : addClass(_pmAcceptNecessaryBtn, btnClass); 1735 } 1736 } 1737 }; 1738 1739 /** 1740 * @callback CreateMainContainer 1741 */ 1742 1743 /** 1744 * Generates preferences modal and appends it to "cc-main" el. 1745 * @param {import("../global").Api} api 1746 * @param {CreateMainContainer} createMainContainer 1747 */ 1748 const createPreferencesModal = (api, createMainContainer) => { 1749 const state = globalObj._state; 1750 const dom = globalObj._dom; 1751 const {hide, hidePreferences, acceptCategory} = api; 1752 1753 /** 1754 * @param {string|string[]} [categories] 1755 */ 1756 const acceptHelper = (categories) => { 1757 acceptCategory(categories); 1758 hidePreferences(); 1759 hide(); 1760 }; 1761 1762 /** 1763 * @type {import("../global").PreferencesModalOptions} 1764 */ 1765 const modalData = state._currentTranslation && state._currentTranslation.preferencesModal; 1766 1767 if (!modalData) 1768 return; 1769 1770 const 1771 titleData = modalData.title, 1772 closeIconLabelData = modalData.closeIconLabel, 1773 acceptAllBtnData = modalData.acceptAllBtn, 1774 acceptNecessaryBtnData = modalData.acceptNecessaryBtn, 1775 savePreferencesBtnData = modalData.savePreferencesBtn, 1776 sectionsData = modalData.sections || [], 1777 createFooter = acceptAllBtnData 1778 || acceptNecessaryBtnData 1779 || savePreferencesBtnData; 1780 1781 if (!dom._pmContainer) { 1782 dom._pmContainer = createNode(DIV_TAG); 1783 addClass(dom._pmContainer, 'pm-wrapper'); 1784 1785 const pmOverlay = createNode('div'); 1786 addClass(pmOverlay, 'pm-overlay'); 1787 appendChild(dom._pmContainer, pmOverlay); 1788 1789 /** 1790 * Hide modal when overlay is clicked 1791 */ 1792 addEvent(pmOverlay, CLICK_EVENT, hidePreferences); 1793 1794 // Preferences modal 1795 dom._pm = createNode(DIV_TAG); 1796 1797 addClass(dom._pm, 'pm'); 1798 setAttribute(dom._pm, 'role', 'dialog'); 1799 setAttribute(dom._pm, ARIA_HIDDEN, true); 1800 setAttribute(dom._pm, 'aria-modal', true); 1801 setAttribute(dom._pm, 'aria-labelledby', 'pm__title'); 1802 1803 // Hide preferences on 'esc' key press 1804 addEvent(dom._htmlDom, 'keydown', (event) => { 1805 if (event.keyCode === 27) 1806 hidePreferences(); 1807 }, true); 1808 1809 dom._pmHeader = createNode(DIV_TAG); 1810 addClassPm(dom._pmHeader, 'header'); 1811 1812 dom._pmTitle = createNode('h2'); 1813 addClassPm(dom._pmTitle, 'title');
1814 dom._pmTitle.id = 'pm__title'; 1815 1816 dom._pmCloseBtn = createNode(BUTTON_TAG); 1817 addClassPm(dom._pmCloseBtn, 'close-btn'); 1818 setAttribute(dom._pmCloseBtn, 'aria-label', modalData.closeIconLabel || ''); 1819 addEvent(dom._pmCloseBtn, CLICK_EVENT, hidePreferences); 1820 1821 dom._pmFocusSpan = createNode('span'); 1822 dom._pmFocusSpan.innerHTML = getSvgIcon(); 1823 appendChild(dom._pmCloseBtn, dom._pmFocusSpan); 1824 1825 dom._pmBody = createNode(DIV_TAG); 1826 addClassPm(dom._pmBody, 'body'); 1827 1828 dom._pmFooter = createNode(DIV_TAG); 1829 addClassPm(dom._pmFooter, 'footer'); 1830 1831 var _pmBtnContainer = createNode(DIV_TAG); 1832 addClass(_pmBtnContainer, 'btns'); 1833 1834 var _pmBtnGroup1 = createNode(DIV_TAG); 1835 var _pmBtnGroup2 = createNode(DIV_TAG); 1836 addClassPm(_pmBtnGroup1, BTN_GROUP_CLASS); 1837 addClassPm(_pmBtnGroup2, BTN_GROUP_CLASS); 1838 1839 appendChild(dom._pmFooter, _pmBtnGroup1); 1840 appendChild(dom._pmFooter, _pmBtnGroup2); 1841 1842 appendChild(dom._pmHeader, dom._pmTitle); 1843 appendChild(dom._pmHeader, dom._pmCloseBtn); 1844 1845 dom._pmDivTabindex = createNode(DIV_TAG); 1846 setAttribute(dom._pmDivTabindex, 'tabIndex', -1); 1847 appendChild(dom._pm, dom._pmDivTabindex); 1848 1849 appendChild(dom._pm, dom._pmHeader); 1850 appendChild(dom._pm, dom._pmBody); 1851 1852 createFooter && appendChild(dom._pm, dom._pmFooter); 1853 1854 appendChild(dom._pmContainer, dom._pm); 1855 } else { 1856 dom._pmNewBody = createNode(DIV_TAG); 1857 addClassPm(dom._pmNewBody, 'body'); 1858 } 1859 1860 if (titleData) { 1861 dom._pmTitle.innerHTML = titleData; 1862 closeIconLabelData && setAttribute(dom._pmCloseBtn, 'aria-label', closeIconLabelData); 1863 } 1864 1865 let sectionToggleContainer; 1866 1867 sectionsData.forEach((section, sectionIndex) => { 1868 const 1869 sTitleData = section.title, 1870 sDescriptionData = section.description, 1871 sLinkedCategory = section.linkedCategory, 1872 sCurrentCategoryObject = sLinkedCategory && state._allDefinedCategories[sLinkedCategory], 1873 sCookieTableData = section.cookieTable, 1874 sCookieTableBody = sCookieTableData && sCookieTableData.body, 1875 sCookieTableCaption = sCookieTableData && sCookieTableData.caption, 1876 sCreateCookieTable = sCookieTableBody && sCookieTableBody.length > 0, 1877 hasToggle = !!sCurrentCategoryObject, 1878 1879 /** 1880 * @type {Object.<string, import('../global').Service>} 1881 */ 1882 sServices = hasToggle && state._allDefinedServices[sLinkedCategory], 1883 sServiceNames = isObject(sServices) && getKeys(sServices) || [], 1884 sIsExpandableToggle = hasToggle && (!!sDescriptionData || !!sCreateCookieTable || getKeys(sServices).length>0); 1885 1886 1887 // section 1888 var s = createNode(DIV_TAG); 1889 addClassPm(s, 'section'); 1890 1891 if (sIsExpandableToggle || sDescriptionData) { 1892 var sDescContainer = createNode(DIV_TAG); 1893 addClassPm(sDescContainer, 'section-desc-wrapper'); 1894 } 1895 1896 let nServices = sServiceNames.length; 1897 1898 if (sIsExpandableToggle) { 1899 if (nServices > 0) { 1900 1901 const servicesContainer = createNode(DIV_TAG); 1902 addClassPm(servicesContainer, 'section-services'); 1903 1904 for (const serviceName of sServiceNames) { 1905 const service = sServices[serviceName]; 1906 const serviceLabel = service && service.label || serviceName; 1907 const serviceDiv = createNode(DIV_TAG); 1908 const serviceHeader = createNode(DIV_TAG); 1909 const serviceIconContainer = createNode(DIV_TAG); 1910 const serviceTitle = createNode(DIV_TAG); 1911 1912 addClassPm(serviceDiv, 'service'); 1913 addClassPm(serviceTitle, 'service-title'); 1914 addClassPm(serviceHeader, 'service-header'); 1915 addClassPm(serviceIconContainer, 'service-icon'); 1916 1917 const toggleLabel = createToggleLabel(serviceLabel, serviceName, sCurrentCategoryObject, true, sLinkedCategory); 1918
1919 serviceTitle.innerHTML = serviceLabel; 1920 1921 appendChild(serviceHeader, serviceIconContainer); 1922 appendChild(serviceHeader, serviceTitle); 1923 appendChild(serviceDiv, serviceHeader); 1924 appendChild(serviceDiv, toggleLabel); 1925 appendChild(servicesContainer, serviceDiv); 1926 } 1927 1928 appendChild(sDescContainer, servicesContainer); 1929 } 1930 } 1931 1932 if (sTitleData) { 1933 var sTitleContainer = createNode(DIV_TAG); 1934 1935 var sTitle = hasToggle 1936 ? createNode(BUTTON_TAG) 1937 : createNode(DIV_TAG); 1938 1939 addClassPm(sTitleContainer, 'section-title-wrapper'); 1940 addClassPm(sTitle, 'section-title'); 1941 1942 sTitle.innerHTML = sTitleData; 1943 appendChild(sTitleContainer, sTitle); 1944 1945 if (hasToggle) { 1946 1947 /** 1948 * Arrow icon span 1949 */ 1950 const sTitleIcon = createNode('span'); 1951 sTitleIcon.innerHTML = getSvgIcon(2, 3.5); 1952 addClassPm(sTitleIcon, 'section-arrow'); 1953 appendChild(sTitleContainer, sTitleIcon); 1954 1955 s.className += '--toggle'; 1956 1957 const toggleLabel = createToggleLabel(sTitleData, sLinkedCategory, sCurrentCategoryObject); 1958 1959 let serviceCounterLabel = modalData.serviceCounterLabel; 1960 1961 if (nServices > 0 && isString(serviceCounterLabel)) { 1962 let serviceCounter = createNode('span'); 1963 1964 addClassPm(serviceCounter, 'badge'); 1965 addClassPm(serviceCounter, 'service-counter'); 1966 setAttribute(serviceCounter, ARIA_HIDDEN, true); 1967 setAttribute(serviceCounter, 'data-servicecounter', nServices); 1968 1969 if (serviceCounterLabel) { 1970 serviceCounterLabel = serviceCounterLabel.split('|'); 1971 1972 if (serviceCounterLabel.length > 1 && nServices > 1) 1973 serviceCounterLabel = serviceCounterLabel[1]; 1974 else 1975 serviceCounterLabel = serviceCounterLabel[0]; 1976 1977 setAttribute(serviceCounter, 'data-counterlabel', serviceCounterLabel); 1978 } 1979 1980 serviceCounter.innerHTML = nServices + (serviceCounterLabel 1981 ? ' ' + serviceCounterLabel 1982 : ''); 1983 1984 appendChild(sTitle, serviceCounter); 1985 } 1986 1987 if (sIsExpandableToggle) { 1988 addClassPm(s, 'section--expandable'); 1989 var expandableDivId = sLinkedCategory + '-desc'; 1990 setAttribute(sTitle, 'aria-expanded', false); 1991 setAttribute(sTitle, 'aria-controls', expandableDivId); 1992 } 1993 1994 appendChild(sTitleContainer, toggleLabel); 1995 1996 } else { 1997 setAttribute(sTitle, 'role', 'heading'); 1998 setAttribute(sTitle, 'aria-level', '3'); 1999 } 2000 2001 appendChild(s, sTitleContainer); 2002 } 2003 2004 if (sDescriptionData) { 2005 var sDesc = createNode('p'); 2006 addClassPm(sDesc, 'section-desc'); 2007 sDesc.innerHTML = sDescriptionData; 2008 appendChild(sDescContainer, sDesc); 2009 } 2010 2011 if (sIsExpandableToggle) { 2012 setAttribute(sDescContainer, ARIA_HIDDEN, 'true'); 2013 sDescContainer.id = expandableDivId; 2014 2015 /** 2016 * On button click handle the following :=> aria-expanded, aria-hidden and act class for current section 2017 */ 2018 ((accordion, section, btn) => { 2019 addEvent(sTitle, CLICK_EVENT, () => { 2020 if (!hasClass(section, 'is-expanded')) { 2021 addClass(section, 'is-expanded'); 2022 setAttribute(btn, 'aria-expanded', 'true'); 2023 setAttribute(accordion, ARIA_HIDDEN, 'false'); 2024 } else { 2025 removeClass(section, 'is-expanded'); 2026 setAttribute(btn, 'aria-expanded', 'false'); 2027 setAttribute(accordion, ARIA_HIDDEN, 'true'); 2028 } 2029 }); 2030 })(sDescContainer, s, sTitle); 2031 2032 2033 if (sCreateCookieTable) { 2034 const table = createNode('table'); 2035 const thead = createNode('thead'); 2036 const tbody = createNode('tbody'); 2037 2038 if (sCookieTableCaption) { 2039 const caption = createNode('caption'); 2040 addClassPm(caption, 'table-caption'); 2041 caption.innerHTML = sCookieTableCaption; 2042 table.appendChild(caption); 2043 } 2044 2045 addClassPm(table, 'section-table'); 2046 addClassPm(thead, 'table-head'); 2047 addClassPm(tbody, 'table-body'); 2048 2049 const headerData = sCookieTableData.headers; 2050 const tableHeadersKeys = getKeys(headerData); 2051 2052 /** 2053 * Create table headers 2054 */ 2055 const trHeadFragment = dom._document.createDocumentFragment(); 2056 const trHead = createNode('tr'); 2057 2058 for (const headerKey of tableHeadersKeys) { 2059 const headerValue = headerData[headerKey]; 2060 const th = createNode('th'); 2061 2062 th.id = 'cc__row-' + headerValue + sectionIndex; 2063 setAttribute(th, 'scope', 'col'); 2064 addClassPm(th, 'table-th'); 2065 2066 th.innerHTML = headerValue; 2067 appendChild(trHeadFragment, th); 2068 } 2069 2070 appendChild(trHead, trHeadFragment); 2071 appendChild(thead, trHead); 2072 2073 /** 2074 * Create table body 2075 */ 2076 const bodyFragment = dom._document.createDocumentFragment(); 2077 2078 for (const bodyItem of sCookieTableBody) { 2079 const tr = createNode('tr'); 2080 addClassPm(tr, 'table-tr'); 2081 2082 for (const tdKey of tableHeadersKeys) { 2083 const tdHeader = headerData[tdKey]; 2084 const tdValue = bodyItem[tdKey]; 2085 2086 const td = createNode('td'); 2087 const tdInner = createNode(DIV_TAG); 2088 2089 addClassPm(td, 'table-td'); 2090 setAttribute(td, 'data-column', tdHeader); 2091 setAttribute(td, 'headers', 'cc__row-' + tdHeader + sectionIndex); 2092 2093 tdInner.insertAdjacentHTML('beforeend', tdValue); 2094 2095 appendChild(td, tdInner); 2096 appendChild(tr, td); 2097 } 2098 2099 appendChild(bodyFragment, tr); 2100 } 2101 2102 appendChild(tbody, bodyFragment); 2103 appendChild(table, thead); 2104 appendChild(table, tbody); 2105 appendChild(sDescContainer, table); 2106 } 2107 } 2108 2109 if (sIsExpandableToggle || sDescriptionData) 2110 appendChild(s, sDescContainer); 2111 2112 const currentBody = dom._pmNewBody || dom._pmBody; 2113 2114 if (hasToggle) { 2115 if (!sectionToggleContainer) { 2116 sectionToggleContainer = createNode(DIV_TAG); 2117 addClassPm(sectionToggleContainer, 'section-toggles'); 2118 } 2119 sectionToggleContainer.appendChild(s); 2120 } else { 2121 sectionToggleContainer = null; 2122 } 2123 2124 appendChild(currentBody, sectionToggleContainer || s); 2125 2126 }); 2127 2128 if (acceptAllBtnData) { 2129 if (!dom._pmAcceptAllBtn) { 2130 dom._pmAcceptAllBtn = createNode(BUTTON_TAG); 2131 addClassPm(dom._pmAcceptAllBtn, 'btn'); 2132 setAttribute(dom._pmAcceptAllBtn, DATA_ROLE, 'all'); 2133 appendChild(_pmBtnGroup1, dom._pmAcceptAllBtn); 2134 addEvent(dom._pmAcceptAllBtn, CLICK_EVENT, () => 2135 acceptHelper('all') 2136 ); 2137 } 2138 2139 dom._pmAcceptAllBtn.innerHTML = acceptAllBtnData; 2140 } 2141 2142 if (acceptNecessaryBtnData) { 2143 if (!dom._pmAcceptNecessaryBtn) { 2144 dom._pmAcceptNecessaryBtn = createNode(BUTTON_TAG); 2145 addClassPm(dom._pmAcceptNecessaryBtn, 'btn'); 2146 setAttribute(dom._pmAcceptNecessaryBtn, DATA_ROLE, 'necessary'); 2147 appendChild(_pmBtnGroup1, dom._pmAcceptNecessaryBtn); 2148 addEvent(dom._pmAcceptNecessaryBtn, CLICK_EVENT, () => 2149 acceptHelper([]) 2150 ); 2151 } 2152 2153 dom._pmAcceptNecessaryBtn.innerHTML = acceptNecessaryBtnData; 2154 } 2155 2156 if (savePreferencesBtnData) { 2157 if (!dom._pmSavePreferencesBtn) { 2158 dom._pmSavePreferencesBtn = createNode(BUTTON_TAG); 2159 addClassPm(dom._pmSavePreferencesBtn, 'btn'); 2160 addClassPm(dom._pmSavePreferencesBtn, 'btn--secondary'); 2161 setAttribute(dom._pmSavePreferencesBtn, DATA_ROLE, 'save'); 2162 appendChild(_pmBtnGroup2, dom._pmSavePreferencesBtn); 2163 2164 addEvent(dom._pmSavePreferencesBtn, CLICK_EVENT, () => 2165 acceptHelper() 2166 ); 2167 } 2168 2169 dom._pmSavePreferencesBtn.innerHTML = savePreferencesBtnData; 2170 } 2171 2172 if (dom._pmNewBody) { 2173 dom._pm.replaceChild(dom._pmNewBody, dom._pmBody); 2174 dom._pmBody = dom._pmNewBody; 2175 } 2176 2177 guiManager(1); 2178 2179 if (!state._preferencesModalExists) { 2180 state._preferencesModalExists = true; 2181 2182 _log('CookieConsent [HTML] created', PREFERENCES_MODAL_NAME); 2183 2184 fireEvent(globalObj._customEvents._onModalReady, PREFERENCES_MODAL_NAME, dom._pm); 2185 createMainContainer(api); 2186 appendChild(dom._ccMain, dom._pmContainer); 2187 handleFocusTrap(dom._pm); 2188 2189 /** 2190 * Enable transition 2191 */ 2192 setTimeout(() => addClass(dom._pmContainer, 'cc--anim'), 100); 2193 } 2194 2195 getModalFocusableData(2); 2196 }; 2197 2198 /** 2199 * Generate toggle 2200 * @param {string} label block title 2201 * @param {string} value category/service 2202 * @param {import('../global').Category} sCurrentCategoryObject 2203 * @param {boolean} [isService] 2204 * @param {string} categoryName 2205 */ 2206 function createToggleLabel(label, value, sCurrentCategoryObject, isService, categoryName) { 2207 const state = globalObj._state; 2208 const dom = globalObj._dom; 2209 2210 /** @type {HTMLLabelElement} */ const toggleLabel = createNode('label'); 2211 /** @type {HTMLInputElement} */ const toggle = createNode('input'); 2212 /** @type {HTMLSpanElement} */ const toggleIcon = createNode('span'); 2213 /** @type {HTMLSpanElement} */ const toggleIconCircle = createNode('span'); 2214 /** @type {HTMLSpanElement} */ const toggleLabelSpan = createNode('span'); 2215 2216 // each will contain 2 pseudo-elements to generate 'tick' and 'x' icons 2217 /** @type {HTMLSpanElement} */ const toggleOnIcon = createNode('span'); 2218 /** @type {HTMLSpanElement} */ const toggleOffIcon = createNode('span'); 2219 2220 toggleOnIcon.innerHTML = getSvgIcon(1, 3); 2221 toggleOffIcon.innerHTML = getSvgIcon(0, 3); 2222 2223 toggle.type = 'checkbox'; 2224 2225 addClass(toggleLabel, 'section__toggle-wrapper'); 2226 addClass(toggle, 'section__toggle'); 2227 addClass(toggleOnIcon, 'toggle__icon-on'); 2228 addClass(toggleOffIcon, 'toggle__icon-off'); 2229 addClass(toggleIcon, 'toggle__icon'); 2230 addClass(toggleIconCircle, 'toggle__icon-circle'); 2231 addClass(toggleLabelSpan, 'toggle__label'); 2232 2233 setAttribute(toggleIcon, ARIA_HIDDEN, 'true'); 2234 2235 if (isService) { 2236 addClass(toggleLabel, 'toggle-service'); 2237 setAttribute(toggle, SCRIPT_TAG_SELECTOR, categoryName); 2238 2239 // Save reference to toggles to avoid using document.querySelector later on 2240 dom._serviceCheckboxInputs[categoryName][value] = toggle; 2241 } else { 2242 dom._categoryCheckboxInputs[value] = toggle; 2243 } 2244 2245 if (!isService) { 2246 ((value)=> { 2247 addEvent(toggle, CLICK_EVENT, () => { 2248 const categoryServicesToggles = dom._serviceCheckboxInputs[value]; 2249 const checked = toggle.checked; 2250 state._enabledServices[value] = []; 2251 2252 /** 2253 * Enable/disable all services 2254 */ 2255 for (let serviceName in categoryServicesToggles) {
2256 categoryServicesToggles[serviceName].checked = checked; 2257 checked && state._enabledServices[value].push(serviceName); 2258 } 2259 }); 2260 })(value); 2261 } else { 2262 ((categoryName) => { 2263 addEvent(toggle, 'change', () => { 2264 const categoryServicesToggles = dom._serviceCheckboxInputs[categoryName]; 2265 const categoryToggle = dom._categoryCheckboxInputs[categoryName]; 2266 2267 state._enabledServices[categoryName] = []; 2268 2269 for (let serviceName in categoryServicesToggles) { 2270 const serviceInput = categoryServicesToggles[serviceName]; 2271 2272 if (serviceInput.checked) { 2273 state._enabledServices[categoryName].push(serviceInput.value); 2274 } 2275 } 2276 2277 categoryToggle.checked = state._enabledServices[categoryName].length > 0; 2278 }); 2279 })(categoryName); 2280 2281 } 2282 2283 toggle.value = value; 2284 toggleLabelSpan.textContent = label.replace(/<.*>.*<\/.*>/gm, ''); 2285 2286 appendChild(toggleIconCircle, toggleOffIcon); 2287 appendChild(toggleIconCircle, toggleOnIcon); 2288 appendChild(toggleIcon, toggleIconCircle); 2289 2290 /** 2291 * If consent is valid => retrieve category states from cookie 2292 * Otherwise use states defined in the userConfig. object 2293 */ 2294 if (!state._invalidConsent) { 2295 if (isService) { 2296 const enabledServices = state._acceptedServices[categoryName]; 2297 toggle.checked = sCurrentCategoryObject.readOnly || elContains(enabledServices, value); 2298 }else if (elContains(state._acceptedCategories, value)) { 2299 toggle.checked = true; 2300 } 2301 }else if (sCurrentCategoryObject.readOnly || sCurrentCategoryObject.enabled) { 2302 toggle.checked = true; 2303 } 2304 2305 /** 2306 * Set toggle as readonly if true (disable checkbox) 2307 */ 2308 if (sCurrentCategoryObject.readOnly) { 2309 toggle.disabled = true; 2310 } 2311 2312 appendChild(toggleLabel, toggle); 2313 appendChild(toggleLabel, toggleIcon); 2314 appendChild(toggleLabel, toggleLabelSpan); 2315 2316 return toggleLabel; 2317 } 2318 2319 /** 2320 * @callback CreateMainContainer 2321 */ 2322 2323 /** 2324 * @returns {HTMLSpanElement} 2325 */ 2326 const createFocusSpan = () => { 2327 const span = createNode('span'); 2328 2329 if (!globalObj._dom._focusSpan) 2330 globalObj._dom._focusSpan = span; 2331 2332 return span; 2333 }; 2334 2335 /** 2336 * Create consent modal and append it to "cc-main" el. 2337 * @param {import("../global").Api} api 2338 * @param {CreateMainContainer} createMainContainer 2339 */ 2340 const createConsentModal = (api, createMainContainer) => { 2341 const state = globalObj._state; 2342 const dom = globalObj._dom; 2343 const {hide, showPreferences, acceptCategory} = api; 2344 2345 /** 2346 * @type {import("../global").ConsentModalOptions} 2347 */ 2348 const consentModalData = state._currentTranslation && state._currentTranslation.consentModal; 2349 2350 if (!consentModalData) 2351 return; 2352 2353 const acceptAllBtnData = consentModalData.acceptAllBtn, 2354 acceptNecessaryBtnData = consentModalData.acceptNecessaryBtn, 2355 showPreferencesBtnData = consentModalData.showPreferencesBtn, 2356 closeIconLabelData = consentModalData.closeIconLabel, 2357 footerData = consentModalData.footer, 2358 consentModalLabelValue = consentModalData.label, 2359 consentModalTitleValue = consentModalData.title; 2360 2361 /** 2362 * @param {string|string[]} [categories] 2363 */ 2364 const acceptAndHide = (categories) => { 2365 hide(); 2366 acceptCategory(categories); 2367 }; 2368 2369 // Create modal if it doesn't exist 2370 if (!dom._cmContainer) { 2371 dom._cmContainer = createNode(DIV_TAG); 2372 dom._cm = createNode(DIV_TAG); 2373 dom._cmBody = createNode(DIV_TAG); 2374 dom._cmTexts = createNode(DIV_TAG); 2375 dom._cmBtns = createNode(DIV_TAG); 2376 2377 addClass(dom._cmContainer, 'cm-wrapper'); 2378 addClass(dom._cm, 'cm'); 2379 addClassCm(dom._cmBody, 'body'); 2380 addClassCm(dom._cmTexts, 'texts'); 2381 addClassCm(dom._cmBtns, 'btns'); 2382 2383 setAttribute(dom._cm, 'role', 'dialog'); 2384 setAttribute(dom._cm, 'aria-modal', 'true'); 2385 setAttribute(dom._cm, ARIA_HIDDEN, 'false'); 2386 setAttribute(dom._cm, 'aria-describedby', 'cm__desc'); 2387 2388 if (consentModalLabelValue) 2389 setAttribute(dom._cm, 'aria-label', consentModalLabelValue); 2390 else if (consentModalTitleValue) 2391 setAttribute(dom._cm, 'aria-labelledby', 'cm__title'); 2392 2393 const 2394 boxLayout = 'box', 2395 guiOptions = state._userConfig.guiOptions, 2396 consentModalOptions = guiOptions && guiOptions.consentModal, 2397 consentModalLayout = consentModalOptions && consentModalOptions.layout || boxLayout, 2398 isBoxLayout = consentModalLayout.split(' ')[0] === boxLayout; 2399 2400 /** 2401 * Close icon-button (visible only in the 'box' layout) 2402 */ 2403 if (consentModalTitleValue && closeIconLabelData && isBoxLayout) {
2404 if (!dom._cmCloseIconBtn) { 2405 dom._cmCloseIconBtn = createNode(BUTTON_TAG); 2406 dom._cmCloseIconBtn.innerHTML = getSvgIcon(); 2407 addClassCm(dom._cmCloseIconBtn, 'btn'); 2408 addClassCm(dom._cmCloseIconBtn, 'btn--close'); 2409 addEvent(dom._cmCloseIconBtn, CLICK_EVENT, () => { 2410 _log('CookieConsent [ACCEPT]: necessary'); 2411 acceptAndHide([]); 2412 }); 2413 appendChild(dom._cmBody, dom._cmCloseIconBtn); 2414 } 2415 2416 setAttribute(dom._cmCloseIconBtn, 'aria-label', closeIconLabelData); 2417 } 2418 2419 appendChild(dom._cmBody, dom._cmTexts); 2420 2421 if (acceptAllBtnData || acceptNecessaryBtnData || showPreferencesBtnData) 2422 appendChild(dom._cmBody, dom._cmBtns); 2423 2424 dom._cmDivTabindex = createNode(DIV_TAG); 2425 setAttribute(dom._cmDivTabindex, 'tabIndex', -1); 2426 appendChild(dom._cm, dom._cmDivTabindex); 2427 2428 appendChild(dom._cm, dom._cmBody); 2429 appendChild(dom._cmContainer, dom._cm); 2430 } 2431 2432 if (consentModalTitleValue) { 2433 if (!dom._cmTitle) { 2434 dom._cmTitle = createNode('h2'); 2435 dom._cmTitle.className = dom._cmTitle.id = 'cm__title'; 2436 appendChild(dom._cmTexts, dom._cmTitle); 2437 } 2438 2439 dom._cmTitle.innerHTML = consentModalTitleValue; 2440 } 2441 2442 let description = consentModalData.description; 2443 2444 if (description) { 2445 if (state._revisionEnabled) { 2446 description = description.replace( 2447 '{{revisionMessage}}', 2448 state._validRevision 2449 ? '' 2450 : consentModalData.revisionMessage || '' 2451 ); 2452 } 2453 2454 if (!dom._cmDescription) { 2455 dom._cmDescription = createNode('p'); 2456 dom._cmDescription.className = dom._cmDescription.id = 'cm__desc'; 2457 appendChild(dom._cmTexts, dom._cmDescription); 2458 } 2459 2460 dom._cmDescription.innerHTML = description; 2461 } 2462 2463 if (acceptAllBtnData) { 2464 if (!dom._cmAcceptAllBtn) { 2465 dom._cmAcceptAllBtn = createNode(BUTTON_TAG); 2466 appendChild(dom._cmAcceptAllBtn, createFocusSpan()); 2467 addClassCm(dom._cmAcceptAllBtn, 'btn'); 2468 setAttribute(dom._cmAcceptAllBtn, DATA_ROLE, 'all'); 2469 2470 addEvent(dom._cmAcceptAllBtn, CLICK_EVENT, () => { 2471 _log('CookieConsent [ACCEPT]: all'); 2472 acceptAndHide('all'); 2473 }); 2474 } 2475 2476 dom._cmAcceptAllBtn.firstElementChild.innerHTML = acceptAllBtnData; 2477 } 2478 2479 if (acceptNecessaryBtnData) { 2480 if (!dom._cmAcceptNecessaryBtn) { 2481 dom._cmAcceptNecessaryBtn = createNode(BUTTON_TAG); 2482 appendChild(dom._cmAcceptNecessaryBtn, createFocusSpan()); 2483 addClassCm(dom._cmAcceptNecessaryBtn, 'btn'); 2484 setAttribute(dom._cmAcceptNecessaryBtn, DATA_ROLE, 'necessary'); 2485 2486 addEvent(dom._cmAcceptNecessaryBtn, CLICK_EVENT, () => { 2487 _log('CookieConsent [ACCEPT]: necessary'); 2488 acceptAndHide([]); 2489 }); 2490 } 2491 2492 dom._cmAcceptNecessaryBtn.firstElementChild.innerHTML = acceptNecessaryBtnData; 2493 } 2494 2495 if (showPreferencesBtnData) { 2496 if (!dom._cmShowPreferencesBtn) { 2497 dom._cmShowPreferencesBtn = createNode(BUTTON_TAG); 2498 appendChild(dom._cmShowPreferencesBtn, createFocusSpan()); 2499 addClassCm(dom._cmShowPreferencesBtn, 'btn'); 2500 addClassCm(dom._cmShowPreferencesBtn, 'btn--secondary'); 2501 setAttribute(dom._cmShowPreferencesBtn, DATA_ROLE, 'show'); 2502 2503 addEvent(dom._cmShowPreferencesBtn, 'mouseenter', () => { 2504 if (!state._preferencesModalExists) 2505 createPreferencesModal(api, createMainContainer); 2506 }); 2507 addEvent(dom._cmShowPreferencesBtn, CLICK_EVENT, showPreferences); 2508 } 2509 2510 dom._cmShowPreferencesBtn.firstElementChild.innerHTML = showPreferencesBtnData; 2511 } 2512
2513 if (!dom._cmBtnGroup) { 2514 dom._cmBtnGroup = createNode(DIV_TAG); 2515 addClassCm(dom._cmBtnGroup, BTN_GROUP_CLASS); 2516 2517 acceptAllBtnData && appendChild(dom._cmBtnGroup, dom._cmAcceptAllBtn); 2518 acceptNecessaryBtnData && appendChild(dom._cmBtnGroup, dom._cmAcceptNecessaryBtn); 2519 2520 (acceptAllBtnData || acceptNecessaryBtnData) && appendChild(dom._cmBody, dom._cmBtnGroup); 2521 appendChild(dom._cmBtns, dom._cmBtnGroup); 2522 } 2523 2524 if (dom._cmShowPreferencesBtn && !dom._cmBtnGroup2) { 2525 dom._cmBtnGroup2 = createNode(DIV_TAG); 2526 2527 if ((!dom._cmAcceptNecessaryBtn || !dom._cmAcceptAllBtn)) { 2528 appendChild(dom._cmBtnGroup, dom._cmShowPreferencesBtn); 2529 addClassCm(dom._cmBtnGroup, BTN_GROUP_CLASS + '--uneven'); 2530 }else { 2531 addClassCm(dom._cmBtnGroup2, BTN_GROUP_CLASS); 2532 appendChild(dom._cmBtnGroup2, dom._cmShowPreferencesBtn); 2533 appendChild(dom._cmBtns, dom._cmBtnGroup2); 2534 } 2535 } 2536 2537 if (footerData) { 2538 if (!dom._cmFooterLinksGroup) { 2539 let _consentModalFooter = createNode(DIV_TAG); 2540 let _consentModalFooterLinks = createNode(DIV_TAG); 2541 dom._cmFooterLinksGroup = createNode(DIV_TAG); 2542 2543 addClassCm(_consentModalFooter, 'footer'); 2544 addClassCm(_consentModalFooterLinks, 'links'); 2545 addClassCm(dom._cmFooterLinksGroup, 'link-group'); 2546 2547 appendChild(_consentModalFooterLinks, dom._cmFooterLinksGroup); 2548 appendChild(_consentModalFooter, _consentModalFooterLinks); 2549 appendChild(dom._cm, _consentModalFooter); 2550 } 2551 2552 dom._cmFooterLinksGroup.innerHTML = footerData; 2553 } 2554 2555 guiManager(0); 2556 2557 if (!state._consentModalExists) { 2558 state._consentModalExists = true; 2559 2560 _log('CookieConsent [HTML] created', CONSENT_MODAL_NAME); 2561 2562 fireEvent(globalObj._customEvents._onModalReady, CONSENT_MODAL_NAME, dom._cm); 2563 createMainContainer(api); 2564 appendChild(dom._ccMain, dom._cmContainer); 2565 handleFocusTrap(dom._cm); 2566 2567 /** 2568 * Enable transition 2569 */ 2570 setTimeout(() => addClass(dom._cmContainer, 'cc--anim'), 100); 2571 } 2572 2573 getModalFocusableData(1); 2574 2575 addDataButtonListeners(dom._cmBody, api, createPreferencesModal, createMainContainer); 2576 }; 2577 2578 /** 2579 * Detect the available language. The language autodetection process prioritizes finding translations 2580 * for the complete language code. If translations for the complete code are unavailable, the detection 2581 * mechanism then resorts to searching for the language-only version. 2582 * Works with 'en', 'en_US' and 'en-US'. 2583 * 2584 * @param {string} languageCode - The language code to be detected. 2585 * @returns {?string} The detected language code, or null if not detected. 2586 */ 2587 const getAvailableLanguage = (languageCode) => { 2588 if (!isString(languageCode)) 2589 return null; 2590 2591 if (languageCode in globalObj._state._allTranslations) 2592 return languageCode; 2593 2594 /** 2595 * @type {string} 2596 */ 2597 let language = languageCode.slice(0, 2); 2598 2599 if (language in globalObj._state._allTranslations) 2600 return language; 2601 2602 return null; 2603 }; 2604 2605 /** 2606 * Returns the current language code 2607 * @returns {string} 2608 */ 2609 const getCurrentLanguageCode = () => { 2610 return globalObj._state._currentLanguageCode || globalObj._state._userConfig.language.default; 2611 }; 2612 2613 /** 2614 * Set language code 2615 * @param {string} newLanguageCode 2616 */ 2617 const setCurrentLanguageCode = (newLanguageCode) => { 2618 newLanguageCode && (globalObj._state._currentLanguageCode = newLanguageCode); 2619 }; 2620 2621 /** 2622 * Get current client's browser language 2623 * returns only the first 2 chars: en-US => en 2624 * @returns {string} language 2625 */ 2626 const getBrowserLanguageCode = () => navigator.language; 2627 2628 /** 2629 * Get the lang attribute 2630 * @returns lang attribute 2631 */ 2632 const getDocumentLanguageCode = () => document.documentElement.lang; 2633 2634 /** 2635 * Resolve the language to use. 2636 * @returns {string} language code 2637 */ 2638 const resolveCurrentLanguageCode = () => { 2639 const autoDetect = globalObj._state._userConfig.language.autoDetect; 2640 2641 if (autoDetect) { 2642 _log('CookieConsent [LANG]: autoDetect strategy: "' + autoDetect + '"'); 2643 2644 const detectionStrategies = { 2645 browser: getBrowserLanguageCode(), 2646 document: getDocumentLanguageCode() 2647 }; 2648 2649 /** 2650 * @type {string} 2651 */ 2652 const newLanguageCode = getAvailableLanguage(detectionStrategies[autoDetect]); 2653 2654 if (newLanguageCode) 2655 return newLanguageCode;
2656 } 2657 2658 /** 2659 * Use current language 2660 */ 2661 return getCurrentLanguageCode(); 2662 }; 2663 2664 /** 2665 * Load translation 2666 * @param {string | null} [desiredLanguageCode] 2667 */ 2668 const loadTranslationData = async (desiredLanguageCode) => { 2669 const state = globalObj._state; 2670 2671 /** 2672 * @type {string} 2673 */ 2674 let currentLanguageCode = getAvailableLanguage(desiredLanguageCode) 2675 ? desiredLanguageCode 2676 : getCurrentLanguageCode(); 2677 2678 let currentTranslation = state._allTranslations[currentLanguageCode]; 2679 2680 if (!currentTranslation) 2681 return false; 2682 2683 /** 2684 * If translation is a string, fetch the external json file and replace 2685 * the string (path to json file) with the parsed object 2686 */ 2687 if (isString(currentTranslation)) { 2688 const fetchedTranslation = await fetchJson(currentTranslation); 2689 2690 if (!fetchedTranslation) 2691 return false; 2692 2693 currentTranslation = fetchedTranslation; 2694 } 2695 2696 state._currentTranslation = currentTranslation; 2697 setCurrentLanguageCode(currentLanguageCode); 2698 2699 _log('CookieConsent [LANG]: set language: "' + currentLanguageCode + '"'); 2700 2701 return true; 2702 }; 2703 2704 /** 2705 * Toggle RTL class on/off based on current language 2706 */ 2707 const handleRtlLanguage = () => { 2708 let rtlLanguages = globalObj._state._userConfig.language.rtl; 2709 let ccMain = globalObj._dom._ccMain; 2710 2711 if (rtlLanguages && ccMain) { 2712 if (!isArray(rtlLanguages)) 2713 rtlLanguages = [rtlLanguages]; 2714 2715 elContains(rtlLanguages, globalObj._state._currentLanguageCode) 2716 ? addClass(ccMain, 'cc--rtl') 2717 : removeClass(ccMain, 'cc--rtl'); 2718 } 2719 }; 2720 2721 const createMainContainer = () => { 2722 const dom = globalObj._dom; 2723 2724 if (dom._ccMain) return; 2725 2726 dom._ccMain = createNode(DIV_TAG); 2727 dom._ccMain.id = 'cc-main'; 2728 dom._ccMain.setAttribute('data-nosnippet', ''); 2729 2730 handleRtlLanguage(); 2731 2732 let root = globalObj._state._userConfig.root; 2733 2734 if (root && isString(root)) 2735 root = document.querySelector(root); 2736 2737 // Append main container to dom 2738 (root || dom._document.body).appendChild(dom._ccMain); 2739 }; 2740 2741 /** 2742 * @param {import('../global').Api} api 2743 */ 2744 const generateHtml = (api) => { 2745 addDataButtonListeners(null, api, createPreferencesModal, createMainContainer); 2746 2747 if (globalObj._state._invalidConsent) 2748 createConsentModal(api, createMainContainer); 2749 2750 if (!globalObj._config.lazyHtmlGeneration) 2751 createPreferencesModal(api, createMainContainer); 2752 }; 2753 2754 const localStorageManager = { 2755 /** 2756 * @param {string} key 2757 * @param {string} value 2758 */ 2759 _setItem: (key, value) => { 2760 safeRun(() => localStorage.setItem(key, value)); 2761 }, 2762 2763 /** 2764 * @param {string} key 2765 */ 2766 _getItem: (key) => safeRun(() => localStorage.getItem(key)) || '', 2767 2768 /** 2769 * @param {string} key 2770 */ 2771 _removeItem: (key) => safeRun(() => localStorage.removeItem(key)) 2772 }; 2773 2774 /** 2775 * @param {boolean} [isFirstConsent] 2776 */ 2777 const getCategoriesWithCookies = (isFirstConsent) => { 2778 const state = globalObj._state; 2779 2780 const categoriesToFilter = isFirstConsent 2781 ? state._allCategoryNames 2782 : state._lastChangedCategoryNames; 2783 2784 /** 2785 * Filter out categories with readOnly=true or don't have an autoClear object 2786 */ 2787 return categoriesToFilter.filter(categoryName => { 2788 const currentCategoryObject = state._allDefinedCategories[categoryName]; 2789 2790 return !!currentCategoryObject 2791 && !currentCategoryObject.readOnly 2792 && !!currentCategoryObject.autoClear; 2793 }); 2794 }; 2795 2796 /** 2797 * @param {string[]} allCookies 2798 * @param {string} cookieName 2799 */ 2800 const findMatchingCookies = (allCookies, cookieName) => { 2801 if (cookieName instanceof RegExp) { 2802 return allCookies.filter(cookie => cookieName.test(cookie)); 2803 } else { 2804 const cookieIndex = indexOf(allCookies, cookieName); 2805 return cookieIndex > -1 2806 ? [allCookies[cookieIndex]] 2807 : []; 2808 } 2809 }; 2810 2811 /** 2812 * Delete all unused cookies 2813 * @param {boolean}
2813 [isFirstConsent] 2814 */ 2815 const autoclearCookiesHelper = (isFirstConsent) => { 2816 const state = globalObj._state; 2817 const allCookiesArray = getAllCookies(); 2818 const categoriesToClear = getCategoriesWithCookies(isFirstConsent); 2819 2820 /** 2821 * Clear cookies for each disabled service 2822 */ 2823 for (const categoryName in state._lastChangedServices) { 2824 for (const serviceName of state._lastChangedServices[categoryName]) { 2825 const serviceCookies = state._allDefinedServices[categoryName][serviceName].cookies; 2826 const serviceIsDisabled = !elContains(state._acceptedServices[categoryName], serviceName); 2827 2828 if (!serviceIsDisabled || !serviceCookies) 2829 continue; 2830 2831 for (const cookieItem of serviceCookies) { 2832 const foundCookies = findMatchingCookies(allCookiesArray, cookieItem.name); 2833 eraseCookiesHelper(foundCookies, cookieItem.path, cookieItem.domain); 2834 } 2835 } 2836 } 2837 2838 for (const currentCategoryName of categoriesToClear) { 2839 const category = state._allDefinedCategories[currentCategoryName]; 2840 const autoClear = category.autoClear; 2841 const autoClearCookies = autoClear && autoClear.cookies || []; 2842 2843 const categoryWasJustChanged = elContains(state._lastChangedCategoryNames, currentCategoryName); 2844 const categoryIsDisabled = !elContains(state._acceptedCategories, currentCategoryName); 2845 const categoryWasJustDisabled = categoryWasJustChanged && categoryIsDisabled; 2846 2847 const shouldClearCookies = isFirstConsent 2848 ? categoryIsDisabled 2849 : categoryWasJustDisabled; 2850 2851 if (!shouldClearCookies) 2852 continue; 2853 2854 if (autoClear.reloadPage && categoryWasJustDisabled) 2855 state._reloadPage = true; 2856 2857 for (const cookieItem of autoClearCookies) { 2858 const foundCookies = findMatchingCookies(allCookiesArray, cookieItem.name); 2859 eraseCookiesHelper(foundCookies, cookieItem.path, cookieItem.domain); 2860 } 2861 } 2862 }; 2863 2864 const saveCookiePreferences = () => { 2865 const state = globalObj._state; 2866 2867 /** 2868 * Determine if categories were changed from last state (saved in the cookie) 2869 */ 2870 state._lastChangedCategoryNames = globalObj._config.mode === OPT_OUT_MODE && state._invalidConsent 2871 ? arrayDiff(state._defaultEnabledCategories, state._acceptedCategories) 2872 : arrayDiff(state._acceptedCategories, state._savedCookieContent.categories); 2873 2874 let categoriesWereChanged = state._lastChangedCategoryNames.length > 0; 2875 let servicesWereChanged = false; 2876 2877 /** 2878 * Determine if services were changed from last state 2879 */ 2880 for (const categoryName of state._allCategoryNames) { 2881 state._lastChangedServices[categoryName] = arrayDiff( 2882 state._acceptedServices[categoryName], 2883 state._lastEnabledServices[categoryName] 2884 ); 2885 2886 if (state._lastChangedServices[categoryName].length > 0) 2887 servicesWereChanged = true; 2888 } 2889 2890 //{{START: GUI}} 2891 const categoryToggles = globalObj._dom._categoryCheckboxInputs; 2892 2893 /** 2894 * If the category is accepted check checkbox, 2895 * otherwise uncheck it 2896 */ 2897 for (const categoryName in categoryToggles) { 2898 categoryToggles[categoryName].checked = elContains(state._acceptedCategories, categoryName); 2899 } 2900 2901 for (const categoryName of state._allCategoryNames) { 2902 const servicesToggles = globalObj._dom._serviceCheckboxInputs[categoryName]; 2903 const enabledServices = state._acceptedServices[categoryName]; 2904 2905 for (const serviceName in servicesToggles) { 2906 const serviceInput = servicesToggles[serviceName]; 2907 serviceInput.checked = elContains(enabledServices, serviceName); 2908 } 2909 } 2910 //{{END: GUI}} 2911 2912 if (!state._consentTimestamp) 2913 state._consentTimestamp = new Date(); 2914 2915 if (!state._consentId) 2916 state._consentId = uuidv4(); 2917 2918 state._savedCookieContent = { 2919 categories: deepCopy(state._acceptedCategories), 2920 revision: globalObj._config.revision, 2921 data: state._cookieData, 2922 consentTimestamp: state._consentTimestamp.toISOString(), 2923 consentId: state._consentId, 2924 services: deepCopy(state._acceptedServices) 2925 }; 2926 2927 let isFirstConsent = false;
2928 const stateChanged = categoriesWereChanged || servicesWereChanged; 2929 2930 if (state._invalidConsent || stateChanged) { 2931 /** 2932 * Set consent as valid 2933 */ 2934 if (state._invalidConsent) { 2935 state._invalidConsent = false; 2936 isFirstConsent = true; 2937 } 2938 2939 state._lastConsentTimestamp = !state._lastConsentTimestamp 2940 ? state._consentTimestamp 2941 : new Date(); 2942 2943 state._savedCookieContent.lastConsentTimestamp = state._lastConsentTimestamp.toISOString(); 2944 2945 setCookie(); 2946 2947 const isAutoClearEnabled = globalObj._config.autoClearCookies; 2948 const shouldClearCookies = isFirstConsent || stateChanged; 2949 2950 if (isAutoClearEnabled && shouldClearCookies) 2951 autoclearCookiesHelper(isFirstConsent); 2952 2953 manageExistingScripts(); 2954 } 2955 2956 if (isFirstConsent) { 2957 fireEvent(globalObj._customEvents._onFirstConsent); 2958 fireEvent(globalObj._customEvents._onConsent); 2959 2960 if (globalObj._config.mode === OPT_IN_MODE) 2961 return; 2962 } 2963 2964 if (stateChanged) 2965 fireEvent(globalObj._customEvents._onChange); 2966 2967 /** 2968 * Reload page if needed 2969 */ 2970 if (state._reloadPage) { 2971 state._reloadPage = false; 2972 location.reload(); 2973 } 2974 }; 2975 2976 /** 2977 * Set plugin's cookie 2978 * @param {boolean} [useRemainingExpirationTime] 2979 */
2980 const setCookie = (useRemainingExpirationTime) => { 2981 const { hostname, protocol } = location; 2982 const { name, path, domain, sameSite, useLocalStorage } = globalObj._config.cookie; 2983 2984 const expiresAfterMs = useRemainingExpirationTime 2985 ? getRemainingExpirationTimeMS() 2986 : getExpiresAfterDaysValue()*86400000; 2987 2988 /** 2989 * Expiration date 2990 */ 2991 const date = new Date(); 2992 date.setTime(date.getTime() + expiresAfterMs); 2993 2994 /** 2995 * Store the expiration date in the cookie (in case localstorage is used) 2996 */ 2997 2998 globalObj._state._savedCookieContent.expirationTime = date.getTime(); 2999 3000 const value = JSON.stringify(globalObj._state._savedCookieContent); 3001 3002 /** 3003 * Encode value (RFC compliant) 3004 */ 3005 const cookieValue = encodeURIComponent(value); 3006 3007 let cookieStr = name + '=' 3008 + cookieValue 3009 + (expiresAfterMs !== 0 ? '; expires=' + date.toUTCString() : '') 3010 + '; Path=' + path 3011 + '; SameSite=' + sameSite; 3012 3013 /** 3014 * Set "domain" only if hostname contains a dot (e.g domain.com) 3015 * to ensure that cookie works with 'localhost' 3016 */ 3017 if (elContains(hostname, '.')) 3018 cookieStr += '; Domain=' + domain; 3019 3020 if (protocol === 'https:') 3021 cookieStr += '; Secure'; 3022 3023 useLocalStorage 3024 ? localStorageManager._setItem(name, value) 3025 : document.cookie = cookieStr; 3026 3027 _log('CookieConsent [SET_COOKIE]: ' + name + ':', globalObj._state._savedCookieContent); 3028 }; 3029 3030 /** 3031 * Parse cookie value using JSON.parse 3032 * @param {string} value 3033 */ 3034 const parseCookie = (value, skipDecode) => { 3035 /** 3036 * @type {import('../../types').CookieValue} 3037 */ 3038 let parsedValue; 3039 3040 parsedValue = safeRun(() => JSON.parse(skipDecode 3041 ? value 3042 : decodeURIComponent(value) 3043 ), true) || {}; 3044 3045 return parsedValue; 3046 }; 3047 3048 /** 3049 * Delete cookie by name & path 3050 * @param {string[]} cookies Array of cookie names 3051 * @param {string} [customPath] 3052 * @param {string} [customDomain] 3053 */ 3054 const eraseCookiesHelper = (cookies, customPath, customDomain) => { 3055 if (cookies.length === 0) 3056 return; 3057 3058 const domain = customDomain || globalObj._config.cookie.domain; 3059 const path = customPath || globalObj._config.cookie.path; 3060 const isWwwSubdomain = domain.slice(0, 4) === 'www.'; 3061 const mainDomain = isWwwSubdomain && domain.substring(4); 3062 3063 /** 3064 * Helper function to erase cookie 3065 * @param {string} cookie 3066 * @param {string} [domain] 3067 */ 3068 const erase = (cookie, domain) => { 3069 document.cookie = cookie + '=' 3070 + '; path=' + path 3071 + (domain ? '; domain=.' + domain : '') 3072 + '; expires=Thu, 01 Jan 1970 00:00:01 GMT;'; 3073 }; 3074 3075 for (const cookieName of cookies) { 3076 3077 /**
3078 * 2 attempts to erase the cookie: 3079 * - without domain 3080 * - with domain 3081 */ 3082 erase(cookieName); 3083 erase(cookieName, domain); 3084 3085 /** 3086 * If domain starts with 'www.', 3087 * also erase the cookie for the 3088 * main domain (without www) 3089 */ 3090 if (isWwwSubdomain) 3091 erase(cookieName, mainDomain); 3092 3093 _log('CookieConsent [AUTOCLEAR]: deleting cookie: "' + cookieName + '" path: "' + path + '" domain:', domain); 3094 } 3095 }; 3096 3097 /** 3098 * Get plugin cookie 3099 * @param {string} [customName] 3100 */ 3101 const getPluginCookie = (customName) => { 3102 const name = customName || globalObj._config.cookie.name; 3103 const useLocalStorage = globalObj._config.cookie.useLocalStorage; 3104 const valueStr = useLocalStorage 3105 ? localStorageManager._getItem(name) 3106 : getSingleCookie(name, true); 3107 return parseCookie(valueStr, useLocalStorage); 3108 }; 3109 3110 /** 3111 * Returns the cookie name/value, if it exists 3112 * @param {string} name 3113 * @param {boolean} getValue 3114 * @returns {string} 3115 */ 3116 const getSingleCookie = (name, getValue) => { 3117 const found = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)'); 3118 3119 return found 3120 ? (getValue ? found.pop() : name) 3121 : ''; 3122 }; 3123 3124 /** 3125 * Returns array with all the cookie names 3126 * @param {RegExp} regex 3127 * @returns {string[]} 3128 */ 3129 const getAllCookies = (regex) => { 3130 const allCookies = document.cookie.split(/;\s*/); 3131 3132 /** 3133 * @type {string[]} 3134 */ 3135 const cookieNames = []; 3136 3137 /** 3138 * Save only the cookie names 3139 */ 3140 for (const cookie of allCookies) { 3141 let name = cookie.split('=')[0]; 3142 3143 if (regex) { 3144 safeRun(() => { 3145 regex.test(name) && cookieNames.push(name); 3146 }); 3147 } else { 3148 cookieNames.push(name); 3149 } 3150 } 3151 3152 return cookieNames; 3153 }; 3154 3155 /** 3156 * Configure CookieConsent 3157 * @param {import("./global").UserConfig} userConfig 3158 */ 3159 const setConfig = (userConfig) => { 3160 const { _dom, _config, _state } = globalObj; 3161 3162 const 3163 config = _config, 3164 state = _state, 3165 { cookie } = config, 3166 callbacks = globalObj._callbacks, 3167 userCookieConfig = userConfig.cookie, 3168 userCategories = userConfig.categories, 3169 allCategoryNames = getKeys(userCategories) || [], 3170 nav = navigator, 3171 doc = document; 3172 3173 /** 3174 * Access the 'window' and 'document' objects 3175 * during execution, rather than on import 3176 * (avoid window/document is not defined error) 3177 */ 3178 _dom._document = doc; 3179 //{{START: GUI}} 3180 _dom._htmlDom = doc.documentElement; 3181 //{{END: GUI}} 3182 cookie.domain = location.hostname; 3183 3184 /** 3185 * Make user configuration globally available 3186 */ 3187 state._userConfig = userConfig; 3188 state._allDefinedCategories = userCategories; 3189 state._allCategoryNames = allCategoryNames; 3190 3191 //{{START: GUI}} 3192 state._allTranslations = userConfig.language.translations; 3193 state._disablePageInteraction = !!userConfig.disablePageInteraction; 3194 //{{END: GUI}} 3195 3196 /** 3197 * Save references to callback functions 3198 */ 3199 callbacks._onFirstConsent = userConfig.onFirstConsent; 3200 callbacks._onConsent = userConfig.onConsent; 3201 callbacks._onChange = userConfig.onChange; 3202 3203 //{{START: GUI}} 3204 callbacks._onModalHide = userConfig.onModalHide; 3205 callbacks._onModalShow = userConfig.onModalShow; 3206 callbacks._onModalReady = userConfig.onModalReady; 3207 //{{END: GUI}} 3208 3209 const { 3210 mode, 3211 //{{START: GUI}} 3212 autoShow, 3213 lazyHtmlGeneration, 3214 //{{END: GUI}} 3215 autoClearCookies, 3216 revision, 3217 manageScriptTags, 3218 hideFromBots, 3219 } = userConfig; 3220 3221 if (mode === OPT_OUT_MODE) 3222 config.mode = mode; 3223 3224 if (typeof autoClearCookies === 'boolean') 3225 config.autoClearCookies = autoClearCookies; 3226 3227 if (typeof manageScriptTags === 'boolean') 3228 config.manageScriptTags = manageScriptTags; 3229 3230 if (typeof revision === 'number' && revision >= 0) { 3231 config.revision = revision; 3232 state._revisionEnabled = true; 3233 } 3234 3235 //{{START: GUI}} 3236 3237 if (typeof autoShow === 'boolean') 3238 config.autoShow = autoShow; 3239 3240 if (typeof lazyHtmlGeneration === 'boolean') 3241 config.lazyHtmlGeneration = lazyHtmlGeneration; 3242 3243 //{{END: GUI}} 3244 3245 if (hideFromBots === false) 3246 config.hideFromBots = false;
3247 3248 if (config.hideFromBots === true && nav) 3249 state._botAgentDetected = (nav.userAgent && /bot|crawl|spider|slurp|teoma/i.test(nav.userAgent)) || nav.webdriver; 3250 3251 if (isObject(userCookieConfig)) 3252 config.cookie = {...cookie, ...userCookieConfig}; 3253 3254 _log('CookieConsent [CONFIG]: configuration:', userConfig); 3255 _log('CookieConsent [CONFIG]: autoClearCookies:', config.autoClearCookies); 3256 _log('CookieConsent [CONFIG]: revision enabled:', state._revisionEnabled); 3257 _log('CookieConsent [CONFIG]: manageScriptTags:', config.manageScriptTags); 3258 3259 fetchCategoriesAndServices(allCategoryNames); 3260 retrieveScriptElements(); 3261 3262 //{{START: GUI}} 3263 setCurrentLanguageCode(resolveCurrentLanguageCode()); 3264 //{{END: GUI}} 3265 }; 3266 3267 /** 3268 * Accept API 3269 * @param {string[]|string} categories - Categories to accept 3270 * @param {string[]} [excludedCategories] 3271 */ 3272 const acceptCategory = (categories, excludedCategories = []) => { 3273 resolveEnabledCategories(categories, excludedCategories); 3274 resolveEnabledServices(); 3275 saveCookiePreferences(); 3276 }; 3277 3278 /** 3279 * Returns true if cookie category is accepted 3280 * @param {string} category 3281 */ 3282 const acceptedCategory = (category) => { 3283 3284 const acceptedCategories = !globalObj._state._invalidConsent 3285 ? globalObj._state._acceptedCategories 3286 : []; 3287 3288 return elContains(acceptedCategories, category); 3289 }; 3290 3291 /** 3292 * Accept one or multiple services under a specific category 3293 * @param {string|string[]} service 3294 * @param {string} category 3295 */ 3296 const acceptService = (service, category) => { 3297 const { _allCategoryNames, _allDefinedServices, } = globalObj._state; 3298 3299 if ( 3300 !service 3301 || !category 3302 || !isString(category) 3303 || !elContains(_allCategoryNames, category) 3304 || getKeys(_allDefinedServices[category]).length === 0 3305 ) { 3306 return false; 3307 } 3308 3309 //{{START: GUI}} 3310 updateModalToggles(service, category); 3311 //{{END: GUI}} 3312 3313 acceptCategory(); 3314 }; 3315 3316 /** 3317 * Returns true if the service in the specified 3318 * category is accepted/enabled 3319 * @param {string} service 3320 * @param {string} category 3321 */ 3322 const acceptedService = (service, category) => { 3323 const acceptedServices = !globalObj._state._invalidConsent 3324 ? globalObj._state._acceptedServices[category] 3325 : []; 3326 3327 return elContains(acceptedServices, service); 3328 }; 3329 3330 /** 3331 * Returns true if cookie was found and has valid value (not an empty string) 3332 * @param {string} cookieName 3333 */ 3334 const validCookie = (cookieName) => getSingleCookie(cookieName, true) !== ''; 3335 3336 /** 3337 * Erase cookies API 3338 * @param {(string|RegExp|(string|RegExp)[])} cookies 3339 * @param {string} [path] 3340 * @param {string} [domain] 3341 */ 3342 const eraseCookies = (cookies, path, domain) => { 3343 let allCookies = []; 3344 3345 /** 3346 * Add cookie to allCookies array if it exists 3347 * @param {string | RegExp} cookieName 3348 */ 3349 const addCookieIfExists = (cookieName) => { 3350 if (isString(cookieName)) { 3351 let name = getSingleCookie(cookieName); 3352 name !== '' && allCookies.push(name); 3353 } else { 3354 allCookies.push(...getAllCookies(cookieName)); 3355 } 3356 }; 3357 3358 if (isArray(cookies)) { 3359 for (let cookie of cookies) { 3360 addCookieIfExists(cookie); 3361 } 3362 } else { 3363 addCookieIfExists(cookies); 3364 } 3365 3366 eraseCookiesHelper(allCookies, path, domain); 3367 }; 3368 3369 //{{START: GUI}} 3370 3371 /** 3372 * Show cookie consent modal 3373 * @param {boolean} [createModal] create modal if it doesn't exist 3374 */ 3375 const show = (createModal) => { 3376 const { _dom, _state } = globalObj; 3377 3378 if (_state._consentModalVisible) 3379 return; 3380 3381 if (!_state._consentModalExists) { 3382 if (createModal) { 3383 createConsentModal(miniAPI, createMainContainer); 3384 } else { 3385 return; 3386 } 3387 } 3388 3389 _state._consentModalVisible = true; 3390 _state._lastFocusedElemBeforeModal = getActiveElement(); 3391 3392 if (_state._disablePageInteraction) 3393 toggleDisableInteraction(true); 3394 3395 focusAfterTransition(_dom._cm, 1); 3396 3397 addClass(_dom._htmlDom, TOGGLE_CONSENT_MODAL_CLASS); 3398 setAttribute(_dom._cm, ARIA_HIDDEN, 'false'); 3399 3400 /** 3401 * Set focus to consentModal 3402 */ 3403 setTimeout(() => { 3404 focus(globalObj._dom._cmDivTabindex); 3405 }, 100); 3406 3407 _log('CookieConsent [TOGGLE]: show consentModal'); 3408
3409 fireEvent(globalObj._customEvents._onModalShow, CONSENT_MODAL_NAME); 3410 }; 3411 3412 /** 3413 * Hide consent modal 3414 */ 3415 const hide = () => { 3416 const { _dom, _state, _customEvents } = globalObj; 3417 3418 if (!_state._consentModalVisible) 3419 return; 3420 3421 _state._consentModalVisible = false; 3422 3423 if (_state._disablePageInteraction) 3424 toggleDisableInteraction(); 3425 3426 /** 3427 * Fix focus restoration to body with Chrome 3428 */ 3429 focus(_dom._focusSpan, true); 3430 3431 removeClass(_dom._htmlDom, TOGGLE_CONSENT_MODAL_CLASS); 3432 setAttribute(_dom._cm, ARIA_HIDDEN, 'true'); 3433 3434 /** 3435 * Restore focus to last focused element 3436 */ 3437 focus(_state._lastFocusedElemBeforeModal); 3438 _state._lastFocusedElemBeforeModal = null; 3439 3440 _log('CookieConsent [TOGGLE]: hide consentModal'); 3441 3442 fireEvent(_customEvents._onModalHide, CONSENT_MODAL_NAME); 3443 }; 3444 3445 /** 3446 * Show preferences modal 3447 */ 3448 const showPreferences = () => { 3449 const state = globalObj._state; 3450 3451 if (state._preferencesModalVisible) 3452 return; 3453 3454 if (!state._preferencesModalExists) 3455 createPreferencesModal(miniAPI, createMainContainer); 3456 3457 state._preferencesModalVisible = true; 3458 3459 // If there is no consent-modal, keep track of the last focused elem. 3460 if (!state._consentModalVisible) { 3461 state._lastFocusedElemBeforeModal = getActiveElement(); 3462 } else { 3463 state._lastFocusedModalElement = getActiveElement(); 3464 } 3465 3466 focusAfterTransition(globalObj._dom._pm, 2); 3467 3468 addClass(globalObj._dom._htmlDom, TOGGLE_PREFERENCES_MODAL_CLASS); 3469 setAttribute(globalObj._dom._pm, ARIA_HIDDEN, 'false'); 3470 3471 /** 3472 * Set focus to preferencesModal 3473 */ 3474 setTimeout(() => { 3475 focus(globalObj._dom._pmDivTabindex); 3476 }, 100); 3477 3478 _log('CookieConsent [TOGGLE]: show preferencesModal'); 3479 3480 fireEvent(globalObj._customEvents._onModalShow, PREFERENCES_MODAL_NAME); 3481 }; 3482 3483 /** 3484 * https://github.com/orestbida/cookieconsent/issues/481 3485 */ 3486 const discardUnsavedPreferences = () => { 3487 const consentIsValid = validConsent(); 3488 const allDefinedCategories = globalObj._state._allDefinedCategories; 3489 const categoryInputs = globalObj._dom._categoryCheckboxInputs; 3490 const serviceInputs = globalObj._dom._serviceCheckboxInputs; 3491 3492 /** 3493 * @param {string} category 3494 */ 3495 const categoryEnabledByDefault = (category) => elContains(globalObj._state._defaultEnabledCategories, category); 3496 3497 for (const category in categoryInputs) { 3498 const isReadOnly = !!allDefinedCategories[category].readOnly; 3499 3500 categoryInputs[category].checked = isReadOnly || (consentIsValid 3501 ? acceptedCategory(category) 3502 : categoryEnabledByDefault(category) 3503 ); 3504 3505 for (const service in serviceInputs[category]) { 3506 serviceInputs[category][service].checked = isReadOnly || (consentIsValid 3507 ? acceptedService(service, category) 3508 : categoryEnabledByDefault(category) 3509 ); 3510 } 3511 } 3512 }; 3513 3514 /** 3515 * Hide preferences modal 3516 */ 3517 const hidePreferences = () => { 3518 const state = globalObj._state; 3519 3520 if (!state._preferencesModalVisible) 3521 return; 3522 3523 state._preferencesModalVisible = false; 3524 3525 discardUnsavedPreferences(); 3526 3527 /** 3528 * Fix focus restoration to body with Chrome 3529 */ 3530 focus(globalObj._dom._pmFocusSpan, true); 3531 3532 removeClass(globalObj._dom._htmlDom, TOGGLE_PREFERENCES_MODAL_CLASS); 3533 setAttribute(globalObj._dom._pm, ARIA_HIDDEN, 'true'); 3534 3535 /** 3536 * If consent modal is visible, focus him (instead of page document) 3537 */ 3538 if (state._consentModalVisible) { 3539 focus(state._lastFocusedModalElement); 3540 state._lastFocusedModalElement = null; 3541 } else { 3542 /** 3543 * Restore focus to last page element which had focu
3543s before modal opening 3544 */ 3545 focus(state._lastFocusedElemBeforeModal); 3546 state._lastFocusedElemBeforeModal = null; 3547 } 3548 3549 _log('CookieConsent [TOGGLE]: hide preferencesModal'); 3550 3551 fireEvent(globalObj._customEvents._onModalHide, PREFERENCES_MODAL_NAME); 3552 }; 3553 3554 var miniAPI = { 3555 show, 3556 hide, 3557 showPreferences, 3558 hidePreferences, 3559 acceptCategory 3560 }; 3561 3562 /** 3563 * Update/change modal's language 3564 * @param {string} lang new language 3565 * @param {boolean} [forceUpdate] update language fields forcefully 3566 * @returns {Promise<boolean>} 3567 */ 3568 const setLanguage = async (newLanguageCode, forceUpdate) => { 3569 if (!getAvailableLanguage(newLanguageCode)) 3570 return false; 3571 3572 const state = globalObj._state; 3573 3574 /** 3575 * Set language only if it differs from current 3576 */ 3577 if (newLanguageCode !== getCurrentLanguageCode() || forceUpdate === true) { 3578 3579 const loaded = await loadTranslationData(newLanguageCode); 3580 3581 if (!loaded) 3582 return false; 3583 3584 setCurrentLanguageCode(newLanguageCode); 3585 3586 if (state._consentModalExists) 3587 createConsentModal(miniAPI, createMainContainer); 3588 3589 if (state._preferencesModalExists) 3590 createPreferencesModal(miniAPI, createMainContainer); 3591 3592 handleRtlLanguage(); 3593 3594 return true; 3595 } 3596 3597 return false; 3598 }; 3599 3600 //{{END: GUI}} 3601 3602 /** 3603 * Retrieve current user preferences (summary) 3604 * @returns {import("./global").UserPreferences} 3605 */ 3606 const getUserPreferences = () => { 3607 const { _acceptType, _acceptedServices } = globalObj._state; 3608 const { accepted, rejected } = getCurrentCategoriesState(); 3609 3610 return deepCopy({ 3611 acceptType: _acceptType, 3612 acceptedCategories: accepted, 3613 rejectedCategories: rejected, 3614 acceptedServices: _acceptedServices, 3615 rejectedServices: retrieveRejectedServices() 3616 }); 3617 }; 3618 3619 /** 3620 * Dynamically load script (append to head) 3621 * @param {string} src 3622 * @param {{[key: string]: string}} [attrs] Custom attributes 3623 * @returns {Promise<boolean>} promise 3624 */ 3625 const loadScript = (src, attrs) => { 3626 /** 3627 * @type {HTMLScriptElement} 3628 */ 3629 let script = document.querySelector('script[src="' + src + '"]'); 3630 3631 return new Promise((resolve) => { 3632 if (script) 3633 return resolve(true); 3634 3635 script = createNode('script'); 3636 3637 /** 3638 * Add custom attributes 3639 */ 3640 if (isObject(attrs)) { 3641 for (const key in attrs) { 3642 setAttribute(script, key, attrs[key]); 3643 } 3644 } 3645 3646 script.onload = () => resolve(true); 3647 script.onerror = () => { 3648 /** 3649 * Remove script from dom if error is thrown 3650 */ 3651 script.remove(); 3652 resolve(false); 3653 }; 3654 3655 script.src = src; 3656 3657 appendChild(document.head, script); 3658 }); 3659 }; 3660 3661 /** 3662 * Save custom data inside cookie 3663 * @param {{ 3664 * value: any, 3665 * mode: string 3666 * }} props 3667 * @returns {boolean} 3668 */
3669 const setCookieData = (props) => { 3670 let newData = props.value, 3671 mode = props.mode, 3672 set = false, 3673 cookieData; 3674 3675 const state = globalObj._state; 3676 3677 /** 3678 * If mode is 'update': 3679 * add/update only the specified props. 3680 */ 3681 if (mode === 'update') { 3682 state._cookieData = cookieData = getCookie('data'); 3683 const sameType = typeof cookieData === typeof newData; 3684 3685 if (sameType && typeof cookieData === 'object') { 3686 !cookieData && (cookieData = {}); 3687 3688 for (let prop in newData) { 3689 if (cookieData[prop] !== newData[prop]) { 3690 cookieData[prop] = newData[prop]; 3691 set = true; 3692 } 3693 } 3694 }else if ((sameType || !cookieData) && cookieData !== newData) { 3695 cookieData = newData; 3696 set = true; 3697 } 3698 } else { 3699 cookieData = newData; 3700 set = true; 3701 } 3702 3703 if (set) { 3704 state._cookieData = cookieData; 3705 state._savedCookieContent.data = cookieData; 3706 setCookie(true); 3707 } 3708 3709 return set; 3710 }; 3711 3712 /** 3713 * Retrieve data from existing cookie 3714 * @param {string} field 3715 * @param {string} [cookieName] 3716 * @returns {any} 3717 */ 3718 const getCookie = (field, cookieName) => { 3719 const cookie = getPluginCookie(cookieName); 3720 3721 return field 3722 ? cookie[field] 3723 : cookie; 3724 }; 3725 3726 /** 3727 * Return configuration object or just one of its fields. 3728 * @param {string} field 3729 * @returns {any} 3730 */ 3731 const getConfig = (field) => { 3732 const config = globalObj._config; 3733 const userConfig = globalObj._state._userConfig; 3734 3735 return field 3736 ? config[field] || userConfig[field] 3737 : {...config, ...userConfig, cookie:{...config.cookie}}; 3738 }; 3739 3740 /** 3741 * Returns true if consent is valid 3742 * @returns {boolean} 3743 */ 3744 const validConsent = () => !globalObj._state._invalidConsent; 3745 3746 const retrieveState = () => { 3747 const state = globalObj._state; 3748 const config = globalObj._config; 3749 3750 const cookieValue = getPluginCookie(); 3751 3752 const { 3753 categories, 3754 services, 3755 consentId, 3756 consentTimestamp, 3757 lastConsentTimestamp, 3758 data, 3759 revision 3760 } = cookieValue; 3761 3762 const validCategories = isArray(categories); 3763 3764 state._savedCookieContent = cookieValue; 3765 state._consentId = consentId; 3766 3767 // If "_consentId" is present => assume that consent was previously given 3768 const validConsentId = !!consentId && isString(consentId); 3769 3770 // Retrieve "_consentTimestamp" 3771 state._consentTimestamp = consentTimestamp; 3772 state._consentTimestamp && (state._consentTimestamp = new Date(consentTimestamp)); 3773 3774 // Retrieve "_lastConsentTimestamp" 3775 state._lastConsentTimestamp = lastConsentTimestamp; 3776 state._lastConsentTimestamp && (state._lastConsentTimestamp = new Date(lastConsentTimestamp)); 3777 3778 // Retrieve "data" 3779 state._cookieData = typeof data !== 'undefined' 3780 ? data 3781 : null; 3782 3783 // If revision is enabled and current value !== saved value inside the cookie => revision is not valid 3784 if (state._revisionEnabled && validConsentId && revision !== config.revision) 3785 state._validRevision = false; 3786 3787 state._invalidConsent = !validConsentId 3788 || !state._validRevision 3789 || !state._consentTimestamp 3790 || !state._lastConsentTimestamp 3791 || !validCategories; 3792 3793 /** 3794 * If localStorage is enabled, also check the stored `expirationTime` 3795 */ 3796 if (config.cookie.useLocalStorage && !state._invalidConsent) { 3797 state._invalidConsent = new Date().getTime() > (cookieValue.expirationTime || 0); 3798 state._invalidConsent && (localStorageManager._removeItem(config.cookie.name)); 3799 } 3800 3801 _log('CookieConsent [STATUS] valid consent:', !state._invalidConsent); 3802 retrieveEnabledCategoriesAndServices(); 3803 3804 /** 3805 * Retrieve last accepted categories from cookie 3806 * and calculate acceptType 3807 */ 3808 if (!state._invalidConsent) { 3809 state._acceptedServices = { 3810 ...state._acceptedServices, 3811 ...services 3812 }; 3813
3814 setAcceptedCategories([ 3815 ...state._readOnlyCategories, 3816 ...categories 3817 ]); 3818 } else { 3819 if (config.mode === OPT_OUT_MODE) { 3820 state._acceptedCategories = [ 3821 ...state._defaultEnabledCategories 3822 ]; 3823 } 3824 } 3825 3826 state._enabledServices = {...state._acceptedServices}; 3827 }; 3828 3829 /** 3830 * Will run once and only if modals do not exist. 3831 * @param {import("./global").UserConfig} userConfig 3832 */ 3833 const run = async (userConfig) => { 3834 const { 3835 _state, 3836 _config, 3837 _customEvents 3838 } = globalObj; 3839 3840 const win = window; 3841 3842 if (!win._ccRun) { 3843 win._ccRun = true; 3844 3845 setConfig(userConfig); 3846 3847 if (_state._botAgentDetected) 3848 return; 3849 3850 retrieveState(); 3851 3852 const consentIsValid = validConsent(); 3853 3854 //{{START: GUI}} 3855 const translationLoaded = await loadTranslationData(); 3856 3857 if (!translationLoaded) 3858 return false; 3859 3860 generateHtml(miniAPI); 3861 3862 if (_config.autoShow && !consentIsValid) 3863 show(true); 3864 //{{END: GUI}} 3865 3866 if (consentIsValid) { 3867 manageExistingScripts(); 3868 return fireEvent(_customEvents._onConsent); 3869 } 3870 3871 if (_config.mode === OPT_OUT_MODE) 3872 manageExistingScripts(_state._defaultEnabledCategories); 3873 } 3874 }; 3875 3876 /** 3877 * Reset cookieconsent. 3878 * @param {boolean} [deleteCookie] Delete plugin's cookie 3879 */ 3880 const reset = (deleteCookie) => { 3881 //{{START: GUI}} 3882 const { _ccMain, _htmlDom } = globalObj._dom; 3883 //{{END: GUI}} 3884 3885 const { name, path, domain, useLocalStorage } = globalObj._config.cookie; 3886 3887 if (deleteCookie) { 3888 useLocalStorage 3889 ? localStorageManager._removeItem(name) 3890 : eraseCookies(name, path, domain); 3891 } 3892 3893 /** 3894 * Remove data-cc event listeners 3895 */ 3896 for (const {_element, _event, _listener} of globalObj._state._dataEventListeners) { 3897 _element.removeEventListener(_event, _listener); 3898 } 3899 3900 //{{START: GUI}} 3901 /** 3902 * Remove main container from DOM 3903 */ 3904 _ccMain && _ccMain.remove(); 3905 3906 /** 3907 * Remove any remaining classes 3908 */ 3909 _htmlDom && _htmlDom.classList.remove( 3910 TOGGLE_DISABLE_INTERACTION_CLASS, 3911 TOGGLE_PREFERENCES_MODAL_CLASS, 3912 TOGGLE_CONSENT_MODAL_CLASS 3913 ); 3914 //{{END: GUI}} 3915 3916 const newGlobal = new GlobalState(); 3917 3918 /** 3919 * Reset all global state props. 3920 */ 3921 for (const key in globalObj) { 3922 globalObj[key] = newGlobal[key]; 3923 } 3924 3925 window._ccRun = false; 3926 }; 3927 3928 exports.acceptCategory = acceptCategory; 3929 exports.acceptService = acceptService; 3930 exports.acceptedCategory = acceptedCategory; 3931 exports.acceptedService = acceptedService; 3932 exports.eraseCookies = eraseCookies; 3933 exports.getConfig = getConfig; 3934 exports.getCookie = getCookie; 3935 exports.getUserPreferences = getUserPreferences; 3936 exports.hide = hide; 3937 exports.hidePreferences = hidePreferences; 3938 exports.loadScript = loadScript; 3939 exports.reset = reset; 3940 exports.run = run; 3941 exports.setCookieData = setCookieData; 3942 exports.setLanguage = setLanguage; 3943 exports.show = show; 3944 exports.showPreferences = showPreferences; 3945 exports.validConsent = validConsent; 3946 exports.validCookie = validCookie; 3947 3948}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.