@tilli-pro/cookieconsent-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.js +13 -0
- package/package.json +38 -0
- package/src/_consts.ts +8 -0
- package/src/assets/cookie.ts +11 -0
- package/src/assets/index.ts +5 -0
- package/src/assets/logo.ts +21 -0
- package/src/config/categories/index.ts +53 -0
- package/src/config/categories/labels.ts +20 -0
- package/src/config/cookies/_utils.server.ts +24 -0
- package/src/config/cookies/_utils.ts +53 -0
- package/src/config/cookies/analytics/datadog.ts +38 -0
- package/src/config/cookies/common-translations.ts +31 -0
- package/src/config/cookies/functional/brf/v2.ts +34 -0
- package/src/config/cookies/index.ts +22 -0
- package/src/config/cookies/necessary/brf/v2-auth.ts +51 -0
- package/src/config/cookies/necessary/next-auth.ts +106 -0
- package/src/config/cookies/types.ts +45 -0
- package/src/config/gui-options/html-components/ManageCookiePrefsButton.ts +121 -0
- package/src/config/gui-options/html-components/TilliXCallout.ts +34 -0
- package/src/config/gui-options/html-components/assets/CookieIcon.ts +15 -0
- package/src/config/gui-options/index.ts +19 -0
- package/src/config/gui-options/scripts/cookiePrefsButtonDragObserver.ts +180 -0
- package/src/config/gui-options/scripts/forceDisableReactRemoveScroll.ts +152 -0
- package/src/config/gui-options/scripts/injectManageCookiePrefsButton.ts +50 -0
- package/src/config/index.ts +15 -0
- package/src/config/translations/de/index.ts +103 -0
- package/src/config/translations/en/index.ts +103 -0
- package/src/config/translations/es/index.ts +103 -0
- package/src/config/translations/fr/index.ts +103 -0
- package/src/config/translations/index.ts +15 -0
- package/src/config/translations/it/index.ts +103 -0
- package/src/config/translations/types.ts +2 -0
- package/src/react-components/CookieConsentInit.client.tsx +34 -0
- package/src/react-components/CookieConsentInit.server.tsx +14 -0
- package/src/react-components/_types.ts +7 -0
- package/src/react-components/_utils.ts +62 -0
- package/src/react-components/styles/tenants/freeman.css +63 -0
- package/src/react-components/styles/tenants/frontier.css +71 -0
- package/tsconfig.json +15 -0
@@ -0,0 +1,121 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import CookieIcon from "./assets/CookieIcon";
|
4
|
+
|
5
|
+
const translations = {
|
6
|
+
en: {
|
7
|
+
manageCookiePreferences: "Manage cookie preferences",
|
8
|
+
},
|
9
|
+
};
|
10
|
+
|
11
|
+
export const buttonId = "tilli-manage-cookie-preferences-floating-button";
|
12
|
+
export const containerId = `${buttonId}-container`;
|
13
|
+
|
14
|
+
/**
|
15
|
+
* returns HTML for a floating "manage cookie preferences" button.
|
16
|
+
*
|
17
|
+
* > by default, the button is visible -- however, if `#cc-main` (which
|
18
|
+
* appears *earlier* in the DOM and is thus a preceding sibling to
|
19
|
+
* `#${containerId}`) is still visible (i.e., the cookie consent modal),
|
20
|
+
* the entire container (`#${containerId}`) will be hidden.
|
21
|
+
*/
|
22
|
+
export default (lang: keyof typeof translations = "en"): string => `
|
23
|
+
<!-- floating cookie consent "manage preferences" icon button -->
|
24
|
+
<abbr title="${translations[lang].manageCookiePreferences}">
|
25
|
+
<button
|
26
|
+
id="${buttonId}"
|
27
|
+
type="button"
|
28
|
+
data-cc="${
|
29
|
+
/**
|
30
|
+
* auto-add "onClick" functionality -- @see https://cookieconsent.orestbida.com/essential/getting-started.html#how-to-open-the-preferences-modal
|
31
|
+
*
|
32
|
+
* @deprecated (can stay, no harm) prefer `CookieConsent.showPreferences()` since `data-cc="show-preferencesModal"` will only work if this cookie prefs button is injected PRIOR to running `CookieConsent.run()` -- @see https://cookieconsent.orestbida.com/reference/api-reference.html#showpreferences
|
33
|
+
*/
|
34
|
+
"show-preferencesModal"
|
35
|
+
}"
|
36
|
+
aria-label="${translations[lang].manageCookiePreferences}"
|
37
|
+
>
|
38
|
+
<style>
|
39
|
+
#${buttonId} {
|
40
|
+
/* floats @ the bottom right */
|
41
|
+
position: fixed;
|
42
|
+
bottom: 1rem;
|
43
|
+
right: 1rem;
|
44
|
+
z-index: 9999999;
|
45
|
+
|
46
|
+
/* sizing + layout */
|
47
|
+
width: 2rem;
|
48
|
+
height: 2rem;
|
49
|
+
display: flex;
|
50
|
+
align-items: center;
|
51
|
+
justify-content: center;
|
52
|
+
|
53
|
+
/* circular button background -- NOTE(IMPORTANT): must adhere to contrast guidelines -- @see https://webaim.org/resources/contrastchecker */
|
54
|
+
background-color: #fff;
|
55
|
+
border: 1px solid #ddd;
|
56
|
+
border-radius: 50%;
|
57
|
+
padding: 0.25rem; /* "fat-finger"-friendly padding */
|
58
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
59
|
+
cursor: pointer;
|
60
|
+
|
61
|
+
/* transitions for hover/focus states */
|
62
|
+
transition: box-shadow 0.2s ease;
|
63
|
+
animation: fadeIn var(--cc-modal-transition-duration) ease-in-out forwards;
|
64
|
+
|
65
|
+
/* ensure always-interactable + visible */
|
66
|
+
pointer-events: auto;
|
67
|
+
visibility: visible !important;
|
68
|
+
}
|
69
|
+
|
70
|
+
.dark #${buttonId} {
|
71
|
+
background-color: #000;
|
72
|
+
border-color: #333;
|
73
|
+
}
|
74
|
+
|
75
|
+
#${buttonId}:hover,
|
76
|
+
#${buttonId}:focus {
|
77
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
78
|
+
}
|
79
|
+
|
80
|
+
#${buttonId}:focus-visible {
|
81
|
+
outline: none; /* remove default blue outline */
|
82
|
+
}
|
83
|
+
|
84
|
+
/* the icon itself (SVG), scaled: */
|
85
|
+
#${buttonId} svg {
|
86
|
+
width: 1.5rem;
|
87
|
+
height: 1.5rem;
|
88
|
+
display: block;
|
89
|
+
}
|
90
|
+
|
91
|
+
@keyframes fadeIn {
|
92
|
+
0% {
|
93
|
+
opacity: 0;
|
94
|
+
transform: translateY(50%);
|
95
|
+
}
|
96
|
+
100% {
|
97
|
+
opacity: 1;
|
98
|
+
transform: translateY(0);
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
/* default: display the container so the button is visible */
|
103
|
+
#${containerId} {
|
104
|
+
display: block;
|
105
|
+
}
|
106
|
+
|
107
|
+
/*
|
108
|
+
hide the entire container IF AND ONLY IF:
|
109
|
+
- #cc-main (earlier sibling) has .cm-wrapper
|
110
|
+
- That .cm-wrapper has a .cm
|
111
|
+
- That .cm has [aria-hidden="false"] (i.e., the cookie consent modal is visible)
|
112
|
+
*/
|
113
|
+
#cc-main:has(.cm-wrapper .cm[aria-hidden="false"]) ~ #${containerId} {
|
114
|
+
display: none !important;
|
115
|
+
}
|
116
|
+
</style>
|
117
|
+
|
118
|
+
${CookieIcon(1)}
|
119
|
+
</button>
|
120
|
+
</abbr>
|
121
|
+
`;
|
@@ -0,0 +1,34 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import assets from "../../../assets";
|
4
|
+
|
5
|
+
const translations = {
|
6
|
+
de: {
|
7
|
+
poweredByTilliX: "unterstützt von tilliX",
|
8
|
+
},
|
9
|
+
en: {
|
10
|
+
poweredByTilliX: "powered by tilliX",
|
11
|
+
},
|
12
|
+
es: {
|
13
|
+
poweredByTilliX: "impulsado por tilliX",
|
14
|
+
},
|
15
|
+
fr: {
|
16
|
+
poweredByTilliX: "propulsé par tilliX",
|
17
|
+
},
|
18
|
+
it: {
|
19
|
+
poweredByTilliX: "alimentato da tilliX",
|
20
|
+
},
|
21
|
+
};
|
22
|
+
|
23
|
+
export default (
|
24
|
+
lang: keyof typeof translations = "en",
|
25
|
+
inPrefs?: boolean | (1 | 0), // 😉
|
26
|
+
): string => `
|
27
|
+
<!-- "powered by tilliX" callout -->
|
28
|
+
<div style="${!inPrefs ? "margin-left: auto; " : ""}margin-top: auto; margin-bottom: auto">
|
29
|
+
<abbr title="${translations[lang].poweredByTilliX}" style="${inPrefs ? "margin-right: 1em" : "margin-left: 1.3rem"}; display: flex; align-items: center">
|
30
|
+
<a href="https://tilli.pro" target="_blank" style="display: inline-block; background-image: none">
|
31
|
+
<img src="data:image/svg+xml;charset=utf8,${encodeURIComponent(assets.logo)}" alt="tilli" style="height: 1em; width: auto; min-height: 1em; min-width: 1em; vertical-align: middle" />
|
32
|
+
</a>
|
33
|
+
</abbr>
|
34
|
+
</div>`;
|
@@ -0,0 +1,15 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import assets from "../../../../assets";
|
4
|
+
|
5
|
+
export default (
|
6
|
+
centered?: boolean | (1 | 0) /* 😉 */,
|
7
|
+
scale = 0.6667, // -> 0.6667 ((100 * n)%)
|
8
|
+
topOffset = 0, // -> -0.25 (em)
|
9
|
+
rightOffset = 0, // -> 0.25 (em)
|
10
|
+
): string => `
|
11
|
+
<span style="display: inline-block; vertical-align: middle; transform: scale(${scale}); transform-origin: top left; margin-right: ${((centered ? -0.5 : -0.25) + rightOffset).toFixed(4)}em; height: 1em${topOffset ? `; margin-top: ${topOffset.toFixed(4)}em` : ""};">
|
12
|
+
${assets.cookie}
|
13
|
+
<!-- invisible "🍪" icon (used for copy-paste/a11y purposes) -->
|
14
|
+
<span style="max-width: 0; max-height: 0; opacity: 0; position: absolute">🍪</span>
|
15
|
+
</span>`;
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import type { GuiOptions } from "@tilli-pro/cookieconsent";
|
2
|
+
|
3
|
+
/** IMPORTANT NOTES FOR GDPR COMPLIANCE:
|
4
|
+
* > the "Accept" and "Reject" buttons should be equally weighted in terms of size, color, and font. This means that the buttons should be equally accessible and prominent– `equalWeightButtons` must be `true`. */
|
5
|
+
|
6
|
+
export default {
|
7
|
+
consentModal: {
|
8
|
+
layout: "box",
|
9
|
+
position: "bottom left",
|
10
|
+
equalWeightButtons: true, // IMPORTANT: MUST BE `true` TO BE GDPR-COMPLIANT
|
11
|
+
flipButtons: false,
|
12
|
+
},
|
13
|
+
preferencesModal: {
|
14
|
+
layout: "box",
|
15
|
+
position: "right", // not in use since we use "box" (aka a modal)– if we use "bar" (aka a drawer), this will be used
|
16
|
+
equalWeightButtons: true, // IMPORTANT: MUST BE `true` TO BE GDPR-COMPLIANT
|
17
|
+
flipButtons: false,
|
18
|
+
},
|
19
|
+
} satisfies GuiOptions;
|
@@ -0,0 +1,180 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import { showPreferences } from "@tilli-pro/cookieconsent";
|
4
|
+
|
5
|
+
import { buttonId } from "../html-components/ManageCookiePrefsButton";
|
6
|
+
|
7
|
+
/**
|
8
|
+
* attach a MutationObserver the **first time** the user hovers over–
|
9
|
+
* enabling a limited "drag up" functionality for the floating button.
|
10
|
+
* > this is a purposeful UX enhancement to allow users to move the button
|
11
|
+
* out of the way in case it’s blocking content/functionality underneath.
|
12
|
+
*
|
13
|
+
* @usage simply attach this function to the `onmouseover` event of the floating button.
|
14
|
+
* @see {@link ./injectManageCookiePrefsButton.tsx HTMLDivElement["onmouseover"]} for the full implementation.
|
15
|
+
*/
|
16
|
+
export default (function <IsTouch extends boolean = false>(
|
17
|
+
this: HTMLDivElement,
|
18
|
+
e: MouseEvent,
|
19
|
+
) {
|
20
|
+
/** we store a data-attribute (`hasTilliCookiePrefsDragObserverInit`) to avoid reattaching
|
21
|
+
* the logic on multiple hovers -- this ensures the script is only initiated once. */
|
22
|
+
if (this.dataset.hasTilliCookiePrefsDragObserverInit === "true") return;
|
23
|
+
this.dataset.hasTilliCookiePrefsDragObserverInit = "true";
|
24
|
+
|
25
|
+
let isDragging = false;
|
26
|
+
let startY = 0;
|
27
|
+
let initialBottomPx = 0;
|
28
|
+
let hasListeners = false; // track whether we've attached the events yet
|
29
|
+
|
30
|
+
/** we'll dynamically calculate these values on the first drag */
|
31
|
+
let lowestBottomPx = 0;
|
32
|
+
let highestBottomPx = 0;
|
33
|
+
|
34
|
+
// DRAG LOGIC
|
35
|
+
function onMouseDown(this: HTMLElement, e: MouseEvent) {
|
36
|
+
if (e.button !== 0) return; // only drag on left-click
|
37
|
+
e.preventDefault();
|
38
|
+
|
39
|
+
isDragging = true;
|
40
|
+
startY = e.clientY;
|
41
|
+
|
42
|
+
/** on each new drag, read the button’s *current* bottom */
|
43
|
+
const buttonStyle = window.getComputedStyle(this);
|
44
|
+
initialBottomPx = parseFloat(buttonStyle.bottom) || 16;
|
45
|
+
}
|
46
|
+
|
47
|
+
function onMouseMove(this: HTMLElement, e: MouseEvent) {
|
48
|
+
if (!isDragging) return;
|
49
|
+
|
50
|
+
/** positive delta => pointer moving down => button goes up */
|
51
|
+
const dragDelta = startY - e.clientY;
|
52
|
+
let newBottomPx = initialBottomPx + dragDelta;
|
53
|
+
|
54
|
+
/** clamp between lowestBottomPx and highestBottomPx */
|
55
|
+
if (newBottomPx < lowestBottomPx) newBottomPx = lowestBottomPx;
|
56
|
+
if (newBottomPx > highestBottomPx) newBottomPx = highestBottomPx;
|
57
|
+
|
58
|
+
this.style.bottom = `${newBottomPx}px`;
|
59
|
+
}
|
60
|
+
|
61
|
+
function onMouseUp() {
|
62
|
+
isDragging = false;
|
63
|
+
}
|
64
|
+
|
65
|
+
// [mobile (touch) support]
|
66
|
+
// - we also want to allow dragging on touch devices
|
67
|
+
// - we're using the same logic as mouse dragging, but with touch events
|
68
|
+
function onTouchStart(this: HTMLElement, e: TouchEvent) {
|
69
|
+
if (e.touches?.length !== 1) return; // only drag with one finger
|
70
|
+
e.preventDefault();
|
71
|
+
isDragging = true;
|
72
|
+
startY = e.touches[0]?.clientY ?? startY;
|
73
|
+
|
74
|
+
const buttonStyle = window.getComputedStyle(this);
|
75
|
+
initialBottomPx = parseFloat(buttonStyle.bottom) || 16;
|
76
|
+
}
|
77
|
+
|
78
|
+
function onTouchMove(this: HTMLElement, e: TouchEvent) {
|
79
|
+
if (!isDragging) return;
|
80
|
+
|
81
|
+
if (!e.touches?.[0]) return;
|
82
|
+
const dragDelta = startY - e.touches[0].clientY;
|
83
|
+
let newBottomPx = initialBottomPx + dragDelta;
|
84
|
+
|
85
|
+
if (newBottomPx < lowestBottomPx) newBottomPx = lowestBottomPx;
|
86
|
+
if (newBottomPx > highestBottomPx) newBottomPx = highestBottomPx;
|
87
|
+
|
88
|
+
this.style.bottom = `${newBottomPx}px`;
|
89
|
+
}
|
90
|
+
|
91
|
+
function onTouchEnd() {
|
92
|
+
isDragging = false;
|
93
|
+
}
|
94
|
+
|
95
|
+
function attachDragEvents(buttonEl: HTMLElement) {
|
96
|
+
if (hasListeners) return; // only attach once
|
97
|
+
hasListeners = true;
|
98
|
+
|
99
|
+
/** determine max drag distance (based on button height) */
|
100
|
+
const buttonStyle = window.getComputedStyle(buttonEl);
|
101
|
+
const originalBottomPx = parseFloat(buttonStyle.bottom) || 16; // fallback to ~1rem
|
102
|
+
const buttonHeightPx = parseFloat(buttonStyle.height) || 32; // fallback to ~2rem
|
103
|
+
const maxDragPx = buttonHeightPx * 1.1; // added 10% extra to be safe
|
104
|
+
|
105
|
+
/** the user can drag from the original position (lowestBottomPx) up by that computed distance (highestBottomPx). */
|
106
|
+
lowestBottomPx = originalBottomPx;
|
107
|
+
highestBottomPx = originalBottomPx + maxDragPx;
|
108
|
+
|
109
|
+
/** attach the mouse events for dragging */
|
110
|
+
buttonEl.addEventListener("mousedown", onMouseDown.bind(buttonEl));
|
111
|
+
document.addEventListener("mousemove", onMouseMove.bind(buttonEl));
|
112
|
+
document.addEventListener("mouseup", onMouseUp);
|
113
|
+
// [mobile (touch) support]
|
114
|
+
buttonEl.addEventListener("touchstart", onTouchStart.bind(buttonEl));
|
115
|
+
document.addEventListener("touchmove", onTouchMove.bind(buttonEl));
|
116
|
+
document.addEventListener("touchend", onTouchEnd);
|
117
|
+
}
|
118
|
+
|
119
|
+
// MUTATION OBSERVER – for if the button is removed/re-added
|
120
|
+
const observer = new MutationObserver(() => {
|
121
|
+
const button = document.getElementById(buttonId);
|
122
|
+
if (!button) {
|
123
|
+
hasListeners = false; // if button was removed, reset flags
|
124
|
+
isDragging = false;
|
125
|
+
return;
|
126
|
+
}
|
127
|
+
attachDragEvents(button);
|
128
|
+
});
|
129
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
130
|
+
|
131
|
+
/** if the button is already present, attach drag events immediately */
|
132
|
+
const button = document.getElementById(buttonId);
|
133
|
+
if (button) attachDragEvents(button);
|
134
|
+
} as <IsTouch extends boolean = false>() => HTMLDivElement[IsTouch extends true
|
135
|
+
? "ontouchstart"
|
136
|
+
: "onmouseover"]);
|
137
|
+
|
138
|
+
export type HTMLDivElementWithDragObserver = HTMLDivElement & {
|
139
|
+
_startX?: number | null;
|
140
|
+
_startY?: number | null;
|
141
|
+
};
|
142
|
+
|
143
|
+
/** determine whether the user "tapped" or "dragged" (open prefs if "tapped") */
|
144
|
+
export const ontouchend = function ontouchend(
|
145
|
+
this: HTMLDivElementWithDragObserver,
|
146
|
+
e: TouchEvent,
|
147
|
+
): void {
|
148
|
+
const touch = e.changedTouches[0];
|
149
|
+
const { clientX: endX, clientY: endY } = touch ?? {};
|
150
|
+
if (!endX || !endY) return;
|
151
|
+
|
152
|
+
/** retrieve stored start coordinates */
|
153
|
+
const startX = this._startX ?? endX;
|
154
|
+
const startY = this._startY ?? endY;
|
155
|
+
|
156
|
+
/** threshold for "same spot" */
|
157
|
+
const threshold = 10; // maximum allowable movement (in pixels)
|
158
|
+
|
159
|
+
/** make sure the user ended their touch in *relatively* the "same spot" as they started */
|
160
|
+
if (
|
161
|
+
Math.abs(endX - startX) <= threshold &&
|
162
|
+
Math.abs(endY - startY) <= threshold
|
163
|
+
)
|
164
|
+
/** user ended the touch near where it started, open preferences */
|
165
|
+
showPreferences(); /** prefer over `data-cc="show-preferencesModal"` -- @see https://cookieconsent.orestbida.com/reference/api-reference.html#showpreferences */
|
166
|
+
|
167
|
+
/** reset coordinates to avoid future issues */
|
168
|
+
this._startX = null;
|
169
|
+
this._startY = null;
|
170
|
+
} as HTMLDivElementWithDragObserver["ontouchend"];
|
171
|
+
|
172
|
+
/** store the touch start coordinates */
|
173
|
+
export const ontouchstart = function ontouchstart(
|
174
|
+
this: HTMLDivElementWithDragObserver,
|
175
|
+
e: TouchEvent,
|
176
|
+
): void {
|
177
|
+
const touch = e.touches[0];
|
178
|
+
this._startX = touch?.clientX;
|
179
|
+
this._startY = touch?.clientY;
|
180
|
+
} as HTMLDivElementWithDragObserver["ontouchstart"];
|
@@ -0,0 +1,152 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import type CookieConsent from "@tilli-pro/cookieconsent";
|
4
|
+
|
5
|
+
// IMPORTANT: this is a fix to force-disable the "React Remove Scroll" wheel listener -- this handles the edge case where there's a shadcn (radix-ui) dialog open underneath the cookie banner's "manage preferences" dialog, which prevents the user from scrolling within the manage prefs dialog.
|
6
|
+
|
7
|
+
/** IMPORTANT: this is what we'll be looking for in the listener's source code -- @see https://github.com/theKashey/react-remove-scroll/blob/master/src/SideEffect.tsx#L107-L145 */
|
8
|
+
const DISTINCTIVE_SUBSTRINGS = ["shards", "noIsolation"];
|
9
|
+
|
10
|
+
/** listener tracking */
|
11
|
+
type ListenerRecord = {
|
12
|
+
type: "wheel" | "touchmove" | "touchstart" | "pointerdown" | "click";
|
13
|
+
listener: EventListenerOrEventListenerObject;
|
14
|
+
options?: boolean | AddEventListenerOptions;
|
15
|
+
};
|
16
|
+
|
17
|
+
const eventListenersMap = new WeakMap<EventTarget, ListenerRecord[]>();
|
18
|
+
|
19
|
+
let isPatched = false; // ensure patching happens only ONCE.
|
20
|
+
|
21
|
+
/** 🦧 monkey(orangutan)-patch `addEventListener` and `removeEventListener` to track listeners. */
|
22
|
+
function patchEventListeners() {
|
23
|
+
if (isPatched) return; // prevent multiple patches
|
24
|
+
isPatched = true;
|
25
|
+
|
26
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
27
|
+
const originalAddEventListener = EventTarget.prototype.addEventListener;
|
28
|
+
EventTarget.prototype.addEventListener = function (
|
29
|
+
type: ListenerRecord["type"],
|
30
|
+
listener: ListenerRecord["listener"],
|
31
|
+
options?: ListenerRecord["options"],
|
32
|
+
) {
|
33
|
+
let records = eventListenersMap.get(this);
|
34
|
+
if (!records) {
|
35
|
+
records = [];
|
36
|
+
eventListenersMap.set(this, records);
|
37
|
+
}
|
38
|
+
records.push({ type, listener, options });
|
39
|
+
|
40
|
+
originalAddEventListener.call(this, type, listener, options);
|
41
|
+
};
|
42
|
+
|
43
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
44
|
+
const originalRemoveEventListener = EventTarget.prototype.removeEventListener;
|
45
|
+
EventTarget.prototype.removeEventListener = function (
|
46
|
+
type: ListenerRecord["type"],
|
47
|
+
listener: ListenerRecord["listener"],
|
48
|
+
options?: ListenerRecord["options"],
|
49
|
+
) {
|
50
|
+
const records = eventListenersMap.get(this);
|
51
|
+
if (records) {
|
52
|
+
const index = records.findIndex(
|
53
|
+
(r) =>
|
54
|
+
r.type === type && r.listener === listener && r.options === options,
|
55
|
+
);
|
56
|
+
if (index !== -1) {
|
57
|
+
records.splice(index, 1);
|
58
|
+
if (records.length === 0) eventListenersMap.delete(this);
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
originalRemoveEventListener.call(this, type, listener, options);
|
63
|
+
};
|
64
|
+
}
|
65
|
+
|
66
|
+
/** attempt to find the "React Remove Scroll" wheel listeners by searching for unique strings in its source code */
|
67
|
+
const findReactRemoveScrollListeners = () =>
|
68
|
+
/** find all matching listeners that have ALL required substrings */
|
69
|
+
(eventListenersMap.get(document) ?? []).filter((record) => {
|
70
|
+
if (
|
71
|
+
record.type === "wheel" ||
|
72
|
+
record.type === "touchmove" ||
|
73
|
+
record.type === "touchstart" ||
|
74
|
+
record.type === "pointerdown" ||
|
75
|
+
record.type === "click"
|
76
|
+
) {
|
77
|
+
const fnStr = record.listener.toString();
|
78
|
+
return DISTINCTIVE_SUBSTRINGS.every((sub) => fnStr.includes(sub));
|
79
|
+
}
|
80
|
+
|
81
|
+
return false;
|
82
|
+
});
|
83
|
+
|
84
|
+
/**
|
85
|
+
* force the "React Remove Scroll" wheel listener to be passive or not.
|
86
|
+
*
|
87
|
+
* @param isPassive - Set `true` to make it `{ passive: true }`,
|
88
|
+
* `false` for `{ passive: false }`.
|
89
|
+
*/
|
90
|
+
export function forceDisableReactRemoveScroll(
|
91
|
+
isPassive: boolean,
|
92
|
+
debug = false,
|
93
|
+
) {
|
94
|
+
const targets = findReactRemoveScrollListeners();
|
95
|
+
if (!targets || targets.length === 0) {
|
96
|
+
if (debug)
|
97
|
+
console.debug('No matching "React Remove Scroll" listener found.');
|
98
|
+
return;
|
99
|
+
}
|
100
|
+
|
101
|
+
/** remove the existing listener */
|
102
|
+
for (const target of targets) {
|
103
|
+
document.removeEventListener(
|
104
|
+
target.type,
|
105
|
+
target.listener,
|
106
|
+
target.options as EventListenerOptions,
|
107
|
+
);
|
108
|
+
|
109
|
+
/** re-add with the new passive value */
|
110
|
+
document.addEventListener(target.type, target.listener, {
|
111
|
+
...(typeof target.options === "boolean" ? {} : target.options),
|
112
|
+
passive: isPassive,
|
113
|
+
});
|
114
|
+
|
115
|
+
if (debug)
|
116
|
+
console.debug(
|
117
|
+
`"React Remove Scroll" ${target.type} listener set to passive: ${isPassive} (aka, we've ${
|
118
|
+
isPassive ? "bypassed" : "re-enabled"
|
119
|
+
} it)`,
|
120
|
+
);
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
/**
|
125
|
+
* inject event listeners for CookieConsent's modal show/hide events,
|
126
|
+
* toggling the "React Remove Scroll" listener's passive state.
|
127
|
+
*
|
128
|
+
* listens for:
|
129
|
+
* - "cc:onModalShow": If "preferencesModal", we set passive = true.
|
130
|
+
* - "cc:onModalHide": If "preferencesModal", we set passive = false.
|
131
|
+
*/
|
132
|
+
export function injectReactRemoveScrollToggle(): void {
|
133
|
+
type OnModalShowHideEvent = CustomEvent<{
|
134
|
+
modalName: CookieConsent.ModalName;
|
135
|
+
}>;
|
136
|
+
|
137
|
+
patchEventListeners(); // ensure patching happens only once
|
138
|
+
|
139
|
+
window.addEventListener("cc:onModalShow", (event: Event) => {
|
140
|
+
const { detail } = (event as OnModalShowHideEvent | null) ?? {};
|
141
|
+
|
142
|
+
if (detail?.modalName === "preferencesModal")
|
143
|
+
forceDisableReactRemoveScroll(true); // force passive
|
144
|
+
});
|
145
|
+
|
146
|
+
window.addEventListener("cc:onModalHide", (event: Event) => {
|
147
|
+
const { detail } = (event as OnModalShowHideEvent | null) ?? {};
|
148
|
+
|
149
|
+
if (detail?.modalName === "preferencesModal")
|
150
|
+
forceDisableReactRemoveScroll(false); // force non-passive
|
151
|
+
});
|
152
|
+
}
|
@@ -0,0 +1,50 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import { showPreferences } from "@tilli-pro/cookieconsent";
|
4
|
+
|
5
|
+
import type { HTMLDivElementWithDragObserver } from "./cookiePrefsButtonDragObserver";
|
6
|
+
import ManageCookiePrefsButton, {
|
7
|
+
containerId,
|
8
|
+
} from "../html-components/ManageCookiePrefsButton";
|
9
|
+
import cookiePrefsButtonDragObserver, {
|
10
|
+
ontouchend,
|
11
|
+
ontouchstart,
|
12
|
+
} from "./cookiePrefsButtonDragObserver";
|
13
|
+
|
14
|
+
/** injects the floating cookie consent "manage preferences" icon button into the DOM */
|
15
|
+
const inject = (): HTMLDivElement => {
|
16
|
+
const container = document.body.appendChild(
|
17
|
+
Object.assign(document.createElement("div"), {
|
18
|
+
id: containerId,
|
19
|
+
innerHTML: ManageCookiePrefsButton(/* TODO: __LANGUAGE__ */),
|
20
|
+
onclick:
|
21
|
+
showPreferences /** prefer over `data-cc="show-preferencesModal"` -- @see https://cookieconsent.orestbida.com/reference/api-reference.html#showpreferences */,
|
22
|
+
onmouseover: cookiePrefsButtonDragObserver,
|
23
|
+
// [mobile (touch) support]
|
24
|
+
ontouchend,
|
25
|
+
ontouchstart,
|
26
|
+
} satisfies Partial<HTMLDivElement>),
|
27
|
+
) as HTMLDivElementWithDragObserver;
|
28
|
+
|
29
|
+
/** load drag observer upon DOM injection (unfortunate fix to prevent the need for 2x "ontouchstart" events) */
|
30
|
+
cookiePrefsButtonDragObserver.call(container);
|
31
|
+
|
32
|
+
return container;
|
33
|
+
};
|
34
|
+
export default (): ReturnType<typeof inject> =>
|
35
|
+
((container) => {
|
36
|
+
/** observe the DOM (to handle the case where the injected manage prefs button somehow gets removed) */
|
37
|
+
new MutationObserver(() => {
|
38
|
+
const alreadyInjected: boolean =
|
39
|
+
!!document.getElementById(containerId) ||
|
40
|
+
document.body.contains(container);
|
41
|
+
if (alreadyInjected) return;
|
42
|
+
|
43
|
+
/** if the container is no longer in <body>, re-inject */
|
44
|
+
container = inject();
|
45
|
+
}).observe(document.body, { childList: true, subtree: true });
|
46
|
+
|
47
|
+
return container;
|
48
|
+
})(
|
49
|
+
(document.getElementById(containerId) as HTMLDivElement | null) ?? inject(),
|
50
|
+
);
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import type { CookieConsentConfig } from "@tilli-pro/cookieconsent";
|
2
|
+
|
3
|
+
import { COOKIE_PREFERENCES_COOKIE_NAME, REVISION } from "../_consts";
|
4
|
+
import categories from "./categories";
|
5
|
+
import guiOptions from "./gui-options";
|
6
|
+
import translations from "./translations";
|
7
|
+
|
8
|
+
export default {
|
9
|
+
revision: REVISION,
|
10
|
+
guiOptions,
|
11
|
+
categories,
|
12
|
+
language: { default: "en", autoDetect: "browser", translations },
|
13
|
+
cookie: { name: COOKIE_PREFERENCES_COOKIE_NAME },
|
14
|
+
hideFromBots: process.env.NODE_ENV === "production", // disabled in local dev to enable ui-testing (via Playwright)
|
15
|
+
} satisfies CookieConsentConfig;
|