astro-consent 1.0.17 → 2.0.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/dist/index.js CHANGED
@@ -1,40 +1,52 @@
1
1
  export default function astroConsent(options = {}) {
2
2
  const siteName = options.siteName ?? "This website";
3
- const policyUrl = options.policyUrl ?? "/privacy";
3
+ const headline = options.headline ?? `Manage cookie preferences for ${siteName}`;
4
+ const description = options.description ??
5
+ "We use cookies to improve site performance, measure traffic, and support marketing.";
6
+ const acceptLabel = options.acceptLabel ?? "Accept all";
7
+ const rejectLabel = options.rejectLabel ?? "Reject all";
8
+ const manageLabel = options.manageLabel ?? "Manage preferences";
9
+ const cookiePolicyUrl = options.cookiePolicyUrl ?? options.policyUrl ?? "/cookie-policy";
10
+ const privacyPolicyUrl = options.privacyPolicyUrl ?? options.policyUrl ?? "/privacy";
11
+ const presentation = options.presentation ?? "banner";
4
12
  const consentDays = options.consent?.days ?? 30;
5
13
  const storageKey = options.consent?.storageKey ?? "astro-consent";
14
+ const displayUntilIdle = options.displayUntilIdle ?? true;
15
+ const displayIdleDelayMs = options.displayIdleDelayMs ?? 1000;
6
16
  const defaultCategories = {
7
17
  essential: true,
8
- analytics: false,
9
- marketing: false,
18
+ analytics: true,
19
+ marketing: true,
10
20
  ...options.categories
11
21
  };
12
22
  const ttl = consentDays * 24 * 60 * 60 * 1000;
23
+ const stateClass = presentation === "overlay"
24
+ ? "cb-mode-overlay"
25
+ : "cb-mode-banner";
13
26
  return {
14
27
  name: "astro-consent",
15
28
  hooks: {
16
29
  "astro:config:setup": ({ injectScript }) => {
17
- /* ─────────────────────────────────────
18
- LOAD USER CSS (required)
19
- ───────────────────────────────────── */
20
- injectScript("head-inline", `
21
- (() => {
22
- const id = "astro-consent-css";
23
- if (document.getElementById(id)) return;
24
- const link = document.createElement("link");
25
- link.id = id;
26
- link.rel = "stylesheet";
27
- link.href = "/src/cookiebanner.css";
28
- document.head.appendChild(link);
29
- })();
30
- `);
31
- /* ─────────────────────────────────────
32
- Consent runtime (NO CSS)
33
- ───────────────────────────────────── */
34
30
  injectScript("page", `
35
31
  (() => {
36
32
  const KEY = "${storageKey}";
37
33
  const TTL = ${ttl};
34
+ const SITE_NAME = ${JSON.stringify(siteName)};
35
+ const HEADLINE = ${JSON.stringify(headline)};
36
+ const DESCRIPTION = ${JSON.stringify(description)};
37
+ const ACCEPT_LABEL = ${JSON.stringify(acceptLabel)};
38
+ const REJECT_LABEL = ${JSON.stringify(rejectLabel)};
39
+ const MANAGE_LABEL = ${JSON.stringify(manageLabel)};
40
+ const DEFAULTS = ${JSON.stringify(defaultCategories)};
41
+ const PRESENTATION = ${JSON.stringify(presentation)};
42
+ const DISPLAY_UNTIL_IDLE = ${displayUntilIdle};
43
+ const DISPLAY_IDLE_DELAY_MS = ${displayIdleDelayMs};
44
+ const STATE_CLASS = ${JSON.stringify(stateClass)};
45
+ const STYLE_ID = "astro-consent-css";
46
+ const BANNER_ID = "astro-consent-banner";
47
+ const MODAL_ID = "astro-consent-modal";
48
+ const COOKIE_POLICY_URL = ${JSON.stringify(cookiePolicyUrl)};
49
+ const PRIVACY_POLICY_URL = ${JSON.stringify(privacyPolicyUrl)};
38
50
 
39
51
  function now() { return Date.now(); }
40
52
 
@@ -61,14 +73,18 @@ export default function astroConsent(options = {}) {
61
73
  }));
62
74
  }
63
75
 
64
- window.astroConsent = {
65
- get: read,
66
- set: write,
67
- reset() {
68
- localStorage.removeItem(KEY);
69
- location.reload();
70
- }
71
- };
76
+ function ensureApi() {
77
+ window.astroConsent = {
78
+ get: read,
79
+ set: write,
80
+ reset() {
81
+ localStorage.removeItem(KEY);
82
+ location.reload();
83
+ }
84
+ };
85
+ }
86
+
87
+ ensureApi();
72
88
  })();
73
89
  `);
74
90
  /* ─────────────────────────────────────
@@ -76,94 +92,220 @@ export default function astroConsent(options = {}) {
76
92
  ───────────────────────────────────── */
77
93
  injectScript("page", `
78
94
  (() => {
79
- if (window.astroConsent.get()) return;
95
+ const SITE_NAME = ${JSON.stringify(siteName)};
96
+ const HEADLINE = ${JSON.stringify(headline)};
97
+ const DESCRIPTION = ${JSON.stringify(description)};
98
+ const ACCEPT_LABEL = ${JSON.stringify(acceptLabel)};
99
+ const REJECT_LABEL = ${JSON.stringify(rejectLabel)};
100
+ const MANAGE_LABEL = ${JSON.stringify(manageLabel)};
101
+ const COOKIE_POLICY_URL = ${JSON.stringify(cookiePolicyUrl)};
102
+ const PRIVACY_POLICY_URL = ${JSON.stringify(privacyPolicyUrl)};
103
+ const DEFAULTS = ${JSON.stringify(defaultCategories)};
104
+ const PRESENTATION = ${JSON.stringify(presentation)};
105
+ const DISPLAY_UNTIL_IDLE = ${displayUntilIdle};
106
+ const DISPLAY_IDLE_DELAY_MS = ${displayIdleDelayMs};
107
+ const STATE_CLASS = ${JSON.stringify(stateClass)};
108
+ const BANNER_ID = "astro-consent-banner";
109
+ const MODAL_ID = "astro-consent-modal";
110
+ const stored = window.astroConsent.get();
111
+ const state = stored?.categories ? { ...DEFAULTS, ...stored.categories } : { ...DEFAULTS };
112
+ let lastTrigger = null;
80
113
 
81
- const state = { ...${JSON.stringify(defaultCategories)} };
114
+ function start() {
115
+ if (window.astroConsent.get()) return;
82
116
 
83
- const banner = document.createElement("div");
84
- banner.id = "astro-consent-banner";
117
+ const banner = document.createElement("div");
118
+ banner.id = BANNER_ID;
119
+ banner.className = STATE_CLASS;
120
+ banner.setAttribute("role", "dialog");
121
+ banner.setAttribute("aria-label", "Cookie consent");
122
+ banner.setAttribute("aria-live", "polite");
85
123
 
86
- banner.innerHTML = \`
87
- <div class="cb-container">
88
- <div>
89
- <div class="cb-title">${siteName} uses cookies</div>
90
- <div class="cb-desc">
91
- Choose how your data is used.
92
- <a href="${policyUrl}">Learn more</a>
124
+ banner.innerHTML = PRESENTATION === "overlay" ? "" : \`
125
+ <div class="cb-container">
126
+ <div>
127
+ <div class="cb-title">\${HEADLINE}</div>
128
+ <div class="cb-desc">
129
+ \${DESCRIPTION}
130
+ Read our <a href="\${COOKIE_POLICY_URL}">Cookie Policy</a> and
131
+ <a href="\${PRIVACY_POLICY_URL}">Privacy Policy</a>.
132
+ </div>
133
+ </div>
134
+ <div class="cb-actions">
135
+ <button class="cb-manage">\${MANAGE_LABEL}</button>
136
+ <button class="cb-reject">\${REJECT_LABEL}</button>
137
+ <button class="cb-accept">\${ACCEPT_LABEL}</button>
93
138
  </div>
94
139
  </div>
95
- <div class="cb-actions">
96
- <button class="cb-manage">Manage</button>
97
- <button class="cb-reject">Reject</button>
98
- <button class="cb-accept">Accept all</button>
99
- </div>
100
- </div>
101
- \`;
102
-
103
- document.body.appendChild(banner);
140
+ \`;
104
141
 
105
- banner.querySelector(".cb-accept").onclick = () => {
106
- window.astroConsent.set({ essential: true, analytics: true, marketing: true });
107
- banner.remove();
108
- };
142
+ if (PRESENTATION !== "overlay") {
143
+ document.body.appendChild(banner);
144
+ requestAnimationFrame(() => banner.classList.add("cb-visible"));
145
+ banner.querySelector(".cb-accept").onclick = () => {
146
+ window.astroConsent.set({ essential: true, analytics: true, marketing: true });
147
+ banner.remove();
148
+ };
109
149
 
110
- banner.querySelector(".cb-reject").onclick = () => {
111
- window.astroConsent.set({ essential: true });
112
- banner.remove();
113
- };
150
+ banner.querySelector(".cb-reject").onclick = () => {
151
+ window.astroConsent.set({ essential: true });
152
+ banner.remove();
153
+ };
114
154
 
115
- banner.querySelector(".cb-manage").onclick = openModal;
155
+ banner.querySelector(".cb-manage").onclick = openModal;
156
+ }
157
+ if (PRESENTATION === "overlay") {
158
+ openModal();
159
+ }
160
+ }
116
161
 
117
162
  function openModal() {
163
+ if (document.getElementById(MODAL_ID)) return;
164
+ const previousOverflow = document.body.style.overflow;
165
+ const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
166
+ lastTrigger = previousFocus;
167
+ document.body.style.overflow = "hidden";
168
+
118
169
  const modal = document.createElement("div");
119
- modal.id = "astro-consent-modal";
170
+ modal.id = MODAL_ID;
171
+ modal.setAttribute("role", "dialog");
172
+ modal.setAttribute("aria-modal", "true");
173
+ modal.setAttribute("aria-labelledby", "astro-consent-title");
120
174
 
121
175
  modal.innerHTML = \`
122
176
  <div class="cb-modal">
123
- <h3>Cookie preferences</h3>
124
-
125
- <div class="cb-row">
126
- <span>Essential</span>
127
- <strong>Always on</strong>
177
+ <div class="cb-modal-header">
178
+ <h3 id="astro-consent-title">\${HEADLINE}</h3>
179
+ <p>
180
+ \${DESCRIPTION}
181
+ Read our <a href="\${COOKIE_POLICY_URL}">Cookie Policy</a> and
182
+ <a href="\${PRIVACY_POLICY_URL}">Privacy Policy</a>.
183
+ </p>
128
184
  </div>
129
-
130
- <div class="cb-row">
131
- <span>Analytics</span>
132
- <div class="cb-toggle" data-key="analytics"><span></span></div>
185
+ <div class="cb-panel">
186
+ <div class="cb-row">
187
+ <span>Essential</span>
188
+ <strong>Always on</strong>
189
+ </div>
190
+ <div class="cb-row">
191
+ <span>Analytics</span>
192
+ <button class="cb-toggle" type="button" data-key="analytics" aria-pressed="false"></button>
193
+ </div>
194
+ <div class="cb-row">
195
+ <span>Marketing</span>
196
+ <button class="cb-toggle" type="button" data-key="marketing" aria-pressed="false"></button>
197
+ </div>
133
198
  </div>
134
-
135
- <div class="cb-row">
136
- <span>Marketing</span>
137
- <div class="cb-toggle" data-key="marketing"><span></span></div>
138
- </div>
139
-
140
- <div class="cb-actions">
141
- <button class="cb-accept">Save preferences</button>
199
+ <div class="cb-actions cb-actions-modal">
200
+ <button class="cb-reject" type="button">\${REJECT_LABEL}</button>
201
+ <button class="cb-accept" type="button">\${ACCEPT_LABEL}</button>
142
202
  </div>
143
203
  </div>
144
204
  \`;
145
205
 
146
206
  document.body.appendChild(modal);
207
+ requestAnimationFrame(() => modal.classList.add("cb-visible"));
208
+
209
+ function closeModal(restoreFocus = true) {
210
+ modal.classList.remove("cb-visible");
211
+ modal.remove();
212
+ document.body.style.overflow = previousOverflow;
213
+ if (restoreFocus) {
214
+ const target = lastTrigger;
215
+ window.setTimeout(() => target?.focus(), 0);
216
+ }
217
+ }
218
+
219
+ function onKeyDown(event) {
220
+ if (event.key === "Escape") {
221
+ event.preventDefault();
222
+ closeModal();
223
+ document.removeEventListener("keydown", onKeyDown);
224
+ return;
225
+ }
226
+
227
+ if (event.key !== "Tab") return;
228
+
229
+ const focusables = Array.from(
230
+ modal.querySelectorAll(
231
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
232
+ )
233
+ ).filter(
234
+ el => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden")
235
+ );
236
+
237
+ if (focusables.length === 0) {
238
+ event.preventDefault();
239
+ return;
240
+ }
241
+
242
+ const first = focusables[0];
243
+ const last = focusables[focusables.length - 1];
244
+ const active = document.activeElement;
245
+
246
+ if (event.shiftKey && active === first) {
247
+ event.preventDefault();
248
+ last.focus();
249
+ } else if (!event.shiftKey && active === last) {
250
+ event.preventDefault();
251
+ first.focus();
252
+ }
253
+ }
254
+
255
+ document.addEventListener("keydown", onKeyDown);
147
256
 
148
257
  modal.querySelectorAll(".cb-toggle").forEach(toggle => {
149
258
  const key = toggle.getAttribute("data-key");
150
- if (state[key]) toggle.classList.add("active");
259
+ const sync = () => {
260
+ const active = Boolean(state[key]);
261
+ toggle.classList.toggle("active", active);
262
+ toggle.setAttribute("aria-pressed", String(active));
263
+ };
264
+ sync();
151
265
 
152
266
  toggle.onclick = () => {
153
267
  state[key] = !state[key];
154
- toggle.classList.toggle("active");
268
+ sync();
155
269
  };
156
270
  });
157
271
 
158
272
  modal.querySelector(".cb-accept").onclick = () => {
159
273
  window.astroConsent.set({ essential: true, ...state });
160
- modal.remove();
161
- banner.remove();
274
+ document.removeEventListener("keydown", onKeyDown);
275
+ closeModal();
276
+ };
277
+
278
+ modal.querySelector(".cb-reject").onclick = () => {
279
+ window.astroConsent.set({ essential: true, analytics: false, marketing: false });
280
+ document.removeEventListener("keydown", onKeyDown);
281
+ closeModal();
162
282
  };
163
283
 
164
284
  modal.onclick = e => {
165
- if (e.target === modal) modal.remove();
285
+ if (e.target === modal) {
286
+ document.removeEventListener("keydown", onKeyDown);
287
+ closeModal();
288
+ }
166
289
  };
290
+
291
+ modal.querySelector(".cb-accept").focus();
292
+ }
293
+
294
+ function runWhenIdle() {
295
+ if (typeof window.requestIdleCallback === "function") {
296
+ window.requestIdleCallback(() => {
297
+ window.setTimeout(start, DISPLAY_IDLE_DELAY_MS);
298
+ }, { timeout: 2000 });
299
+ return;
300
+ }
301
+
302
+ window.setTimeout(start, 300 + DISPLAY_IDLE_DELAY_MS);
303
+ }
304
+
305
+ if (DISPLAY_UNTIL_IDLE) {
306
+ runWhenIdle();
307
+ } else {
308
+ start();
167
309
  }
168
310
  })();
169
311
  `);