Add headless web components for cookie banner UI
Introduce Shadow DOM-based custom elements that let customers build their own cookie banner and preference panel while the SDK validates structural compliance and auto-renders categories/cookies from config. Components: probo-cookie-banner (root), probo-banner, probo-accept-button, probo-reject-button, probo-customize-button, probo-preference-panel, probo-category-list, probo-category, probo-category-toggle, probo-cookie-list, probo-cookie, probo-save-button, probo-settings-button. Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
72
packages/cookie-banner/src/components/banner.ts
Normal file
72
packages/cookie-banner/src/components/banner.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { ProboElement } from "./base";
|
||||||
|
import type { ProboRootElement } from "./base";
|
||||||
|
import type { ProboCookieBannerRoot } from "./cookie-banner-root";
|
||||||
|
|
||||||
|
const REQUIRED_BUTTONS = [
|
||||||
|
"probo-accept-button",
|
||||||
|
"probo-reject-button",
|
||||||
|
"probo-customize-button",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export class ProboBanner extends ProboElement {
|
||||||
|
private root: ProboRootElement | null = null;
|
||||||
|
private onStateChange = (e: Event): void => {
|
||||||
|
const { state } = (e as CustomEvent).detail;
|
||||||
|
this.hidden = state !== "banner";
|
||||||
|
};
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.shadow.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host { display: block; }
|
||||||
|
:host([hidden]) { display: none; }
|
||||||
|
</style>
|
||||||
|
<slot></slot>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.hidden = true;
|
||||||
|
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||||
|
|
||||||
|
if (this.root) {
|
||||||
|
this.root.addEventListener("probo-state", this.onStateChange);
|
||||||
|
if (this.root.state === "banner") {
|
||||||
|
this.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduleValidation(() => this.validate());
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
if (this.root) {
|
||||||
|
this.root.removeEventListener("probo-state", this.onStateChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private validate(): void {
|
||||||
|
const missing: string[] = [];
|
||||||
|
for (const tag of REQUIRED_BUTTONS) {
|
||||||
|
if (!this.querySelector(tag)) {
|
||||||
|
missing.push(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (missing.length > 0) {
|
||||||
|
this.warn(`<probo-banner> is missing required children: ${missing.join(", ")}`);
|
||||||
|
this.emitValidation(missing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
73
packages/cookie-banner/src/components/base.ts
Normal file
73
packages/cookie-banner/src/components/base.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import type { CookieBannerClient, BannerConfig } from "../client";
|
||||||
|
|
||||||
|
export type ProboState = "loading" | "banner" | "panel" | "hidden";
|
||||||
|
|
||||||
|
export interface ConsentDraft {
|
||||||
|
[category: string]: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProboElement extends HTMLElement {
|
||||||
|
protected shadow: ShadowRoot;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.shadow = this.attachShadow({ mode: "open" });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected findAncestor<T extends HTMLElement>(tagName: string): T | null {
|
||||||
|
let node: Node | null = this as Node;
|
||||||
|
while (node) {
|
||||||
|
const root = node.getRootNode();
|
||||||
|
if (root instanceof ShadowRoot) {
|
||||||
|
node = root.host;
|
||||||
|
} else {
|
||||||
|
node = (node as HTMLElement).parentElement;
|
||||||
|
}
|
||||||
|
if (node instanceof HTMLElement && node.tagName.toLowerCase() === tagName) {
|
||||||
|
return node as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected scheduleValidation(fn: () => void): void {
|
||||||
|
queueMicrotask(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected warn(message: string): void {
|
||||||
|
console.warn(`[probo] ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected emitValidation(missing: string[]): void {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("probo-validation", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: { missing },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProboRootElement extends ProboElement {
|
||||||
|
readonly client: CookieBannerClient;
|
||||||
|
readonly bannerConfig: BannerConfig;
|
||||||
|
readonly state: ProboState;
|
||||||
|
readonly consentDraft: ConsentDraft;
|
||||||
|
setState(state: ProboState): void;
|
||||||
|
updateDraft(category: string, value: boolean): void;
|
||||||
|
}
|
||||||
75
packages/cookie-banner/src/components/buttons.ts
Normal file
75
packages/cookie-banner/src/components/buttons.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { ProboElement } from "./base";
|
||||||
|
import type { ProboRootElement } from "./base";
|
||||||
|
import type { ProboCookieBannerRoot } from "./cookie-banner-root";
|
||||||
|
|
||||||
|
class ProboActionButton extends ProboElement {
|
||||||
|
protected root: ProboRootElement | null = null;
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.shadow.innerHTML = `
|
||||||
|
<style>:host { display: contents; }</style>
|
||||||
|
<slot></slot>
|
||||||
|
`;
|
||||||
|
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||||
|
this.addEventListener("click", this.handleClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
this.removeEventListener("click", this.handleClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected handleClick = (_e: Event): void => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProboAcceptButton extends ProboActionButton {
|
||||||
|
protected handleClick = (): void => {
|
||||||
|
if (!this.root) return;
|
||||||
|
void this.root.client.acceptAll().then(() => {
|
||||||
|
this.root!.setState("hidden");
|
||||||
|
this.root!.dispatchEvent(
|
||||||
|
new CustomEvent("probo-consent", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: { action: "ACCEPT_ALL" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProboRejectButton extends ProboActionButton {
|
||||||
|
protected handleClick = (): void => {
|
||||||
|
if (!this.root) return;
|
||||||
|
void this.root.client.rejectAll().then(() => {
|
||||||
|
this.root!.setState("hidden");
|
||||||
|
this.root!.dispatchEvent(
|
||||||
|
new CustomEvent("probo-consent", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: { action: "REJECT_ALL" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProboCustomizeButton extends ProboActionButton {
|
||||||
|
protected handleClick = (): void => {
|
||||||
|
if (!this.root) return;
|
||||||
|
this.root.setState("panel");
|
||||||
|
};
|
||||||
|
}
|
||||||
92
packages/cookie-banner/src/components/category-list.ts
Normal file
92
packages/cookie-banner/src/components/category-list.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import type { Category } from "../client";
|
||||||
|
import { ProboElement } from "./base";
|
||||||
|
import type { ProboRootElement } from "./base";
|
||||||
|
import type { ProboCookieBannerRoot } from "./cookie-banner-root";
|
||||||
|
|
||||||
|
export class ProboCategoryList extends ProboElement {
|
||||||
|
private root: ProboRootElement | null = null;
|
||||||
|
private template: HTMLTemplateElement | null = null;
|
||||||
|
private onReady = (e: Event): void => {
|
||||||
|
const { config } = (e as CustomEvent).detail;
|
||||||
|
this.stamp(config.categories);
|
||||||
|
};
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.shadow.innerHTML = `
|
||||||
|
<style>:host { display: contents; }</style>
|
||||||
|
<slot name="items"></slot>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.template = this.querySelector("template");
|
||||||
|
if (!this.template) {
|
||||||
|
this.warn("<probo-category-list> requires a <template> child");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root = this.findAncestor<ProboCookieBannerRoot>("-root");
|
||||||
|
if (!this.root) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = this.root.bannerConfig;
|
||||||
|
this.stamp(config.categories);
|
||||||
|
} catch {
|
||||||
|
this.root.addEventListener("probo-ready", this.onReady, { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
if (this.root) {
|
||||||
|
this.root.removeEventListener("probo-ready", this.onReady);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stamp(categories: Category[]): void {
|
||||||
|
if (!this.template) return;
|
||||||
|
|
||||||
|
for (const cat of categories) {
|
||||||
|
const wrapper = document.createElement("probo-category");
|
||||||
|
wrapper.setAttribute("name", cat.name);
|
||||||
|
wrapper.setAttribute("slot", "items");
|
||||||
|
if (cat.required) {
|
||||||
|
wrapper.setAttribute("required", "");
|
||||||
|
}
|
||||||
|
wrapper.setAttribute("description", cat.description);
|
||||||
|
wrapper.setAttribute("cookies", JSON.stringify(cat.cookies));
|
||||||
|
|
||||||
|
const clone = this.template.content.cloneNode(true) as DocumentFragment;
|
||||||
|
this.fillSlots(clone, {
|
||||||
|
name: cat.name,
|
||||||
|
description: cat.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.appendChild(clone);
|
||||||
|
this.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillSlots(
|
||||||
|
fragment: DocumentFragment,
|
||||||
|
data: Record<string, string>,
|
||||||
|
): void {
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
const els = fragment.querySelectorAll(`[data-slot="${key}"]`);
|
||||||
|
for (const el of els) {
|
||||||
|
el.textContent = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
packages/cookie-banner/src/components/category-toggle.ts
Normal file
85
packages/cookie-banner/src/components/category-toggle.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { ProboElement } from "./base";
|
||||||
|
import type { ProboRootElement } from "./base";
|
||||||
|
import type { ProboCategory } from "./category";
|
||||||
|
import type { ProboCookieBannerRoot } from "./cookie-banner-root";
|
||||||
|
|
||||||
|
export class ProboCategoryToggle extends ProboElement {
|
||||||
|
private root: ProboRootElement | null = null;
|
||||||
|
private category: ProboCategory | null = null;
|
||||||
|
private checkbox: HTMLInputElement | null = null;
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.shadow.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host { display: inline-block; }
|
||||||
|
</style>
|
||||||
|
<slot></slot>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||||
|
this.category = this.findAncestor<ProboCategory>("probo-category");
|
||||||
|
|
||||||
|
this.scheduleValidation(() => this.setup());
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
if (this.checkbox) {
|
||||||
|
this.checkbox.removeEventListener("change", this.handleChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setup(): void {
|
||||||
|
this.checkbox = this.querySelector<HTMLInputElement>("input[type=checkbox]");
|
||||||
|
|
||||||
|
if (!this.checkbox) {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "checkbox";
|
||||||
|
input.part.add("toggle");
|
||||||
|
this.appendChild(input);
|
||||||
|
this.checkbox = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.category || !this.root) return;
|
||||||
|
|
||||||
|
const name = this.category.categoryName;
|
||||||
|
const isRequired = this.category.required;
|
||||||
|
|
||||||
|
if (isRequired) {
|
||||||
|
this.checkbox.checked = true;
|
||||||
|
this.checkbox.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const draft = this.root.consentDraft;
|
||||||
|
this.checkbox.checked = !!draft[name];
|
||||||
|
this.checkbox.addEventListener("change", this.handleChange);
|
||||||
|
|
||||||
|
if (this.root) {
|
||||||
|
this.root.addEventListener("probo-state", (e: Event) => {
|
||||||
|
const { state } = (e as CustomEvent).detail;
|
||||||
|
if (state === "panel" && this.checkbox && this.category && this.root) {
|
||||||
|
this.checkbox.checked = !!this.root.consentDraft[this.category.categoryName];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleChange = (): void => {
|
||||||
|
if (!this.checkbox || !this.category || !this.root) return;
|
||||||
|
this.root.updateDraft(this.category.categoryName, this.checkbox.checked);
|
||||||
|
};
|
||||||
|
}
|
||||||
41
packages/cookie-banner/src/components/category.ts
Normal file
41
packages/cookie-banner/src/components/category.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import type { CookieItem } from "../client";
|
||||||
|
import { ProboElement } from "./base";
|
||||||
|
|
||||||
|
export class ProboCategory extends ProboElement {
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.shadow.innerHTML = `
|
||||||
|
<style>:host { display: contents; }</style>
|
||||||
|
<slot></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get categoryName(): string {
|
||||||
|
return this.getAttribute("name") ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
get required(): boolean {
|
||||||
|
return this.hasAttribute("required");
|
||||||
|
}
|
||||||
|
|
||||||
|
get cookies(): CookieItem[] {
|
||||||
|
try {
|
||||||
|
return JSON.parse(this.getAttribute("cookies") ?? "[]");
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
packages/cookie-banner/src/components/cookie-banner-root.ts
Normal file
130
packages/cookie-banner/src/components/cookie-banner-root.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { CookieBannerClient } from "../client";
|
||||||
|
import type { BannerConfig } from "../client";
|
||||||
|
import { ProboElement } from "./base";
|
||||||
|
import type { ProboState, ProboRootElement, ConsentDraft } from "./base";
|
||||||
|
|
||||||
|
export class ProboCookieBannerRoot extends ProboElement implements ProboRootElement {
|
||||||
|
private _client: CookieBannerClient | null = null;
|
||||||
|
private _config: BannerConfig | null = null;
|
||||||
|
private _state: ProboState = "loading";
|
||||||
|
private _draft: ConsentDraft = {};
|
||||||
|
|
||||||
|
static get observedAttributes(): string[] {
|
||||||
|
return ["banner-id", "base-url"];
|
||||||
|
}
|
||||||
|
|
||||||
|
get client(): CookieBannerClient {
|
||||||
|
if (!this._client) {
|
||||||
|
throw new Error("<probo-cookie-banner-root> not loaded yet");
|
||||||
|
}
|
||||||
|
return this._client;
|
||||||
|
}
|
||||||
|
|
||||||
|
get bannerConfig(): BannerConfig {
|
||||||
|
if (!this._config) {
|
||||||
|
throw new Error("<probo-cookie-banner-root> not loaded yet");
|
||||||
|
}
|
||||||
|
return this._config;
|
||||||
|
}
|
||||||
|
|
||||||
|
get state(): ProboState {
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
|
||||||
|
get consentDraft(): ConsentDraft {
|
||||||
|
return this._draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.shadow.innerHTML = `<style>:host { display: contents; }</style><slot></slot>`;
|
||||||
|
this.initClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(state: ProboState): void {
|
||||||
|
const prev = this._state;
|
||||||
|
this._state = state;
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("probo-state", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: { state, prev },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDraft(category: string, value: boolean): void {
|
||||||
|
this._draft[category] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetDraft(): void {
|
||||||
|
if (!this._config) return;
|
||||||
|
this._draft = this.buildDraft(this._config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildDraft(config: BannerConfig): ConsentDraft {
|
||||||
|
const draft: ConsentDraft = {};
|
||||||
|
const existing = this._client?.visitorConsent?.consent_data;
|
||||||
|
|
||||||
|
for (const cat of config.categories) {
|
||||||
|
if (cat.required) {
|
||||||
|
draft[cat.name] = true;
|
||||||
|
} else if (existing && cat.name in existing) {
|
||||||
|
draft[cat.name] = existing[cat.name];
|
||||||
|
} else {
|
||||||
|
draft[cat.name] = config.consent_mode === "OPT_OUT";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initClient(): Promise<void> {
|
||||||
|
const bannerId = this.getAttribute("banner-id");
|
||||||
|
const baseUrl = this.getAttribute("base-url");
|
||||||
|
|
||||||
|
if (!bannerId || !baseUrl) {
|
||||||
|
this.warn("<probo-cookie-banner-root> requires banner-id and base-url attributes");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._client = new CookieBannerClient({ bannerId, baseUrl });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._client.load();
|
||||||
|
} catch (err) {
|
||||||
|
this.warn(`failed to load banner config: ${err}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._config = this._client.config;
|
||||||
|
this._draft = this.buildDraft(this._config);
|
||||||
|
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent("probo-ready", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: { config: this._config },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this._client.hasConsent) {
|
||||||
|
this.setState("hidden");
|
||||||
|
} else {
|
||||||
|
this.setState("banner");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
packages/cookie-banner/src/components/cookie-list.ts
Normal file
87
packages/cookie-banner/src/components/cookie-list.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import type { CookieItem } from "../client";
|
||||||
|
import { ProboElement } from "./base";
|
||||||
|
import type { ProboCategory } from "./category";
|
||||||
|
|
||||||
|
export class ProboCookieList extends ProboElement {
|
||||||
|
private category: ProboCategory | null = null;
|
||||||
|
private template: HTMLTemplateElement | null = null;
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.shadow.innerHTML = `
|
||||||
|
<style>:host { display: contents; }</style>
|
||||||
|
<slot name="items"></slot>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.template = this.querySelector("template");
|
||||||
|
if (!this.template) {
|
||||||
|
this.warn("<probo-cookie-list> requires a <template> child");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.category = this.findAncestor<ProboCategory>("probo-category");
|
||||||
|
|
||||||
|
this.scheduleValidation(() => this.stamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
private stamp(): void {
|
||||||
|
if (!this.template || !this.category) return;
|
||||||
|
|
||||||
|
const cookies = this.category.cookies;
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
this.stampCookie(cookie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stampCookie(cookie: CookieItem): void {
|
||||||
|
if (!this.template) return;
|
||||||
|
|
||||||
|
const wrapper = document.createElement("probo-cookie");
|
||||||
|
wrapper.setAttribute("name", cookie.name);
|
||||||
|
wrapper.setAttribute("slot", "items");
|
||||||
|
|
||||||
|
const clone = this.template.content.cloneNode(true) as DocumentFragment;
|
||||||
|
this.fillSlots(clone, {
|
||||||
|
name: cookie.name,
|
||||||
|
duration: cookie.duration,
|
||||||
|
description: cookie.description,
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.appendChild(clone);
|
||||||
|
this.appendChild(wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fillSlots(
|
||||||
|
fragment: DocumentFragment,
|
||||||
|
data: Record<string, string>,
|
||||||
|
): void {
|
||||||
|
for (const [key, value] of Object.entries(data)) {
|
||||||
|
const els = fragment.querySelectorAll(`[data-slot="${key}"]`);
|
||||||
|
for (const el of els) {
|
||||||
|
el.textContent = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProboCookie extends ProboElement {
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.shadow.innerHTML = `
|
||||||
|
<style>:host { display: contents; }</style>
|
||||||
|
<slot></slot>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
packages/cookie-banner/src/components/index.ts
Normal file
30
packages/cookie-banner/src/components/index.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
export { ProboElement } from "./base";
|
||||||
|
export type { ProboState, ProboRootElement, ConsentDraft } from "./base";
|
||||||
|
export { ProboBanner } from "./banner";
|
||||||
|
export {
|
||||||
|
ProboAcceptButton,
|
||||||
|
ProboCustomizeButton,
|
||||||
|
ProboRejectButton,
|
||||||
|
} from "./buttons";
|
||||||
|
export { ProboCategory } from "./category";
|
||||||
|
export { ProboCategoryList } from "./category-list";
|
||||||
|
export { ProboCategoryToggle } from "./category-toggle";
|
||||||
|
export { ProboCookieBannerRoot } from "./cookie-banner-root";
|
||||||
|
export { ProboCookie, ProboCookieList } from "./cookie-list";
|
||||||
|
export { ProboPreferencePanel, ProboSaveButton } from "./preference-panel";
|
||||||
|
export { ProboSettingsButton } from "./settings-button";
|
||||||
|
export { registerComponents } from "./register";
|
||||||
86
packages/cookie-banner/src/components/preference-panel.ts
Normal file
86
packages/cookie-banner/src/components/preference-panel.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { ProboElement } from "./base";
|
||||||
|
import type { ProboRootElement } from "./base";
|
||||||
|
import type { ProboCookieBannerRoot } from "./cookie-banner-root";
|
||||||
|
|
||||||
|
export class ProboPreferencePanel extends ProboElement {
|
||||||
|
private root: ProboRootElement | null = null;
|
||||||
|
private onStateChange = (e: Event): void => {
|
||||||
|
const { state } = (e as CustomEvent).detail;
|
||||||
|
this.hidden = state !== "panel";
|
||||||
|
};
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.shadow.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host { display: block; }
|
||||||
|
:host([hidden]) { display: none; }
|
||||||
|
</style>
|
||||||
|
<slot></slot>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.hidden = true;
|
||||||
|
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||||
|
|
||||||
|
if (this.root) {
|
||||||
|
this.root.addEventListener("probo-state", this.onStateChange);
|
||||||
|
this.root.addEventListener("probo-state", (e: Event) => {
|
||||||
|
const { state } = (e as CustomEvent).detail;
|
||||||
|
if (state === "panel") {
|
||||||
|
(this.root as ProboCookieBannerRoot).resetDraft();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
if (this.root) {
|
||||||
|
this.root.removeEventListener("probo-state", this.onStateChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProboSaveButton extends ProboElement {
|
||||||
|
private root: ProboRootElement | null = null;
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.shadow.innerHTML = `
|
||||||
|
<style>:host { display: contents; }</style>
|
||||||
|
<slot></slot>
|
||||||
|
`;
|
||||||
|
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||||
|
this.addEventListener("click", this.handleClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
this.removeEventListener("click", this.handleClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClick = (): void => {
|
||||||
|
if (!this.root) return;
|
||||||
|
const draft = { ...this.root.consentDraft };
|
||||||
|
void this.root.client.customize(draft).then(() => {
|
||||||
|
this.root!.setState("hidden");
|
||||||
|
this.root!.dispatchEvent(
|
||||||
|
new CustomEvent("probo-consent", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
detail: { action: "CUSTOMIZE", consent_data: draft },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
51
packages/cookie-banner/src/components/register.ts
Normal file
51
packages/cookie-banner/src/components/register.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { ProboBanner } from "./banner";
|
||||||
|
import {
|
||||||
|
ProboAcceptButton,
|
||||||
|
ProboCustomizeButton,
|
||||||
|
ProboRejectButton,
|
||||||
|
} from "./buttons";
|
||||||
|
import { ProboCategory } from "./category";
|
||||||
|
import { ProboCategoryList } from "./category-list";
|
||||||
|
import { ProboCategoryToggle } from "./category-toggle";
|
||||||
|
import { ProboCookieBannerRoot } from "./cookie-banner-root";
|
||||||
|
import { ProboCookie, ProboCookieList } from "./cookie-list";
|
||||||
|
import { ProboPreferencePanel, ProboSaveButton } from "./preference-panel";
|
||||||
|
import { ProboSettingsButton } from "./settings-button";
|
||||||
|
|
||||||
|
const elements: [string, CustomElementConstructor][] = [
|
||||||
|
["probo-cookie-banner-root", ProboCookieBannerRoot],
|
||||||
|
["probo-banner", ProboBanner],
|
||||||
|
["probo-accept-button", ProboAcceptButton],
|
||||||
|
["probo-reject-button", ProboRejectButton],
|
||||||
|
["probo-customize-button", ProboCustomizeButton],
|
||||||
|
["probo-preference-panel", ProboPreferencePanel],
|
||||||
|
["probo-category-list", ProboCategoryList],
|
||||||
|
["probo-category", ProboCategory],
|
||||||
|
["probo-category-toggle", ProboCategoryToggle],
|
||||||
|
["probo-cookie-list", ProboCookieList],
|
||||||
|
["probo-cookie", ProboCookie],
|
||||||
|
["probo-save-button", ProboSaveButton],
|
||||||
|
["probo-settings-button", ProboSettingsButton],
|
||||||
|
];
|
||||||
|
|
||||||
|
export function registerComponents(): void {
|
||||||
|
for (const [name, ctor] of elements) {
|
||||||
|
if (!customElements.get(name)) {
|
||||||
|
customElements.define(name, ctor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
packages/cookie-banner/src/components/settings-button.ts
Normal file
100
packages/cookie-banner/src/components/settings-button.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { ProboElement } from "./base";
|
||||||
|
import type { ProboRootElement } from "./base";
|
||||||
|
import type { ProboCookieBannerRoot } from "./cookie-banner-root";
|
||||||
|
|
||||||
|
const COOKIE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="8" cy="9" r="1" fill="currentColor"/><circle cx="15" cy="11" r="1" fill="currentColor"/><circle cx="10" cy="15" r="1" fill="currentColor"/><circle cx="13" cy="7" r="1" fill="currentColor"/></svg>`;
|
||||||
|
|
||||||
|
export class ProboSettingsButton extends ProboElement {
|
||||||
|
private root: ProboRootElement | null = null;
|
||||||
|
|
||||||
|
static get observedAttributes(): string[] {
|
||||||
|
return ["position"];
|
||||||
|
}
|
||||||
|
|
||||||
|
private get position(): string {
|
||||||
|
return this.getAttribute("position") ?? "bottom-left";
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
const pos = this.position;
|
||||||
|
const isRight = pos === "bottom-right";
|
||||||
|
|
||||||
|
this.shadow.innerHTML = `
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
position: fixed;
|
||||||
|
bottom: var(--probo-settings-bottom, 16px);
|
||||||
|
${isRight ? "right" : "left"}: var(--probo-settings-offset, 16px);
|
||||||
|
z-index: var(--probo-settings-z-index, 2147483645);
|
||||||
|
}
|
||||||
|
:host([hidden]) { display: none; }
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--probo-settings-radius, 9999px);
|
||||||
|
background: var(--probo-settings-bg, #1a1a1a);
|
||||||
|
color: var(--probo-settings-color, #ffffff);
|
||||||
|
padding: var(--probo-settings-padding, 10px);
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: var(--probo-settings-font-size, 14px);
|
||||||
|
box-shadow: var(--probo-settings-shadow, 0 2px 8px rgba(0, 0, 0, 0.15));
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
button:hover { opacity: 0.85; }
|
||||||
|
.icon { display: flex; flex-shrink: 0; }
|
||||||
|
::slotted(*) { display: contents; }
|
||||||
|
</style>
|
||||||
|
<button part="button">
|
||||||
|
<span class="icon" part="icon">${COOKIE_ICON}</span>
|
||||||
|
<slot></slot>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.hidden = true;
|
||||||
|
this.root = this.findAncestor<ProboCookieBannerRoot>("probo-cookie-banner-root");
|
||||||
|
|
||||||
|
if (this.root) {
|
||||||
|
this.root.addEventListener("probo-state", this.onStateChange);
|
||||||
|
if (this.root.state === "hidden") {
|
||||||
|
this.hidden = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = this.shadow.querySelector("button");
|
||||||
|
btn?.addEventListener("click", this.handleClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
if (this.root) {
|
||||||
|
this.root.removeEventListener("probo-state", this.onStateChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onStateChange = (e: Event): void => {
|
||||||
|
const { state } = (e as CustomEvent).detail;
|
||||||
|
this.hidden = state !== "hidden";
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleClick = (): void => {
|
||||||
|
if (!this.root) return;
|
||||||
|
this.root.setState("panel");
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -25,6 +25,24 @@ export type {
|
|||||||
CookieItem,
|
CookieItem,
|
||||||
VisitorConsent,
|
VisitorConsent,
|
||||||
} from "./client";
|
} from "./client";
|
||||||
|
export {
|
||||||
|
ProboElement,
|
||||||
|
ProboBanner,
|
||||||
|
ProboAcceptButton,
|
||||||
|
ProboRejectButton,
|
||||||
|
ProboCustomizeButton,
|
||||||
|
ProboCookieBannerRoot,
|
||||||
|
ProboPreferencePanel,
|
||||||
|
ProboSaveButton,
|
||||||
|
ProboCategoryList,
|
||||||
|
ProboCategory,
|
||||||
|
ProboCategoryToggle,
|
||||||
|
ProboCookieList,
|
||||||
|
ProboCookie,
|
||||||
|
ProboSettingsButton,
|
||||||
|
registerComponents,
|
||||||
|
} from "./components";
|
||||||
|
export type { ProboState, ProboRootElement, ConsentDraft } from "./components";
|
||||||
export type { ConsentCookie } from "./cookie";
|
export type { ConsentCookie } from "./cookie";
|
||||||
export {
|
export {
|
||||||
ApiError,
|
ApiError,
|
||||||
@@ -36,3 +54,6 @@ export {
|
|||||||
} from "./errors";
|
} from "./errors";
|
||||||
export { fetchJSON } from "./http";
|
export { fetchJSON } from "./http";
|
||||||
export type { RequestOptions } from "./http";
|
export type { RequestOptions } from "./http";
|
||||||
|
|
||||||
|
import { registerComponents } from "./components";
|
||||||
|
registerComponents();
|
||||||
|
|||||||
Reference in New Issue
Block a user