372 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			372 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import * as shared from './shared.js';
 | 
						||
 | 
						||
import { LitElement, html, css, type TemplateResult } from 'lit';
 | 
						||
import { customElement } from 'lit/decorators.js';
 | 
						||
import { property } from 'lit/decorators/property.js';
 | 
						||
 | 
						||
import * as csInterfaces from '@consent.software/interfaces';
 | 
						||
import * as csWebclient from '@consent.software/webclient';
 | 
						||
import { delayFor } from '@push.rocks/smartdelay';
 | 
						||
 | 
						||
declare global {
 | 
						||
  interface HTMLElementTagNameMap {
 | 
						||
    'consentsoftware-cookieconsent': ConsentsoftwareCookieconsent;
 | 
						||
  }
 | 
						||
}
 | 
						||
 | 
						||
@customElement('consentsoftware-cookieconsent')
 | 
						||
export class ConsentsoftwareCookieconsent extends LitElement {
 | 
						||
  public static demo = () => html`<consentsoftware-cookieconsent></consentsoftware-cookieconsent>`;
 | 
						||
 | 
						||
  public csWebclientInstance = new csWebclient.CsWebclient();
 | 
						||
  public csWebclientRan = false;
 | 
						||
 | 
						||
  @property({ type: String, reflect: true })
 | 
						||
  public theme: 'light' | 'dark' = 'light';
 | 
						||
 | 
						||
  /**
 | 
						||
   * We bind `heightPixels` to a CSS variable (--cookieconsent-height).
 | 
						||
   * Then we can do margin-bottom animations in CSS.
 | 
						||
   */
 | 
						||
  public static styles = css`
 | 
						||
    :host {
 | 
						||
      font-family: ${shared.fontStack};
 | 
						||
      user-select: none;
 | 
						||
      /* Default theme variables */
 | 
						||
      --text-color: #333;
 | 
						||
      --background-color: #eeeeee;
 | 
						||
      --accent-color: #333333;
 | 
						||
      --button-bg: #ffffff;
 | 
						||
      --button-hover-bg: #f2f2f2;
 | 
						||
      --icon-color: #4496f5;
 | 
						||
      --link-color: #333;
 | 
						||
      --padding-sides: 16px;
 | 
						||
    }
 | 
						||
 | 
						||
    :host([theme='dark']) {
 | 
						||
      --text-color: #fff;
 | 
						||
      --background-color: #111;
 | 
						||
      --accent-color: #333333;
 | 
						||
      --button-bg: #252525;
 | 
						||
      --button-hover-bg: #222222;
 | 
						||
      --icon-color: #4496f5;
 | 
						||
      --link-color: #fff;
 | 
						||
    }
 | 
						||
 | 
						||
    :host([theme='light']) {
 | 
						||
      --text-color: #333;
 | 
						||
      --background-color: #eeeeee;
 | 
						||
      --accent-color: #333333;
 | 
						||
      --button-bg: #ffffff;
 | 
						||
      --button-hover-bg: #f2f2f2;
 | 
						||
      --icon-color: #4496f5;
 | 
						||
      --link-color: #333;
 | 
						||
    }
 | 
						||
 | 
						||
    .pageOverlay {
 | 
						||
      position: fixed;
 | 
						||
      top: 0px;
 | 
						||
      bottom: 0px;
 | 
						||
      right: 0px;
 | 
						||
      left: 0px;
 | 
						||
      display: grid;
 | 
						||
      align-items: center;
 | 
						||
      justify-content: center;
 | 
						||
      z-index: 1000; /* standard z-index for fixed elements */
 | 
						||
      background: rgba(255, 255, 255, 0);
 | 
						||
      backdrop-filter: blur(0px);
 | 
						||
      transition: all 0.2s;
 | 
						||
    }
 | 
						||
 | 
						||
    .pageOverlay.shake {
 | 
						||
      background: rgba(0, 0, 0, 0.5) !important;
 | 
						||
      backdrop-filter: blur(20px) !important;
 | 
						||
    }
 | 
						||
 | 
						||
    .modalBox {
 | 
						||
      display: block;
 | 
						||
      color: var(--text-color);
 | 
						||
      background: var(--background-color);
 | 
						||
      box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.6);
 | 
						||
      position: realtive;
 | 
						||
      border: 1px dotted rgba(255, 255, 255, 0.1);
 | 
						||
      border-top: 1px solid var(--accent-color);
 | 
						||
      border-radius: 16px;
 | 
						||
      max-width: 1100px;
 | 
						||
      min-width: calc(100vw / 3);
 | 
						||
      box-sizing: border-box;
 | 
						||
      overflow: hidden;
 | 
						||
      will-change: transform; /* ensure efficient rendering */
 | 
						||
      transition: all 0.3s;
 | 
						||
      transform: scale(0.95);
 | 
						||
      opacity: 0;
 | 
						||
    }
 | 
						||
 | 
						||
    .modalBox.shake {
 | 
						||
      animation: shake 150ms 2 linear;
 | 
						||
      box-shadow: 0px 0px 8px rgba(255, 255, 255, 0.6);
 | 
						||
    }
 | 
						||
 | 
						||
    @keyframes shake {
 | 
						||
      0% {
 | 
						||
        transform: translate(3px, 0);
 | 
						||
      }
 | 
						||
      50% {
 | 
						||
        transform: translate(-3px, 0);
 | 
						||
      }
 | 
						||
      100% {
 | 
						||
        transform: translate(0, 0);
 | 
						||
      }
 | 
						||
    }
 | 
						||
 | 
						||
    /* 
 | 
						||
     * Toggle display based on [show] attribute 
 | 
						||
     * (so if show=false, the banner doesn't show at all).
 | 
						||
     */
 | 
						||
    :host([show='false']) {
 | 
						||
      display: none;
 | 
						||
    }
 | 
						||
    :host([show='true']) {
 | 
						||
      display: block;
 | 
						||
    }
 | 
						||
 | 
						||
    .content {
 | 
						||
      margin: auto;
 | 
						||
    }
 | 
						||
 | 
						||
    .text-container {
 | 
						||
      padding-left: var(--padding-sides);
 | 
						||
      padding-right: var(--padding-sides);
 | 
						||
      display: grid;
 | 
						||
      grid-template-columns: auto;
 | 
						||
    }
 | 
						||
 | 
						||
    .text-container a {
 | 
						||
      color: var(--link-color);
 | 
						||
      text-decoration: none;
 | 
						||
    }
 | 
						||
 | 
						||
    .button-container {
 | 
						||
      padding: var(--padding-sides);
 | 
						||
      display: grid;
 | 
						||
      grid-template-columns: repeat(3, 1fr);
 | 
						||
      gap: 16px;
 | 
						||
    }
 | 
						||
 | 
						||
    .info-container {
 | 
						||
      color: var(--text-color);
 | 
						||
      text-align: center;
 | 
						||
      line-height: 3em;
 | 
						||
      background: rgba(0, 0, 0, 0.1);
 | 
						||
      border-top: 1px dotted rgba(255, 255, 255, 0.1);
 | 
						||
      font-size: 0.8em;
 | 
						||
      color: rgba(255, 255, 255, 0.5);
 | 
						||
    }
 | 
						||
 | 
						||
    .info-container a {
 | 
						||
      text-decoration: underline;
 | 
						||
      color: var(--link-color);
 | 
						||
      transition: color 0.2s;
 | 
						||
    }
 | 
						||
    .info-container a:hover {
 | 
						||
      color: #ffffff;
 | 
						||
    }
 | 
						||
 | 
						||
    .consent-button {
 | 
						||
      border-radius: 3px;
 | 
						||
      background: var(--button-bg);
 | 
						||
      box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
 | 
						||
      padding: 10px;
 | 
						||
      line-height: 30px;
 | 
						||
      text-align: center;
 | 
						||
      cursor: pointer;
 | 
						||
      transition: all 0.2s;
 | 
						||
    }
 | 
						||
 | 
						||
    .consent-button:hover {
 | 
						||
      background: var(--button-hover-bg);
 | 
						||
    }
 | 
						||
  `;
 | 
						||
 | 
						||
  constructor() {
 | 
						||
    super();
 | 
						||
    this.setAttribute('show', 'false');
 | 
						||
  }
 | 
						||
 | 
						||
  public render(): TemplateResult {
 | 
						||
    return html`
 | 
						||
      <div class="pageOverlay" @click=${this.pageOverlayClick}>
 | 
						||
        <div class="modalBox">
 | 
						||
          <div class="content">
 | 
						||
            <consentsoftware-header></consentsoftware-header>
 | 
						||
            <consentsoftware-tabs></consentsoftware-tabs>
 | 
						||
            <div class="text-container" style="padding: 16px;">
 | 
						||
              <div class="toptext">
 | 
						||
                This page uses cookies. Please review our
 | 
						||
                <a href="https://lossless.gmbh/cookie" target="_blank">cookie policy</a>
 | 
						||
                and choose which cookie level you are willing to accept.
 | 
						||
              </div>
 | 
						||
            </div>
 | 
						||
            <!-- <div class="button-container">
 | 
						||
              <div
 | 
						||
                class="consent-button"
 | 
						||
                @click=${(event: MouseEvent) =>
 | 
						||
              this.handleConsentButtonClick(event, ['functional'])}
 | 
						||
              >
 | 
						||
                Functional cookies
 | 
						||
              </div>
 | 
						||
              <div
 | 
						||
                class="consent-button"
 | 
						||
                @click=${(event: MouseEvent) =>
 | 
						||
              this.handleConsentButtonClick(event, ['functional', 'analytics'])}
 | 
						||
              >
 | 
						||
                Analytics cookies
 | 
						||
              </div>
 | 
						||
              <div
 | 
						||
                class="consent-button"
 | 
						||
                @click=${(event: MouseEvent) =>
 | 
						||
              this.handleConsentButtonClick(event, ['functional', 'analytics', 'marketing'])}
 | 
						||
              >
 | 
						||
                Marketing cookies
 | 
						||
              </div>
 | 
						||
              <div
 | 
						||
                class="consent-button"
 | 
						||
                @click=${(event: MouseEvent) =>
 | 
						||
              this.handleConsentButtonClick(event, ['functional', 'analytics', 'marketing', 'all'])}
 | 
						||
              >
 | 
						||
                All cookies
 | 
						||
              </div>
 | 
						||
            </div> -->
 | 
						||
            <consentsoftware-mainselection></consentsoftware-mainselection>
 | 
						||
            <div class="button-container">
 | 
						||
              <div
 | 
						||
                class="consent-button"
 | 
						||
                @click=${(event: MouseEvent) =>
 | 
						||
                  this.handleConsentButtonClick(event, ['functional'])}
 | 
						||
              >
 | 
						||
                Deny
 | 
						||
              </div>
 | 
						||
              <div
 | 
						||
                class="consent-button"
 | 
						||
                @click=${(event: MouseEvent) =>
 | 
						||
                  this.handleConsentButtonClick(event, ['functional', 'analytics'])}
 | 
						||
              >
 | 
						||
                Accept selection
 | 
						||
              </div>
 | 
						||
              <div
 | 
						||
                class="consent-button"
 | 
						||
                @click=${(event: MouseEvent) =>
 | 
						||
                  this.handleConsentButtonClick(event, ['functional', 'analytics', 'marketing'])}
 | 
						||
              >
 | 
						||
                Accept all cookies
 | 
						||
              </div>
 | 
						||
            </div>
 | 
						||
            <div class="info-container">
 | 
						||
              consent management powered by
 | 
						||
              <a href="https://consent.software">consent.software</a>
 | 
						||
              (Open Source)
 | 
						||
            </div>
 | 
						||
          </div>
 | 
						||
        </div>
 | 
						||
      </div>
 | 
						||
    `;
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Called when the element is inserted into the DOM.
 | 
						||
   */
 | 
						||
  public async connectedCallback() {
 | 
						||
    super.connectedCallback();
 | 
						||
    this.updateTheme();
 | 
						||
 | 
						||
    const cookieLevel = await this.csWebclientInstance.getCookieLevels();
 | 
						||
    if (!cookieLevel) {
 | 
						||
      this.setAttribute('show', 'true');
 | 
						||
      requestAnimationFrame(async () => {
 | 
						||
        await this.updated();
 | 
						||
        const pageOverlay: HTMLDivElement = this.shadowRoot?.querySelector('.pageOverlay');
 | 
						||
        if (pageOverlay) {
 | 
						||
          pageOverlay.style.background = 'rgba(255,255,255, 0.1)';
 | 
						||
          pageOverlay.style.backdropFilter = 'blur(0px)';
 | 
						||
        }
 | 
						||
        const modalBox: HTMLDivElement = this.shadowRoot?.querySelector('.modalBox');
 | 
						||
        if (modalBox) {
 | 
						||
          modalBox.style.transform = `scale(1)`;
 | 
						||
          modalBox.style.opacity = '1';
 | 
						||
        }
 | 
						||
      });
 | 
						||
    } else {
 | 
						||
      this.setAttribute('show', 'false');
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Called after the first render of the component.
 | 
						||
   * We measure the actual height of the banner and update the CSS variable.
 | 
						||
   */
 | 
						||
  public async firstUpdated() {}
 | 
						||
 | 
						||
  /**
 | 
						||
   * Called whenever the element is updated or re-rendered.
 | 
						||
   */
 | 
						||
  public async updated() {
 | 
						||
    console.log(`The height of the cookie banner is ${this.shadowRoot?.host?.clientHeight}px`);
 | 
						||
    const acceptedCookieLevels = await this.csWebclientInstance.getCookieLevels();
 | 
						||
    if (!this.csWebclientRan && acceptedCookieLevels) {
 | 
						||
      this.csWebclientRan = true;
 | 
						||
      await this.csWebclientInstance.getAndRunConsentTuples();
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Sets the user’s chosen cookie level(s) and hides the banner.
 | 
						||
   */
 | 
						||
  private async handleConsentButtonClick(
 | 
						||
    event: MouseEvent,
 | 
						||
    levelsArg: csInterfaces.TCookieLevel[]
 | 
						||
  ) {
 | 
						||
    console.log(`Set level to ${levelsArg}`);
 | 
						||
    const pageOverlay: HTMLDivElement = this.shadowRoot?.querySelector('.pageOverlay');
 | 
						||
    if (pageOverlay) {
 | 
						||
      pageOverlay.style.background = 'rgba(255,255,255, 0)';
 | 
						||
      pageOverlay.style.backdropFilter = 'blur(0px)';
 | 
						||
    }
 | 
						||
    const modalBox: HTMLDivElement = this.shadowRoot?.querySelector('.modalBox');
 | 
						||
    if (modalBox) {
 | 
						||
      modalBox.style.transform = `scale(0.95)`;
 | 
						||
      modalBox.style.opacity = '0';
 | 
						||
    }
 | 
						||
    await this.csWebclientInstance.setCookieLevels(levelsArg);
 | 
						||
    await delayFor(300);
 | 
						||
    this.setAttribute('show', 'false');
 | 
						||
    // After user selection, re-check for any required scripts to run
 | 
						||
    this.updated();
 | 
						||
  }
 | 
						||
 | 
						||
  private async pageOverlayClick(e: MouseEvent) {
 | 
						||
    if (e.target === e.currentTarget) {
 | 
						||
      const pageOverlay: HTMLDivElement = this.shadowRoot?.querySelector('.pageOverlay');
 | 
						||
      const modalBox: HTMLDivElement = this.shadowRoot?.querySelector('.modalBox');
 | 
						||
      if (pageOverlay && modalBox) {
 | 
						||
        pageOverlay.classList.add('shake');
 | 
						||
        modalBox.classList.add('shake');
 | 
						||
        await delayFor(2000);
 | 
						||
        pageOverlay.classList.remove('shake');
 | 
						||
        modalBox.classList.remove('shake');
 | 
						||
      }
 | 
						||
    }
 | 
						||
  }
 | 
						||
 | 
						||
  /**
 | 
						||
   * Dynamically switches the theme between light/dark,
 | 
						||
   * respecting `prefers-color-scheme` by default.
 | 
						||
   */
 | 
						||
  private updateTheme() {
 | 
						||
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
 | 
						||
    this.theme = prefersDark ? 'dark' : 'light';
 | 
						||
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
 | 
						||
      this.theme = e.matches ? 'dark' : 'light';
 | 
						||
    });
 | 
						||
  }
 | 
						||
}
 |