Add element deactivation with cookie removal on consent withdrawal

When a user changes their consent preferences, elements for rejected
categories are now deactivated: data attributes are restored, resources
are unloaded, and associated cookies are removed.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-21 19:38:05 +04:00
parent fbdff595fe
commit 1fb0464948
2 changed files with 117 additions and 3 deletions

View File

@@ -31,6 +31,7 @@ const ACTIVATABLE_TAGS = new Set([
function activateScript(el: HTMLScriptElement): void {
const replacement = document.createElement("script");
const originalType = el.getAttribute("data-type");
const category = el.getAttribute(ATTR_CATEGORY);
for (const attr of el.attributes) {
if (
@@ -50,7 +51,7 @@ function activateScript(el: HTMLScriptElement): void {
if (originalType) {
replacement.setAttribute("type", originalType);
}
replacement.setAttribute(ATTR_ACTIVATED, "");
replacement.setAttribute(ATTR_ACTIVATED, category || "");
if (el.textContent) {
replacement.textContent = el.textContent;
@@ -60,6 +61,8 @@ function activateScript(el: HTMLScriptElement): void {
}
function activateElement(el: Element): void {
const category = el.getAttribute(ATTR_CATEGORY);
const src = el.getAttribute(ATTR_SRC);
if (src) {
el.setAttribute("src", src);
@@ -73,7 +76,7 @@ function activateElement(el: Element): void {
}
el.removeAttribute(ATTR_CATEGORY);
el.setAttribute(ATTR_ACTIVATED, "");
el.setAttribute(ATTR_ACTIVATED, category || "");
}
function tryActivate(
@@ -100,6 +103,107 @@ function tryActivate(
}
}
function deactivateScript(el: HTMLScriptElement): void {
const category = el.getAttribute(ATTR_ACTIVATED);
const currentType = el.getAttribute("type");
const replacement = document.createElement("script");
for (const attr of el.attributes) {
if (
attr.name === ATTR_ACTIVATED ||
attr.name === "type" ||
attr.name === "src"
) {
continue;
}
replacement.setAttribute(attr.name, attr.value);
}
const src = el.getAttribute("src");
if (src) {
replacement.setAttribute(ATTR_SRC, src);
}
if (currentType) {
replacement.setAttribute("data-type", currentType);
}
replacement.setAttribute("type", "text/plain");
if (category) {
replacement.setAttribute(ATTR_CATEGORY, category);
}
if (el.textContent) {
replacement.textContent = el.textContent;
}
el.parentNode!.replaceChild(replacement, el);
}
function deactivateElement(el: Element): void {
const category = el.getAttribute(ATTR_ACTIVATED);
const src = el.getAttribute("src");
if (src) {
el.setAttribute(ATTR_SRC, src);
el.removeAttribute("src");
}
const href = el.getAttribute("href");
if (href) {
el.setAttribute(ATTR_HREF, href);
el.removeAttribute("href");
}
if (category) {
el.setAttribute(ATTR_CATEGORY, category);
}
el.removeAttribute(ATTR_ACTIVATED);
}
function removeCookies(names: string[]): void {
const parts = location.hostname.split(".");
const rootDomain =
parts.length > 1 ? "." + parts.slice(-2).join(".") : location.hostname;
for (const name of names) {
document.cookie = `${name}=; path=/; max-age=0`;
document.cookie = `${name}=; path=/; domain=${rootDomain}; max-age=0`;
}
}
export function deactivateElements(
consentData: Record<string, boolean>,
categoryCookies: Record<string, string[]>,
): void {
const elements = document.querySelectorAll(`[${ATTR_ACTIVATED}]`);
const cookiesToRemove = new Set<string>();
for (const el of elements) {
const category = el.getAttribute(ATTR_ACTIVATED);
if (!category || consentData[category]) {
continue;
}
if (el instanceof HTMLScriptElement) {
deactivateScript(el);
} else {
deactivateElement(el);
}
const cookies = categoryCookies[category];
if (cookies) {
for (const name of cookies) {
cookiesToRemove.add(name);
}
}
}
if (cookiesToRemove.size > 0) {
removeCookies([...cookiesToRemove]);
}
}
export function activateElements(
consentData: Record<string, boolean>,
): void {

View File

@@ -12,7 +12,11 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { activateElements, observeAndActivate } from "./activation";
import {
activateElements,
deactivateElements,
observeAndActivate,
} from "./activation";
import { getConsentCookie, setConsentCookie } from "./cookie";
import { NotFoundError } from "./errors";
import { fetchJSON } from "./http";
@@ -236,6 +240,12 @@ export class CookieBannerClient {
}
private activate(consentData: Record<string, boolean>): void {
const categoryCookies: Record<string, string[]> = {};
for (const cat of this.config.categories) {
categoryCookies[cat.name] = cat.cookies.map((c) => c.name);
}
deactivateElements(consentData, categoryCookies);
activateElements(consentData);
if (this.observer) {
this.observer.disconnect();