@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.
Files changed (39) hide show
  1. package/eslint.config.js +13 -0
  2. package/package.json +38 -0
  3. package/src/_consts.ts +8 -0
  4. package/src/assets/cookie.ts +11 -0
  5. package/src/assets/index.ts +5 -0
  6. package/src/assets/logo.ts +21 -0
  7. package/src/config/categories/index.ts +53 -0
  8. package/src/config/categories/labels.ts +20 -0
  9. package/src/config/cookies/_utils.server.ts +24 -0
  10. package/src/config/cookies/_utils.ts +53 -0
  11. package/src/config/cookies/analytics/datadog.ts +38 -0
  12. package/src/config/cookies/common-translations.ts +31 -0
  13. package/src/config/cookies/functional/brf/v2.ts +34 -0
  14. package/src/config/cookies/index.ts +22 -0
  15. package/src/config/cookies/necessary/brf/v2-auth.ts +51 -0
  16. package/src/config/cookies/necessary/next-auth.ts +106 -0
  17. package/src/config/cookies/types.ts +45 -0
  18. package/src/config/gui-options/html-components/ManageCookiePrefsButton.ts +121 -0
  19. package/src/config/gui-options/html-components/TilliXCallout.ts +34 -0
  20. package/src/config/gui-options/html-components/assets/CookieIcon.ts +15 -0
  21. package/src/config/gui-options/index.ts +19 -0
  22. package/src/config/gui-options/scripts/cookiePrefsButtonDragObserver.ts +180 -0
  23. package/src/config/gui-options/scripts/forceDisableReactRemoveScroll.ts +152 -0
  24. package/src/config/gui-options/scripts/injectManageCookiePrefsButton.ts +50 -0
  25. package/src/config/index.ts +15 -0
  26. package/src/config/translations/de/index.ts +103 -0
  27. package/src/config/translations/en/index.ts +103 -0
  28. package/src/config/translations/es/index.ts +103 -0
  29. package/src/config/translations/fr/index.ts +103 -0
  30. package/src/config/translations/index.ts +15 -0
  31. package/src/config/translations/it/index.ts +103 -0
  32. package/src/config/translations/types.ts +2 -0
  33. package/src/react-components/CookieConsentInit.client.tsx +34 -0
  34. package/src/react-components/CookieConsentInit.server.tsx +14 -0
  35. package/src/react-components/_types.ts +7 -0
  36. package/src/react-components/_utils.ts +62 -0
  37. package/src/react-components/styles/tenants/freeman.css +63 -0
  38. package/src/react-components/styles/tenants/frontier.css +71 -0
  39. 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;