@el7ven/cookie-kit 0.2.20 → 0.2.21
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/package.json +4 -2
- package/src/core/analytics.js +220 -0
- package/src/index.js +5 -0
- package/src/vue/CookieConsent.vue +165 -62
- package/src/vue/CookieConsentDebug.vue +168 -0
- package/src/vue/CookieDrawer.vue +274 -104
- package/src/vue/index.js +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@el7ven/cookie-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"module": "./src/index.js",
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
"./dist/styles": "./dist/styles/index.css",
|
|
12
12
|
"./react": "./src/react/index.js",
|
|
13
13
|
"./vue": "./src/vue/index.js",
|
|
14
|
-
"./
|
|
14
|
+
"./analytics": "./src/core/analytics.js",
|
|
15
|
+
"./config": "./src/core/index.js",
|
|
15
16
|
"./composables/useCookieConsent": "./src/vue/composables/useCookieConsent.js",
|
|
17
|
+
"./src/composables/useCookieConsent": "./src/vue/composables/useCookieConsent.js",
|
|
16
18
|
"./package.json": "./package.json"
|
|
17
19
|
},
|
|
18
20
|
"files": [
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @el7ven/cookie-kit - Analytics Integration Module
|
|
3
|
+
* Google Consent Mode v2, GTM, GA4, Meta Pixel, Yandex Metrica
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Initialize Google Consent Mode v2 with default denied state
|
|
8
|
+
*/
|
|
9
|
+
export function initGoogleConsentMode() {
|
|
10
|
+
if (typeof window === 'undefined') return
|
|
11
|
+
|
|
12
|
+
window.dataLayer = window.dataLayer || []
|
|
13
|
+
window.gtag = window.gtag || function gtag() { window.dataLayer.push(arguments) }
|
|
14
|
+
|
|
15
|
+
// Set default consent state - all denied until user consents
|
|
16
|
+
window.gtag('consent', 'default', {
|
|
17
|
+
ad_storage: 'denied',
|
|
18
|
+
ad_user_data: 'denied',
|
|
19
|
+
ad_personalization: 'denied',
|
|
20
|
+
analytics_storage: 'denied',
|
|
21
|
+
functionality_storage: 'denied',
|
|
22
|
+
personalization_storage: 'denied',
|
|
23
|
+
security_storage: 'granted' // Always granted for security
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Update Google Consent Mode based on cookie consent categories
|
|
29
|
+
* @param {Object} categories - { analytics: true, marketing: false, preferences: true }
|
|
30
|
+
*/
|
|
31
|
+
export function updateGoogleConsent(categories = {}) {
|
|
32
|
+
if (typeof window === 'undefined' || !window.gtag) return
|
|
33
|
+
|
|
34
|
+
const consentUpdate = {
|
|
35
|
+
analytics_storage: categories.analytics ? 'granted' : 'denied',
|
|
36
|
+
ad_storage: categories.marketing ? 'granted' : 'denied',
|
|
37
|
+
ad_user_data: categories.marketing ? 'granted' : 'denied',
|
|
38
|
+
ad_personalization: categories.marketing ? 'granted' : 'denied',
|
|
39
|
+
functionality_storage: categories.preferences ? 'granted' : 'denied',
|
|
40
|
+
personalization_storage: categories.preferences ? 'granted' : 'denied',
|
|
41
|
+
security_storage: 'granted'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
window.gtag('consent', 'update', consentUpdate)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load Google Tag Manager
|
|
49
|
+
* @param {string} gtmId - GTM container ID (e.g., 'GTM-XXXXX')
|
|
50
|
+
*/
|
|
51
|
+
export function loadGTM(gtmId) {
|
|
52
|
+
if (!gtmId || typeof document === 'undefined') return
|
|
53
|
+
|
|
54
|
+
const script = document.createElement('script')
|
|
55
|
+
script.async = true
|
|
56
|
+
script.src = `https://www.googletagmanager.com/gtm.js?id=${gtmId}`
|
|
57
|
+
document.head.appendChild(script)
|
|
58
|
+
|
|
59
|
+
window.dataLayer = window.dataLayer || []
|
|
60
|
+
window.dataLayer.push({
|
|
61
|
+
'gtm.start': new Date().getTime(),
|
|
62
|
+
event: 'gtm.js'
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Load Google Analytics 4
|
|
68
|
+
* @param {string} ga4Id - GA4 measurement ID (e.g., 'G-XXXXX')
|
|
69
|
+
*/
|
|
70
|
+
export function loadGA4(ga4Id) {
|
|
71
|
+
if (!ga4Id || typeof document === 'undefined') return
|
|
72
|
+
|
|
73
|
+
const script = document.createElement('script')
|
|
74
|
+
script.async = true
|
|
75
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${ga4Id}`
|
|
76
|
+
document.head.appendChild(script)
|
|
77
|
+
|
|
78
|
+
window.dataLayer = window.dataLayer || []
|
|
79
|
+
window.gtag = window.gtag || function gtag() { window.dataLayer.push(arguments) }
|
|
80
|
+
window.gtag('js', new Date())
|
|
81
|
+
window.gtag('config', ga4Id)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Load Meta (Facebook) Pixel
|
|
86
|
+
* @param {string} pixelId - Meta Pixel ID
|
|
87
|
+
*/
|
|
88
|
+
export function loadMetaPixel(pixelId) {
|
|
89
|
+
if (!pixelId || typeof window === 'undefined') return
|
|
90
|
+
|
|
91
|
+
/* eslint-disable */
|
|
92
|
+
!function(f,b,e,v,n,t,s)
|
|
93
|
+
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
|
94
|
+
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
|
|
95
|
+
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
|
|
96
|
+
n.queue=[];t=b.createElement(e);t.async=!0;
|
|
97
|
+
t.src=v;s=b.getElementsByTagName(e)[0];
|
|
98
|
+
s.parentNode.insertBefore(t,s)}(window, document,'script',
|
|
99
|
+
'https://connect.facebook.net/en_US/fbevents.js');
|
|
100
|
+
/* eslint-enable */
|
|
101
|
+
|
|
102
|
+
window.fbq('init', pixelId)
|
|
103
|
+
window.fbq('track', 'PageView')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load Yandex Metrica
|
|
108
|
+
* @param {string} counterId - Yandex Metrica counter ID
|
|
109
|
+
*/
|
|
110
|
+
export function loadYandexMetrica(counterId) {
|
|
111
|
+
if (!counterId || typeof document === 'undefined') return
|
|
112
|
+
|
|
113
|
+
/* eslint-disable */
|
|
114
|
+
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
|
115
|
+
m[i].l=1*new Date();
|
|
116
|
+
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
|
117
|
+
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
|
|
118
|
+
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
|
|
119
|
+
/* eslint-enable */
|
|
120
|
+
|
|
121
|
+
window.ym(counterId, 'init', {
|
|
122
|
+
clickmap: true,
|
|
123
|
+
trackLinks: true,
|
|
124
|
+
accurateTrackBounce: true,
|
|
125
|
+
webvisor: true
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Block/unblock scripts based on consent categories
|
|
131
|
+
* Scripts with data-cookie-category attribute will be managed
|
|
132
|
+
* @param {Object} categories - { analytics: true, marketing: false }
|
|
133
|
+
*/
|
|
134
|
+
export function manageScriptBlocking(categories = {}) {
|
|
135
|
+
if (typeof document === 'undefined') return
|
|
136
|
+
|
|
137
|
+
document.querySelectorAll('script[data-cookie-category]').forEach(script => {
|
|
138
|
+
const category = script.getAttribute('data-cookie-category')
|
|
139
|
+
if (categories[category]) {
|
|
140
|
+
// Re-enable script
|
|
141
|
+
if (script.type === 'text/plain' && script.dataset.originalSrc) {
|
|
142
|
+
const newScript = document.createElement('script')
|
|
143
|
+
newScript.src = script.dataset.originalSrc
|
|
144
|
+
newScript.async = true
|
|
145
|
+
script.parentNode.replaceChild(newScript, script)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Create analytics manager that auto-integrates with cookie consent
|
|
153
|
+
* @param {Object} config - { ga4: 'G-XXX', gtm: 'GTM-XXX', metaPixel: 'XXX', yandexMetrica: 'XXX' }
|
|
154
|
+
* @returns {Object} Analytics manager instance
|
|
155
|
+
*/
|
|
156
|
+
export function createAnalyticsManager(config = {}) {
|
|
157
|
+
let initialized = false
|
|
158
|
+
|
|
159
|
+
const init = () => {
|
|
160
|
+
if (initialized) return
|
|
161
|
+
initialized = true
|
|
162
|
+
|
|
163
|
+
// Always init Google Consent Mode first (denied by default)
|
|
164
|
+
initGoogleConsentMode()
|
|
165
|
+
|
|
166
|
+
// Load GTM if configured (it respects consent mode)
|
|
167
|
+
if (config.gtm) loadGTM(config.gtm)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const onConsentUpdate = (categories = {}) => {
|
|
171
|
+
// Update Google Consent Mode
|
|
172
|
+
updateGoogleConsent(categories)
|
|
173
|
+
|
|
174
|
+
// Load analytics scripts based on consent
|
|
175
|
+
if (categories.analytics) {
|
|
176
|
+
if (config.ga4) loadGA4(config.ga4)
|
|
177
|
+
if (config.yandexMetrica) loadYandexMetrica(config.yandexMetrica)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (categories.marketing) {
|
|
181
|
+
if (config.metaPixel) loadMetaPixel(config.metaPixel)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Manage script blocking
|
|
185
|
+
manageScriptBlocking(categories)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const connectToCookieConsent = () => {
|
|
189
|
+
if (typeof window === 'undefined') return
|
|
190
|
+
|
|
191
|
+
// Wait for CookieConsent to be available
|
|
192
|
+
const checkInterval = setInterval(() => {
|
|
193
|
+
if (window.CookieConsent) {
|
|
194
|
+
clearInterval(checkInterval)
|
|
195
|
+
|
|
196
|
+
// Check if consent already given
|
|
197
|
+
const currentConsent = window.CookieConsent.getConsent()
|
|
198
|
+
if (currentConsent?.categories) {
|
|
199
|
+
onConsentUpdate(currentConsent.categories)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Listen for future changes
|
|
203
|
+
window.CookieConsent.on('consentChanged', ({ consent }) => {
|
|
204
|
+
if (consent?.categories) {
|
|
205
|
+
onConsentUpdate(consent.categories)
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}, 100)
|
|
210
|
+
|
|
211
|
+
// Stop checking after 10 seconds
|
|
212
|
+
setTimeout(() => clearInterval(checkInterval), 10000)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
init,
|
|
217
|
+
onConsentUpdate,
|
|
218
|
+
connectToCookieConsent
|
|
219
|
+
}
|
|
220
|
+
}
|
package/src/index.js
CHANGED
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
// Core functionality
|
|
9
9
|
export { createCookieKit, CookieKitCore, DEFAULT_CONFIG } from './core/index.js'
|
|
10
10
|
|
|
11
|
+
// Analytics integration
|
|
12
|
+
export { createAnalyticsManager, initGoogleConsentMode, updateGoogleConsent, loadGTM, loadGA4, loadMetaPixel, loadYandexMetrica, manageScriptBlocking } from './core/analytics.js'
|
|
13
|
+
|
|
11
14
|
// Import for default export
|
|
12
15
|
import { createCookieKit, CookieKitCore, DEFAULT_CONFIG } from './core/index.js'
|
|
16
|
+
import { createAnalyticsManager } from './core/analytics.js'
|
|
13
17
|
import { initCookieKit } from './js/index.js'
|
|
14
18
|
import { VERSION } from './core/version.js'
|
|
15
19
|
|
|
@@ -62,6 +66,7 @@ export function autoCookieConsent(config = {}) {
|
|
|
62
66
|
export default {
|
|
63
67
|
createCookieKit,
|
|
64
68
|
createCookieConsent,
|
|
69
|
+
createAnalyticsManager,
|
|
65
70
|
autoCookieConsent,
|
|
66
71
|
initCookieKit,
|
|
67
72
|
CookieKitCore,
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="cookie-consent" :data-cookie-kit-theme="theme" :style="themeVarsStyle">
|
|
3
|
-
<!-- Cookie Drawer (Unified Component) -->
|
|
4
3
|
<CookieDrawer
|
|
5
4
|
ref="drawerRef"
|
|
6
5
|
:is-visible="isVisible"
|
|
7
6
|
:is-settings-mode="isSettingsMode"
|
|
8
7
|
:current-tab="currentTab"
|
|
9
8
|
:categories="categories"
|
|
10
|
-
:config="
|
|
9
|
+
:config="mergedConfig"
|
|
11
10
|
:consent-version="consentVersion"
|
|
12
11
|
:capabilities="capabilities"
|
|
13
12
|
:is-v2="isV2"
|
|
@@ -23,88 +22,129 @@
|
|
|
23
22
|
</template>
|
|
24
23
|
|
|
25
24
|
<script setup>
|
|
26
|
-
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
|
25
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
27
26
|
import { useCookieKitVue, DEFAULT_CONFIG } from './index.js'
|
|
28
27
|
import CookieDrawer from './CookieDrawer.vue'
|
|
29
28
|
|
|
30
|
-
// Props for customization
|
|
31
29
|
const props = defineProps({
|
|
32
30
|
config: {
|
|
33
31
|
type: Object,
|
|
34
|
-
default: () =>
|
|
32
|
+
default: () => ({})
|
|
35
33
|
}
|
|
36
34
|
})
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
const emit = defineEmits(['consentChanged', 'consentCleared'])
|
|
37
|
+
|
|
38
|
+
// Deep merge user config with defaults
|
|
39
|
+
const mergedConfig = computed(() => deepMerge(DEFAULT_CONFIG, props.config))
|
|
40
|
+
|
|
41
|
+
// Use the Vue composable with merged config
|
|
39
42
|
const {
|
|
43
|
+
core,
|
|
40
44
|
consent,
|
|
45
|
+
categories: coreCategories,
|
|
41
46
|
acceptAll,
|
|
42
47
|
rejectAll,
|
|
43
48
|
acceptSelected,
|
|
44
49
|
hasConsented,
|
|
45
|
-
hasCategoryConsent
|
|
46
|
-
|
|
50
|
+
hasCategoryConsent,
|
|
51
|
+
resetConsent
|
|
52
|
+
} = useCookieKitVue(mergedConfig.value)
|
|
47
53
|
|
|
48
|
-
// Local state
|
|
49
|
-
const isVisible =
|
|
54
|
+
// Local UI state
|
|
55
|
+
const isVisible = ref(false)
|
|
50
56
|
const isSettingsMode = ref(false)
|
|
51
57
|
const currentTab = ref('privacy')
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
58
|
+
const drawerRef = ref(null)
|
|
59
|
+
|
|
60
|
+
// Category state (reactive, for toggles)
|
|
61
|
+
const categories = ref(buildCategoryState(mergedConfig.value.categories))
|
|
62
|
+
|
|
63
|
+
// Computed
|
|
64
|
+
const consentVersion = computed(() => mergedConfig.value.version || 'v2')
|
|
65
|
+
const capabilities = computed(() => mergedConfig.value.capabilities || {})
|
|
66
|
+
const isV2 = computed(() => consentVersion.value === 'v2')
|
|
67
|
+
const theme = computed(() => mergedConfig.value.theme || 'light')
|
|
57
68
|
const themeVarsStyle = computed(() => {
|
|
58
|
-
const vars =
|
|
69
|
+
const vars = mergedConfig.value.themeVars || {}
|
|
59
70
|
return Object.fromEntries(
|
|
60
|
-
Object.entries(vars).map(([
|
|
71
|
+
Object.entries(vars).map(([k, v]) => [k.startsWith('--') ? k : `--${k}`, v])
|
|
61
72
|
)
|
|
62
73
|
})
|
|
63
|
-
const drawerRef = ref(null)
|
|
64
74
|
|
|
65
75
|
const enabledSettingsTabs = computed(() => {
|
|
66
|
-
return Object.keys(
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
return Object.keys(mergedConfig.value.categories || {}).filter(id =>
|
|
77
|
+
mergedConfig.value.categories[id]?.enabled
|
|
78
|
+
)
|
|
69
79
|
})
|
|
70
80
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const closeSettings = () => {
|
|
76
|
-
if (consent.value?.hasConsented) {
|
|
77
|
-
return
|
|
81
|
+
// Show/hide logic
|
|
82
|
+
onMounted(() => {
|
|
83
|
+
if (!hasConsented.value && mergedConfig.value.autoShow !== false) {
|
|
84
|
+
isVisible.value = true
|
|
78
85
|
}
|
|
79
|
-
isSettingsMode.value = false
|
|
80
|
-
}
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// Listen for consent events from core
|
|
88
|
+
if (core) {
|
|
89
|
+
core.on('consentChanged', (data) => {
|
|
90
|
+
emit('consentChanged', data)
|
|
91
|
+
})
|
|
92
|
+
core.on('consentCleared', () => {
|
|
93
|
+
isVisible.value = true
|
|
94
|
+
emit('consentCleared')
|
|
95
|
+
})
|
|
96
|
+
}
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
// Expose global API
|
|
99
|
+
if (typeof window !== 'undefined' && mergedConfig.value.mountGlobal !== false) {
|
|
100
|
+
window.CookieConsent = {
|
|
101
|
+
hasConsent: () => hasConsented.value,
|
|
102
|
+
hasCategoryConsent: (cat) => hasCategoryConsent(cat),
|
|
103
|
+
getConsent: () => consent.value,
|
|
104
|
+
acceptAll: () => handleAcceptAll(),
|
|
105
|
+
rejectAll: () => handleRejectAll(),
|
|
106
|
+
acceptSelected: (ids) => acceptSelected(ids),
|
|
107
|
+
clearConsent: () => { resetConsent(); isVisible.value = true },
|
|
108
|
+
show: () => { isVisible.value = true },
|
|
109
|
+
showSettings: () => { isSettingsMode.value = true; isVisible.value = true },
|
|
110
|
+
on: (event, cb) => core?.on(event, cb),
|
|
111
|
+
off: (event, cb) => core?.off(event, cb)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
})
|
|
93
115
|
|
|
116
|
+
// Focus trap
|
|
94
117
|
const getDrawerDialogElement = () => {
|
|
95
118
|
const exposed = drawerRef.value?.dialogElement
|
|
96
|
-
|
|
97
|
-
return result
|
|
119
|
+
return typeof exposed === 'function' ? exposed() : exposed?.value || exposed || null
|
|
98
120
|
}
|
|
99
121
|
|
|
100
|
-
|
|
122
|
+
let previousActiveElement = null
|
|
123
|
+
|
|
101
124
|
watch(isVisible, async (visible) => {
|
|
102
125
|
if (visible) {
|
|
126
|
+
previousActiveElement = document.activeElement
|
|
127
|
+
await nextTick()
|
|
128
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
129
|
+
const el = getDrawerDialogElement()
|
|
130
|
+
if (el) {
|
|
131
|
+
const focusable = el.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
|
132
|
+
if (focusable.length) focusable[0].focus()
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
if (previousActiveElement?.focus) previousActiveElement.focus()
|
|
136
|
+
previousActiveElement = null
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
watch([isSettingsMode, currentTab], async () => {
|
|
141
|
+
if (isVisible.value) {
|
|
103
142
|
await nextTick()
|
|
104
143
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
105
|
-
const
|
|
106
|
-
if (
|
|
107
|
-
|
|
144
|
+
const el = getDrawerDialogElement()
|
|
145
|
+
if (el) {
|
|
146
|
+
const focusable = el.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])')
|
|
147
|
+
if (focusable.length) focusable[0].focus()
|
|
108
148
|
}
|
|
109
149
|
}
|
|
110
150
|
})
|
|
@@ -112,58 +152,121 @@ watch(isVisible, async (visible) => {
|
|
|
112
152
|
// Event handlers
|
|
113
153
|
const handleAcceptAll = () => {
|
|
114
154
|
acceptAll()
|
|
155
|
+
isVisible.value = false
|
|
156
|
+
isSettingsMode.value = false
|
|
115
157
|
}
|
|
116
158
|
|
|
117
159
|
const handleRejectAll = () => {
|
|
118
160
|
rejectAll()
|
|
161
|
+
isVisible.value = false
|
|
162
|
+
isSettingsMode.value = false
|
|
119
163
|
}
|
|
120
164
|
|
|
121
165
|
const handleAcceptSelection = () => {
|
|
122
|
-
|
|
166
|
+
const mode = mergedConfig.value.mode
|
|
167
|
+
|
|
168
|
+
if (mode === 'essential') {
|
|
169
|
+
handleAcceptAll()
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!isSettingsMode.value) {
|
|
174
|
+
const selectedIds = Object.keys(categories.value).filter(id => categories.value[id])
|
|
175
|
+
acceptSelected(selectedIds)
|
|
176
|
+
isVisible.value = false
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Settings mode: navigate tabs or save
|
|
181
|
+
const tabs = enabledSettingsTabs.value
|
|
182
|
+
if (currentTab.value === 'privacy' && tabs.length) {
|
|
183
|
+
currentTab.value = tabs[0]
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const idx = tabs.indexOf(currentTab.value)
|
|
188
|
+
if (idx >= 0 && idx < tabs.length - 1) {
|
|
189
|
+
currentTab.value = tabs[idx + 1]
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const selectedIds = Object.keys(categories.value).filter(id => categories.value[id])
|
|
194
|
+
acceptSelected(selectedIds)
|
|
195
|
+
isVisible.value = false
|
|
196
|
+
isSettingsMode.value = false
|
|
123
197
|
}
|
|
124
198
|
|
|
125
199
|
const handleOpenSettings = () => {
|
|
126
|
-
|
|
200
|
+
isSettingsMode.value = true
|
|
127
201
|
}
|
|
128
202
|
|
|
129
203
|
const handleSelectTab = (tabId) => {
|
|
130
|
-
|
|
204
|
+
currentTab.value = tabId
|
|
131
205
|
}
|
|
132
206
|
|
|
133
207
|
const handleToggleCategory = (categoryId) => {
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
136
|
-
categories.value[categoryId] =
|
|
208
|
+
const cat = mergedConfig.value.categories[categoryId]
|
|
209
|
+
if (cat && !cat.locked && !cat.required) {
|
|
210
|
+
categories.value[categoryId] = !categories.value[categoryId]
|
|
137
211
|
}
|
|
138
212
|
}
|
|
139
213
|
|
|
140
214
|
const handleClose = () => {
|
|
141
215
|
if (isSettingsMode.value) {
|
|
142
|
-
|
|
216
|
+
if (hasConsented.value) {
|
|
217
|
+
isVisible.value = false
|
|
218
|
+
isSettingsMode.value = false
|
|
219
|
+
} else {
|
|
220
|
+
isSettingsMode.value = false
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
if (mergedConfig.value.mode === 'essential') {
|
|
224
|
+
handleAcceptAll()
|
|
225
|
+
}
|
|
143
226
|
}
|
|
144
227
|
}
|
|
145
228
|
|
|
146
|
-
// Cleanup on unmount
|
|
147
229
|
onUnmounted(() => {
|
|
148
|
-
|
|
230
|
+
if (typeof document !== 'undefined') {
|
|
231
|
+
document.body.style.overflow = ''
|
|
232
|
+
}
|
|
233
|
+
if (typeof window !== 'undefined') {
|
|
234
|
+
delete window.CookieConsent
|
|
235
|
+
}
|
|
149
236
|
})
|
|
150
237
|
|
|
151
238
|
// Expose methods for external use
|
|
152
239
|
defineExpose({
|
|
153
240
|
acceptAll: handleAcceptAll,
|
|
154
241
|
rejectAll: handleRejectAll,
|
|
155
|
-
resetConsent
|
|
156
|
-
|
|
157
|
-
}
|
|
242
|
+
resetConsent,
|
|
243
|
+
show: () => { isVisible.value = true },
|
|
244
|
+
showSettings: () => { isSettingsMode.value = true; isVisible.value = true }
|
|
158
245
|
})
|
|
246
|
+
|
|
247
|
+
// Helpers
|
|
248
|
+
function buildCategoryState(cats) {
|
|
249
|
+
if (!cats) return {}
|
|
250
|
+
return Object.fromEntries(
|
|
251
|
+
Object.keys(cats).map(id => [id, !!cats[id].enabled])
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function deepMerge(target, source) {
|
|
256
|
+
const result = { ...target }
|
|
257
|
+
for (const key of Object.keys(source)) {
|
|
258
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
259
|
+
result[key] = deepMerge(target[key] || {}, source[key])
|
|
260
|
+
} else {
|
|
261
|
+
result[key] = source[key]
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return result
|
|
265
|
+
}
|
|
159
266
|
</script>
|
|
160
267
|
|
|
161
268
|
<style lang="scss" scoped>
|
|
162
269
|
.cookie-consent {
|
|
163
270
|
position: relative;
|
|
164
|
-
|
|
165
|
-
&--has-modal {
|
|
166
|
-
min-height: 100vh;
|
|
167
|
-
}
|
|
168
271
|
}
|
|
169
272
|
</style>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="shouldShow" class="cookie-consent-debug">
|
|
3
|
+
<button
|
|
4
|
+
@click="showPanel = !showPanel"
|
|
5
|
+
class="cookie-consent-debug__button"
|
|
6
|
+
title="Cookie Consent Debug"
|
|
7
|
+
>
|
|
8
|
+
🍪
|
|
9
|
+
</button>
|
|
10
|
+
|
|
11
|
+
<div v-if="showPanel" class="cookie-consent-debug__panel">
|
|
12
|
+
<div class="cookie-consent-debug__header">
|
|
13
|
+
<h4>Cookie Consent Debug</h4>
|
|
14
|
+
<button @click="showPanel = false" class="cookie-consent-debug__close">×</button>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="cookie-consent-debug__info">
|
|
17
|
+
<p><strong>Status:</strong> {{ consentStatus }}</p>
|
|
18
|
+
<p v-for="(status, cat) in categoryStatuses" :key="cat">
|
|
19
|
+
<strong>{{ cat }}:</strong> {{ status }}
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="cookie-consent-debug__actions">
|
|
24
|
+
<button @click="doAcceptAll" class="cookie-consent-debug__btn cookie-consent-debug__btn--accept">Accept All</button>
|
|
25
|
+
<button @click="doRejectAll" class="cookie-consent-debug__btn cookie-consent-debug__btn--reject">Reject All</button>
|
|
26
|
+
<button @click="doClear" class="cookie-consent-debug__btn cookie-consent-debug__btn--clear">Clear</button>
|
|
27
|
+
<button @click="doShow" class="cookie-consent-debug__btn cookie-consent-debug__btn--show">Show Banner</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup>
|
|
34
|
+
import { ref, computed } from 'vue'
|
|
35
|
+
|
|
36
|
+
const props = defineProps({
|
|
37
|
+
debug: { type: Boolean, default: false },
|
|
38
|
+
categories: { type: Array, default: () => ['necessary', 'analytics', 'marketing', 'preferences'] }
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const showPanel = ref(false)
|
|
42
|
+
|
|
43
|
+
const shouldShow = computed(() => {
|
|
44
|
+
if (props.debug) return true
|
|
45
|
+
if (typeof window === 'undefined') return false
|
|
46
|
+
const host = window.location?.hostname || ''
|
|
47
|
+
return host === 'localhost' || host.includes('test') || host.includes('.local')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const consentStatus = computed(() => {
|
|
51
|
+
return window.CookieConsent?.hasConsent() ? 'Given' : 'Not given'
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const categoryStatuses = computed(() => {
|
|
55
|
+
const result = {}
|
|
56
|
+
props.categories.forEach(cat => {
|
|
57
|
+
result[cat] = window.CookieConsent?.hasCategoryConsent(cat) ? 'Enabled' : 'Disabled'
|
|
58
|
+
})
|
|
59
|
+
return result
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const doAcceptAll = () => window.CookieConsent?.acceptAll()
|
|
63
|
+
const doRejectAll = () => window.CookieConsent?.rejectAll()
|
|
64
|
+
const doClear = () => window.CookieConsent?.clearConsent()
|
|
65
|
+
const doShow = () => window.CookieConsent?.show()
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<style scoped>
|
|
69
|
+
.cookie-consent-debug {
|
|
70
|
+
position: fixed;
|
|
71
|
+
bottom: 20px;
|
|
72
|
+
right: 20px;
|
|
73
|
+
z-index: 10000;
|
|
74
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.cookie-consent-debug__button {
|
|
78
|
+
width: 40px;
|
|
79
|
+
height: 40px;
|
|
80
|
+
border-radius: 50%;
|
|
81
|
+
background: #0026aa;
|
|
82
|
+
color: white;
|
|
83
|
+
border: none;
|
|
84
|
+
cursor: pointer;
|
|
85
|
+
font-size: 16px;
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
justify-content: center;
|
|
89
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
90
|
+
transition: all 0.2s;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.cookie-consent-debug__button:hover {
|
|
94
|
+
background: #001d88;
|
|
95
|
+
transform: scale(1.1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.cookie-consent-debug__panel {
|
|
99
|
+
position: absolute;
|
|
100
|
+
bottom: 50px;
|
|
101
|
+
right: 0;
|
|
102
|
+
background: white;
|
|
103
|
+
border: 1px solid #e0e0e0;
|
|
104
|
+
border-radius: 8px;
|
|
105
|
+
padding: 16px;
|
|
106
|
+
min-width: 250px;
|
|
107
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.cookie-consent-debug__header {
|
|
111
|
+
display: flex;
|
|
112
|
+
justify-content: space-between;
|
|
113
|
+
align-items: center;
|
|
114
|
+
margin-bottom: 12px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.cookie-consent-debug__header h4 {
|
|
118
|
+
margin: 0;
|
|
119
|
+
font-size: 14px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
color: #333;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.cookie-consent-debug__close {
|
|
125
|
+
background: none;
|
|
126
|
+
border: none;
|
|
127
|
+
font-size: 18px;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
color: #666;
|
|
130
|
+
padding: 0 4px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.cookie-consent-debug__info p {
|
|
134
|
+
margin: 4px 0;
|
|
135
|
+
font-size: 12px;
|
|
136
|
+
color: #666;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.cookie-consent-debug__actions {
|
|
140
|
+
display: flex;
|
|
141
|
+
flex-direction: column;
|
|
142
|
+
gap: 6px;
|
|
143
|
+
margin-top: 12px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.cookie-consent-debug__btn {
|
|
147
|
+
padding: 6px 12px;
|
|
148
|
+
border: none;
|
|
149
|
+
border-radius: 4px;
|
|
150
|
+
font-size: 11px;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
transition: all 0.2s;
|
|
153
|
+
color: white;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.cookie-consent-debug__btn--accept { background: #28a745; }
|
|
157
|
+
.cookie-consent-debug__btn--accept:hover { background: #218838; }
|
|
158
|
+
.cookie-consent-debug__btn--reject { background: #dc3545; }
|
|
159
|
+
.cookie-consent-debug__btn--reject:hover { background: #c82333; }
|
|
160
|
+
.cookie-consent-debug__btn--clear { background: #ffc107; color: #212529; }
|
|
161
|
+
.cookie-consent-debug__btn--clear:hover { background: #e0a800; }
|
|
162
|
+
.cookie-consent-debug__btn--show { background: #17a2b8; }
|
|
163
|
+
.cookie-consent-debug__btn--show:hover { background: #138496; }
|
|
164
|
+
|
|
165
|
+
@media (max-width: 767px) {
|
|
166
|
+
.cookie-consent-debug { display: none; }
|
|
167
|
+
}
|
|
168
|
+
</style>
|
package/src/vue/CookieDrawer.vue
CHANGED
|
@@ -17,17 +17,17 @@
|
|
|
17
17
|
<div ref="dialogRef" class="cookie-drawer__content" role="dialog" aria-modal="true" @click.stop>
|
|
18
18
|
<!-- Header -->
|
|
19
19
|
<div class="cookie-drawer__header">
|
|
20
|
-
<
|
|
20
|
+
<div class="cookie-drawer__title">
|
|
21
21
|
{{ isSettingsMode
|
|
22
22
|
? (config.texts?.settings?.title || 'Cookie Settings')
|
|
23
23
|
: (config.mode === 'essential'
|
|
24
24
|
? (config.texts?.essential?.title || 'Essential Cookies')
|
|
25
25
|
: (config.texts?.gdpr?.title || 'Cookie Consent'))
|
|
26
26
|
}}
|
|
27
|
-
</
|
|
28
|
-
|
|
27
|
+
</div>
|
|
28
|
+
<!-- Message -->
|
|
29
29
|
<div v-if="!isSettingsMode && !isEssentialBanner" class="cookie-drawer__message">
|
|
30
|
-
|
|
30
|
+
{{ config.texts?.gdpr?.message || 'We use cookies to enhance your experience.' }}
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
33
|
<button
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
class="cookie-drawer__close"
|
|
37
37
|
aria-label="Close"
|
|
38
38
|
>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
40
|
+
<path d="M6.40013 18.3079L5.69238 17.6001L11.2924 12.0001L5.69238 6.40013L6.40013 5.69238L12.0001 11.2924L17.6001 5.69238L18.3079 6.40013L12.7079 12.0001L18.3079 17.6001L17.6001 18.3079L12.0001 12.7079L6.40013 18.3079Z" fill="currentColor"/>
|
|
41
|
+
</svg>
|
|
42
42
|
</button>
|
|
43
43
|
</div>
|
|
44
44
|
|
|
@@ -46,101 +46,191 @@
|
|
|
46
46
|
<div v-if="!isSettingsMode && isEssentialBanner" class="cookie-drawer__banner">
|
|
47
47
|
<div class="cookie-drawer__essential">
|
|
48
48
|
<div class="cookie-drawer__message cookie-drawer__message--essential">
|
|
49
|
-
{{ config.texts?.essential?.message || '
|
|
50
|
-
</div>
|
|
51
|
-
<div class="cookie-drawer__actions">
|
|
52
|
-
<button @click="$emit('acceptAll')" class="cookie-drawer__button cookie-drawer__button--primary">
|
|
53
|
-
{{ config.texts?.essential?.accept || 'Accept' }}
|
|
54
|
-
</button>
|
|
49
|
+
{{ config.texts?.essential?.message || 'This site uses only essential cookies.' }}
|
|
55
50
|
</div>
|
|
56
51
|
</div>
|
|
57
52
|
</div>
|
|
58
53
|
|
|
59
|
-
<!-- GDPR Banner
|
|
60
|
-
<div v-else class="cookie-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<div class="cookie-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
54
|
+
<!-- GDPR Banner -->
|
|
55
|
+
<div v-else-if="!isSettingsMode" class="cookie-drawer__banner">
|
|
56
|
+
<div class="cookie-drawer__gdpr">
|
|
57
|
+
<!-- Categories -->
|
|
58
|
+
<div class="cookie-drawer__categories">
|
|
59
|
+
<div
|
|
60
|
+
v-for="category in enabledCategories"
|
|
61
|
+
:key="category.id"
|
|
62
|
+
class="cookie-drawer__category"
|
|
63
|
+
>
|
|
64
|
+
<div class="cookie-drawer__category-header">
|
|
65
|
+
<span class="cookie-drawer__category-name">
|
|
66
|
+
{{ category.label }}
|
|
67
|
+
</span>
|
|
68
|
+
<div class="cookie-drawer__toggle">
|
|
69
|
+
<input
|
|
70
|
+
type="checkbox"
|
|
71
|
+
:id="`drawer-category-${category.id}`"
|
|
72
|
+
:checked="categories[category.id]"
|
|
73
|
+
@change="toggleCategory(category.id)"
|
|
74
|
+
:disabled="category.locked"
|
|
75
|
+
class="cookie-drawer__toggle-input"
|
|
76
|
+
/>
|
|
77
|
+
<label
|
|
78
|
+
:for="`drawer-category-${category.id}`"
|
|
79
|
+
:class="[
|
|
80
|
+
'cookie-drawer__toggle-label',
|
|
81
|
+
{ 'cookie-drawer__toggle-label--disabled': category.locked }
|
|
82
|
+
]"
|
|
83
|
+
></label>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div v-if="category.descriptionShow" class="cookie-drawer__category-description">
|
|
87
|
+
{{ category.description }}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
76
90
|
</div>
|
|
77
|
-
</div>
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<!-- Tabs -->
|
|
82
|
-
<div v-if="enabledSettingsTabs.length > 1" class="cookie-drawer__tabs">
|
|
92
|
+
<!-- Settings Link -->
|
|
93
|
+
<div class="cookie-drawer__settings-link">
|
|
83
94
|
<button
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
:class="['cookie-drawer__tab', { 'cookie-drawer__tab--active': currentTab === tab }]"
|
|
87
|
-
@click="$emit('selectTab', tab)"
|
|
95
|
+
@click="openSettings"
|
|
96
|
+
class="cookie-drawer__settings-button"
|
|
88
97
|
>
|
|
89
|
-
{{
|
|
98
|
+
{{ config.texts?.buttons?.settings || config.texts?.gdpr?.buttons?.settings || 'Settings' }}
|
|
90
99
|
</button>
|
|
91
100
|
</div>
|
|
92
101
|
|
|
93
|
-
<!--
|
|
94
|
-
<div class="cookie-
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
</
|
|
102
|
+
<!-- Capabilities -->
|
|
103
|
+
<div v-if="isV2" class="cookie-drawer__capabilities" aria-live="polite">
|
|
104
|
+
<span v-if="capabilities.gdpr" class="cookie-drawer__capability">GDPR</span>
|
|
105
|
+
<span v-if="capabilities.ccpa" class="cookie-drawer__capability">CCPA</span>
|
|
106
|
+
<span v-if="capabilities.pipeda" class="cookie-drawer__capability">PIPEDA</span>
|
|
107
|
+
<span v-if="capabilities.iabTcf22" class="cookie-drawer__capability">IAB TCF v2.2</span>
|
|
108
|
+
<span v-if="capabilities.googleConsentModeV2" class="cookie-drawer__capability">Google Consent Mode v2</span>
|
|
109
|
+
<span v-if="capabilities.scriptBlocking" class="cookie-drawer__capability">Script Blocking</span>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
99
113
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
<!-- Settings Mode -->
|
|
115
|
+
<div v-else class="cookie-drawer__settings">
|
|
116
|
+
<!-- Tabs -->
|
|
117
|
+
<div class="cookie-drawer__tabs">
|
|
118
|
+
<button
|
|
119
|
+
v-for="tab in availableTabs"
|
|
120
|
+
:key="tab.id"
|
|
121
|
+
@click="selectTab(tab.id)"
|
|
122
|
+
:class="[
|
|
123
|
+
'cookie-drawer__tab',
|
|
124
|
+
{ 'cookie-drawer__tab--active': currentTab === tab.id }
|
|
125
|
+
]"
|
|
126
|
+
>
|
|
127
|
+
{{ tab.label }}
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<!-- Tab Content -->
|
|
132
|
+
<div class="cookie-drawer__tab-content">
|
|
133
|
+
<!-- Privacy Tab -->
|
|
134
|
+
<div v-if="currentTab === 'privacy'" class="cookie-drawer__privacy">
|
|
135
|
+
<div class="cookie-drawer__privacy-content">
|
|
136
|
+
<div class="cookie-drawer__text">
|
|
137
|
+
{{ config.texts?.settings?.privacy?.intro || 'We use cookies and similar technologies to ensure the site works properly.' }}
|
|
115
138
|
</div>
|
|
139
|
+
<a :href="policyLinks.privacy || '#'" class="cookie-drawer__link">
|
|
140
|
+
{{ config.texts?.settings?.links?.moreInfo || 'More information' }}
|
|
141
|
+
</a>
|
|
116
142
|
</div>
|
|
117
143
|
</div>
|
|
118
144
|
|
|
119
|
-
<!--
|
|
120
|
-
<div class="cookie-
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
145
|
+
<!-- Category Tabs -->
|
|
146
|
+
<div v-else class="cookie-drawer__category">
|
|
147
|
+
<div class="cookie-drawer__category-header">
|
|
148
|
+
<div class="cookie-drawer__category-title">
|
|
149
|
+
{{ currentCategoryData.label }}
|
|
150
|
+
</div>
|
|
151
|
+
<div class="cookie-drawer__toggle">
|
|
152
|
+
<input
|
|
153
|
+
type="checkbox"
|
|
154
|
+
:id="`settings-category-${currentTab}`"
|
|
155
|
+
:checked="categories[currentTab]"
|
|
156
|
+
@change="toggleCategory(currentTab)"
|
|
157
|
+
:disabled="currentCategoryData.locked"
|
|
158
|
+
class="cookie-drawer__toggle-input"
|
|
159
|
+
/>
|
|
160
|
+
<label
|
|
161
|
+
:for="`settings-category-${currentTab}`"
|
|
162
|
+
:class="[
|
|
163
|
+
'cookie-drawer__toggle-label',
|
|
164
|
+
{ 'cookie-drawer__toggle-label--disabled': currentCategoryData.locked }
|
|
165
|
+
]"
|
|
166
|
+
></label>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="cookie-drawer__category-description">
|
|
170
|
+
{{ currentCategoryData.description }}
|
|
171
|
+
</div>
|
|
172
|
+
<a :href="policyLinks.cookies || '#'" class="cookie-drawer__link">
|
|
173
|
+
{{ config.texts?.settings?.links?.cookieDetails || 'Cookie details' }}
|
|
174
|
+
</a>
|
|
124
175
|
</div>
|
|
125
176
|
</div>
|
|
126
177
|
</div>
|
|
178
|
+
|
|
179
|
+
<!-- Actions: GDPR mode -->
|
|
180
|
+
<div v-if="!isEssentialBanner" class="cookie-drawer__actions">
|
|
181
|
+
<button
|
|
182
|
+
@click="handleAcceptSelection"
|
|
183
|
+
class="cookie-drawer__button cookie-drawer__button--outline"
|
|
184
|
+
>
|
|
185
|
+
{{ config.texts?.buttons?.acceptSelection || config.texts?.gdpr?.buttons?.acceptSelection || 'Accept Selection' }}
|
|
186
|
+
</button>
|
|
187
|
+
<button
|
|
188
|
+
@click="handleRejectAll"
|
|
189
|
+
class="cookie-drawer__button cookie-drawer__button--secondary"
|
|
190
|
+
>
|
|
191
|
+
{{ config.texts?.buttons?.rejectAll || config.texts?.gdpr?.buttons?.rejectAll || 'Reject All' }}
|
|
192
|
+
</button>
|
|
193
|
+
<button
|
|
194
|
+
@click="handleAcceptAll"
|
|
195
|
+
class="cookie-drawer__button cookie-drawer__button--primary"
|
|
196
|
+
>
|
|
197
|
+
{{ config.texts?.buttons?.acceptAll || config.texts?.gdpr?.buttons?.acceptAll || 'Accept All' }}
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
200
|
+
<!-- Actions: Essential mode -->
|
|
201
|
+
<div v-else class="cookie-drawer__actions cookie-drawer__actions_essential">
|
|
202
|
+
<button
|
|
203
|
+
@click="handleAcceptAll"
|
|
204
|
+
class="cookie-drawer__button cookie-drawer__button--primary cookie-drawer__button--essential"
|
|
205
|
+
>
|
|
206
|
+
{{ config.texts?.essential?.button || 'Accept' }}
|
|
207
|
+
</button>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
127
210
|
</div>
|
|
128
211
|
</div>
|
|
129
212
|
</Transition>
|
|
130
213
|
</template>
|
|
131
214
|
|
|
132
215
|
<script setup>
|
|
133
|
-
import { computed, ref } from 'vue'
|
|
216
|
+
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
|
|
134
217
|
|
|
135
218
|
const props = defineProps({
|
|
136
|
-
isVisible: Boolean,
|
|
137
|
-
isSettingsMode: Boolean,
|
|
138
|
-
currentTab: String,
|
|
139
|
-
categories: Object,
|
|
140
|
-
config: Object,
|
|
141
|
-
consentVersion: String,
|
|
142
|
-
capabilities: Object,
|
|
143
|
-
isV2: Boolean
|
|
219
|
+
isVisible: { type: Boolean, required: true },
|
|
220
|
+
isSettingsMode: { type: Boolean, default: false },
|
|
221
|
+
currentTab: { type: String, required: true },
|
|
222
|
+
categories: { type: Object, required: true },
|
|
223
|
+
config: { type: Object, required: true },
|
|
224
|
+
consentVersion: { type: String, default: 'v1' },
|
|
225
|
+
capabilities: { type: Object, default: () => ({}) },
|
|
226
|
+
isV2: { type: Boolean, default: false }
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const dialogRef = ref(null)
|
|
230
|
+
const windowWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1024)
|
|
231
|
+
|
|
232
|
+
defineExpose({
|
|
233
|
+
dialogElement: () => dialogRef.value
|
|
144
234
|
})
|
|
145
235
|
|
|
146
236
|
const emit = defineEmits([
|
|
@@ -153,51 +243,131 @@ const emit = defineEmits([
|
|
|
153
243
|
'rejectAll'
|
|
154
244
|
])
|
|
155
245
|
|
|
156
|
-
|
|
246
|
+
// Computed properties
|
|
247
|
+
const enabledCategories = computed(() => {
|
|
248
|
+
if (!props.config.categories) return []
|
|
249
|
+
return Object.keys(props.config.categories)
|
|
250
|
+
.filter(catId => props.config.categories[catId].enabled)
|
|
251
|
+
.map(catId => props.config.categories[catId])
|
|
252
|
+
})
|
|
157
253
|
|
|
158
|
-
const
|
|
254
|
+
const availableTabs = computed(() => {
|
|
255
|
+
const tabs = [
|
|
256
|
+
{ id: 'privacy', label: props.config.texts?.settings?.tabs?.privacy || 'Privacy' }
|
|
257
|
+
]
|
|
159
258
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
259
|
+
if (props.config.categories) {
|
|
260
|
+
Object.keys(props.config.categories).forEach(categoryId => {
|
|
261
|
+
if (props.config.categories[categoryId].enabled) {
|
|
262
|
+
tabs.push({
|
|
263
|
+
id: categoryId,
|
|
264
|
+
label: props.config.categories[categoryId].label
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
}
|
|
164
269
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}))
|
|
270
|
+
return tabs
|
|
271
|
+
})
|
|
168
272
|
|
|
169
|
-
const
|
|
170
|
-
props.
|
|
171
|
-
)
|
|
273
|
+
const currentCategoryData = computed(() => {
|
|
274
|
+
return props.config.categories?.[props.currentTab] || {}
|
|
275
|
+
})
|
|
172
276
|
|
|
173
|
-
const
|
|
174
|
-
return
|
|
175
|
-
props.categories[id]?.enabled
|
|
176
|
-
)]
|
|
277
|
+
const policyLinks = computed(() => {
|
|
278
|
+
return props.config.policies || {}
|
|
177
279
|
})
|
|
178
280
|
|
|
179
|
-
const
|
|
180
|
-
return props.
|
|
181
|
-
}
|
|
281
|
+
const isEssentialBanner = computed(() => {
|
|
282
|
+
return props.config.mode === 'essential' && !props.isSettingsMode
|
|
283
|
+
})
|
|
182
284
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
285
|
+
// Reactive mobile detection
|
|
286
|
+
const isMobile = computed(() => {
|
|
287
|
+
return windowWidth.value <= 767
|
|
288
|
+
})
|
|
186
289
|
|
|
187
|
-
const
|
|
188
|
-
if (
|
|
189
|
-
|
|
290
|
+
const handleResize = () => {
|
|
291
|
+
if (typeof window !== 'undefined') {
|
|
292
|
+
windowWidth.value = window.innerWidth
|
|
190
293
|
}
|
|
191
294
|
}
|
|
192
295
|
|
|
296
|
+
onMounted(() => {
|
|
297
|
+
if (typeof window !== 'undefined') {
|
|
298
|
+
window.addEventListener('resize', handleResize)
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
onUnmounted(() => {
|
|
303
|
+
if (typeof window !== 'undefined') {
|
|
304
|
+
window.removeEventListener('resize', handleResize)
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Root classes for essential mode
|
|
309
|
+
const rootClasses = computed(() => {
|
|
310
|
+
return {
|
|
311
|
+
'cookie-drawer--essential-mode': isEssentialBanner.value
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
// Drawer classes and animations
|
|
316
|
+
const drawerClasses = computed(() => {
|
|
317
|
+
return {
|
|
318
|
+
'cookie-drawer--mobile': isMobile.value && !isEssentialBanner.value
|
|
319
|
+
}
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
const drawerAnimation = computed(() => {
|
|
323
|
+
if (isEssentialBanner.value) return 'drawer-slide-up'
|
|
324
|
+
if (isMobile.value) return 'drawer-slide-up'
|
|
325
|
+
return 'drawer-slide-right'
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// Event handlers
|
|
193
329
|
const handleClose = () => {
|
|
194
330
|
emit('close')
|
|
195
331
|
}
|
|
196
332
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
333
|
+
const handleBackdropClick = () => {
|
|
334
|
+
emit('close')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const openSettings = () => {
|
|
338
|
+
emit('openSettings')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const selectTab = (tabId) => {
|
|
342
|
+
emit('selectTab', tabId)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const toggleCategory = (categoryId) => {
|
|
346
|
+
emit('toggleCategory', categoryId)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const handleAcceptSelection = () => {
|
|
350
|
+
emit('acceptSelection')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const handleAcceptAll = () => {
|
|
354
|
+
emit('acceptAll')
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const handleRejectAll = () => {
|
|
358
|
+
emit('rejectAll')
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Watch for drawer visibility to control body overflow
|
|
362
|
+
watch(
|
|
363
|
+
() => props.isVisible,
|
|
364
|
+
(newVal) => {
|
|
365
|
+
if (isEssentialBanner.value) return
|
|
366
|
+
if (typeof document === 'undefined') return
|
|
367
|
+
document.body.style.overflow = newVal ? 'hidden' : ''
|
|
368
|
+
},
|
|
369
|
+
{ immediate: true }
|
|
370
|
+
)
|
|
201
371
|
</script>
|
|
202
372
|
|
|
203
373
|
<style src="@el7ven/cookie-kit/dist/styles"></style>
|
package/src/vue/index.js
CHANGED
|
@@ -2,8 +2,12 @@ import { computed, ref } from 'vue'
|
|
|
2
2
|
import { createCookieKit, DEFAULT_CONFIG } from '../core/index.js'
|
|
3
3
|
import { VERSION, getVersion, getVersionInfo, logVersion } from './version.js'
|
|
4
4
|
import { useCookieConsent } from './composables/useCookieConsent.js'
|
|
5
|
+
import { createAnalyticsManager } from '../core/analytics.js'
|
|
5
6
|
|
|
6
|
-
export { createCookieKit, DEFAULT_CONFIG, VERSION, getVersion, getVersionInfo, logVersion, useCookieConsent }
|
|
7
|
+
export { createCookieKit, DEFAULT_CONFIG, VERSION, getVersion, getVersionInfo, logVersion, useCookieConsent, createAnalyticsManager }
|
|
8
|
+
export { default as CookieConsent } from './CookieConsent.vue'
|
|
9
|
+
export { default as CookieDrawer } from './CookieDrawer.vue'
|
|
10
|
+
export { default as CookieConsentDebug } from './CookieConsentDebug.vue'
|
|
7
11
|
|
|
8
12
|
export function useCookieKitVue(userConfig = {}) {
|
|
9
13
|
const core = createCookieKit(userConfig)
|