@el7ven/cookie-kit 0.2.20 → 0.3.1

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.
@@ -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="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: () => DEFAULT_CONFIG
32
+ default: () => ({})
35
33
  }
36
34
  })
37
35
 
38
- // Use the Vue composable
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
- } = useCookieKitVue(props.config)
50
+ hasCategoryConsent,
51
+ resetConsent
52
+ } = useCookieKitVue(mergedConfig.value)
47
53
 
48
- // Local state
49
- const isVisible = computed(() => !hasConsented.value)
54
+ // Local UI state
55
+ const isVisible = ref(false)
50
56
  const isSettingsMode = ref(false)
51
57
  const currentTab = ref('privacy')
52
- const categories = computed(() => consent.value?.categories || props.config.categories)
53
- const consentVersion = computed(() => props.config.version || '1.0.0')
54
- const capabilities = computed(() => ({}))
55
- const isV2 = computed(() => true)
56
- const theme = computed(() => props.config.theme || 'light')
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 = props.config.themeVars || {}
69
+ const vars = mergedConfig.value.themeVars || {}
59
70
  return Object.fromEntries(
60
- Object.entries(vars).map(([key, value]) => [key.startsWith('--') ? key : `--${key}`, value])
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(props.config.categories || {}).filter(categoryId => {
67
- return props.config.categories[categoryId]?.enabled
68
- })
76
+ return Object.keys(mergedConfig.value.categories || {}).filter(id =>
77
+ mergedConfig.value.categories[id]?.enabled
78
+ )
69
79
  })
70
80
 
71
- const openSettings = () => {
72
- isSettingsMode.value = true
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
- const acceptSelection = () => {
83
- // Get selected category IDs
84
- const selectedIds = Object.keys(categories.value)
85
- .filter(id => categories.value[id]?.enabled)
86
-
87
- acceptSelected(selectedIds)
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
- const selectTab = (tabId) => {
91
- currentTab.value = tabId
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
- const result = typeof exposed === 'function' ? exposed() : exposed?.value || exposed || null
97
- return result
119
+ return typeof exposed === 'function' ? exposed() : exposed?.value || exposed || null
98
120
  }
99
121
 
100
- // Focus trap logic (simplified)
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 dialogElement = getDrawerDialogElement()
106
- if (dialogElement && props.config.debug) {
107
- console.log('[CookieConsent] Focus trap activated')
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
- acceptSelection()
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
- openSettings()
200
+ isSettingsMode.value = true
127
201
  }
128
202
 
129
203
  const handleSelectTab = (tabId) => {
130
- selectTab(tabId)
204
+ currentTab.value = tabId
131
205
  }
132
206
 
133
207
  const handleToggleCategory = (categoryId) => {
134
- const current = categories.value[categoryId]
135
- if (current) {
136
- categories.value[categoryId] = { ...current, enabled: !current.enabled }
208
+ const cat = mergedConfig.value.categories[categoryId]
209
+ if (cat && !cat.disabled && !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
- closeSettings()
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
- // Add cleanup if needed
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
- // Reset logic if needed
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">&times;</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>