1/** 2 * @description CookieBar IMPnet s.r.o. 3 * @author Daniel Žůrek 4 * @version 2.0 5 */ 6class CookieBar 7{ 8 9 #UI; 10 #logged; 11 12 constructor() 13 { 14 window.cookieBar = this; 15 this.#UI = (document.currentScript.src).split('/').slice(0,-1).join('/') + '/' + 'Api/'; 16 this.#logged = false; 17 18 this.#initConsent(); 19 // Update posÃláme až když návÅ¡tÄvnÃk opravdu rozhodl. Stará komponenta ho pÅed 20 // rozhodnutÃm neposÃlala vůbec (consent.latte, n:if="!$show"), takže okno 21 // wait_for_update zůstalo otevÅené. Okamžitý update s "denied" ho zavÅe a GTM 22 // ho Äte jako rozhodnutà uživatele â marketingu tÃm mizà consent mode modelovánÃ. 23 if(this.#hasDecision()) { 24 this.#updateConsent(); 25 } 26 27 document.addEventListener('DOMContentLoaded', () => { 28 29 this.#initEvents(); 30 31 this.#renderCss('default').then( 32 data => this.#message(data) 33 ).catch( 34 error => this.#error(error) 35 ); 36 37 let operational = new CookieBarStorage('operational'); 38 let marketing = new CookieBarStorage('marketing'); 39 let analytic = new CookieBarStorage('analytic'); 40 41 if(operational.value === null || marketing.value === null || analytic.value === null) { 42 this.#renderCookieBar().then( 43 data => this.#message(data) 44 ).catch( 45 error => this.#error(error) 46 ); 47 } else { 48 this.#renderButton().then( 49 data => this.#message(data) 50 ).catch( 51 error => this.#error(error) 52 ); 53 } 54 55 }); 56 } 57 58 #initEvents() 59 { 60 let CookieBar = this; 61 62 // Rozbalovánà akordeonu ÅeÅ¡Ãme v capture fázi a cÃlový stav vynutÃme až po dobÄhnutà 63 // ostatnÃch posluchaÄů. NÄkteré weby majà v theme JS vlastnà jQuery handler, který dÄlá 64 // to samé (toggleClass('active')); dva toggly na jeden klik se vyrušà a liÅ¡ta se pak 65 // tváÅÃ, že na ni nejde kliknout. Takhle je jedno, kolik dalÅ¡Ãch pÅepÃnaÄů tam visÃ. 66 document.addEventListener('click', function(event){ 67 let toggle = event.target.closest('[data-cookie-bar-accordion-toggle]'); 68 if(!toggle) { 69 return; 70 } 71 72 let accordion = toggle.closest('[data-cookie-bar-accordion]'); 73 if(!accordion) { 74 return; 75 } 76 77 let target = !accordion.classList.contains('active'); 78 setTimeout(() => accordion.classList.toggle('active', target), 0); 79 }, true); 80 81 document.addEventListener('click', function(event){ 82 let setup = event.target.closest('[data-cookie-bar-setup]'); 83 if(setup) { 84 event.preventDefault(); 85 let type = setup.getAttribute('data-cookie-bar-setup'); 86 let inputs = document.querySelectorAll('input[data-cookie-bar-input]'); 87 inputs.forEach(el => { 88 let name = el.getAttribute('data-cookie-bar-input'); 89 let value = type === 'enable' || (type === 'select' && el.checked) ? 'enable' : 'disable'; 90 let cookieStorage = new CookieBarStorage(name); 91 cookieStorage.value = value; 92 cookieStorage.expires = 60 * 60 * 24 * 365; 93 cookieStorage.save(); 94 }); 95 CookieBar.#updateConsent(); 96 CookieBar.#redraw(); 97 CookieBar.#renderButton().then( 98 data => CookieBar.#message(data) 99 ).catch( 100 error => CookieBar.#error(error) 101 ); 102 } 103 104 let addGroup = event.target.closest('[data-cookie-bar-settings-add-group]'); 105 if(addGroup) { 106 event.preventDefault(); 107 108 let xhttpParams = { 109 'action': 'html', 110 'data': 'terminalGroup' 111 }; 112 113 CookieBar.#xhttp(xhttpParams).then( 114 data => { 115 let groupTemplate = data.content; 116 let group = {'name': '------', 'url': '', 'index': Math.floor(Math.random() * 1000)}; 117 let groupHtml = CookieBar.#interpolate(groupTemplate, group); 118 addGroup.insertAdjacentHTML('afterend', groupHtml); 119 CookieBar.#renderImages(); 120 } 121 ).catch( 122 error => CookieBar.#error(error) 123 ); 124 } 125 126 let addItem = event.target.closest('[data-cookie-bar-settings-add-item]'); 127 if(addItem) { 128 event.preventDefault(); 129 130 let xhttpParams = { 131 'action': 'html', 132 'data': 'terminalItem' 133 }; 134 135 CookieBar.#xhttp(xhttpParams).then( 136 data => { 137 let itemTemplate = data.content; 138 let item = {'name': '------', 'description': '', 'expire': '', 'type': '', 'group': {'index': Math.floor(Math.random() * 1000)}, 'index': Math.floor(Math.random() * 1000)}; 139 let itemHtml = CookieBar.#interpolate(itemTemplate, item); 140 addItem.insertAdjacentHTML('afterend', itemHtml); 141 CookieBar.#renderImages(); 142 } 143 ).catch( 144 error => CookieBar.#error(error) 145 ); 146 } 147 148 let removeGroup = event.target.closest('[data-cookie-bar-settings-remove-group]'); 149 if(removeGroup) { 150 event.preventDefault(); 151 removeGroup.closest('[data-cookie-bar-settings-group]').remove(); 152 } 153 154 let removeItem = event.target.closest('[data-cookie-bar-settings-remove-item]'); 155 if(removeItem) { 156 event.preventDefault(); 157 removeItem.closest('[data-cookie-bar-settings-item]').remove(); 158 } 159 }); 160 161 document.addEventListener('submit', function(event){ 162 if(event.target.hasAttribute('data-cookie-bar-login')) { 163 event.preventDefault(); 164 let formData = new FormData(event.target); 165 CookieBar.#getLogin(formData.get('username'), formData.get('password')).then( 166 data => { 167 CookieBar.#logged = true; 168 CookieBar.#message(data); 169 CookieBar.#renderTerminal(); 170 } 171 ).catch( 172 error => CookieBar.#error(error) 173 ); 174 } 175 176 if(event.target.hasAttribute('data-cookie-bar-terminal')) { 177 event.preventDefault(); 178 let formData = new FormData(event.target); 179 CookieBar.#setSettings(formData).then( 180 data => CookieBar.#message(data) 181 ).catch( 182 error => CookieBar.#error(error) 183 ); 184 } 185 186 if(event.target.hasAttribute('data-cookie-bar-button')) { 187 event.preventDefault(); 188 CookieBar.#renderCookieBar(); 189 190 } 191 }); 192 } 193 194 /** 195 * @param {object} params 196 */ 197 #xhttp(params) 198 { 199 return new Promise((resolve, reject) => { 200 let xhttp = new XMLHttpRequest(); 201 xhttp.open('POST', this.#UI, true); 202 xhttp.setRequestHeader('Content-type', 'application/json'); 203 xhttp.ontimeout = () => reject('Request timed out'); 204 xhttp.onerror = () => reject('Request error: ' + xhttp.status); 205 xhttp.onload = () => { 206 if(xhttp.readyState === 4) { 207 if (xhttp.status === 200) { 208 try { 209 resolve(JSON.parse(xhttp.responseText)); 210 } catch(error) { 211 reject('Response error: ' + error); 212 } 213 } else { 214 reject('Response error: ' + xhttp.status); 215 } 216 } 217 }; 218 xhttp.send(JSON.stringify(params)); 219 }); 220 } 221 222 /** 223 * @param {string} message 224 */ 225 #error(message) 226 {
227 console.error('%cCookie Bar%c' + message, 'background: #f0506e; color: #fff'); 228 } 229 230 /** 231 * @param {string} message 232 */ 233 #message(message) 234 { 235 console.info('%cCookie Bar%c' + message, 'background: #41ba55; color: #fff'); 236 } 237 238 /** 239 * 240 * @param {string} template 241 * @param {object} object 242 */ 243 #interpolate(template, object) 244 { 245 return (new Function(...Object.keys(object), 'return `' + template + '`;'))(...Object.values(object)); 246 } 247 248 /** 249 * @param {FormData} formData 250 */ 251 #setSettings(formData) 252 { 253 let data = []; 254 let groupIndex = 0; 255 let itemIndex = 0; 256 for(let [key, value] of formData.entries()) { 257 if(key === 'sklik') { 258 if(value !== '') { 259 groupIndex = data.length; 260 data[groupIndex] = {'sklik': value}; 261 } 262 continue; 263 } 264 265 let [,name,,isItem] = key.match(/([^\]]*)\[(\d+)?\](\[(\d+)?\])?/); 266 267 if(name === 'name' && isItem === undefined) { 268 groupIndex = data.length; 269 itemIndex = 0; 270 } 271 272 data[groupIndex] = data[groupIndex] || {}; 273 274 if(name === 'name' && isItem !== undefined) { 275 data[groupIndex].items = data[groupIndex].items || []; 276 itemIndex = data[groupIndex].items.length 277 } 278 279 if(isItem !== undefined) { 280 data[groupIndex].items[itemIndex] = data[groupIndex].items[itemIndex] || {}; 281 data[groupIndex].items[itemIndex][name] = value; 282 } else { 283 data[groupIndex][name] = value; 284 } 285 } 286 287 return new Promise((resolve, reject) => { 288 let xhttpParams = { 289 'action': 'settings', 290 'data': JSON.stringify(data) 291 }; 292 293 this.#xhttp(xhttpParams).then( 294 () => resolve('Settings saved') 295 ).catch( 296 error => reject(error) 297 ); 298 }); 299 } 300 301 #setDataLayer() 302 { 303 window.dataLayer = window.dataLayer || []; 304 window.dataLayer.push(arguments); 305 } 306 307 /** 308 * @param {bool} consent 309 */ 310 #setSklik(consent) 311 { 312 this.#getSettings().then(groups => { 313 groups.forEach((group) => { 314 if(typeof group['sklik'] === 'undefined') { 315 return; 316 } 317 318 if(typeof window.rc === 'undefined' || typeof window.sznIVA === 'undefined' || typeof window.sznIVA.IS === 'undefined') { 319 this.#error('Missing implements Sklik retargeting code.'); 320 return; 321 } 322 323 window.sznIVA.IS.updateIdentities({ 324 eid: null 325 }); 326 327 window.rc.retargetingHit({ 328 rtgId: group['sklik'], 329 consent: consent ? 1 : 0, 330 }); 331 }); 332 }); 333 } 334 335 #initConsent() 336 { 337 this.#setDataLayer( 338 'consent', 339 'default', 340 { 341 // Technická a bezpeÄnostnà kategorie je vždy povolená (nejde vypnout), 342 // stará liÅ¡ta je proto grantovala už v defaultu â držÃme stejnÄ. 343 'functionality_storage': 'granted', 344 'security_storage': 'granted', 345 'personalization_storage': 'denied', 346 'ad_storage': 'denied', 347 'analytics_storage': 'denied', 348 'ad_user_data': 'denied', 349 'ad_personalization': 'denied', 350 'wait_for_update': 2000, 351 } 352 ); 353 354 this.#setSklik(false); 355 } 356 357 /** 358 * Rozhodl už návÅ¡tÄvnÃk o souhlasu? (vÅ¡echny tÅi kategorie majà uloženou hodnotu) 359 * 360 * @return {boolean} 361 */ 362 #hasDecision() 363 { 364 return ['operational', 'marketing', 'analytic'] 365 .every(name => new CookieBarStorage(name).value !== null); 366 } 367 368 #updateConsent() 369 { 370 let operational = new CookieBarStorage('operational'); 371 let marketing = new CookieBarStorage('marketing'); 372 let analytic = new CookieBarStorage('analytic'); 373 374 this.#setDataLayer( 375 'consent', 376 'update', 377 { 378 'functionality_storage': 'granted', 379 'security_storage': 'granted', 380 'personalization_storage': operational.value === 'enable' ? 'granted' : 'denied', 381 'ad_storage': marketing.value === 'enable' ? 'granted' : 'denied', 382 'ad_personalization': marketing.value === 'enable' ? 'granted' : 'denied', 383 'ad_user_data': marketing.value === 'enable' ? 'granted' : 'denied', 384 'analytics_storage': analytic.value === 'enable' ? 'granted' : 'denied', 385 } 386 ); 387 388 this.#setSklik(marketing.value === 'enable'); 389 } 390 391 #getSettings() 392 { 393 return new Promise((resolve, reject) => { 394 let xhttpParams = { 395 'action': 'settings', 396 }; 397 398 this.#xhttp(xhttpParams).then( 399 data => resolve(data) 400 ).catch( 401 error => reject(error) 402 ); 403 }); 404 } 405 406 /** 407 * @param {string} username 408 * @param {string} password 409 */ 410 #getLogin(username, password) 411 { 412 return new Promise((resolve, reject) => { 413 let xhttpParams = { 414 'action': 'login', 415 'data': username + ':' + password 416 }; 417 418 this.#xhttp(xhttpParams).then( 419 data => resolve('Logged in')
420 ).catch( 421 error => reject(error) 422 ); 423 }); 424 } 425 426 /** 427 * @param {string} filename 428 */ 429 #getImage(filename) 430 { 431 return new Promise((resolve, reject) => { 432 let xhttpParams = { 433 'action': 'image', 434 'data': filename 435 }; 436 437 this.#xhttp(xhttpParams).then( 438 data => resolve(data) 439 ).catch( 440 error => reject(error) 441 ); 442 }); 443 } 444 445 #renderlogin() 446 { 447 return new Promise((resolve, reject) => { 448 let xhttpParams = { 449 'action': 'html', 450 'data': 'login' 451 }; 452 453 this.#renderCss('terminal').then( 454 data => this.#message(data) 455 ).catch( 456 error => this.#error(error) 457 ); 458 459 this.#xhttp(xhttpParams).then( 460 data => { 461 this.#redraw(); 462 document.body.insertAdjacentHTML('beforeend', data.content); 463 this.#renderImages(); 464 resolve('Login loaded'); 465 } 466 ).catch( 467 error => reject(error) 468 ); 469 }); 470 } 471 472 /** 473 * @param {string} filename 474 */ 475 #renderCss(filename) 476 { 477 return new Promise((resolve, reject) => { 478 let xhttpParams = { 479 'action': 'css', 480 'data': filename, 481 }; 482 483 this.#xhttp(xhttpParams).then( 484 data => { 485 let style = document.createElement('style'); 486 style.textContent = data.content; 487 document.head.insertAdjacentElement('beforeend', style); 488 resolve('CSS loaded') 489 } 490 ).catch( 491 error => reject(error) 492 ); 493 }); 494 } 495 496 #renderImages() 497 { 498 let images = document.querySelectorAll('[data-cookie-bar-image]'); 499 images.forEach(el => { 500 this.#getImage( el.getAttribute('data-cookie-bar-image') ).then( 501 data => { 502 el.src = data.content; 503 el.removeAttribute('data-cookie-bar-image'); 504 this.#message('Image loaded'); 505 } 506 ); 507 }); 508 } 509 510 #renderButton() 511 { 512 return new Promise((resolve, reject) => { 513 let xhttpParams = { 514 'action': 'html', 515 'data': 'button' 516 }; 517 518 this.#xhttp(xhttpParams).then( 519 data => { 520 this.#redraw(); 521 document.body.insertAdjacentHTML('beforeend', data.content); 522 this.#renderImages(); 523 resolve('Button loaded'); 524 } 525 ).catch( 526 error => reject( error ) 527 ); 528 }); 529 } 530 531 #renderCookieBar() 532 { 533 return new Promise((resolve, reject) => { 534 let xhttpParams = { 535 'action': 'html', 536 'data': 'default' 537 }; 538 539 this.#xhttp(xhttpParams).then( 540 data => { 541 this.#redraw(); 542 document.body.insertAdjacentHTML('beforeend', data.content); 543 544 let checkboxes = document.querySelectorAll('[data-cookie-bar-input]'); 545 checkboxes.forEach(el => { 546 let name = el.getAttribute('data-cookie-bar-input'); 547 let cookieStorage = new CookieBarStorage(name); 548 if(cookieStorage.value === 'enable') { 549 el.setAttribute('checked', ''); 550 } 551 }); 552 553 let xhttpParams = { 554 'action': 'html', 555 'data': 'defaultGroup' 556 }; 557 this.#xhttp(xhttpParams).then( 558 data => { 559 let groupTemplate = data.content; 560 561 let xhttpParams = { 562 'action': 'html', 563 'data': 'defaultItem' 564 }; 565 566 this.#xhttp(xhttpParams).then( 567 data => { 568 let itemTemplate = data.content; 569 570 this.#getSettings().then( 571 groups => { 572 let divs = document.querySelectorAll('[data-cookie-bar-cookies-group]'); 573 divs.forEach(el => { 574 let type = el.getAttribute('data-cookie-bar-cookies-group'); 575 let divCount = document.querySelector('[data-cookie-bar-cookies-group-count="' + type + '"]'); 576 let groupCount = 0; 577 578 groups.forEach((group) => { 579 if(typeof group[type] === 'undefined') { 580 return; 581 } 582 groupCount++; 583 584 group.renderItems = ''; 585 if(group.items !== undefined) { 586 group.items.forEach(item => { 587 let itemHtml = this.#interpolate(itemTemplate, item); 588 group.renderItems += ' ' + itemHtml; 589 }); 590 } 591 592 let groupHtml = this.#interpolate(groupTemplate, group); 593 el.insertAdjacentHTML('beforeend', groupHtml); 594 }); 595 divCount.textContent = groupCount ? ' - ' + groupCount : '' 596 }); 597 598 this.#renderImages(); 599 } 600 ).catch( 601 error => this.#error(error) 602 ); 603 } 604 ).catch( 605 error => this.#error(error) 606 ); 607 } 608 ).catch( 609 error => this.#error(error) 610 ); 611 this.#renderImages(); 612 resolve('Default loaded'); 613 }
614 ).catch( 615 error => reject( error ) 616 ); 617 }); 618 } 619 620 621 622 #renderSettings() 623 { 624 let div = document.querySelector('[data-cookie-bar-settings]'); 625 626 let xhttpParams = { 627 'action': 'html', 628 'data': 'terminalGroup' 629 }; 630 631 this.#xhttp(xhttpParams).then( 632 data => { 633 let groupTemplate = data.content; 634 635 let xhttpParams = { 636 'action': 'html', 637 'data': 'terminalItem' 638 }; 639 640 this.#xhttp(xhttpParams).then( 641 data => { 642 let itemTemplate = data.content; 643 644 this.#getSettings().then( 645 groups => { 646 groups.forEach((group, groupIndex) => { 647 if(typeof group['sklik'] !== 'undefined') { 648 document.querySelector('input[name="sklik"]').value = group['sklik']; 649 return; 650 } 651 652 group.index = groupIndex; 653 let groupHtml = this.#interpolate(groupTemplate, group); 654 div.insertAdjacentHTML('beforeend', groupHtml); 655 656 if(group.items === undefined) { 657 return; 658 } 659 660 group.items.forEach((item, itemIndex) => { 661 item.group = group; 662 item.index = itemIndex; 663 let groupItemHtml = this.#interpolate(itemTemplate, item); 664 div.querySelector('[data-cookie-bar-settings-group-items]').insertAdjacentHTML('beforeend', groupItemHtml); 665 }); 666 667 this.#renderImages(); 668 }); 669 } 670 ).catch( 671 error => this.#error(error) 672 ); 673 } 674 ).catch( 675 error => this.#error(error) 676 ); 677 } 678 ).catch( 679 error => this.#error(error) 680 ); 681 } 682 683 #renderTerminal() 684 { 685 return new Promise((resolve, reject) => { 686 let xhttpParams = { 687 'action': 'html', 688 'data': 'terminal' 689 }; 690 691 this.#xhttp(xhttpParams).then(data => { 692 this.#redraw(); 693 document.body.insertAdjacentHTML('beforeend', data.content); 694 this.#renderSettings(); 695 696 this.#renderImages(); 697 resolve('Terminal loaded'); 698 }) 699 .catch( 700 error => reject(error) 701 ); 702 }); 703 } 704 705 #redraw() 706 { 707 let elements = document.querySelectorAll('[data-cookie-bar-modal],[data-cookie-bar-button]'); 708 elements.forEach(element => { 709 element.remove(); 710 }); 711 } 712 713 get terminal() 714 { 715 if(!this.#logged) { 716 this.#renderlogin().then( 717 data => this.#message(data) 718 ).catch( 719 error => this.#error(error) 720 ); 721 } else { 722 this.#renderTerminal().then( 723 data => this.#message(data) 724 ).catch( 725 error => this.#error(error) 726 ); 727 } 728 729 return null; 730 } 731 732} 733 734class CookieBarStorage 735{ 736 737 /** 738 * @type {string} 739 */ 740 #name; 741 742 /** 743 * @type {int} 744 */ 745 #expires = 0; 746 747 /** 748 * @type {string} 749 */ 750 #prefix; 751 752 /** 753 * @param {string} name 754 */ 755 constructor(name) 756 { 757 this.#prefix = `_${this.constructor.name}_`; 758 this.#name = name; 759 let match = document.cookie.match(RegExp(this.#prefix + this.#name + '=([^;]*)')); 760 this.value = match ? decodeURIComponent(match[1]) : null; 761 } 762 763 /** 764 * @param {int} seconds 765 */ 766 set expires(seconds) 767 { 768 this.#expires = parseInt(seconds); 769 } 770 771 save() 772 { 773 let date = new Date(); 774 date.setTime(date.getTime() + this.#expires * 1000); 775 776 let name = this.#prefix + this.#name; 777 let value = encodeURIComponent(this.value); 778 let expires = date.toUTCString(); 779 780 document.cookie = `${name}=${value}; expires=${expires}; path=/`; 781 782 // Legacy cookie pro serverovou Äást Brave CMS. Äte ji vkládánà skriptů z administrace 783 // (App/FrontendModule/Components/Scripts/Scripts.php pÅes CookieBar::getCookie) a 784 // serverové mÄÅenà CAPI/sGTM (App/Domain/Tracking/Consent/ConsentReader). Obojà zná 785 // jen jméno `cookie-bar-<kategorie>` s hodnotou 1/0 â bez nà by marketingové skripty 786 // a server-side eventy po pÅechodu na tuhle liÅ¡tu tiÅ¡e pÅestaly chodit. 787 let legacyValue = this.value === 'enable' ? '1' : '0'; 788 document.cookie = `cookie-bar-${this.#name}=${legacyValue}; expires=${expires}; path=/`; 789 } 790 791} 792 793new CookieBar();
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.