@ecomconsult/consentkit 0.5.5 → 0.5.7
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 +65 -2
- package/npm/core.cjs +1 -0
- package/npm/core.mjs +3 -0
- package/npm/index.cjs +1 -0
- package/npm/index.d.ts +25 -1
- package/npm/index.mjs +3 -0
- package/npm/internal-stub.mjs +2 -1
- package/package.json +1 -1
- package/src/ck-core.js +23 -1
- package/src/ck-locales.js +6 -0
- package/src/ck-ui-branding.js +45 -2
- package/src/ck-ui.js +454 -8
package/README.md
CHANGED
|
@@ -188,13 +188,15 @@ Pass any subset to `init()`. Nested objects merge with the defaults.
|
|
|
188
188
|
| `theme.mode` | `"auto" \| "light" \| "dark"` | `"auto"` | `auto` follows `prefers-color-scheme` |
|
|
189
189
|
| `theme.dark` | `{ bg, ink, accent, onAccent }` | built-in | Overrides the dark palette |
|
|
190
190
|
| `texts.policyUrl` | `string` | — | v0.5.0. Cookie policy address. `http(s)` only; anything else is ignored |
|
|
191
|
-
| `texts.detailsAction` | `"policy" \| "settings" \| "hide"` | see notes | v0.5.0. What «Learn more» does. Defaults to `policy` when `policyUrl` is set, `settings` when it is not. `policy` without a usable URL falls back to `settings` rather than rendering a dead link |
|
|
191
|
+
| `texts.detailsAction` | `"policy" \| "settings" \| "hide" \| "declaration"` | see notes | v0.5.0, `declaration` in v0.5.7. What «Learn more» does. Defaults to `policy` when `policyUrl` is set, `settings` when it is not. `policy` or `declaration` without a usable URL falls back to `settings` rather than rendering a dead link |
|
|
192
|
+
| `texts.declarationUrl` | `string` | — | v0.5.7. Address of the cookie declaration page, used by `detailsAction: "declaration"`. `http(s)` only. Filled by the hosted service; the client only reads it |
|
|
192
193
|
| `categories.*.enabled` | `boolean` | `true` | Per category: `functional`, `analytics`, `marketing`. Hides the toggle when `false` |
|
|
193
194
|
| `consentTtlDays` | `number` | `365` | Lifetime of the stored decision |
|
|
194
195
|
| `integrations.gcm` | `boolean` | `true` | Google Consent Mode v2 signals |
|
|
195
196
|
| `integrations.gtmDataLayer` | `boolean` | `true` | Push consent events to `window.dataLayer` |
|
|
196
197
|
| `blocking.mode` | `"known" \| "strict"` | `"known"` | `strict` also holds back unknown third-party scripts and iframes — see [Strict mode](#strict-mode) |
|
|
197
198
|
| `blocking.allow` | `string[]` | `[]` | Hosts strict mode must never intercept. Matched by suffix, so `partner.com` also covers `cdn.partner.com` |
|
|
199
|
+
| `blocking.placeholders` | `boolean` | `true` | v0.5.7. Draw a card in place of an embed held back before consent — see [Placeholders for blocked embeds](#placeholders-for-blocked-embeds). `false` restores the pre-0.5.7 behaviour: the frame is still blocked, just invisible |
|
|
198
200
|
| `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 |
|
|
199
201
|
| `cookieTable` | `CkCookieTableEntry[]` | `[]` | Declared cookies, listed per category in the panel |
|
|
200
202
|
|
|
@@ -267,14 +269,69 @@ resolved colours, its contrast ratio, and whether the value was adjusted.
|
|
|
267
269
|
| `"policy"` | A link to `texts.policyUrl`, opened with `target="_blank" rel="noopener"` |
|
|
268
270
|
| `"settings"` | A button that opens the preferences panel |
|
|
269
271
|
| `"hide"` | Nothing at all |
|
|
272
|
+
| `"declaration"` | v0.5.7. A link to `texts.declarationUrl` — the cookie declaration page — opened the same way as `policy` |
|
|
270
273
|
|
|
271
274
|
The default follows `policyUrl`: `policy` when one is set, `settings` when it
|
|
272
|
-
is not — so supplying only a URL does the obvious thing.
|
|
275
|
+
is not — so supplying only a URL does the obvious thing. `declarationUrl`
|
|
276
|
+
deliberately does *not* affect that default: a site that gains a declaration
|
|
277
|
+
address keeps whatever «Learn more» already did until it asks for the change.
|
|
278
|
+
|
|
279
|
+
Both link forms accept `http(s)` addresses only. A `javascript:` or `data:` URL
|
|
280
|
+
in a control the visitor is invited to click is an XSS vector, so anything else
|
|
281
|
+
is refused and the link degrades to `settings`.
|
|
273
282
|
|
|
274
283
|
> Before 0.5.0 this control was rendered as `<a href="#">` with no handler at
|
|
275
284
|
> all: clicking it jumped to the top of the page and nothing else. Any site
|
|
276
285
|
> running 0.4.x or earlier has a dead «Learn more» link.
|
|
277
286
|
|
|
287
|
+
### Reopening the settings
|
|
288
|
+
|
|
289
|
+
`ConsentKit.openSettings()` opens the preferences panel from anywhere on the
|
|
290
|
+
page — a footer link, a menu item, a button in your own cookie policy. It is
|
|
291
|
+
safe to call before the banner has mounted: a call that arrives while the UI
|
|
292
|
+
file is still loading is remembered and honoured on mount, so a link clicked
|
|
293
|
+
during a slow page load still works.
|
|
294
|
+
|
|
295
|
+
The same panel has an address. Ссылка «Изменить выбор cookie» →
|
|
296
|
+
`https://site/#ck-settings`: любая ссылка на страницу сайта с этим хвостом
|
|
297
|
+
открывает окно настроек — и при загрузке страницы, и при переходе по ссылке на
|
|
298
|
+
уже открытой странице. Хвост убирается из адреса через `history.replaceState`,
|
|
299
|
+
поэтому перезагрузка или «назад» не открывают окно повторно. Это тот адрес, на
|
|
300
|
+
который ведёт кнопка «Изменить выбор cookie» на странице декларации cookie, и
|
|
301
|
+
его же удобно поставить в подвал сайта:
|
|
302
|
+
|
|
303
|
+
```html
|
|
304
|
+
<a href="#ck-settings">Изменить выбор cookie</a>
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Placeholders for blocked embeds
|
|
308
|
+
|
|
309
|
+
When the engine holds back an `<iframe>` before consent — a known tracker, or
|
|
310
|
+
any third-party frame in strict mode — the visitor would otherwise see an empty
|
|
311
|
+
hole where a video or a map should be. Since 0.5.7 ConsentKit draws a card in
|
|
312
|
+
its place: the name of the service, the category the embed is waiting for, a
|
|
313
|
+
primary button «Разрешить и показать» that grants **that one category** and
|
|
314
|
+
loads the embed, and a link to the full settings panel.
|
|
315
|
+
|
|
316
|
+
The card is sized from the frame's own `width`/`height` (or its computed size),
|
|
317
|
+
never shorter than 120px, and never wider than its container. It is rendered in
|
|
318
|
+
its own Shadow DOM and takes the banner's theme — the page's font, your accent
|
|
319
|
+
colour and corner radius — so it looks like part of the site rather than part of
|
|
320
|
+
a third-party widget. Strings ship in ru, ro and en; every other language falls
|
|
321
|
+
back to en.
|
|
322
|
+
|
|
323
|
+
The button grants one category through the ordinary consent path: the decision
|
|
324
|
+
is stored and journalled as `method: 'custom'`, the usual `ck:consent` /
|
|
325
|
+
`ck:change` events fire, and consent the visitor had already given to *other*
|
|
326
|
+
categories is preserved rather than overwritten. The frame itself is restored by
|
|
327
|
+
the core's normal revival pass, which is the same code path the panel's switches
|
|
328
|
+
and «Accept all» already use.
|
|
329
|
+
|
|
330
|
+
Frames that are `display:none`, 1×1 tracking pixels, or outside `<body>` are
|
|
331
|
+
left alone, and a frame the site allowed never gets a card at all — an allowed
|
|
332
|
+
frame is never intercepted in the first place. Set `blocking.placeholders:
|
|
333
|
+
false` to restore the pre-0.5.7 behaviour.
|
|
334
|
+
|
|
278
335
|
### Infrastructure
|
|
279
336
|
|
|
280
337
|
Some third-party hosts are not a consent decision at all: they are where a site
|
|
@@ -310,6 +367,7 @@ All methods are safe to call at any time and never throw.
|
|
|
310
367
|
| `rejectAll()` | `CkState` | Denies every opt-in category. `method: 'reject_all'` |
|
|
311
368
|
| `withdraw()` | `CkState` | Clears storage and known cookies, sends GCM `denied`, resets to `decided: false` |
|
|
312
369
|
| `show()` | `void` | Opens the preferences panel |
|
|
370
|
+
| `openSettings()` | `void` | v0.5.7. Opens the preferences panel. Safe before the UI has loaded — the request is remembered and honoured as soon as the banner mounts |
|
|
313
371
|
| `hide()` | `void` | Closes the panel |
|
|
314
372
|
| `config` | `CkConfig` | The merged, effective config |
|
|
315
373
|
| `version` | `string` | Core version string |
|
|
@@ -555,6 +613,11 @@ Removing it is a supported, first-class option — no obligation, no nag:
|
|
|
555
613
|
node tools/build-inline.mjs --langs=en,ru --no-branding # block without it
|
|
556
614
|
```
|
|
557
615
|
|
|
616
|
+
The line is `branding.poweredBy.text`; with `language: 'auto'` supply
|
|
617
|
+
`branding.poweredBy.texts` instead — `{ ru: '…', ro: '…', en: '…' }`, resolved
|
|
618
|
+
against the language the banner actually picked (`texts[lang]` → base code →
|
|
619
|
+
`texts.en` → `text`), so the attribution matches what the visitor is reading.
|
|
620
|
+
|
|
558
621
|
If you write the config by hand, simply omit the `branding` object; nothing
|
|
559
622
|
renders without it. Either way costs you ~200 bytes, not a licence: the client
|
|
560
623
|
is MIT and the line is yours to drop.
|
package/npm/core.cjs
CHANGED
|
@@ -30,6 +30,7 @@ function createStub() {
|
|
|
30
30
|
rejectAll: function () { return undecidedState(); },
|
|
31
31
|
withdraw: function () { return undecidedState(); },
|
|
32
32
|
show: function () {},
|
|
33
|
+
openSettings: function () {},
|
|
33
34
|
hide: function () {},
|
|
34
35
|
_categories: CATEGORIES.slice(),
|
|
35
36
|
_isStub: true
|
package/npm/core.mjs
CHANGED
|
@@ -28,6 +28,9 @@ export const {
|
|
|
28
28
|
rejectAll,
|
|
29
29
|
withdraw,
|
|
30
30
|
show,
|
|
31
|
+
// v0.5.7: the public name for «open the cookie settings», safe to call
|
|
32
|
+
// before the UI has loaded.
|
|
33
|
+
openSettings,
|
|
31
34
|
hide,
|
|
32
35
|
// v0.4.0 (§1.3): merging service overrides into the tracker database is part
|
|
33
36
|
// of the public surface, so it must be reachable as a named import too.
|
package/npm/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ function createStub() {
|
|
|
33
33
|
rejectAll: function () { return undecidedState(); },
|
|
34
34
|
withdraw: function () { return undecidedState(); },
|
|
35
35
|
show: function () {},
|
|
36
|
+
openSettings: function () {},
|
|
36
37
|
hide: function () {},
|
|
37
38
|
_categories: CATEGORIES.slice(),
|
|
38
39
|
_isStub: true
|
package/npm/index.d.ts
CHANGED
|
@@ -136,8 +136,17 @@ export interface CkTextsConfig {
|
|
|
136
136
|
* What «Learn more» does. Defaults to `'policy'` when `policyUrl` is set and
|
|
137
137
|
* `'settings'` when it is not. `'policy'` without a usable URL falls back to
|
|
138
138
|
* `'settings'` rather than rendering a dead link. `'hide'` renders nothing.
|
|
139
|
+
* v0.5.7 adds `'declaration'`: a link to {@link CkTextsConfig.declarationUrl},
|
|
140
|
+
* the cookie declaration page, which likewise degrades to `'settings'`
|
|
141
|
+
* without a usable URL.
|
|
139
142
|
*/
|
|
140
|
-
detailsAction?: 'policy' | 'settings' | 'hide';
|
|
143
|
+
detailsAction?: 'policy' | 'settings' | 'hide' | 'declaration';
|
|
144
|
+
/**
|
|
145
|
+
* v0.5.7. Address of the cookie declaration page, used by
|
|
146
|
+
* `detailsAction: 'declaration'`. `http(s)` only. Supplied by the hosted
|
|
147
|
+
* service; the client only reads it.
|
|
148
|
+
*/
|
|
149
|
+
declarationUrl?: string;
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
/** Whether a category is offered in the preferences panel at all. */
|
|
@@ -170,6 +179,12 @@ export interface CkBlockingConfig {
|
|
|
170
179
|
mode?: 'known' | 'strict';
|
|
171
180
|
/** Hosts strict mode must never intercept. Suffix match: `p.com` covers `cdn.p.com`. */
|
|
172
181
|
allow?: string[];
|
|
182
|
+
/**
|
|
183
|
+
* v0.5.7. Draw a card in place of an `<iframe>` held back before consent,
|
|
184
|
+
* offering «Разрешить и показать» for that one category. Default `true`;
|
|
185
|
+
* `false` restores the pre-0.5.7 behaviour (still blocked, just invisible).
|
|
186
|
+
*/
|
|
187
|
+
placeholders?: boolean;
|
|
173
188
|
}
|
|
174
189
|
|
|
175
190
|
/** One declared cookie, shown under its category in the preferences panel. */
|
|
@@ -234,6 +249,12 @@ export interface ConsentKitApi {
|
|
|
234
249
|
withdraw(): CkState;
|
|
235
250
|
/** Dispatches `ck:ui:open-preferences`. */
|
|
236
251
|
show(): void;
|
|
252
|
+
/**
|
|
253
|
+
* v0.5.7. Opens the preferences panel. Unlike {@link show}, a call made
|
|
254
|
+
* before the UI has loaded is remembered and honoured once the banner
|
|
255
|
+
* mounts, so a footer link clicked during a slow load still works.
|
|
256
|
+
*/
|
|
257
|
+
openSettings(): void;
|
|
237
258
|
/** Dispatches `ck:ui:close`. */
|
|
238
259
|
hide(): void;
|
|
239
260
|
|
|
@@ -380,6 +401,7 @@ export declare function accept(choice?: CkAcceptArg): CkState;
|
|
|
380
401
|
export declare function rejectAll(): CkState;
|
|
381
402
|
export declare function withdraw(): CkState;
|
|
382
403
|
export declare function show(): void;
|
|
404
|
+
export declare function openSettings(): void;
|
|
383
405
|
export declare function hide(): void;
|
|
384
406
|
export declare function _extendHostDb(map: Record<string, CkCategory>): number;
|
|
385
407
|
|
|
@@ -410,6 +432,7 @@ declare module '@ecomconsult/consentkit' {
|
|
|
410
432
|
export function rejectAll(): CkState;
|
|
411
433
|
export function withdraw(): CkState;
|
|
412
434
|
export function show(): void;
|
|
435
|
+
export function openSettings(): void;
|
|
413
436
|
export function hide(): void;
|
|
414
437
|
}
|
|
415
438
|
|
|
@@ -424,6 +447,7 @@ declare module '@ecomconsult/consentkit/core' {
|
|
|
424
447
|
export function rejectAll(): CkState;
|
|
425
448
|
export function withdraw(): CkState;
|
|
426
449
|
export function show(): void;
|
|
450
|
+
export function openSettings(): void;
|
|
427
451
|
export function hide(): void;
|
|
428
452
|
}
|
|
429
453
|
|
package/npm/index.mjs
CHANGED
|
@@ -51,6 +51,9 @@ export const {
|
|
|
51
51
|
rejectAll,
|
|
52
52
|
withdraw,
|
|
53
53
|
show,
|
|
54
|
+
// v0.5.7: the public name for «open the cookie settings», safe to call
|
|
55
|
+
// before the UI has loaded.
|
|
56
|
+
openSettings,
|
|
54
57
|
hide,
|
|
55
58
|
// v0.4.0 (§1.3): merging service overrides into the tracker database is part
|
|
56
59
|
// of the public surface, so it must be reachable as a named import too.
|
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.7',
|
|
32
32
|
config: {},
|
|
33
33
|
init: function () { return undecidedState(); },
|
|
34
34
|
allowed: function (cat) { return cat === 'necessary'; },
|
|
@@ -37,6 +37,7 @@ export function createStub() {
|
|
|
37
37
|
rejectAll: function () { return undecidedState(); },
|
|
38
38
|
withdraw: function () { return undecidedState(); },
|
|
39
39
|
show: function () {},
|
|
40
|
+
openSettings: function () {},
|
|
40
41
|
hide: function () {},
|
|
41
42
|
_categories: CATEGORIES.slice(),
|
|
42
43
|
// v0.4.0: the stub mirrors the real surface, so consumer code that calls
|
package/package.json
CHANGED
package/src/ck-core.js
CHANGED
|
@@ -1537,7 +1537,7 @@
|
|
|
1537
1537
|
// Public API
|
|
1538
1538
|
// ---------------------------------------------------------------------------
|
|
1539
1539
|
var ConsentKit = {
|
|
1540
|
-
version: '0.5.
|
|
1540
|
+
version: '0.5.7',
|
|
1541
1541
|
config: config,
|
|
1542
1542
|
|
|
1543
1543
|
init: function (userConfig) {
|
|
@@ -1597,6 +1597,28 @@
|
|
|
1597
1597
|
show: function () { dispatch('ck:ui:open-preferences', { state: publicState(), config: config }); },
|
|
1598
1598
|
hide: function () { dispatch('ck:ui:close', { state: publicState() }); },
|
|
1599
1599
|
|
|
1600
|
+
/* SPEC V1.10 §1 — the public name for «открыть настройки cookie». A site
|
|
1601
|
+
puts it behind a footer link, and the declaration page links to
|
|
1602
|
+
`https://site/#ck-settings`, which ck-ui turns into the same call.
|
|
1603
|
+
|
|
1604
|
+
Deliberately not just an alias of show(): the event is a broadcast, and a
|
|
1605
|
+
page that calls openSettings() before ck-ui.js has been PARSED has no
|
|
1606
|
+
listener to receive it — the request would be silently lost, which is the
|
|
1607
|
+
one case a footer link hits (a click during a slow load). So the request
|
|
1608
|
+
is latched here as well as dispatched, and ck-ui's mount() consumes the
|
|
1609
|
+
latch at the end of its first render. ck-ui already writes to this object
|
|
1610
|
+
(_contrast, _resolvePageFont), so the coupling direction is established.
|
|
1611
|
+
|
|
1612
|
+
`_pendingOpen` is read-and-cleared by ck-ui; nothing else touches it. */
|
|
1613
|
+
openSettings: function () {
|
|
1614
|
+
try { ConsentKit._pendingOpen = true; } catch (e) { /* noop */ }
|
|
1615
|
+
dispatch('ck:ui:open-preferences', { state: publicState(), config: config });
|
|
1616
|
+
},
|
|
1617
|
+
|
|
1618
|
+
// Set by openSettings() when the UI may not be listening yet; cleared by
|
|
1619
|
+
// ck-ui.js the moment it can honour it. Underscored: not a public API.
|
|
1620
|
+
_pendingOpen: false,
|
|
1621
|
+
|
|
1600
1622
|
// Introspection helpers for the demo status panel (read-only).
|
|
1601
1623
|
_categoryForUrl: categoryForUrl,
|
|
1602
1624
|
_categories: CATEGORIES.slice(),
|
package/src/ck-locales.js
CHANGED
|
@@ -572,6 +572,12 @@
|
|
|
572
572
|
colPurpose: 'Pentru ce',
|
|
573
573
|
colExpiry: 'Expiră',
|
|
574
574
|
floating: 'Setări cookie-uri',
|
|
575
|
+
// SPEC V1.10 §2 — заглушка встраивания. Only ro carries these here: en and
|
|
576
|
+
// ru are builtin in ck-ui.js, and every other locale falls back to en.
|
|
577
|
+
phText: 'Aici este conținut de la {host}. Se va încărca după acordul pentru «{cat}».',
|
|
578
|
+
phAllow: 'Permite și arată',
|
|
579
|
+
phSettings: 'Setări cookie-uri',
|
|
580
|
+
phLabel: 'Conținut blocat',
|
|
575
581
|
cat: {
|
|
576
582
|
necessary: { title: 'Necesare', desc: 'Sunt necesare pentru funcționarea site-ului: autentificare, securitate, reținerea alegerii dumneavoastră. Nu pot fi dezactivate.' },
|
|
577
583
|
functional: { title: 'Funcționale', desc: 'Rețin alegerile dumneavoastră: limba, coșul, chatul.' },
|
package/src/ck-ui-branding.js
CHANGED
|
@@ -348,6 +348,34 @@
|
|
|
348
348
|
return out.join('\n');
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
/* Pick the attribution line for the language the banner actually resolved to.
|
|
352
|
+
|
|
353
|
+
A SaaS server renders one config per site but cannot know the visitor's
|
|
354
|
+
language: with `language: 'auto'` the code that decides is ck-ui's
|
|
355
|
+
resolveLang(), in the browser, long after the config was serialised. So a
|
|
356
|
+
single `poweredBy.text` produced "Made by E-COM Consult" under a fully
|
|
357
|
+
Russian banner. `poweredBy.texts` lets the server send every line it has
|
|
358
|
+
and leaves the choice here, where the resolved language is known.
|
|
359
|
+
|
|
360
|
+
host.lang is the code ck-ui resolved ('ru', 'pt-br', ...). It is optional
|
|
361
|
+
on purpose: a page still running an older cached ck-ui.js passes no lang,
|
|
362
|
+
and that must degrade to the English/`text` line rather than throw. */
|
|
363
|
+
function pickPoweredByText(texts, lang, str) {
|
|
364
|
+
if (!texts || typeof texts !== 'object' || Array.isArray(texts)) return null;
|
|
365
|
+
var code = (typeof lang === 'string') ? lang.toLowerCase() : '';
|
|
366
|
+
// Own keys only. ck-ui's resolveLang() can only return a real locale key or
|
|
367
|
+
// 'en', so 'constructor' never arrives through the normal path — but `host`
|
|
368
|
+
// is caller-supplied on a published extension entry point, and reading an
|
|
369
|
+
// inherited key would hand str() a function instead of a missing line.
|
|
370
|
+
// Values go through str() as well, so anything non-string falls through.
|
|
371
|
+
function own(k) {
|
|
372
|
+
return (k && Object.prototype.hasOwnProperty.call(texts, k)) ? str(texts[k]) : null;
|
|
373
|
+
}
|
|
374
|
+
var hit = own(code);
|
|
375
|
+
if (!hit && code.length > 2) hit = own(code.slice(0, 2)); // ru-RU -> ru
|
|
376
|
+
return hit || own('en');
|
|
377
|
+
}
|
|
378
|
+
|
|
351
379
|
/* Powered-by line. true -> localised default; object -> caller's text/url.
|
|
352
380
|
Rendered after the actions in DOM order and styled quiet on purpose. */
|
|
353
381
|
function buildPoweredBy(cfg, host) {
|
|
@@ -361,7 +389,8 @@
|
|
|
361
389
|
if (pb === true) {
|
|
362
390
|
text = T.poweredBy; // en fallback guaranteed by STR_KEYS
|
|
363
391
|
} else if (typeof pb === 'object' && !Array.isArray(pb)) {
|
|
364
|
-
|
|
392
|
+
// texts[lang] -> texts[base] -> texts.en -> text -> T.poweredBy
|
|
393
|
+
text = pickPoweredByText(pb.texts, host.lang, str) || str(pb.text) || T.poweredBy;
|
|
365
394
|
url = safeLinkUrl(pb.url);
|
|
366
395
|
} else {
|
|
367
396
|
return null;
|
|
@@ -467,12 +496,26 @@
|
|
|
467
496
|
styling, so a config that gains a logo after the first ck:init has to
|
|
468
497
|
rebuild rather than restyle. Returns '-' when there is no branding config,
|
|
469
498
|
which is what keeps an unbranded page's signature stable. */
|
|
499
|
+
function textsSig(texts) {
|
|
500
|
+
if (!texts || typeof texts !== 'object' || Array.isArray(texts)) return '-';
|
|
501
|
+
var keys = [], k;
|
|
502
|
+
for (k in texts) {
|
|
503
|
+
if (Object.prototype.hasOwnProperty.call(texts, k) && str(texts[k])) keys.push(k);
|
|
504
|
+
}
|
|
505
|
+
keys.sort();
|
|
506
|
+
var out = [];
|
|
507
|
+
for (var i = 0; i < keys.length; i++) out.push(keys[i] + '=' + str(texts[keys[i]]));
|
|
508
|
+
return out.join(',');
|
|
509
|
+
}
|
|
510
|
+
|
|
470
511
|
function brandSignature(cfg) {
|
|
471
512
|
var b = brandingCfg(cfg);
|
|
472
513
|
if (!b) return '-';
|
|
473
514
|
var pb = b.poweredBy;
|
|
515
|
+
// texts is folded in with sorted keys so the signature is stable across key
|
|
516
|
+
// order — without it a server swapping only `texts` would not remount.
|
|
474
517
|
var pbSig = (pb && typeof pb === 'object')
|
|
475
|
-
? 'o:' + String(pb.text || '') + ':' + String(pb.url || '')
|
|
518
|
+
? 'o:' + String(pb.text || '') + ':' + String(pb.url || '') + ':' + textsSig(pb.texts)
|
|
476
519
|
: String(!!pb);
|
|
477
520
|
// Logos are hashed by length + head so a long data: URI does not bloat the key.
|
|
478
521
|
function tag(v) {
|
package/src/ck-ui.js
CHANGED
|
@@ -32,6 +32,14 @@
|
|
|
32
32
|
colExpiry: 'Expires',
|
|
33
33
|
floating: 'Cookie settings',
|
|
34
34
|
poweredBy: 'Powered by ConsentKit',
|
|
35
|
+
// SPEC V1.10 §2 — the blocked-embed placeholder. {host} is the vendor
|
|
36
|
+
// label when the database knows one and the bare host otherwise; {cat} is
|
|
37
|
+
// the localized category title, taken from cat.<name>.title below, so the
|
|
38
|
+
// name in the placeholder is the name on the switch in the panel.
|
|
39
|
+
phText: 'Content from {host} goes here. It will load once you allow «{cat}».',
|
|
40
|
+
phAllow: 'Allow and show',
|
|
41
|
+
phSettings: 'Cookie settings',
|
|
42
|
+
phLabel: 'Blocked content',
|
|
35
43
|
cat: {
|
|
36
44
|
necessary: {
|
|
37
45
|
title: 'Necessary',
|
|
@@ -72,6 +80,10 @@
|
|
|
72
80
|
colExpiry: 'Срок',
|
|
73
81
|
floating: 'Настройки cookie',
|
|
74
82
|
poweredBy: 'Работает на ConsentKit',
|
|
83
|
+
phText: 'Здесь содержимое от {host}. Оно загрузится после согласия на «{cat}».',
|
|
84
|
+
phAllow: 'Разрешить и показать',
|
|
85
|
+
phSettings: 'Настроить cookie',
|
|
86
|
+
phLabel: 'Заблокированное содержимое',
|
|
75
87
|
cat: {
|
|
76
88
|
necessary: {
|
|
77
89
|
title: 'Необходимые',
|
|
@@ -103,7 +115,11 @@
|
|
|
103
115
|
'bannerTitle', 'bannerText', 'more', 'acceptAll', 'rejectAll', 'customize',
|
|
104
116
|
'bannerLabel', 'panelTitle', 'panelIntro', 'save', 'close', 'alwaysOn',
|
|
105
117
|
'cookiesIn', 'noCookies', 'colName', 'colVendor', 'colPurpose', 'colExpiry', 'floating',
|
|
106
|
-
'poweredBy'
|
|
118
|
+
'poweredBy',
|
|
119
|
+
// SPEC V1.10 §2. Present in ck-locales.js for ro only; every other external
|
|
120
|
+
// locale falls back to DICT.en through buildStrings(), which is what §2
|
|
121
|
+
// asks for («остальные языки — en»).
|
|
122
|
+
'phText', 'phAllow', 'phSettings', 'phLabel'
|
|
107
123
|
];
|
|
108
124
|
|
|
109
125
|
// builtin(en,ru) <- window.__ckLocales, read at render time so the locales
|
|
@@ -161,6 +177,47 @@
|
|
|
161
177
|
return out;
|
|
162
178
|
}
|
|
163
179
|
|
|
180
|
+
/* ------------------------------------------- blocked-embed placeholder (§2) */
|
|
181
|
+
|
|
182
|
+
/* The one piece of the placeholder that is worth testing without a DOM: which
|
|
183
|
+
sentence a visitor reads. PURE — takes a host (or vendor label), a category
|
|
184
|
+
name and a language, returns the finished string.
|
|
185
|
+
|
|
186
|
+
`host` is used verbatim: the core has no vendor-label table (only the SaaS
|
|
187
|
+
scanner's VENDOR_DB does, server-side), so §2's «ярлык хоста; без ярлыка —
|
|
188
|
+
содержимое с <host>» collapses to the host here. When a label lookup is
|
|
189
|
+
added to the core later, this signature already accepts it — pass the label
|
|
190
|
+
instead of the host and nothing else changes.
|
|
191
|
+
|
|
192
|
+
The category name is resolved through the SAME buildStrings() the panel
|
|
193
|
+
uses, so «Маркетинг» in the placeholder is «Маркетинг» on the switch. An
|
|
194
|
+
unknown language falls back to en, an unknown category to marketing — the
|
|
195
|
+
category a strict-mode interception is filed under. */
|
|
196
|
+
function placeholderText(host, category, lang) {
|
|
197
|
+
var table = localeTable();
|
|
198
|
+
var T2 = buildStrings(resolveLang(lang, table), table);
|
|
199
|
+
var cat = (category && T2.cat[category]) ? category : 'marketing';
|
|
200
|
+
var name = String(host || '').trim();
|
|
201
|
+
return T2.phText
|
|
202
|
+
.replace('{host}', name || T2.phLabel)
|
|
203
|
+
.replace('{cat}', T2.cat[cat].title);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Hostname of a blocked frame's real address, for the sentence above. The
|
|
207
|
+
data-src is whatever the page asked for, which may be protocol-relative or
|
|
208
|
+
relative, so it is resolved against the page like the core does it. */
|
|
209
|
+
function hostOf(src) {
|
|
210
|
+
var s = String(src || '');
|
|
211
|
+
try {
|
|
212
|
+
var base = (typeof location !== 'undefined' && location.href) || 'http://localhost/';
|
|
213
|
+
var h = new URL(s, base).hostname || '';
|
|
214
|
+
return h.toLowerCase().replace(/^www\./, '');
|
|
215
|
+
} catch (e) {
|
|
216
|
+
var m = /^(?:[a-z]+:)?\/\/([^/?#]+)/i.exec(s);
|
|
217
|
+
return m ? m[1].toLowerCase().replace(/:\d+$/, '').replace(/^www\./, '') : '';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
164
221
|
/* --------------------------------------------------------------- styles */
|
|
165
222
|
|
|
166
223
|
var CSS = [
|
|
@@ -370,8 +427,12 @@
|
|
|
370
427
|
return (b && typeof b === 'object') ? b : null;
|
|
371
428
|
}
|
|
372
429
|
|
|
430
|
+
/* LANG travels with T: an extension that renders language-dependent text of
|
|
431
|
+
its own (branding.poweredBy.texts) needs the code the banner resolved to,
|
|
432
|
+
which only this file knows — the config may say 'auto' and the answer then
|
|
433
|
+
comes from the visitor's browser at mount time. */
|
|
373
434
|
function extHost() {
|
|
374
|
-
return { el: el, str: str, T: T };
|
|
435
|
+
return { el: el, str: str, T: T, lang: LANG };
|
|
375
436
|
}
|
|
376
437
|
|
|
377
438
|
function buildBrandLogo(cfg) {
|
|
@@ -427,6 +488,7 @@
|
|
|
427
488
|
var mountedSig = null;
|
|
428
489
|
var host = null, root = null;
|
|
429
490
|
var T = DICT.en;
|
|
491
|
+
var LANG = 'en'; // the code T was built from, reassigned per mount
|
|
430
492
|
var nodes = {}; // banner/panel/fab refs
|
|
431
493
|
var switches = {}; // category -> button
|
|
432
494
|
var panelOpen = false;
|
|
@@ -1283,12 +1345,30 @@
|
|
|
1283
1345
|
var url = str(texts.policyUrl);
|
|
1284
1346
|
if (url && !/^https?:\/\//i.test(url)) url = null;
|
|
1285
1347
|
|
|
1348
|
+
/* SPEC V1.10 §1 — the third destination: our own cookie declaration page.
|
|
1349
|
+
`declarationUrl` is server-owned (the SaaS config injects it, like
|
|
1350
|
+
`branding`); the client only reads it and never invents one. It is
|
|
1351
|
+
validated exactly like policyUrl — http(s) only, because a link the
|
|
1352
|
+
visitor is invited to click must not be able to carry javascript: — and
|
|
1353
|
+
it deliberately does NOT influence the default action: the URL-sensitive
|
|
1354
|
+
default above stays policyUrl-driven, so a site that gains a declaration
|
|
1355
|
+
address does not silently lose its «Подробнее» → политика link. */
|
|
1356
|
+
var decl = str(texts.declarationUrl);
|
|
1357
|
+
if (decl && !/^https?:\/\//i.test(decl)) decl = null;
|
|
1358
|
+
|
|
1286
1359
|
var action = texts.detailsAction;
|
|
1287
|
-
if (action !== 'policy' && action !== 'settings' && action !== 'hide') {
|
|
1360
|
+
if (action !== 'policy' && action !== 'settings' && action !== 'hide' && action !== 'declaration') {
|
|
1288
1361
|
action = url ? 'policy' : 'settings';
|
|
1289
1362
|
}
|
|
1290
1363
|
if (action === 'policy' && !url) action = 'settings';
|
|
1291
|
-
|
|
1364
|
+
// Asked for the declaration with no address to send anyone to: fall back to
|
|
1365
|
+
// opening the settings rather than rendering a dead link.
|
|
1366
|
+
if (action === 'declaration' && !decl) action = 'settings';
|
|
1367
|
+
|
|
1368
|
+
var href = null;
|
|
1369
|
+
if (action === 'policy') href = url;
|
|
1370
|
+
else if (action === 'declaration') href = decl;
|
|
1371
|
+
return { kind: action, href: href };
|
|
1292
1372
|
}
|
|
1293
1373
|
|
|
1294
1374
|
// Unknown type -> bar/bottom. Known type with an unrecognized position ->
|
|
@@ -1330,11 +1410,15 @@
|
|
|
1330
1410
|
p.appendChild(document.createTextNode(T.bannerText));
|
|
1331
1411
|
} else {
|
|
1332
1412
|
p.appendChild(document.createTextNode(T.bannerText + ' '));
|
|
1333
|
-
|
|
1413
|
+
// 'policy' and 'declaration' are the same DOM shape — an outbound link in
|
|
1414
|
+
// a new tab — and differ only in where they point (resolveDetails picked
|
|
1415
|
+
// the address). Both must be listed here: a missing branch would silently
|
|
1416
|
+
// render 'declaration' as the settings BUTTON instead of the link.
|
|
1417
|
+
if (det.kind === 'policy' || det.kind === 'declaration') {
|
|
1334
1418
|
var link = el('a', 'ck-banner__more', T.more);
|
|
1335
1419
|
link.href = det.href;
|
|
1336
1420
|
link.target = '_blank';
|
|
1337
|
-
link.rel = 'noopener'; // never hand the
|
|
1421
|
+
link.rel = 'noopener'; // never hand the linked page window.opener
|
|
1338
1422
|
p.appendChild(link);
|
|
1339
1423
|
} else {
|
|
1340
1424
|
// A control that changes what is on screen is a button, not a link:
|
|
@@ -1602,6 +1686,290 @@
|
|
|
1602
1686
|
if (nodes.scrim) nodes.scrim.classList.toggle('ck-hidden', decided || !nodes.bannerModal);
|
|
1603
1687
|
if (nodes.fab) nodes.fab.classList.toggle('ck-hidden', !decided);
|
|
1604
1688
|
if (!panelOpen) syncSwitches(s);
|
|
1689
|
+
|
|
1690
|
+
/* §2 — a consent change is exactly when a card must go: the core's
|
|
1691
|
+
applyConsentToDom() has already put the frame's src back by the time
|
|
1692
|
+
ck:change reaches us (commit() revives before it dispatches), so the
|
|
1693
|
+
sweep sees a frame with a src and retires its placeholder. It runs on
|
|
1694
|
+
every sync, which also catches a category the visitor turned back OFF —
|
|
1695
|
+
though a frame that already loaded cannot be un-loaded, so that direction
|
|
1696
|
+
only matters for frames still held back. */
|
|
1697
|
+
try { sweepPlaceholders(); } catch (e) { /* noop */ }
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/* ------------------------------------------- blocked-embed placeholders (§2) */
|
|
1701
|
+
|
|
1702
|
+
/* SPEC V1.10 §2. When the core holds an <iframe> back — a known tracker, or
|
|
1703
|
+
any third-party frame in strict mode — it leaves it in one shape:
|
|
1704
|
+
`data-ck` + `data-src` and NO src (markBlockedIframe in ck-core.js). This
|
|
1705
|
+
module draws a block of the same size in its place, offering the visitor
|
|
1706
|
+
the one decision that would make the embed appear.
|
|
1707
|
+
|
|
1708
|
+
WHY A SIBLING, NOT A WRAPPER: sites style embeds through the parent
|
|
1709
|
+
(`.video-wrap > iframe`, grid children, aspect-ratio boxes). Wrapping the
|
|
1710
|
+
frame inserts a node into that relationship and breaks the layout it was
|
|
1711
|
+
meant to preserve. A sibling inserted before the frame inherits the same
|
|
1712
|
+
parent context, and removing it later restores the DOM exactly.
|
|
1713
|
+
|
|
1714
|
+
WHO RESTORES THE FRAME: not this file. `Element.prototype.setAttribute` is
|
|
1715
|
+
patched by the core, and only applyConsentToDom() may write a blocked
|
|
1716
|
+
frame's src — under `bypass`, through the native setter it captured before
|
|
1717
|
+
patching. So «Разрешить и показать» grants the category through the normal
|
|
1718
|
+
consent path and the CORE brings the frame back; the next sweep sees a
|
|
1719
|
+
frame that has a src again and drops the placeholder. One code path for
|
|
1720
|
+
the button, the panel's switches and «Принять всё» alike. */
|
|
1721
|
+
|
|
1722
|
+
var PH_ATTR = 'data-ck-ph'; // marks a frame this file hid
|
|
1723
|
+
var PH_DISPLAY = 'data-ck-ph-display'; // its previous inline display value
|
|
1724
|
+
var placeholders = []; // [{ frame, node }]
|
|
1725
|
+
|
|
1726
|
+
function placeholdersEnabled(cfg) {
|
|
1727
|
+
// No default in the core's DEFAULT_CONFIG, exactly like detailsAction:
|
|
1728
|
+
// absent means on, and only an explicit false opts out.
|
|
1729
|
+
try {
|
|
1730
|
+
var b = cfg && cfg.blocking;
|
|
1731
|
+
return !(b && b.placeholders === false);
|
|
1732
|
+
} catch (e) { return true; }
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
/* §2: «Не трогать фреймы display:none, 1×1, и те, что не в <body>.»
|
|
1736
|
+
Order matters — a frame this file has already hidden reads as display:none,
|
|
1737
|
+
which would make it skip its own placeholder and leave it stranded forever.
|
|
1738
|
+
Frames we marked are therefore exempted before the display test runs. */
|
|
1739
|
+
function frameEligible(frame) {
|
|
1740
|
+
try {
|
|
1741
|
+
if (!frame || !document.body) return false;
|
|
1742
|
+
if (!document.body.contains(frame)) return false;
|
|
1743
|
+
if (frame.getAttribute(PH_ATTR)) return true; // ours: already measured
|
|
1744
|
+
var cs = null;
|
|
1745
|
+
try { cs = window.getComputedStyle(frame); } catch (e) { cs = null; }
|
|
1746
|
+
if (cs && (cs.display === 'none' || cs.visibility === 'hidden')) return false;
|
|
1747
|
+
var r = null;
|
|
1748
|
+
try { r = frame.getBoundingClientRect(); } catch (e) { r = null; }
|
|
1749
|
+
// A 1×1 (or 0×0) frame is a tracking pixel dressed as an embed: there is
|
|
1750
|
+
// nothing for a visitor to watch and a card in its place would be noise.
|
|
1751
|
+
if (r && r.width <= 1 && r.height <= 1) {
|
|
1752
|
+
// Zero-sized because it is not laid out YET (a lazy tab, a collapsed
|
|
1753
|
+
// section) is indistinguishable here from a real pixel except via the
|
|
1754
|
+
// attributes, which a pixel sets to 1 and a video does not.
|
|
1755
|
+
var aw = parseInt(frame.getAttribute('width') || '0', 10);
|
|
1756
|
+
var ah = parseInt(frame.getAttribute('height') || '0', 10);
|
|
1757
|
+
if (!(aw > 1 || ah > 1)) return false;
|
|
1758
|
+
}
|
|
1759
|
+
return true;
|
|
1760
|
+
} catch (e) { return false; }
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/* Size the card to the hole the frame left. Attributes first (an embed is
|
|
1764
|
+
nearly always `width="560" height="315"`), computed size second. Read
|
|
1765
|
+
BEFORE the frame is hidden — getComputedStyle on a display:none element
|
|
1766
|
+
reports nothing worth copying. */
|
|
1767
|
+
function frameSize(frame) {
|
|
1768
|
+
var w = '', h = '';
|
|
1769
|
+
try {
|
|
1770
|
+
var aw = frame.getAttribute('width');
|
|
1771
|
+
var ah = frame.getAttribute('height');
|
|
1772
|
+
if (aw && /^\d+$/.test(String(aw).trim())) w = String(aw).trim() + 'px';
|
|
1773
|
+
if (ah && /^\d+$/.test(String(ah).trim())) h = String(ah).trim() + 'px';
|
|
1774
|
+
if (!w || !h) {
|
|
1775
|
+
var cs = window.getComputedStyle(frame);
|
|
1776
|
+
if (!w && cs && cs.width && cs.width !== 'auto' && cs.width !== '0px') w = cs.width;
|
|
1777
|
+
if (!h && cs && cs.height && cs.height !== 'auto' && cs.height !== '0px') h = cs.height;
|
|
1778
|
+
}
|
|
1779
|
+
} catch (e) { /* fall through to the defaults in PH_CSS */ }
|
|
1780
|
+
return { width: w, height: h };
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
var PLAY_ICON =
|
|
1784
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" ' +
|
|
1785
|
+
'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' +
|
|
1786
|
+
'<rect x="2.5" y="4.5" width="19" height="15" rx="2.5"/>' +
|
|
1787
|
+
'<path d="M10.5 9.2v5.6l4.6-2.8-4.6-2.8Z"/></svg>';
|
|
1788
|
+
|
|
1789
|
+
// Own sheet: a placeholder is its own shadow root, so it cannot borrow the
|
|
1790
|
+
// banner's. Tokens come from buildThemeCss() (theme.accent, radius, font),
|
|
1791
|
+
// which is why a placeholder repaints with the banner.
|
|
1792
|
+
var PH_CSS = [
|
|
1793
|
+
':host{all:initial;display:block;max-width:100%}',
|
|
1794
|
+
'*,*::before,*::after{box-sizing:border-box}',
|
|
1795
|
+
'.ck-ph{width:100%;height:100%;min-height:120px;max-width:100%;',
|
|
1796
|
+
'display:flex;flex-direction:column;align-items:center;justify-content:center;',
|
|
1797
|
+
'gap:10px;padding:20px;text-align:center;',
|
|
1798
|
+
'font-family:var(--ck-font);font-size:14px;line-height:1.5;color:var(--ck-ink);',
|
|
1799
|
+
'background:var(--ck-soft);border:1px solid var(--ck-line);',
|
|
1800
|
+
'border-radius:var(--ck-radius-card)}',
|
|
1801
|
+
'.ck-ph svg{width:32px;height:32px;display:block;color:var(--ck-muted);flex:none}',
|
|
1802
|
+
'.ck-ph p{margin:0;color:var(--ck-muted);max-width:44ch}',
|
|
1803
|
+
'.ck-ph__row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center}',
|
|
1804
|
+
'.ck-ph__btn{font:inherit;font-weight:600;cursor:pointer;padding:9px 16px;',
|
|
1805
|
+
'border-radius:var(--ck-radius-btn);border:1px solid var(--ck-accent);',
|
|
1806
|
+
'background:var(--ck-accent);color:var(--ck-on-accent)}',
|
|
1807
|
+
'.ck-ph__link{font:inherit;background:none;border:0;padding:4px;cursor:pointer;',
|
|
1808
|
+
'color:var(--ck-link);text-decoration:underline}',
|
|
1809
|
+
':focus-visible{outline:2px solid var(--ck-accent);outline-offset:2px;border-radius:4px}'
|
|
1810
|
+
].join('\n');
|
|
1811
|
+
|
|
1812
|
+
function categoryEnabled(cfg, cat) {
|
|
1813
|
+
try {
|
|
1814
|
+
if (cat === 'necessary') return false; // nothing to grant
|
|
1815
|
+
var c = cfg && cfg.categories && cfg.categories[cat];
|
|
1816
|
+
return !c || c.enabled !== false;
|
|
1817
|
+
} catch (e) { return true; }
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function buildPlaceholder(frame, cat, cfg) {
|
|
1821
|
+
var mountEl = document.createElement('div');
|
|
1822
|
+
mountEl.setAttribute('data-ck-placeholder', '1');
|
|
1823
|
+
var size = frameSize(frame);
|
|
1824
|
+
var st = mountEl.style;
|
|
1825
|
+
st.setProperty('max-width', '100%');
|
|
1826
|
+
if (size.width) st.setProperty('width', size.width);
|
|
1827
|
+
if (size.height) st.setProperty('min-height', size.height);
|
|
1828
|
+
|
|
1829
|
+
var sr = mountEl.attachShadow({ mode: 'open' });
|
|
1830
|
+
var sheet = document.createElement('style');
|
|
1831
|
+
/* buildThemeCss writes :host tokens — the same tokens, in this root, which
|
|
1832
|
+
is what makes a placeholder repaint with the banner.
|
|
1833
|
+
|
|
1834
|
+
The font gets applyTheme()'s treatment for the same reason it needs it
|
|
1835
|
+
there: `:host{all:initial}` resets the family, so a bare `inherit` in
|
|
1836
|
+
this root resolves to the UA default (Times), not to the page's type.
|
|
1837
|
+
The probed page font when it can be read, the system stack when it
|
|
1838
|
+
cannot — never a bare `inherit`. */
|
|
1839
|
+
var built = buildThemeCss(cfg);
|
|
1840
|
+
var phFont = '';
|
|
1841
|
+
if (built.font === 'inherit') {
|
|
1842
|
+
phFont = '\n:host{--ck-font:' + (resolvePageFont() || SYSTEM_FONT) + '}';
|
|
1843
|
+
}
|
|
1844
|
+
sheet.textContent = PH_CSS + '\n' + built.css + phFont;
|
|
1845
|
+
sr.appendChild(sheet);
|
|
1846
|
+
|
|
1847
|
+
var card = el('div', 'ck-ph');
|
|
1848
|
+
card.setAttribute('role', 'group');
|
|
1849
|
+
card.setAttribute('aria-label', T.phLabel);
|
|
1850
|
+
|
|
1851
|
+
var icon = document.createElement('div');
|
|
1852
|
+
icon.innerHTML = PLAY_ICON;
|
|
1853
|
+
card.appendChild(icon);
|
|
1854
|
+
|
|
1855
|
+
var label = hostOf(frame.getAttribute('data-src'));
|
|
1856
|
+
card.appendChild(el('p', null, placeholderText(label, cat, LANG)));
|
|
1857
|
+
|
|
1858
|
+
var row = el('div', 'ck-ph__row');
|
|
1859
|
+
/* Only offer the grant when it can actually take effect: the core's
|
|
1860
|
+
filterByConfig() drops a category the site disabled in config, so the
|
|
1861
|
+
button would consume the click and change nothing. The panel link stays
|
|
1862
|
+
either way — it is always an honest answer. */
|
|
1863
|
+
if (categoryEnabled(cfg, cat)) {
|
|
1864
|
+
var allow = el('button', 'ck-ph__btn', T.phAllow);
|
|
1865
|
+
allow.type = 'button';
|
|
1866
|
+
allow.addEventListener('click', function () { grantCategory(cat); });
|
|
1867
|
+
row.appendChild(allow);
|
|
1868
|
+
}
|
|
1869
|
+
var settings = el('button', 'ck-ph__link', T.phSettings);
|
|
1870
|
+
settings.type = 'button';
|
|
1871
|
+
settings.addEventListener('click', function () { openPanel(settings); });
|
|
1872
|
+
row.appendChild(settings);
|
|
1873
|
+
card.appendChild(row);
|
|
1874
|
+
|
|
1875
|
+
sr.appendChild(card);
|
|
1876
|
+
return mountEl;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
/* «Разрешить и показать» — one category, through the normal consent path.
|
|
1880
|
+
|
|
1881
|
+
MERGE, DO NOT REPLACE: ConsentKit.accept({...}) SETS the whole opt-in set
|
|
1882
|
+
from the object it is given, so passing `{marketing:true}` alone would
|
|
1883
|
+
silently switch OFF an analytics consent the visitor had already given.
|
|
1884
|
+
The current state is read first and only the one category is flipped.
|
|
1885
|
+
|
|
1886
|
+
accept(object) files the decision as method 'custom', which is what §2 asks
|
|
1887
|
+
the journal to record, and ck-saas.js logs it off the ck:consent/ck:change
|
|
1888
|
+
the core dispatches — so there is no logging code here. The core's
|
|
1889
|
+
applyConsentToDom() restores the frame; sweepPlaceholders() then removes
|
|
1890
|
+
this card, driven by the ck:change that same commit dispatches. */
|
|
1891
|
+
function grantCategory(cat) {
|
|
1892
|
+
var ck = api();
|
|
1893
|
+
if (!ck || typeof ck.accept !== 'function') return;
|
|
1894
|
+
var cur = safeState().categories || {};
|
|
1895
|
+
var next = {
|
|
1896
|
+
functional: cur.functional === true,
|
|
1897
|
+
analytics: cur.analytics === true,
|
|
1898
|
+
marketing: cur.marketing === true
|
|
1899
|
+
};
|
|
1900
|
+
if (cat === 'functional' || cat === 'analytics' || cat === 'marketing') next[cat] = true;
|
|
1901
|
+
try { ck.accept(next); } catch (e) { /* noop */ }
|
|
1902
|
+
syncFromState();
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function hideFrame(frame) {
|
|
1906
|
+
try {
|
|
1907
|
+
frame.setAttribute(PH_DISPLAY, frame.style.display || '');
|
|
1908
|
+
frame.setAttribute(PH_ATTR, '1');
|
|
1909
|
+
frame.style.display = 'none';
|
|
1910
|
+
} catch (e) { /* noop */ }
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
function restoreFrame(frame) {
|
|
1914
|
+
try {
|
|
1915
|
+
var prev = frame.getAttribute(PH_DISPLAY);
|
|
1916
|
+
frame.style.display = prev || '';
|
|
1917
|
+
frame.removeAttribute(PH_DISPLAY);
|
|
1918
|
+
frame.removeAttribute(PH_ATTR);
|
|
1919
|
+
} catch (e) { /* noop */ }
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
function dropPlaceholder(entry) {
|
|
1923
|
+
try {
|
|
1924
|
+
if (entry.node && entry.node.parentNode) entry.node.parentNode.removeChild(entry.node);
|
|
1925
|
+
} catch (e) { /* noop */ }
|
|
1926
|
+
restoreFrame(entry.frame);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function clearPlaceholders() {
|
|
1930
|
+
for (var i = 0; i < placeholders.length; i++) dropPlaceholder(placeholders[i]);
|
|
1931
|
+
placeholders = [];
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
/* Idempotent: called from mount() and from every consent change. Adds cards
|
|
1935
|
+
for frames still held back, removes them from frames the core revived. */
|
|
1936
|
+
function sweepPlaceholders(cfg) {
|
|
1937
|
+
if (typeof document === 'undefined' || !document.body) return;
|
|
1938
|
+
var c = cfg || safeConfig();
|
|
1939
|
+
|
|
1940
|
+
// Placeholders turned off after some were drawn: take them all down.
|
|
1941
|
+
if (!placeholdersEnabled(c)) { clearPlaceholders(); return; }
|
|
1942
|
+
|
|
1943
|
+
// 1. Retire cards whose frame came back (or left the document).
|
|
1944
|
+
var kept = [];
|
|
1945
|
+
for (var i = 0; i < placeholders.length; i++) {
|
|
1946
|
+
var e = placeholders[i];
|
|
1947
|
+
var revived = false;
|
|
1948
|
+
try {
|
|
1949
|
+
revived = !document.body.contains(e.frame) || !!e.frame.getAttribute('src');
|
|
1950
|
+
} catch (e2) { revived = true; }
|
|
1951
|
+
if (revived) dropPlaceholder(e); else kept.push(e);
|
|
1952
|
+
}
|
|
1953
|
+
placeholders = kept;
|
|
1954
|
+
|
|
1955
|
+
// 2. Draw cards for frames the core is holding back. The selector is the
|
|
1956
|
+
// core's own revival selector, which is why a frame the SITE allowed can
|
|
1957
|
+
// never match: an allowed frame keeps its src and is never marked.
|
|
1958
|
+
var list;
|
|
1959
|
+
try { list = document.querySelectorAll('iframe[data-ck][data-src]'); } catch (e3) { return; }
|
|
1960
|
+
for (var j = 0; j < list.length; j++) {
|
|
1961
|
+
var frame = list[j];
|
|
1962
|
+
try {
|
|
1963
|
+
if (frame.getAttribute('src')) continue; // already revived
|
|
1964
|
+
if (frame.getAttribute(PH_ATTR)) continue; // already carded
|
|
1965
|
+
if (!frameEligible(frame)) continue;
|
|
1966
|
+
var cat = frame.getAttribute('data-ck') || 'marketing';
|
|
1967
|
+
var node = buildPlaceholder(frame, cat, c);
|
|
1968
|
+
if (frame.parentNode) frame.parentNode.insertBefore(node, frame);
|
|
1969
|
+
hideFrame(frame);
|
|
1970
|
+
placeholders.push({ frame: frame, node: node });
|
|
1971
|
+
} catch (e4) { /* one bad frame must not stop the sweep */ }
|
|
1972
|
+
}
|
|
1605
1973
|
}
|
|
1606
1974
|
|
|
1607
1975
|
/* ----------------------------------------------------------------- mount */
|
|
@@ -1632,6 +2000,10 @@
|
|
|
1632
2000
|
mounted = false;
|
|
1633
2001
|
panelOpen = false;
|
|
1634
2002
|
lastFocus = null;
|
|
2003
|
+
// The cards belong to the render about to be replaced: their listeners close
|
|
2004
|
+
// over the old T and the old shadow root. mount() sweeps again and draws
|
|
2005
|
+
// fresh ones in the new language and theme.
|
|
2006
|
+
clearPlaceholders();
|
|
1635
2007
|
// The pending schedule belongs to the shadow root about to be rebuilt; the
|
|
1636
2008
|
// fresh mount() starts its own ladder from a clean count.
|
|
1637
2009
|
clearFontTimers();
|
|
@@ -1649,7 +2021,8 @@
|
|
|
1649
2021
|
}
|
|
1650
2022
|
|
|
1651
2023
|
var table = localeTable(); // read at render time
|
|
1652
|
-
|
|
2024
|
+
LANG = resolveLang(cfg && cfg.language, table);
|
|
2025
|
+
T = buildStrings(LANG, table);
|
|
1653
2026
|
|
|
1654
2027
|
host = document.getElementById('ck-root');
|
|
1655
2028
|
if (!host) {
|
|
@@ -1686,6 +2059,55 @@
|
|
|
1686
2059
|
// Only now: scheduleFontProbes() re-applies the theme from a timer, and
|
|
1687
2060
|
// applyTheme() is a no-op until there is a host and a themeStyle to write.
|
|
1688
2061
|
scheduleFontProbes(cfg);
|
|
2062
|
+
|
|
2063
|
+
// SPEC V1.10 §2 — заглушки вместо задержанных встраиваний. After the shadow
|
|
2064
|
+
// root exists, because a placeholder's «Настроить cookie» calls openPanel().
|
|
2065
|
+
sweepPlaceholders(cfg);
|
|
2066
|
+
|
|
2067
|
+
/* SPEC V1.10 §1 — honour a settings request that arrived before there was
|
|
2068
|
+
anything to open: either ConsentKit.openSettings() called while this file
|
|
2069
|
+
was still loading (the core latches it), or a page opened directly on
|
|
2070
|
+
`#ck-settings`. Both end in the same panel. */
|
|
2071
|
+
consumePendingOpen();
|
|
2072
|
+
openFromHash();
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
/* ---------------------------------------------------- settings deep link */
|
|
2076
|
+
|
|
2077
|
+
var SETTINGS_HASH = '#ck-settings';
|
|
2078
|
+
|
|
2079
|
+
// The core latches openSettings() calls made before ck-ui.js was parsed, so a
|
|
2080
|
+
// footer link clicked during a slow load still opens the panel once we exist.
|
|
2081
|
+
function consumePendingOpen() {
|
|
2082
|
+
var ck = api();
|
|
2083
|
+
if (!ck || !ck._pendingOpen) return;
|
|
2084
|
+
try { ck._pendingOpen = false; } catch (e) { /* noop */ }
|
|
2085
|
+
openPanel(null);
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
/* `https://site/#ck-settings` — the address the cookie declaration page links
|
|
2089
|
+
«Изменить выбор cookie» to. The hash is removed again via replaceState so a
|
|
2090
|
+
reload, a shared link or a back-navigation does not re-open the panel, and
|
|
2091
|
+
so the address bar does not keep a control fragment in it.
|
|
2092
|
+
|
|
2093
|
+
replaceState is fed pathname+search rather than '' — an empty URL argument
|
|
2094
|
+
is a no-op in some engines, which would leave the hash in place and re-open
|
|
2095
|
+
the panel on the next hashchange. Everything is guarded: a sandboxed iframe
|
|
2096
|
+
throws on replaceState, and a panel that opened is worth more than a tidy
|
|
2097
|
+
address bar. */
|
|
2098
|
+
function openFromHash() {
|
|
2099
|
+
try {
|
|
2100
|
+
if (typeof location === 'undefined' || location.hash !== SETTINGS_HASH) return;
|
|
2101
|
+
} catch (e) { return; }
|
|
2102
|
+
clearSettingsHash();
|
|
2103
|
+
openPanel(null);
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
function clearSettingsHash() {
|
|
2107
|
+
try {
|
|
2108
|
+
if (typeof history === 'undefined' || typeof history.replaceState !== 'function') return;
|
|
2109
|
+
history.replaceState(null, '', location.pathname + location.search);
|
|
2110
|
+
} catch (e) { /* noop */ }
|
|
1689
2111
|
}
|
|
1690
2112
|
|
|
1691
2113
|
/* ---------------------------------------------------------------- events */
|
|
@@ -1716,7 +2138,11 @@
|
|
|
1716
2138
|
nextProbeDelay: nextProbeDelay,
|
|
1717
2139
|
shouldReprobe: shouldReprobe,
|
|
1718
2140
|
resolveDetails: resolveDetails,
|
|
1719
|
-
buildThemeCss: buildThemeCss
|
|
2141
|
+
buildThemeCss: buildThemeCss,
|
|
2142
|
+
// SPEC V1.10 §2: which sentence a blocked embed shows. Pure, so the
|
|
2143
|
+
// wording is testable (and quotable by the cabinet) without a DOM.
|
|
2144
|
+
placeholderText: placeholderText,
|
|
2145
|
+
placeholdersEnabled: placeholdersEnabled
|
|
1720
2146
|
};
|
|
1721
2147
|
// The page-font probe reads the DOM, so it is not part of the pure block —
|
|
1722
2148
|
// but the debug panel must be able to quote the family the banner painted
|
|
@@ -1762,9 +2188,29 @@
|
|
|
1762
2188
|
|
|
1763
2189
|
document.addEventListener('ck:ui:open-preferences', function () {
|
|
1764
2190
|
if (!mounted) mount(safeConfig());
|
|
2191
|
+
// mount() consumes the core's latch itself; clear it here too so a call
|
|
2192
|
+
// made while we were already mounted cannot leave a stale flag behind for
|
|
2193
|
+
// a later remount to act on.
|
|
2194
|
+
var ck = api();
|
|
2195
|
+
if (ck && ck._pendingOpen) { try { ck._pendingOpen = false; } catch (e) { /* noop */ } }
|
|
1765
2196
|
openPanel(null);
|
|
1766
2197
|
});
|
|
1767
2198
|
|
|
2199
|
+
/* SPEC V1.10 §1 — navigating to `#ck-settings` on a page that is already
|
|
2200
|
+
loaded (a footer link, or the declaration page opened in the same tab).
|
|
2201
|
+
mount() covers the other half: a page ENTERED on that hash.
|
|
2202
|
+
|
|
2203
|
+
Feature-checked rather than assumed: the SSR guard above only proves there
|
|
2204
|
+
is a `document`, and the branding suite evaluates this file against a stub
|
|
2205
|
+
window that has none of the event plumbing. A missing hashchange costs the
|
|
2206
|
+
deep link, not the banner. */
|
|
2207
|
+
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
2208
|
+
window.addEventListener('hashchange', function () {
|
|
2209
|
+
if (!mounted) mount(safeConfig());
|
|
2210
|
+
openFromHash();
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
|
|
1768
2214
|
document.addEventListener('ck:ui:close', function () {
|
|
1769
2215
|
closePanel();
|
|
1770
2216
|
});
|