@ecomconsult/consentkit 0.5.11 → 0.5.13
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/README.md +336 -70
- package/npm/index.d.ts +6 -0
- package/npm/internal-stub.mjs +1 -1
- package/package.json +1 -1
- package/src/ck-core.js +40 -3
- package/src/ck-locales.js +4 -0
- package/src/ck-ui.js +178 -48
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ConsentKit
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|

|
|
5
5
|

|
|
6
6
|

|
|
@@ -24,17 +24,18 @@ Vanilla ES2020, zero dependencies, no build step.
|
|
|
24
24
|
(e.g. a direct GA4 `gtag/js` tag) cannot be intercepted before the request
|
|
25
25
|
leaves — mark those up manually; the SaaS install check points at the exact tag
|
|
26
26
|
- **UI:** banner (`bar` / `box` / `modal`), preferences panel, floating re-open
|
|
27
|
-
button, light/dark,
|
|
27
|
+
button, light/dark, 34 locales
|
|
28
28
|
- **SSR-safe:** importing on the server never touches the DOM
|
|
29
29
|
- **Equal-weight buttons, no pre-ticked boxes** — the consent invariants are
|
|
30
|
-
fixed by design, see [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
30
|
+
fixed by design, see [CONTRIBUTING.md](https://github.com/vermoh/ConsentKit/blob/main/CONTRIBUTING.md)
|
|
31
31
|
|
|
32
|
-
> **Status: prototype (v0.
|
|
33
|
-
> browser
|
|
34
|
-
>
|
|
32
|
+
> **Status: prototype (v0.5.13).** The core, the UI and the demo are verified in
|
|
33
|
+
> a browser and covered by an automated suite (`npm test`); several distribution
|
|
34
|
+
> paths are not yet tested against live systems. See
|
|
35
|
+
> [Project status](#project-status) before shipping this to production.
|
|
35
36
|
|
|
36
37
|
**Не программист?** Пошаговая инструкция по-русски, с картинками и разбором по
|
|
37
|
-
кликам: **[INSTALL.ru.md](INSTALL.ru.md)**.
|
|
38
|
+
кликам: **[INSTALL.ru.md](https://github.com/vermoh/ConsentKit/blob/main/INSTALL.ru.md)**.
|
|
38
39
|
|
|
39
40
|
## Install
|
|
40
41
|
|
|
@@ -42,10 +43,11 @@ Four ways to add ConsentKit to a site, from simplest to most integrated.
|
|
|
42
43
|
|
|
43
44
|
| # | Method | Best for | Docs |
|
|
44
45
|
|---|---|---|---|
|
|
46
|
+
| 0 | **Prebuilt block** — copy one file from `ready/` into `<head>`, nothing to install | Tilda and other site builders; no developer needed | [`ready/README.md`](https://github.com/vermoh/ConsentKit/blob/main/ready/README.md) |
|
|
45
47
|
| 1 | **Script tags** — copy `src/` to your server, three `<script>` tags in `<head>` | Any site you control | [Quickstart below](#quickstart--script-tags) |
|
|
46
48
|
| 2 | **npm** — `npm install @ecomconsult/consentkit` | Bundled apps, React | [Quickstart below](#quickstart--npm) |
|
|
47
|
-
| 3 | **WordPress plugin** — copy the plugin folder to `wp-content/plugins/`; rewrites static tracker tags server-side | WordPress / WooCommerce | [`plugins/wordpress/consentkit/`](plugins/wordpress/consentkit
|
|
48
|
-
| 4 | **Google Tag Manager** — import the container, trigger tags on consent events | Sites already running GTM | [`integrations/gtm/README.md`](integrations/gtm/README.md) |
|
|
49
|
+
| 3 | **WordPress plugin** — copy the plugin folder to `wp-content/plugins/`; rewrites static tracker tags server-side | WordPress / WooCommerce | [`plugins/wordpress/consentkit/`](https://github.com/vermoh/ConsentKit/tree/main/plugins/wordpress/consentkit) |
|
|
50
|
+
| 4 | **Google Tag Manager** — import the container, trigger tags on consent events | Sites already running GTM | [`integrations/gtm/README.md`](https://github.com/vermoh/ConsentKit/blob/main/integrations/gtm/README.md) |
|
|
49
51
|
|
|
50
52
|
```sh
|
|
51
53
|
npm install @ecomconsult/consentkit
|
|
@@ -201,6 +203,7 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
|
|
|
201
203
|
| `hostdb` | `Record<string, Category>` | — | Extra `host: category` pairs merged into the tracker database, applied before the initial scan. SaaS mode fills this from the service; `ConsentKit._extendHostDb()` does the same at any later point |
|
|
202
204
|
| `cookieTable` | `CkCookieTableEntry[]` | `[]` | Declared cookies, listed per category in the panel |
|
|
203
205
|
| `services` | `CkService[]` | `[]` | v0.5.8. Third-party services the site declares. Each gets its own toggle inside its category group in the panel, and can be refused individually — see [Services](#services). At most 50 |
|
|
206
|
+
| `branding` | `object` | absent | v0.3.5. The attribution line (and optional logo) at the foot of the banner, rendered by `src/ck-ui-branding.js`. Absent from the defaults: omit the key and nothing renders — see [Branding](#branding) |
|
|
204
207
|
|
|
205
208
|
`cookieTable` entries:
|
|
206
209
|
|
|
@@ -230,6 +233,26 @@ services: [{
|
|
|
230
233
|
}]
|
|
231
234
|
```
|
|
232
235
|
|
|
236
|
+
**How the list is laid out (v0.5.12).** A group's «N services · M cookies» line
|
|
237
|
+
is a disclosure **button**, and it is **collapsed by default** — eight services
|
|
238
|
+
no longer push the switches the visitor came for off the screen. Opening it
|
|
239
|
+
reveals the group's services and, at the end, the group's own «Which cookies
|
|
240
|
+
(N)» table. Each service is **one line**: name, vendor, and its switch on the
|
|
241
|
+
right. Everything else — the purpose, the privacy link, «Cookies it sets (N)» —
|
|
242
|
+
lives behind that row's own small «Details» disclosure. Both controls are real
|
|
243
|
+
focusable controls with a visible focus ring, and both are translated in ru, ro
|
|
244
|
+
and en (every other language falls back to en, as elsewhere).
|
|
245
|
+
|
|
246
|
+
A group with **no services** is unchanged from 0.5.7: no counter, no disclosure,
|
|
247
|
+
and its cookie table sits at the top level where it always did.
|
|
248
|
+
|
|
249
|
+
**A service in the `necessary` group gets no switch** (v0.5.12). That group
|
|
250
|
+
cannot be refused, so a control that could only ever sit at «off» would be
|
|
251
|
+
telling the visitor something untrue — the «always on» badge is rendered in its
|
|
252
|
+
place. `allowedService(id)` answers `true` for such a service unconditionally,
|
|
253
|
+
and a refusal for one is stripped from the denial map on every read and write,
|
|
254
|
+
including a record written by hand or by an older client.
|
|
255
|
+
|
|
233
256
|
What the toggle does:
|
|
234
257
|
|
|
235
258
|
- **Group off** — every service of that group is off and blocked, as before.
|
|
@@ -245,7 +268,8 @@ the block map under the row's own category, so a service host ConsentKit has
|
|
|
245
268
|
never heard of is still held back.
|
|
246
269
|
|
|
247
270
|
Refusals are stored in `ck_consent` as `services: { '<id>': false }` — denials
|
|
248
|
-
only. An id absent from the map is allowed, subject to its category.
|
|
271
|
+
only. An id absent from the map is allowed, subject to its category. A
|
|
272
|
+
`necessary` service is never in the map.
|
|
249
273
|
|
|
250
274
|
### Button appearance
|
|
251
275
|
|
|
@@ -281,8 +305,8 @@ border rule apply to *derived* values — the automatic text on a filled button,
|
|
|
281
305
|
the border and text an `outline` button takes from `theme.accent`, the link
|
|
282
306
|
colour read off the accent. A `fg`, `border` or `onAccent` you wrote yourself
|
|
283
307
|
is never repainted, however low it measures: the debug panel reports the ratio
|
|
284
|
-
and warns
|
|
285
|
-
yours. Concretely:
|
|
308
|
+
and warns that it is below the recommended floor (for example, "contrast 4.32 —
|
|
309
|
+
below the recommended 4.5"), and the choice stays yours. Concretely:
|
|
286
310
|
|
|
287
311
|
- a `fg` you set is painted as set and only measured against the fill behind
|
|
288
312
|
it; the *derived* text on a filled button still becomes white or `#161616`,
|
|
@@ -300,8 +324,9 @@ arithmetic cannot read — a CSS colour name, an `rgb()` string — is left exac
|
|
|
300
324
|
as you wrote it rather than being silently replaced.
|
|
301
325
|
|
|
302
326
|
**The preferences panel follows the banner, with no settings of its own**
|
|
303
|
-
(v0.5.11):
|
|
304
|
-
reject as `settings`, and the floating button takes the
|
|
327
|
+
(v0.5.11): the panel's «save choice» button is styled as `accept`, the panel's
|
|
328
|
+
own accept and reject as `settings`, and the floating button takes the
|
|
329
|
+
`accept` button's fill
|
|
305
330
|
(its border colour when `accept` is `outline`), while the category and service
|
|
306
331
|
switches keep `theme.accent`.
|
|
307
332
|
|
|
@@ -350,16 +375,16 @@ safe to call before the banner has mounted: a call that arrives while the UI
|
|
|
350
375
|
file is still loading is remembered and honoured on mount, so a link clicked
|
|
351
376
|
during a slow page load still works.
|
|
352
377
|
|
|
353
|
-
The same panel has an address.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
378
|
+
The same panel also has an address. Any link to a page of the site ending in
|
|
379
|
+
`#ck-settings` opens the preferences panel — both when the page loads with that
|
|
380
|
+
fragment and when the link is followed on an already-open page. The fragment is
|
|
381
|
+
then removed from the address with `history.replaceState`, so a reload or a
|
|
382
|
+
«back» does not reopen the panel. This is the address the «change your cookie
|
|
383
|
+
choice» button on a cookie declaration page points at, and the one to put in a
|
|
384
|
+
site footer:
|
|
360
385
|
|
|
361
386
|
```html
|
|
362
|
-
<a href="#ck-settings"
|
|
387
|
+
<a href="#ck-settings">Change your cookie choice</a>
|
|
363
388
|
```
|
|
364
389
|
|
|
365
390
|
### Placeholders for blocked embeds
|
|
@@ -368,7 +393,7 @@ When the engine holds back an `<iframe>` before consent — a known tracker, or
|
|
|
368
393
|
any third-party frame in strict mode — the visitor would otherwise see an empty
|
|
369
394
|
hole where a video or a map should be. Since 0.5.7 ConsentKit draws a card in
|
|
370
395
|
its place: the name of the service, the category the embed is waiting for, a
|
|
371
|
-
primary button
|
|
396
|
+
primary button ("Allow and show") that grants **that one category** and
|
|
372
397
|
loads the embed, and a link to the full settings panel.
|
|
373
398
|
|
|
374
399
|
The card is sized from the frame's own `width`/`height` (or its computed size),
|
|
@@ -408,8 +433,9 @@ decide. Membership is a claim that a host delivers the site's own assets, not
|
|
|
408
433
|
that it is harmless in general — anything that *measures* keeps a real consent
|
|
409
434
|
category instead, which is why `static.cloudflareinsights.com` (Cloudflare Web
|
|
410
435
|
Analytics) is classified as `analytics` and blocked before consent even though
|
|
411
|
-
the rest of Cloudflare's CDN is infrastructure.
|
|
412
|
-
and returned as copies, so reading them cannot
|
|
436
|
+
the rest of Cloudflare's CDN is infrastructure. The list holds **37 entries**.
|
|
437
|
+
Both lists are matched by suffix and returned as copies, so reading them cannot
|
|
438
|
+
widen what strict mode allows.
|
|
413
439
|
|
|
414
440
|
## API
|
|
415
441
|
|
|
@@ -448,6 +474,44 @@ All methods are safe to call at any time and never throw.
|
|
|
448
474
|
Already-loaded scripts are not unloaded by `withdraw()` — cookies are cleared
|
|
449
475
|
and the next page load is clean.
|
|
450
476
|
|
|
477
|
+
### Introspection
|
|
478
|
+
|
|
479
|
+
Members prefixed with `_` are **not** private-by-convention placeholders: they
|
|
480
|
+
are a deliberate read-only surface for tooling — the debug panel, the hosted
|
|
481
|
+
cabinet's theme editor, and tests — and they are documented because those
|
|
482
|
+
consumers depend on them. They are stable within a minor version, and every one
|
|
483
|
+
of them returns a copy, so reading can never widen what the engine allows.
|
|
484
|
+
|
|
485
|
+
| Member | Returns | Description |
|
|
486
|
+
|---|---|---|
|
|
487
|
+
| `_blocked()` | `array` | What the engine is currently holding back, plus a sweep of blocked markup. Drives the debug panel's list |
|
|
488
|
+
| `_categoryForUrl(url)` | `string \| null` | The category the database gives a URL — the same lookup the engine uses |
|
|
489
|
+
| `_categories` | `string[]` | The four category names, as a copy |
|
|
490
|
+
| `_services()` | `array` | The normalised service rows from the config |
|
|
491
|
+
| `_serviceForUrl(url)` | `object \| null` | Which declared service a URL belongs to |
|
|
492
|
+
| `_deniedServices()` | `string[]` | Ids the visitor refused individually |
|
|
493
|
+
| `_extendHostDb(map)` | `number` | Merge extra `host: category` pairs; returns how many were accepted — see [Extending the tracker database](#extending-the-tracker-database) |
|
|
494
|
+
| `_infra()` | `string[]` | The 37 infrastructure hosts, as a copy |
|
|
495
|
+
| `_isInfra(url)` | `boolean` | Is this URL or hostname infrastructure? |
|
|
496
|
+
| `_baseAllow` | `object` | The built-in strict-mode allowlist (hosts plus path-scoped entries), as a copy |
|
|
497
|
+
|
|
498
|
+
`ConsentKit._contrast`, published by `src/ck-ui.js`, exposes the theme
|
|
499
|
+
arithmetic as pure functions so a theme editor can show exactly the numbers the
|
|
500
|
+
banner paints rather than reimplementing them. Nothing in it touches the DOM, so
|
|
501
|
+
it is safe to call in Node:
|
|
502
|
+
|
|
503
|
+
| Group | Functions |
|
|
504
|
+
|---|---|
|
|
505
|
+
| Colour maths | `relativeLuminance`, `contrastRatio`, `ensureContrast`, `stepToContrast` |
|
|
506
|
+
| Resolution | `resolveButtonStyles`, `resolveRadius`, `resolveFont`, `pickPageFont`, `resolveDetails`, `buildThemeCss` |
|
|
507
|
+
| Font probing | `nextProbeDelay`, `shouldReprobe` |
|
|
508
|
+
| Placeholders | `placeholderText`, `placeholdersEnabled` |
|
|
509
|
+
| Services panel | `cookieRowsForService`, `looseCookies`, `servicePurpose`, `groupCountLabel`, `serviceSignature`, `signature` |
|
|
510
|
+
| Wording | `plural`, `pluralIndex`, `buildStrings`, `localeTable`, `resolveLang` |
|
|
511
|
+
|
|
512
|
+
`ConsentKit._resolvePageFont()` reports the font family the banner resolved from
|
|
513
|
+
the page.
|
|
514
|
+
|
|
451
515
|
## Events
|
|
452
516
|
|
|
453
517
|
All are `CustomEvent` on `document`, with the payload in `detail`.
|
|
@@ -470,6 +534,20 @@ document.addEventListener('ck:change', (e) => {
|
|
|
470
534
|
The core never touches the UI directly; it only dispatches these events, and the
|
|
471
535
|
UI layer only calls the public API.
|
|
472
536
|
|
|
537
|
+
### dataLayer events (GTM)
|
|
538
|
+
|
|
539
|
+
Separately from the DOM events above, `integrations.gtmDataLayer` (on by
|
|
540
|
+
default) pushes to `window.dataLayer`, which is what GTM triggers listen to:
|
|
541
|
+
|
|
542
|
+
| Push | When |
|
|
543
|
+
|---|---|
|
|
544
|
+
| `ck_consent_update` with `ck_consent: { necessary, functional, analytics, marketing }` and `ck_method` | Every decision, including `withdraw()` |
|
|
545
|
+
| `ck_consent_functional` / `ck_consent_analytics` / `ck_consent_marketing` | Once per granted category, on the decision and again on a return visit when stored consent is restored. Each fires at most once per page |
|
|
546
|
+
|
|
547
|
+
The per-category events exist so a GTM tag can trigger on exactly the category
|
|
548
|
+
it needs without reading the payload. See
|
|
549
|
+
[`integrations/gtm/README.md`](https://github.com/vermoh/ConsentKit/blob/main/integrations/gtm/README.md).
|
|
550
|
+
|
|
473
551
|
## Blocking trackers
|
|
474
552
|
|
|
475
553
|
### Manual markup
|
|
@@ -517,9 +595,23 @@ document.head.appendChild(s);
|
|
|
517
595
|
```
|
|
518
596
|
|
|
519
597
|
Blocked elements are marked `data-ck-blocked` and their URL is remembered, so
|
|
520
|
-
granting consent later loads them without a reload.
|
|
521
|
-
|
|
522
|
-
and
|
|
598
|
+
granting consent later loads them without a reload.
|
|
599
|
+
|
|
600
|
+
The database ships **116 hosts** and **11 path rules**, matched by suffix (a
|
|
601
|
+
bare registrable domain also covers its subdomains) and by substring
|
|
602
|
+
respectively:
|
|
603
|
+
|
|
604
|
+
| Table | Entries | By category |
|
|
605
|
+
|---|---|---|
|
|
606
|
+
| `HOST_DB` | 116 | 40 `functional`, 32 `marketing`, 31 `analytics`, 13 `necessary` |
|
|
607
|
+
| `PATH_DB` | 11 | 4 `marketing`, 3 `functional`, 2 `analytics`, 2 `necessary` |
|
|
608
|
+
| `INFRA_DB` | 37 | not a category — see [Infrastructure](#infrastructure) |
|
|
609
|
+
|
|
610
|
+
Recognised hosts include Google Analytics, Facebook, Yandex Metrica, Hotjar,
|
|
611
|
+
TikTok and DoubleClick. The GTM **container** is deliberately not blocked (the
|
|
612
|
+
tags inside it obey Consent Mode); `/gtag/js` is blocked by path instead. The
|
|
613
|
+
same tables are exported to the WordPress plugin, so server and browser
|
|
614
|
+
classify a host identically.
|
|
523
615
|
|
|
524
616
|
Because the patches install at parse time, `ck-core.js` must load before any
|
|
525
617
|
tracker — put it first in `<head>` and do not add `defer`.
|
|
@@ -607,8 +699,8 @@ Two limits are worth stating plainly:
|
|
|
607
699
|
|
|
608
700
|
Runtime injection is covered by the patches above. A tracker tag written
|
|
609
701
|
**directly into the HTML** is not: the parser starts that request before the
|
|
610
|
-
first line of `ck-core.js` runs. The gap was measured (debt
|
|
611
|
-
14 ms, our script at 18 ms) and it is negative — no client-side technique
|
|
702
|
+
first line of `ck-core.js` runs. The gap was measured (debt D9 in SPEC.md:
|
|
703
|
+
request at 14 ms, our script at 18 ms) and it is negative — no client-side technique
|
|
612
704
|
closes it. Such tags need either manual markup, or a server that rewrites them
|
|
613
705
|
before the page is sent.
|
|
614
706
|
|
|
@@ -649,10 +741,38 @@ Outside WordPress the same idea applies to any server-side template: emit the
|
|
|
649
741
|
|
|
650
742
|
## Google Consent Mode v2
|
|
651
743
|
|
|
652
|
-
With `integrations.gcm` (default), the core pushes `consent: default`
|
|
653
|
-
|
|
654
|
-
`
|
|
655
|
-
|
|
744
|
+
With `integrations.gcm` (the default), the core pushes `consent: default` at
|
|
745
|
+
parse time — before any tag can load — with every signal `denied` and
|
|
746
|
+
`wait_for_update: 500`, then `consent: update` after each choice. Seven signals
|
|
747
|
+
are set, always as one block:
|
|
748
|
+
|
|
749
|
+
| Signal | Follows |
|
|
750
|
+
|---|---|
|
|
751
|
+
| `analytics_storage` | `analytics` |
|
|
752
|
+
| `ad_storage`, `ad_user_data`, `ad_personalization` | `marketing` |
|
|
753
|
+
| `functionality_storage`, `personalization_storage` | `functional` |
|
|
754
|
+
| `security_storage` | always `granted` |
|
|
755
|
+
|
|
756
|
+
`integrations.gtmDataLayer` (also on by default) is an independent gate: it
|
|
757
|
+
pushes a `ck_consent_update` event carrying `ck_consent` (the four categories)
|
|
758
|
+
and `ck_method`, so GTM triggers work even with `gcm: false`.
|
|
759
|
+
|
|
760
|
+
**What "denied" actually means.** Consent Mode is Google's own mechanism, not a
|
|
761
|
+
block: a Google tag that runs under denied signals sets **no cookies and no
|
|
762
|
+
identifiers**, but it still sends *cookieless pings* to Google, and those pings
|
|
763
|
+
carry the **page URL, the referrer and the user agent**, from an IP address
|
|
764
|
+
Google necessarily sees. That is enough for Google to see the
|
|
765
|
+
visit, and in the EU an IP address is personal data. Consent Mode alone is
|
|
766
|
+
therefore not the same as not being measured.
|
|
767
|
+
|
|
768
|
+
ConsentKit's blocking engine is the part that makes the difference: a tag it
|
|
769
|
+
holds back never runs at all, so it sends nothing — no ping, no URL, no IP. The
|
|
770
|
+
two work together, and Consent Mode is the fallback for the case the engine
|
|
771
|
+
cannot cover (a tag inside a GTM container, or a static `<script src>` the
|
|
772
|
+
parser requested before ConsentKit loaded — see
|
|
773
|
+
[Static tags](#static-tags-what-the-browser-cannot-catch)). If you need "nothing
|
|
774
|
+
reaches Google before consent", rely on the blocking engine and mark such tags
|
|
775
|
+
up; do not rely on Consent Mode by itself.
|
|
656
776
|
|
|
657
777
|
## Storage
|
|
658
778
|
|
|
@@ -696,36 +816,31 @@ hosting, the cabinet and the scanner — not the line itself.
|
|
|
696
816
|
For site builders that will not let you upload files, `ready/` holds
|
|
697
817
|
ready-to-paste `<script>` blocks — copy one wholesale into `<head>`. Zero
|
|
698
818
|
external requests. Rebuild them with `tools/build-inline.mjs` (see
|
|
699
|
-
[`tools/README.md`](tools/README.md)); each block's header records the exact
|
|
819
|
+
[`tools/README.md`](https://github.com/vermoh/ConsentKit/blob/main/tools/README.md)); each block's header records the exact
|
|
700
820
|
command that produced it.
|
|
701
821
|
|
|
702
|
-
ConsentKit 0.5.
|
|
703
|
-
this roughly
|
|
704
|
-
|
|
705
|
-
|
|
822
|
+
ConsentKit 0.5.13, rebuilt 2026-09-07, uncompressed — gzip on the server cuts
|
|
823
|
+
this roughly threefold. Every block includes the branding extension and the
|
|
824
|
+
attribution line; `--no-branding` drops both the code and the config and takes
|
|
825
|
+
**~26 KB** back off:
|
|
706
826
|
|
|
707
827
|
| Block | Languages | Bytes | gzip | `--no-branding` |
|
|
708
828
|
|---|---|---|---|---|
|
|
709
|
-
| `ready/en-bar.txt` | en |
|
|
710
|
-
| `ready/ru-bar.txt` | ru, ro, en |
|
|
711
|
-
| `ready/ru-box.txt` | ru, ro, en |
|
|
712
|
-
| `ready/ru-box-right.txt` | ru, ro, en |
|
|
713
|
-
| `ready/ru-modal.txt` | ru, ro, en |
|
|
714
|
-
| `ready/eu-bar.txt` | 34 languages |
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
allowlists, the public-suffix table) and the extensible tracker database.
|
|
722
|
-
|
|
723
|
-
Size is driven almost entirely by the bundled languages: `en` and `ru` are
|
|
724
|
-
built into the UI and cost nothing extra, while layout, position, theme and
|
|
725
|
-
accent change only a few bytes of config.
|
|
829
|
+
| `ready/en-bar.txt` | en | 264,816 | 82,798 | 238,438 |
|
|
830
|
+
| `ready/ru-bar.txt` | ru, ro, en | 266,806 | 83,673 | 240,242 |
|
|
831
|
+
| `ready/ru-box.txt` | ru, ro, en | 266,821 | 83,682 | 240,257 |
|
|
832
|
+
| `ready/ru-box-right.txt` | ru, ro, en | 266,830 | 83,687 | 240,266 |
|
|
833
|
+
| `ready/ru-modal.txt` | ru, ro, en | 266,814 | 83,679 | 240,250 |
|
|
834
|
+
| `ready/eu-bar.txt` | 34 languages | 315,460 | 101,998 | 288,916 |
|
|
835
|
+
|
|
836
|
+
The blocks are dominated by the core and the UI (roughly 97 KB and 129 KB of
|
|
837
|
+
source respectively, comments included — the builder concatenates the sources
|
|
838
|
+
as they are and does not minify). Bundled languages account for the rest:
|
|
839
|
+
`en` and `ru` are built into the UI and cost nothing extra, while layout,
|
|
840
|
+
position, theme and accent change only a few bytes of config.
|
|
726
841
|
|
|
727
842
|
The debug panel is **not** in these numbers. Blocks carry a ~5.1 KB loader
|
|
728
|
-
(`src/ck-debug-loader.js`) which fetches the
|
|
843
|
+
(`src/ck-debug-loader.js`) which fetches the ~54 KB panel only when someone opens
|
|
729
844
|
the page with `?ck_debug=1` — see [Debug mode](#debug-mode). An ordinary visitor
|
|
730
845
|
downloads the loader and nothing more.
|
|
731
846
|
|
|
@@ -768,7 +883,7 @@ a support ticket.
|
|
|
768
883
|
|
|
769
884
|
### How the panel gets onto the page
|
|
770
885
|
|
|
771
|
-
The panel is ~
|
|
886
|
+
The panel is ~54 KB, and on any given page exactly one person will ever open
|
|
772
887
|
it. So `ready/*.txt` and the WordPress plugin ship **`src/ck-debug-loader.js`**
|
|
773
888
|
(~5.1 KB) instead, and the loader fetches the panel on demand. With no flag set
|
|
774
889
|
the loader creates no DOM, installs no observers and makes **no network
|
|
@@ -877,6 +992,24 @@ When the configuration contains a `log` endpoint, each decision is POSTed with
|
|
|
877
992
|
via `sendBeacon` on `pagehide`. Withdrawals are sent with `method: "withdraw"`.
|
|
878
993
|
No other network requests are made.
|
|
879
994
|
|
|
995
|
+
The payload is a **closed schema** — anything outside this list is rejected by
|
|
996
|
+
the server as a 400:
|
|
997
|
+
|
|
998
|
+
| Field | Always? | Value |
|
|
999
|
+
|---|---|---|
|
|
1000
|
+
| `siteId`, `key` | yes | The site id and the log key from the config |
|
|
1001
|
+
| `cfg` | yes | Version of the config the decision was made under |
|
|
1002
|
+
| `id`, `ts` | yes | uuid and ISO timestamp of the record. A withdrawal gets a fresh pair |
|
|
1003
|
+
| `categories` | yes | Exactly three booleans: `functional`, `analytics`, `marketing`. `necessary` is not part of the schema |
|
|
1004
|
+
| `method` | yes | `accept_all` \| `reject_all` \| `custom` \| `withdraw` |
|
|
1005
|
+
| `lang`, `layout` | when resolved | The language and layout the visitor actually saw |
|
|
1006
|
+
| `services` | only on `custom` | v0.5.8. The ids the visitor refused, sent only when the list is non-empty. Capped at 50 ids of at most 64 characters |
|
|
1007
|
+
|
|
1008
|
+
The client puts no page URL, referrer or user agent in the body. The request
|
|
1009
|
+
itself is still an ordinary HTTP request to the API host, so that host sees the
|
|
1010
|
+
connection's IP address like any server would — what the *payload* carries is
|
|
1011
|
+
the list above and nothing more.
|
|
1012
|
+
|
|
880
1013
|
To try it locally, a mock API is included:
|
|
881
1014
|
|
|
882
1015
|
```sh
|
|
@@ -884,10 +1017,134 @@ node demo/mock-api.mjs # http://localhost:8788
|
|
|
884
1017
|
# serve the repo root, then open demo/saas.html
|
|
885
1018
|
```
|
|
886
1019
|
|
|
1020
|
+
## Changelog
|
|
1021
|
+
|
|
1022
|
+
Client versions. The WordPress plugin tracks the same numbers and keeps its own
|
|
1023
|
+
notes in
|
|
1024
|
+
[`plugins/wordpress/consentkit/readme.txt`](https://github.com/vermoh/ConsentKit/blob/main/plugins/wordpress/consentkit/readme.txt).
|
|
1025
|
+
|
|
1026
|
+
### 0.5.13
|
|
1027
|
+
|
|
1028
|
+
- Host database: `fbcdn.net` → marketing (Facebook SDK chunks and plugin images); `aichat.md`, `bubble.aichat.md` → functional (chat widget); `challenges.cloudflare.com` (Cloudflare challenge / Turnstile) and `i.imgur.com` → infrastructure, never held.
|
|
1029
|
+
|
|
1030
|
+
### 0.5.12
|
|
1031
|
+
- **Services in the preferences panel are collapsed by default and compact.**
|
|
1032
|
+
The group's «N services · M cookies» line became a disclosure button; opening
|
|
1033
|
+
it reveals the services and the group's cookie table. Each service row is one
|
|
1034
|
+
line — name, vendor, switch — with the purpose, the privacy link and its
|
|
1035
|
+
cookie list behind a small «Details» disclosure inside the row.
|
|
1036
|
+
- **A service in the «necessary» group has no switch.** It carries the «always
|
|
1037
|
+
on» badge instead, `allowedService()` is unconditionally `true` for it, and a
|
|
1038
|
+
refusal for such a service is stripped from the denial map on read and write.
|
|
1039
|
+
- Tracker database: `c.bing.com`.
|
|
1040
|
+
|
|
1041
|
+
### 0.5.11
|
|
1042
|
+
- The preferences panel and the floating button follow the **banner's** buttons
|
|
1043
|
+
by role, with no theme settings of their own: «save choice» is styled as
|
|
1044
|
+
`accept`, the panel's own accept and reject as `settings`, and the floating
|
|
1045
|
+
button takes the `accept` fill (its border colour when `accept` is `outline`).
|
|
1046
|
+
|
|
1047
|
+
### 0.5.10
|
|
1048
|
+
- **A colour the site owner set is painted as set.** The 4.5:1 text rule and the
|
|
1049
|
+
3:1 border rule now correct only *derived* colours; an explicit `fg`, `border`
|
|
1050
|
+
or `onAccent` is left alone however low it measures.
|
|
1051
|
+
- `theme.light.onAccent` — the light-mode mirror of `theme.dark.onAccent`.
|
|
1052
|
+
- The debug panel reports the measured ratio and warns, instead of claiming a
|
|
1053
|
+
value was fixed automatically.
|
|
1054
|
+
|
|
1055
|
+
### 0.5.9
|
|
1056
|
+
- Tracker database: +42 entries — YouTube, Vimeo, Facebook and Instagram embeds,
|
|
1057
|
+
chats and CRM (Freshworks, Viber, Telegram, Bitrix24, amoCRM), forms and
|
|
1058
|
+
scheduling (Calendly, Typeform), payments (Stripe, PayPal, paynet.md, MAIB),
|
|
1059
|
+
Sentry, 999.md. Vercel and Netlify are classified as infrastructure.
|
|
1060
|
+
- The debug panel is honest about Consent Mode: "no cookies, but the page
|
|
1061
|
+
address and browser type are sent".
|
|
1062
|
+
|
|
1063
|
+
### 0.5.8
|
|
1064
|
+
- **Services** (`services`): a site declares individual third parties, each of
|
|
1065
|
+
which gets its own toggle inside its category group in the panel, with its own
|
|
1066
|
+
vendor, purpose, privacy link and cookies. A visitor can accept a category and
|
|
1067
|
+
still refuse one service; the refusal survives the group being switched off
|
|
1068
|
+
and on again. Refusals are stored in `ck_consent` as `services: { id: false }`
|
|
1069
|
+
and, in SaaS mode, reported in the beacon's `services` field.
|
|
1070
|
+
- `ConsentKit.allowedService(id)`.
|
|
1071
|
+
|
|
1072
|
+
### 0.5.7
|
|
1073
|
+
- Placeholders for blocked embeds: a card in place of a held-back video or map,
|
|
1074
|
+
with an "Allow and show" button that grants just that category
|
|
1075
|
+
(`blocking.placeholders`, on by default).
|
|
1076
|
+
- `#ck-settings` in a page address opens the preferences panel;
|
|
1077
|
+
`ConsentKit.openSettings()` is safe to call before the UI has mounted.
|
|
1078
|
+
- `texts.detailsAction: "declaration"` and `texts.declarationUrl`.
|
|
1079
|
+
|
|
1080
|
+
### 0.5.6
|
|
1081
|
+
- The attribution line follows the banner's language
|
|
1082
|
+
(`branding.poweredBy.texts`).
|
|
1083
|
+
|
|
1084
|
+
### 0.5.5
|
|
1085
|
+
- Banner and panel texts rewritten in plain language (ru, ro, en).
|
|
1086
|
+
|
|
1087
|
+
### 0.5.4
|
|
1088
|
+
- Database: Google Maps is `functional`; Tilda platform services and
|
|
1089
|
+
`fonts.google.com` are infrastructure; Google Ads pings
|
|
1090
|
+
(`/pagead/1p-user-list`, `/ads/ga-audiences`) are `marketing`.
|
|
1091
|
+
|
|
1092
|
+
### 0.5.3
|
|
1093
|
+
- The banner re-resolves the page font after full load (on Tilda a reload from
|
|
1094
|
+
cache left the banner in Times), and falls back to the system stack when the
|
|
1095
|
+
page font cannot be determined.
|
|
1096
|
+
|
|
1097
|
+
### 0.5.2
|
|
1098
|
+
- The banner takes its font from the page's real text rather than from `body`.
|
|
1099
|
+
|
|
1100
|
+
### 0.5.1
|
|
1101
|
+
- Database: general CDNs (gstatic.com, aspnetcdn.com, kxcdn.com, jsDelivr,
|
|
1102
|
+
cdnjs, unpkg, CloudFront and others) are infrastructure, not trackers; Google
|
|
1103
|
+
reCAPTCHA is `necessary`; Searchanise and iuteCredit are `functional`.
|
|
1104
|
+
|
|
1105
|
+
### 0.5.0
|
|
1106
|
+
- **The theme engine.** `theme.font` (the banner takes the site's font by
|
|
1107
|
+
default, `'system'` restores the old stack), `theme.radius: { card, button }`
|
|
1108
|
+
in px, and `theme.buttons` — variant, background, text, border and border
|
|
1109
|
+
width per button. Accept and reject are always equal in size, weight and
|
|
1110
|
+
variant, by construction. Contrast is checked automatically: text below 4.5:1
|
|
1111
|
+
and borders below 3:1 are corrected.
|
|
1112
|
+
- `texts.policyUrl` and `texts.detailsAction` (`policy` / `settings` / `hide`):
|
|
1113
|
+
«Learn more» now does something. Before 0.5.0 it was `<a href="#">` with no
|
|
1114
|
+
handler — any site on 0.4.x or earlier has a dead link.
|
|
1115
|
+
- The corner card gained its reference geometry: up to 540px wide, 24px padding,
|
|
1116
|
+
buttons in a row with an 8px gap and stacked on narrow screens.
|
|
1117
|
+
|
|
1118
|
+
### 0.4.1
|
|
1119
|
+
- Infrastructure list (`ConsentKit._infra()`): builder CDNs, Google Fonts and
|
|
1120
|
+
captcha, never intercepted by strict mode.
|
|
1121
|
+
`static.cloudflareinsights.com` is deliberately excluded — it is analytics.
|
|
1122
|
+
|
|
1123
|
+
### 0.4.0
|
|
1124
|
+
- **Strict mode** (`blocking.mode: 'strict'`): before consent, any third-party
|
|
1125
|
+
script or iframe that is not same-site, allow-listed or in the built-in
|
|
1126
|
+
allowlist is held back and filed as `marketing`.
|
|
1127
|
+
- `ConsentKit._extendHostDb()` and the `hostdb` config key.
|
|
1128
|
+
- `<iframe src>` is intercepted, not just scripts.
|
|
1129
|
+
|
|
1130
|
+
### 0.3.6
|
|
1131
|
+
- Branding moved to `src/ck-ui-branding.js`, so `--no-branding` drops the code
|
|
1132
|
+
as well as the config (~26 KB off a block).
|
|
1133
|
+
|
|
1134
|
+
### 0.3.5
|
|
1135
|
+
- **Server-side tracker markup in the WordPress plugin, on by default** — the
|
|
1136
|
+
one case the browser engine cannot cover. The database shipped to PHP is
|
|
1137
|
+
generated from `src/ck-core.js`, and a test fails when the two drift.
|
|
1138
|
+
- The debug panel became a lazily-loaded file behind a ~5 KB loader.
|
|
1139
|
+
|
|
1140
|
+
### 0.3.2
|
|
1141
|
+
- The GTM container is no longer blocked: tags inside it obey Consent Mode, and
|
|
1142
|
+
blocking the container breaks that. `/gtag/js` is still blocked by path.
|
|
1143
|
+
|
|
887
1144
|
## Project status
|
|
888
1145
|
|
|
889
|
-
**This is a prototype (v0.
|
|
890
|
-
has been verified and what has not.
|
|
1146
|
+
**This is a prototype (v0.5.13), not a released product.** It is honest about
|
|
1147
|
+
what has been verified and what has not.
|
|
891
1148
|
|
|
892
1149
|
### Verified
|
|
893
1150
|
|
|
@@ -899,6 +1156,11 @@ has been verified and what has not.
|
|
|
899
1156
|
- `dataLayer` event trace for consent restore, upgrade and withdrawal.
|
|
900
1157
|
- npm entry points and TypeScript types: syntax and import smoke tests in Node
|
|
901
1158
|
without a DOM.
|
|
1159
|
+
- An automated suite runs under `npm test` (`node --test test/*.test.mjs`):
|
|
1160
|
+
blocking engine, tracker-database classification and its PHP export, services,
|
|
1161
|
+
the theme and contrast arithmetic, the settings panel, branding, the debug
|
|
1162
|
+
loader and panel, the generated site, and a version guard that fails when
|
|
1163
|
+
`package.json`, `src/ck-core.js` and the blocks in `ready/` drift apart.
|
|
902
1164
|
- PHP files of the WordPress plugin pass `php -l` on 7.4, 8.3 and 8.5.
|
|
903
1165
|
- The server-side rewriting engine has its own suite of 61 cases
|
|
904
1166
|
(`plugins/wordpress/consentkit/tests/rewrite.test.php`), green on PHP 7.4,
|
|
@@ -916,16 +1178,20 @@ has been verified and what has not.
|
|
|
916
1178
|
and structurally modelled on the documented export format, but Tag Manager has
|
|
917
1179
|
not accepted it in practice; some field names (notably GA4 config
|
|
918
1180
|
`measurementId` vs `tagId`) may need correction on first import.
|
|
919
|
-
- **
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1181
|
+
- **The 32 locales of the language pack are drafts.** Only `en` and `ru` — the
|
|
1182
|
+
two built into the UI — are authored rather than translated. The rest are
|
|
1183
|
+
usable but have not been reviewed by native speakers (the weakest are `mt`,
|
|
1184
|
+
`ga`, `is`, `sq`, `mk`; `pl` mixes politeness forms). Legal wording — "Reject
|
|
1185
|
+
all", "always active" — should be checked by someone who knows the local
|
|
1186
|
+
regulator's language before you rely on it.
|
|
923
1187
|
- **There is no server-side consent log.** Consent lives only in the visitor's
|
|
924
1188
|
browser (cookie plus `localStorage`). GDPR accountability may require you to
|
|
925
1189
|
be able to *demonstrate* that consent was given; that record-keeping is not
|
|
926
1190
|
part of this prototype and you would have to build it yourself.
|
|
927
|
-
- No
|
|
928
|
-
|
|
1191
|
+
- **No end-to-end browser tests and no CI beyond the Pages deployment.** The
|
|
1192
|
+
`npm test` suite runs in Node against DOM stubs, not a real browser; browser
|
|
1193
|
+
verification is still the manual smoke checklist in
|
|
1194
|
+
[CONTRIBUTING.md](https://github.com/vermoh/ConsentKit/blob/main/CONTRIBUTING.md).
|
|
929
1195
|
- Not audited by a lawyer. ConsentKit is a technical building block, not legal
|
|
930
1196
|
advice, and it cannot make a site compliant on its own — your privacy policy,
|
|
931
1197
|
your cookie inventory and your record-keeping are still yours.
|
|
@@ -933,7 +1199,7 @@ has been verified and what has not.
|
|
|
933
1199
|
### Contributing
|
|
934
1200
|
|
|
935
1201
|
Structure of the repository, the GDPR invariants that must not change, and how
|
|
936
|
-
to run the checks: [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
1202
|
+
to run the checks: [CONTRIBUTING.md](https://github.com/vermoh/ConsentKit/blob/main/CONTRIBUTING.md).
|
|
937
1203
|
|
|
938
1204
|
## License
|
|
939
1205
|
|
|
@@ -941,8 +1207,8 @@ Copyright (c) 2026 E-COM CONSULT PLUS.
|
|
|
941
1207
|
|
|
942
1208
|
| Part | Licence |
|
|
943
1209
|
|---|---|
|
|
944
|
-
| Client (`src/`), npm package, inline builder, demo | [MIT](LICENSE) |
|
|
945
|
-
| WordPress plugin (`plugins/wordpress/consentkit/`) | [GPL-2.0-or-later](plugins/wordpress/consentkit/LICENSE) |
|
|
1210
|
+
| Client (`src/`), npm package, inline builder, demo | [MIT](https://github.com/vermoh/ConsentKit/blob/main/LICENSE) |
|
|
1211
|
+
| WordPress plugin (`plugins/wordpress/consentkit/`) | [GPL-2.0-or-later](https://github.com/vermoh/ConsentKit/blob/main/plugins/wordpress/consentkit/LICENSE) |
|
|
946
1212
|
|
|
947
1213
|
The client is MIT so it can be embedded anywhere without licence friction. The
|
|
948
1214
|
WordPress plugin ships under GPLv2+ because the WordPress ecosystem effectively
|
|
@@ -950,4 +1216,4 @@ requires it; MIT permits the plugin to bundle copies of the client in its
|
|
|
950
1216
|
`assets/` directory.
|
|
951
1217
|
|
|
952
1218
|
Contributions require a `Signed-off-by` line (DCO) — see
|
|
953
|
-
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
1219
|
+
[CONTRIBUTING.md](https://github.com/vermoh/ConsentKit/blob/main/CONTRIBUTING.md).
|
package/npm/index.d.ts
CHANGED
|
@@ -141,6 +141,12 @@ export interface CkThemeConfig {
|
|
|
141
141
|
mode?: 'auto' | 'light' | 'dark';
|
|
142
142
|
/** v0.2. Overrides the built-in dark palette. */
|
|
143
143
|
dark?: CkDarkTheme;
|
|
144
|
+
/**
|
|
145
|
+
* v0.5.10. The light-mode mirror of `dark`, for one value: an `onAccent` the
|
|
146
|
+
* owner set for the filled buttons' text in light mode. A typed value is
|
|
147
|
+
* painted as typed (contrast is a warning, never a correction).
|
|
148
|
+
*/
|
|
149
|
+
light?: { onAccent?: string };
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
/** v0.5.0 (SPEC V1.6 §2). Copy and the «Learn more» control. */
|
package/npm/internal-stub.mjs
CHANGED
|
@@ -28,7 +28,7 @@ export function undecidedState() {
|
|
|
28
28
|
*/
|
|
29
29
|
export function createStub() {
|
|
30
30
|
const stub = {
|
|
31
|
-
version: '0.5.
|
|
31
|
+
version: '0.5.13',
|
|
32
32
|
config: {},
|
|
33
33
|
init: function () { return undecidedState(); },
|
|
34
34
|
allowed: function (cat) { return cat === 'necessary'; },
|
package/package.json
CHANGED
package/src/ck-core.js
CHANGED
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
|
|
93
93
|
// --- marketing -----------------------------------------------------
|
|
94
94
|
'connect.facebook.net': 'marketing',
|
|
95
|
+
'fbcdn.net': 'marketing', // Facebook CDN: SDK chunks (static.xx) and plugin images (scontent.*)
|
|
95
96
|
'facebook.net': 'marketing', // connect.facebook.net covered above
|
|
96
97
|
'analytics.tiktok.com': 'marketing',
|
|
97
98
|
'googleadservices.com': 'marketing',
|
|
@@ -108,6 +109,7 @@
|
|
|
108
109
|
'ct.pinterest.com': 'marketing', // subdomain: pinterest.com is a normal site
|
|
109
110
|
'sc-static.net': 'marketing', // Snapchat pixel CDN
|
|
110
111
|
'bat.bing.com': 'marketing', // subdomain: bing.com is a normal site
|
|
112
|
+
'c.bing.com': 'marketing', // Bing pixel (/c.gif) fired by UET and Clarity
|
|
111
113
|
'adroll.com': 'marketing',
|
|
112
114
|
'hs-analytics.net': 'marketing', // HubSpot tracking
|
|
113
115
|
'hs-scripts.com': 'marketing',
|
|
@@ -204,6 +206,7 @@
|
|
|
204
206
|
// Booking and form embeds — the visitor came to the page to use them.
|
|
205
207
|
'calendly.com': 'functional',
|
|
206
208
|
'typeform.com': 'functional',
|
|
209
|
+
'aichat.md': 'functional', // aichat.md chat widget (bubble.aichat.md loader)
|
|
207
210
|
// 999.md — the Moldovan classifieds platform. Shops embed its listing
|
|
208
211
|
// widgets; simpalsmedia.com is the group's asset host that serves them.
|
|
209
212
|
'999.md': 'functional',
|
|
@@ -431,6 +434,9 @@
|
|
|
431
434
|
// places is classified, not waved through — so a bare `cloudflare.com` here
|
|
432
435
|
// would be a bug.
|
|
433
436
|
var INFRA_DB = [
|
|
437
|
+
// --- bot checks and image hosts: not a consent decision -------------
|
|
438
|
+
'challenges.cloudflare.com', // Cloudflare challenge / Turnstile (incl. *.challenges.cloudflare.com)
|
|
439
|
+
'i.imgur.com', // hot-linked images
|
|
434
440
|
// --- site builders and hosting platforms ---------------------------
|
|
435
441
|
'tildacdn.com',
|
|
436
442
|
'tildacdn.net',
|
|
@@ -796,13 +802,26 @@
|
|
|
796
802
|
// key that is not a service id — is dropped rather than trusted: this record
|
|
797
803
|
// is attacker-writable (it lives in a cookie and in localStorage), and a
|
|
798
804
|
// malformed entry must not be able to widen or narrow what gets blocked.
|
|
805
|
+
//
|
|
806
|
+
// 0.5.12 — a service in the `necessary` group is ALSO dropped, on read and on
|
|
807
|
+
// write alike. Such a service has no switch in the panel (the group is always
|
|
808
|
+
// on, so a control that could only ever refuse it would be a lie), and
|
|
809
|
+
// allowedService() answers `true` for it unconditionally — so a denial for one
|
|
810
|
+
// could only ever be dead weight that survives in the record and confuses the
|
|
811
|
+
// beacon and the debug report. The filter is safe here because init() runs
|
|
812
|
+
// buildServices() BEFORE loadRecord(), so SERVICE_BY_ID is already populated
|
|
813
|
+
// when a stored map is read back; an id the registry does not know is left
|
|
814
|
+
// alone, exactly as before, because nothing can be claimed about its category.
|
|
799
815
|
function readServices(raw) {
|
|
800
816
|
var out = {};
|
|
801
817
|
if (!isPlainObject(raw)) { return out; }
|
|
802
818
|
var keys = Object.keys(raw);
|
|
803
819
|
for (var i = 0; i < keys.length && i < SERVICE_MAX; i++) {
|
|
804
820
|
var k = keys[i];
|
|
805
|
-
if (raw[k]
|
|
821
|
+
if (raw[k] !== false || !SERVICE_ID_RE.test(k)) { continue; }
|
|
822
|
+
var s = SERVICE_BY_ID[k];
|
|
823
|
+
if (s && s.category === 'necessary') { continue; }
|
|
824
|
+
out[k] = false;
|
|
806
825
|
}
|
|
807
826
|
return out;
|
|
808
827
|
}
|
|
@@ -1201,6 +1220,14 @@
|
|
|
1201
1220
|
// silent block of something the visitor was never asked about.
|
|
1202
1221
|
function serviceDenied(id) {
|
|
1203
1222
|
if (!id || typeof id !== 'string') { return false; }
|
|
1223
|
+
/* 0.5.12 — a service in the `necessary` group is NEVER denied, whatever the
|
|
1224
|
+
map says. readServices() already strips such an entry on every read and
|
|
1225
|
+
write, so this is the single choke point that also covers a map mutated
|
|
1226
|
+
by some other path: deniedForSrc() (the blocking patches), and
|
|
1227
|
+
deniedServiceIds() (the beacon field and the debug report) both ask this
|
|
1228
|
+
question, so answering it once here keeps the three from drifting. */
|
|
1229
|
+
var s = SERVICE_BY_ID[id];
|
|
1230
|
+
if (s && s.category === 'necessary') { return false; }
|
|
1204
1231
|
return state.services[id] === false;
|
|
1205
1232
|
}
|
|
1206
1233
|
|
|
@@ -1212,6 +1239,13 @@
|
|
|
1212
1239
|
function allowedService(id) {
|
|
1213
1240
|
var s = SERVICE_BY_ID[id];
|
|
1214
1241
|
if (!s) { return true; } // unknown id: nothing to withhold
|
|
1242
|
+
/* 0.5.12 — a `necessary` service is allowed, full stop. The group cannot be
|
|
1243
|
+
switched off (allowed('necessary') is always true), the panel renders no
|
|
1244
|
+
switch for it, and readServices() drops any denial that reaches the map.
|
|
1245
|
+
This is the belt to that braces: a denial arriving by some path neither
|
|
1246
|
+
covers — a stale in-memory map, a caller reaching past accept() — must
|
|
1247
|
+
still not be able to hold back something the visitor cannot re-enable. */
|
|
1248
|
+
if (s.category === 'necessary') { return true; }
|
|
1215
1249
|
if (!allowed(s.category)) { return false; }
|
|
1216
1250
|
return !serviceDenied(id);
|
|
1217
1251
|
}
|
|
@@ -1229,7 +1263,10 @@
|
|
|
1229
1263
|
function deniedServiceIds() {
|
|
1230
1264
|
var out = [];
|
|
1231
1265
|
for (var i = 0; i < SERVICES.length; i++) {
|
|
1232
|
-
|
|
1266
|
+
// Through serviceDenied(), not the raw map: that is the one place the
|
|
1267
|
+
// `necessary` rule lives, so the beacon and the debug report can never
|
|
1268
|
+
// name a service the blocking engine is in fact allowing.
|
|
1269
|
+
if (serviceDenied(SERVICES[i].id)) { out.push(SERVICES[i].id); }
|
|
1233
1270
|
}
|
|
1234
1271
|
return out;
|
|
1235
1272
|
}
|
|
@@ -1941,7 +1978,7 @@
|
|
|
1941
1978
|
// Public API
|
|
1942
1979
|
// ---------------------------------------------------------------------------
|
|
1943
1980
|
var ConsentKit = {
|
|
1944
|
-
version: '0.5.
|
|
1981
|
+
version: '0.5.13',
|
|
1945
1982
|
config: config,
|
|
1946
1983
|
|
|
1947
1984
|
init: function (userConfig) {
|
package/src/ck-locales.js
CHANGED
|
@@ -584,6 +584,10 @@
|
|
|
584
584
|
ckCount: ['{n} cookie', '{n} cookie-uri', '{n} de cookie-uri'],
|
|
585
585
|
svcPolicy: 'Politica',
|
|
586
586
|
svcCookies: 'Ce cookie-uri pune',
|
|
587
|
+
// 0.5.12 — dezvăluirea din rând. NU `more`, care aici este o propoziție
|
|
588
|
+
// («Aflați mai multe»), nu un control de rând.
|
|
589
|
+
svcDetails: 'Detalii',
|
|
590
|
+
svcListLabel: 'Servicii și cookie-uri din acest grup',
|
|
587
591
|
cat: {
|
|
588
592
|
necessary: { title: 'Necesare', desc: 'Sunt necesare pentru funcționarea site-ului: autentificare, securitate, reținerea alegerii dumneavoastră. Nu pot fi dezactivate.' },
|
|
589
593
|
functional: { title: 'Funcționale', desc: 'Rețin alegerile dumneavoastră: limba, coșul, chatul.' },
|
package/src/ck-ui.js
CHANGED
|
@@ -44,6 +44,14 @@
|
|
|
44
44
|
// The service's own cookies, listed under it rather than in the group's
|
|
45
45
|
// «Which cookies» table.
|
|
46
46
|
svcCookies: 'Cookies it sets',
|
|
47
|
+
// 0.5.12 — the per-row disclosure. A row is one line until this is opened;
|
|
48
|
+
// the purpose, the policy link and the service's cookie table live behind
|
|
49
|
+
// it. NOT `more`: that string is the banner's «Подробнее» and reads
|
|
50
|
+
// «Aflați mai multe» in ro, which is a sentence, not a row control.
|
|
51
|
+
svcDetails: 'Details',
|
|
52
|
+
// The group's own disclosure: how many services are hidden behind the
|
|
53
|
+
// «N services · M cookies» button. Only ever read by a screen reader.
|
|
54
|
+
svcListLabel: 'Services and cookies in this group',
|
|
47
55
|
// SPEC V1.10 §2 — the blocked-embed placeholder. {host} is the vendor
|
|
48
56
|
// label when the database knows one and the bare host otherwise; {cat} is
|
|
49
57
|
// the localized category title, taken from cat.<name>.title below, so the
|
|
@@ -97,6 +105,8 @@
|
|
|
97
105
|
ckCount: ['{n} cookie', '{n} cookie', '{n} cookie'],
|
|
98
106
|
svcPolicy: 'Политика',
|
|
99
107
|
svcCookies: 'Какие cookie ставит',
|
|
108
|
+
svcDetails: 'Подробнее',
|
|
109
|
+
svcListLabel: 'Сервисы и cookie этой группы',
|
|
100
110
|
phText: 'Здесь содержимое от {host}. Оно загрузится после согласия на «{cat}».',
|
|
101
111
|
phAllow: 'Разрешить и показать',
|
|
102
112
|
phSettings: 'Настроить cookie',
|
|
@@ -142,7 +152,12 @@
|
|
|
142
152
|
// branch in buildStrings() — listing them here would be a bug, because this
|
|
143
153
|
// loop only copies values that are `typeof === 'string'` and would leave
|
|
144
154
|
// every external locale on the English plurals.
|
|
145
|
-
'svcPolicy', 'svcCookies'
|
|
155
|
+
'svcPolicy', 'svcCookies',
|
|
156
|
+
// 0.5.12 — the per-row «Подробнее» disclosure and the accessible name of the
|
|
157
|
+
// group's own one. Both are plain strings, so they belong here: a key left
|
|
158
|
+
// out of this list is `undefined` for all 32 external locales and renders
|
|
159
|
+
// the literal word "undefined" on the card.
|
|
160
|
+
'svcDetails', 'svcListLabel'
|
|
146
161
|
];
|
|
147
162
|
|
|
148
163
|
// Plural-form keys, filled separately from STR_KEYS (see above).
|
|
@@ -461,21 +476,50 @@
|
|
|
461
476
|
/* SPEC V1.12 §3 — «N сервисов · M cookie». --ck-muted, like every other
|
|
462
477
|
secondary label on the card, and it is measured for AA against the card
|
|
463
478
|
background by the same rule the description above answers to. */
|
|
479
|
+
/* 0.5.12 — the counter is now the group's disclosure BUTTON. It keeps the
|
|
480
|
+
muted label's size and colour (it is still a secondary label, measured
|
|
481
|
+
for AA against the card background by the same rule as the description),
|
|
482
|
+
and gains a caret and a hit area. `border:0;background:transparent` is
|
|
483
|
+
needed because the panel's reset styles buttons, not because a link is
|
|
484
|
+
being faked — it is a real button and the global :focus-visible rule
|
|
485
|
+
gives it a visible ring. */
|
|
464
486
|
'.ck-cat__count{font-size:12px;font-weight:500;color:var(--ck-muted)}',
|
|
487
|
+
'.ck-cat__toggle{border:0;background:transparent;padding:2px 0;cursor:pointer;',
|
|
488
|
+
'display:inline-flex;align-items:center;gap:5px;font-family:inherit;text-align:left}',
|
|
489
|
+
'.ck-cat__caret{width:0;height:0;flex:none;border:4px solid transparent;',
|
|
490
|
+
'border-left-color:currentColor;border-right:0}',
|
|
491
|
+
'.ck-cat__toggle[aria-expanded="true"] .ck-cat__caret{transform:rotate(90deg)}',
|
|
465
492
|
|
|
466
|
-
/* ---- services inside a group (SPEC V1.12 §3) ---- */
|
|
493
|
+
/* ---- services inside a group (SPEC V1.12 §3, compacted in 0.5.12) ---- */
|
|
467
494
|
/* Indented and rule-separated so the nesting reads without colour: a
|
|
468
495
|
service belongs to the group above it, and its own cookie table belongs
|
|
469
496
|
to it. The left border is the only decoration; everything else is
|
|
470
497
|
spacing, which survives forced-colours mode intact. */
|
|
471
|
-
'.ck-
|
|
472
|
-
'.ck-
|
|
498
|
+
'.ck-cat__region{margin-top:12px}',
|
|
499
|
+
'.ck-svcs{padding-left:12px;border-left:2px solid var(--ck-line)}',
|
|
500
|
+
'.ck-svc{padding:8px 0;border-bottom:1px solid var(--ck-line)}',
|
|
473
501
|
'.ck-svc:first-child{padding-top:2px}',
|
|
474
502
|
'.ck-svc:last-child{border-bottom:0;padding-bottom:2px}',
|
|
475
|
-
|
|
503
|
+
/* One line: name, vendor, then the switch pinned to the right. `center`
|
|
504
|
+
rather than flex-start, because the row is a single line of text now and
|
|
505
|
+
the switch reads as belonging to it. */
|
|
506
|
+
'.ck-svc__top{display:flex;gap:10px;align-items:center}',
|
|
476
507
|
'.ck-svc__txt{flex:1 1 auto;min-width:0}',
|
|
477
|
-
|
|
478
|
-
|
|
508
|
+
/* The name and the vendor share a line and WRAP rather than truncate: at
|
|
509
|
+
320px a long «Google Analytics · Google Ireland Limited» becomes two
|
|
510
|
+
lines and the switch stays where it is, still reachable. */
|
|
511
|
+
'.ck-svc__name{display:flex;align-items:baseline;gap:6px;flex-wrap:wrap;',
|
|
512
|
+
'font-size:14px;font-weight:600;line-height:1.35}',
|
|
513
|
+
'.ck-svc__vendor{margin:0;font-size:12.5px;font-weight:400;color:var(--ck-muted)}',
|
|
514
|
+
/* «всегда активны» on a necessary service's row, where its switch used to
|
|
515
|
+
be: the same pill the group header wears, one step smaller. */
|
|
516
|
+
'.ck-svc__badge{font-size:11px;font-weight:500;color:var(--ck-muted);',
|
|
517
|
+
'border:1px solid var(--ck-line);border-radius:999px;padding:0 7px}',
|
|
518
|
+
/* The row's own «Подробнее». Tighter than a group's <details> — it sits
|
|
519
|
+
inside a row, not between two of them. */
|
|
520
|
+
'.ck-svc__det{margin-top:2px}',
|
|
521
|
+
'.ck-svc__det>summary{font-size:12.5px}',
|
|
522
|
+
'.ck-svc__body{padding:2px 0 4px}',
|
|
479
523
|
'.ck-svc__desc{margin:4px 0 0;font-size:13px;color:var(--ck-muted)}',
|
|
480
524
|
/* --ck-link, not --ck-accent: accent-coloured TEXT goes through the same
|
|
481
525
|
>= 4.5:1 rule as «Подробнее» — see the note on the `a{}` rule above. */
|
|
@@ -1708,7 +1752,23 @@
|
|
|
1708
1752
|
}
|
|
1709
1753
|
}
|
|
1710
1754
|
|
|
1755
|
+
/* SPEC V1.12 §3, reshaped in 0.5.12 — one service, on ONE line.
|
|
1756
|
+
|
|
1757
|
+
0.5.8 rendered the name, the vendor, the purpose paragraph, the policy link
|
|
1758
|
+
and a cookie table for every service, all at once. Eight services filled the
|
|
1759
|
+
panel and the visitor scrolled past the switches they came for. The row is
|
|
1760
|
+
now: name (bold) · vendor (muted) · switch, and nothing else until the row's
|
|
1761
|
+
own «Подробнее» is opened — behind which the purpose, the policy link and
|
|
1762
|
+
«Какие cookie ставит (N)» live, unchanged.
|
|
1763
|
+
|
|
1764
|
+
`cat === 'necessary'` renders NO switch. The group is always on and cannot
|
|
1765
|
+
be refused, so a control that could only ever sit at «off» (which is what
|
|
1766
|
+
0.5.11 showed, because syncGroup() only ever ran for the opt-in groups)
|
|
1767
|
+
tells the visitor a lie about what they can change. The «всегда активны»
|
|
1768
|
+
badge the group header already carries says the true thing instead. */
|
|
1711
1769
|
function buildService(svc, cat, rows) {
|
|
1770
|
+
var locked = cat === 'necessary';
|
|
1771
|
+
|
|
1712
1772
|
var wrap = el('div', 'ck-svc');
|
|
1713
1773
|
var top = el('div', 'ck-svc__top');
|
|
1714
1774
|
var txt = el('div', 'ck-svc__txt');
|
|
@@ -1718,45 +1778,68 @@
|
|
|
1718
1778
|
var nameSpan = el('span', null, svc.name);
|
|
1719
1779
|
nameSpan.id = nameId;
|
|
1720
1780
|
name.appendChild(nameSpan);
|
|
1781
|
+
// Vendor on the SAME line as the name now, not a paragraph under it. The
|
|
1782
|
+
// row wraps rather than truncates, so a long pair still reads at 320px.
|
|
1783
|
+
if (svc.vendor) name.appendChild(el('span', 'ck-svc__vendor', svc.vendor));
|
|
1784
|
+
if (locked) name.appendChild(el('span', 'ck-svc__badge', T.alwaysOn));
|
|
1721
1785
|
txt.appendChild(name);
|
|
1722
1786
|
|
|
1723
|
-
|
|
1787
|
+
top.appendChild(txt);
|
|
1724
1788
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
// The link text is the same word on every row, so a screen reader needs
|
|
1743
|
-
// the service name to tell them apart.
|
|
1744
|
-
a.setAttribute('aria-label', T.svcPolicy + ' — ' + svc.name);
|
|
1745
|
-
txt.appendChild(a);
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
var sw = makeServiceSwitch(svc, cat);
|
|
1749
|
-
sw.setAttribute('aria-labelledby', nameId);
|
|
1750
|
-
if (descId) sw.setAttribute('aria-describedby', descId);
|
|
1751
|
-
if (!serviceSwitches[cat]) serviceSwitches[cat] = [];
|
|
1752
|
-
serviceSwitches[cat].push(sw);
|
|
1789
|
+
/* No switch for a necessary service — and no entry in serviceSwitches[],
|
|
1790
|
+
which is what keeps readSwitches() from ever writing such an id into the
|
|
1791
|
+
denial map. (It iterates OPT_IN, so it could not today; not registering
|
|
1792
|
+
the switch means it still cannot if that loop ever widens.) */
|
|
1793
|
+
if (!locked) {
|
|
1794
|
+
var sw = makeServiceSwitch(svc, cat);
|
|
1795
|
+
/* aria-labelledby only. 0.5.8 also pointed aria-describedby at the purpose
|
|
1796
|
+
paragraph; in 0.5.12 that paragraph lives inside a CLOSED <details>, and
|
|
1797
|
+
a description a screen reader cannot reach until the visitor opens
|
|
1798
|
+
something else is worse than none — the name and the switch's own state
|
|
1799
|
+
are what the control has to announce. The purpose is still there, one
|
|
1800
|
+
«Подробнее» away, as its own readable text. */
|
|
1801
|
+
sw.setAttribute('aria-labelledby', nameId);
|
|
1802
|
+
if (!serviceSwitches[cat]) serviceSwitches[cat] = [];
|
|
1803
|
+
serviceSwitches[cat].push(sw);
|
|
1804
|
+
top.appendChild(sw);
|
|
1805
|
+
}
|
|
1753
1806
|
|
|
1754
|
-
top.appendChild(txt);
|
|
1755
|
-
top.appendChild(sw);
|
|
1756
1807
|
wrap.appendChild(top);
|
|
1757
1808
|
|
|
1809
|
+
/* Everything else goes behind the row's own disclosure. <details>/<summary>
|
|
1810
|
+
rather than a button: the keyboard behaviour, the focus ring and the
|
|
1811
|
+
expanded state come from the browser, and it is the same control the
|
|
1812
|
+
cookie tables have used since 0.5.7. */
|
|
1813
|
+
var purpose = servicePurpose(svc);
|
|
1758
1814
|
var own = cookieRowsForService(rows, svc);
|
|
1759
|
-
if (
|
|
1815
|
+
if (purpose || svc.privacyUrl || own.length) {
|
|
1816
|
+
var det = el('details', 'ck-det ck-svc__det');
|
|
1817
|
+
var sum = el('summary');
|
|
1818
|
+
sum.appendChild(document.createTextNode(T.svcDetails));
|
|
1819
|
+
// The word is the same on every row, so a screen reader needs the service
|
|
1820
|
+
// name to tell one «Подробнее» from the next.
|
|
1821
|
+
sum.setAttribute('aria-label', T.svcDetails + ' — ' + svc.name);
|
|
1822
|
+
det.appendChild(sum);
|
|
1823
|
+
|
|
1824
|
+
var body = el('div', 'ck-svc__body');
|
|
1825
|
+
if (purpose) body.appendChild(el('p', 'ck-svc__desc', purpose));
|
|
1826
|
+
|
|
1827
|
+
/* «Политика» — target=_blank rel=noopener, per §3. The URL is already
|
|
1828
|
+
http(s)-validated by the core's normalizeService(), which is where a
|
|
1829
|
+
javascript: address is dropped; nothing unvalidated reaches an href. */
|
|
1830
|
+
if (svc.privacyUrl) {
|
|
1831
|
+
var a = el('a', 'ck-svc__policy', T.svcPolicy);
|
|
1832
|
+
a.href = svc.privacyUrl;
|
|
1833
|
+
a.target = '_blank';
|
|
1834
|
+
a.rel = 'noopener noreferrer';
|
|
1835
|
+
a.setAttribute('aria-label', T.svcPolicy + ' — ' + svc.name);
|
|
1836
|
+
body.appendChild(a);
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
if (own.length) body.appendChild(cookieTable(own, T.svcCookies + ' (' + own.length + ')'));
|
|
1840
|
+
det.appendChild(body);
|
|
1841
|
+
wrap.appendChild(det);
|
|
1842
|
+
}
|
|
1760
1843
|
|
|
1761
1844
|
return wrap;
|
|
1762
1845
|
}
|
|
@@ -1798,6 +1881,27 @@
|
|
|
1798
1881
|
return det;
|
|
1799
1882
|
}
|
|
1800
1883
|
|
|
1884
|
+
/* 0.5.12 — the group's disclosure button.
|
|
1885
|
+
|
|
1886
|
+
«N сервисов · M cookie» was a static label in 0.5.8; it is now the control
|
|
1887
|
+
that reveals the group's services and its cookie list, COLLAPSED by default.
|
|
1888
|
+
A real <button aria-expanded> rather than a <details>, because the panel
|
|
1889
|
+
already owns the region it toggles — the services list and the loose-cookie
|
|
1890
|
+
table are siblings that must move together, and wrapping both in a
|
|
1891
|
+
<details> would put a second summary line above the one the header already
|
|
1892
|
+
has. Keyboard and focus come free with a button; the ring is the panel's
|
|
1893
|
+
own :focus-visible rule. */
|
|
1894
|
+
function makeGroupToggle(cat, label, regionId) {
|
|
1895
|
+
var b = el('button', 'ck-cat__count ck-cat__toggle');
|
|
1896
|
+
b.type = 'button';
|
|
1897
|
+
b.setAttribute('aria-expanded', 'false');
|
|
1898
|
+
b.setAttribute('aria-controls', regionId);
|
|
1899
|
+
b.appendChild(el('span', 'ck-cat__caret'));
|
|
1900
|
+
b.appendChild(document.createTextNode(label));
|
|
1901
|
+
b.dataset.cat = cat;
|
|
1902
|
+
return b;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1801
1905
|
function buildCategory(cfg, cat) {
|
|
1802
1906
|
var meta = T.cat[cat] || { title: cat, desc: '' };
|
|
1803
1907
|
var locked = cat === 'necessary';
|
|
@@ -1829,9 +1933,11 @@
|
|
|
1829
1933
|
«0 сервисов · 3 cookie». The cookie half counts the whole group, services
|
|
1830
1934
|
and loose rows alike: it answers «сколько cookie в этой группе», which is
|
|
1831
1935
|
the question the line is there to answer. */
|
|
1936
|
+
var toggle = null;
|
|
1937
|
+
var regionId = nameId + '-svcs';
|
|
1832
1938
|
if (svcs.length) {
|
|
1833
|
-
|
|
1834
|
-
|
|
1939
|
+
toggle = makeGroupToggle(cat, groupCountLabel(svcs.length, rows.length, T, LANG), regionId);
|
|
1940
|
+
name.appendChild(toggle);
|
|
1835
1941
|
}
|
|
1836
1942
|
|
|
1837
1943
|
var sw = makeSwitch(cat, locked);
|
|
@@ -1843,23 +1949,47 @@
|
|
|
1843
1949
|
top.appendChild(sw);
|
|
1844
1950
|
wrap.appendChild(top);
|
|
1845
1951
|
|
|
1952
|
+
// «Cookie в этой группе (N)» keeps its old meaning: what is left once each
|
|
1953
|
+
// service has claimed its own. With no services that is the whole table and
|
|
1954
|
+
// the summary line is byte-for-byte what 0.5.7 rendered.
|
|
1955
|
+
var loose = looseCookies(rows, svcs);
|
|
1956
|
+
|
|
1846
1957
|
if (svcs.length) {
|
|
1958
|
+
/* Both halves live inside the collapsed region: the services and, at the
|
|
1959
|
+
end, the group's own «Какие cookie (N)». `hidden` rather than a class,
|
|
1960
|
+
so the region is out of the accessibility tree and out of the tab order
|
|
1961
|
+
while collapsed — a class that only sets display:none would leave the
|
|
1962
|
+
switches focusable to a keyboard user who cannot see them. */
|
|
1963
|
+
var region = el('div', 'ck-cat__region');
|
|
1964
|
+
region.id = regionId;
|
|
1965
|
+
region.hidden = true;
|
|
1966
|
+
|
|
1847
1967
|
var list = el('div', 'ck-svcs');
|
|
1848
1968
|
// A list, so a screen reader announces «3 items» before reading them.
|
|
1849
1969
|
list.setAttribute('role', 'list');
|
|
1970
|
+
list.setAttribute('aria-label', T.svcListLabel);
|
|
1850
1971
|
for (var s = 0; s < svcs.length; s++) {
|
|
1851
1972
|
var item = buildService(svcs[s], cat, rows);
|
|
1852
1973
|
item.setAttribute('role', 'listitem');
|
|
1853
1974
|
list.appendChild(item);
|
|
1854
1975
|
}
|
|
1855
|
-
|
|
1856
|
-
}
|
|
1976
|
+
region.appendChild(list);
|
|
1857
1977
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1978
|
+
if (loose.length) {
|
|
1979
|
+
region.appendChild(cookieTable(loose, T.cookiesIn + ' (' + loose.length + ')'));
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
toggle.addEventListener('click', function () {
|
|
1983
|
+
var open = toggle.getAttribute('aria-expanded') === 'true';
|
|
1984
|
+
toggle.setAttribute('aria-expanded', open ? 'false' : 'true');
|
|
1985
|
+
region.hidden = open;
|
|
1986
|
+
});
|
|
1987
|
+
|
|
1988
|
+
wrap.appendChild(region);
|
|
1989
|
+
} else if (loose.length) {
|
|
1990
|
+
// No services: the cookie table sits where it always did, top level and
|
|
1991
|
+
// never behind a disclosure — a pre-0.5.8 config renders as it did in
|
|
1992
|
+
// 0.5.7, which is what «старый конфиг рендерится как раньше» asks for.
|
|
1863
1993
|
wrap.appendChild(cookieTable(loose, T.cookiesIn + ' (' + loose.length + ')'));
|
|
1864
1994
|
}
|
|
1865
1995
|
|