@ecomconsult/consentkit 0.5.6 → 0.5.8
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 +103 -3
- package/npm/core.cjs +4 -0
- package/npm/core.mjs +7 -0
- package/npm/index.cjs +4 -0
- package/npm/index.d.ts +92 -2
- package/npm/index.mjs +7 -0
- package/npm/internal-stub.mjs +12 -1
- package/npm/react.mjs +10 -1
- package/package.json +3 -2
- package/src/ck-core.js +406 -12
- package/src/ck-debug.js +273 -3
- package/src/ck-locales.js +12 -0
- package/src/ck-saas.js +33 -0
- package/src/ck-ui.js +982 -39
package/src/ck-ui.js
CHANGED
|
@@ -32,6 +32,26 @@
|
|
|
32
32
|
colExpiry: 'Expires',
|
|
33
33
|
floating: 'Cookie settings',
|
|
34
34
|
poweredBy: 'Powered by ConsentKit',
|
|
35
|
+
// SPEC V1.12 §3 — services inside a category group.
|
|
36
|
+
//
|
|
37
|
+
// `svcCount`/`ckCount` are PLURAL FORMS, not plain strings: «1 сервис»,
|
|
38
|
+
// «2 сервиса», «5 сервисов». Each is an array read by plural() below, and
|
|
39
|
+
// each language supplies as many forms as its own grammar needs — one for
|
|
40
|
+
// English, three for Russian. {n} is the number.
|
|
41
|
+
svcCount: ['{n} service', '{n} services'],
|
|
42
|
+
ckCount: ['{n} cookie', '{n} cookies'],
|
|
43
|
+
svcPolicy: 'Privacy policy',
|
|
44
|
+
// The service's own cookies, listed under it rather than in the group's
|
|
45
|
+
// «Which cookies» table.
|
|
46
|
+
svcCookies: 'Cookies it sets',
|
|
47
|
+
// SPEC V1.10 §2 — the blocked-embed placeholder. {host} is the vendor
|
|
48
|
+
// label when the database knows one and the bare host otherwise; {cat} is
|
|
49
|
+
// the localized category title, taken from cat.<name>.title below, so the
|
|
50
|
+
// name in the placeholder is the name on the switch in the panel.
|
|
51
|
+
phText: 'Content from {host} goes here. It will load once you allow «{cat}».',
|
|
52
|
+
phAllow: 'Allow and show',
|
|
53
|
+
phSettings: 'Cookie settings',
|
|
54
|
+
phLabel: 'Blocked content',
|
|
35
55
|
cat: {
|
|
36
56
|
necessary: {
|
|
37
57
|
title: 'Necessary',
|
|
@@ -72,6 +92,15 @@
|
|
|
72
92
|
colExpiry: 'Срок',
|
|
73
93
|
floating: 'Настройки cookie',
|
|
74
94
|
poweredBy: 'Работает на ConsentKit',
|
|
95
|
+
// SPEC V1.12 §3. Три формы: 1 сервис, 2 сервиса, 5 сервисов.
|
|
96
|
+
svcCount: ['{n} сервис', '{n} сервиса', '{n} сервисов'],
|
|
97
|
+
ckCount: ['{n} cookie', '{n} cookie', '{n} cookie'],
|
|
98
|
+
svcPolicy: 'Политика',
|
|
99
|
+
svcCookies: 'Какие cookie ставит',
|
|
100
|
+
phText: 'Здесь содержимое от {host}. Оно загрузится после согласия на «{cat}».',
|
|
101
|
+
phAllow: 'Разрешить и показать',
|
|
102
|
+
phSettings: 'Настроить cookie',
|
|
103
|
+
phLabel: 'Заблокированное содержимое',
|
|
75
104
|
cat: {
|
|
76
105
|
necessary: {
|
|
77
106
|
title: 'Необходимые',
|
|
@@ -103,9 +132,22 @@
|
|
|
103
132
|
'bannerTitle', 'bannerText', 'more', 'acceptAll', 'rejectAll', 'customize',
|
|
104
133
|
'bannerLabel', 'panelTitle', 'panelIntro', 'save', 'close', 'alwaysOn',
|
|
105
134
|
'cookiesIn', 'noCookies', 'colName', 'colVendor', 'colPurpose', 'colExpiry', 'floating',
|
|
106
|
-
'poweredBy'
|
|
135
|
+
'poweredBy',
|
|
136
|
+
// SPEC V1.10 §2. Present in ck-locales.js for ro only; every other external
|
|
137
|
+
// locale falls back to DICT.en through buildStrings(), which is what §2
|
|
138
|
+
// asks for («остальные языки — en»).
|
|
139
|
+
'phText', 'phAllow', 'phSettings', 'phLabel',
|
|
140
|
+
// SPEC V1.12 §3. svcPolicy/svcCookies are plain strings and belong here;
|
|
141
|
+
// svcCount/ckCount are ARRAYS of plural forms and are filled by their own
|
|
142
|
+
// branch in buildStrings() — listing them here would be a bug, because this
|
|
143
|
+
// loop only copies values that are `typeof === 'string'` and would leave
|
|
144
|
+
// every external locale on the English plurals.
|
|
145
|
+
'svcPolicy', 'svcCookies'
|
|
107
146
|
];
|
|
108
147
|
|
|
148
|
+
// Plural-form keys, filled separately from STR_KEYS (see above).
|
|
149
|
+
var PLURAL_KEYS = ['svcCount', 'ckCount'];
|
|
150
|
+
|
|
109
151
|
// builtin(en,ru) <- window.__ckLocales, read at render time so the locales
|
|
110
152
|
// file may load in any order relative to this one.
|
|
111
153
|
function localeTable() {
|
|
@@ -137,6 +179,60 @@
|
|
|
137
179
|
return 'en';
|
|
138
180
|
}
|
|
139
181
|
|
|
182
|
+
/* SPEC V1.12 §3 — plural forms.
|
|
183
|
+
|
|
184
|
+
Validated as a WHOLE: an array of at least one non-empty string, or null.
|
|
185
|
+
A locale that supplies `['{n} сервис']` alone is honest — it says «this
|
|
186
|
+
language has one form» — and plural() below simply always picks it. */
|
|
187
|
+
function pluralForms(v) {
|
|
188
|
+
if (!v || !Array.isArray(v) || !v.length) return null;
|
|
189
|
+
var out = [];
|
|
190
|
+
for (var i = 0; i < v.length && i < 3; i++) {
|
|
191
|
+
if (typeof v[i] !== 'string' || !v[i]) return null;
|
|
192
|
+
out.push(v[i]);
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Picks the form for `n` and substitutes it in.
|
|
198
|
+
|
|
199
|
+
Three families, chosen by the language code rather than by
|
|
200
|
+
Intl.PluralRules: Intl is present in every browser this client supports,
|
|
201
|
+
but its category NAMES ('one'|'few'|'many'|'other') vary per language, and
|
|
202
|
+
mapping them onto a positional array is more code and more failure modes
|
|
203
|
+
than the two rules that actually matter here.
|
|
204
|
+
|
|
205
|
+
ru/uk/sr/hr/… 1, 21, 31 -> [0]; 2-4, 22-24 -> [1]; 0, 5-20 -> [2]
|
|
206
|
+
ro 1 -> [0]; 0 and 2-19 -> [1]; 20+ -> [2] («20 de servicii»)
|
|
207
|
+
everything else 1 -> [0]; otherwise -> [1]
|
|
208
|
+
|
|
209
|
+
A locale with fewer forms than the rule asks for clamps to its last one, so
|
|
210
|
+
a single-form array can never index past its end. */
|
|
211
|
+
var SLAVIC_PLURAL = { ru: 1, uk: 1, sr: 1, hr: 1, cs: 1, sk: 1, pl: 1, be: 1, bs: 1 };
|
|
212
|
+
|
|
213
|
+
function pluralIndex(lang, n) {
|
|
214
|
+
var code = String(lang || 'en').slice(0, 2).toLowerCase();
|
|
215
|
+
var mod10 = n % 10, mod100 = n % 100;
|
|
216
|
+
if (SLAVIC_PLURAL[code]) {
|
|
217
|
+
if (mod10 === 1 && mod100 !== 11) return 0;
|
|
218
|
+
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return 1;
|
|
219
|
+
return 2;
|
|
220
|
+
}
|
|
221
|
+
if (code === 'ro' || code === 'mo') {
|
|
222
|
+
if (n === 1) return 0;
|
|
223
|
+
if (n === 0 || (mod100 >= 1 && mod100 <= 19)) return 1;
|
|
224
|
+
return 2;
|
|
225
|
+
}
|
|
226
|
+
return n === 1 ? 0 : 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function plural(forms, n, lang) {
|
|
230
|
+
var list = pluralForms(forms) || ['{n}'];
|
|
231
|
+
var idx = pluralIndex(lang, n);
|
|
232
|
+
if (idx >= list.length) idx = list.length - 1;
|
|
233
|
+
return list[idx].replace('{n}', String(n));
|
|
234
|
+
}
|
|
235
|
+
|
|
140
236
|
// Deep two-level fill from en: a partial locale must never yield undefined,
|
|
141
237
|
// which would render the literal string "undefined".
|
|
142
238
|
function buildStrings(lang, table) {
|
|
@@ -148,6 +244,15 @@
|
|
|
148
244
|
var k = STR_KEYS[i];
|
|
149
245
|
out[k] = (typeof src[k] === 'string' && src[k]) ? src[k] : base[k];
|
|
150
246
|
}
|
|
247
|
+
/* SPEC V1.12 §3 — plural forms. An array of 1..3 strings, taken from the
|
|
248
|
+
locale only when it is a non-empty array of strings; anything else falls
|
|
249
|
+
back to English wholesale rather than per-slot, because a half-filled
|
|
250
|
+
plural table produces «2 services» inside a Russian sentence. */
|
|
251
|
+
for (i = 0; i < PLURAL_KEYS.length; i++) {
|
|
252
|
+
var pk = PLURAL_KEYS[i];
|
|
253
|
+
out[pk] = pluralForms(src[pk]) || base[pk].slice();
|
|
254
|
+
}
|
|
255
|
+
|
|
151
256
|
out.cat = {};
|
|
152
257
|
var sc = (src.cat && typeof src.cat === 'object') ? src.cat : {};
|
|
153
258
|
for (i = 0; i < ALL_CATS.length; i++) {
|
|
@@ -161,6 +266,74 @@
|
|
|
161
266
|
return out;
|
|
162
267
|
}
|
|
163
268
|
|
|
269
|
+
/* ------------------------------------------- blocked-embed placeholder (§2) */
|
|
270
|
+
|
|
271
|
+
/* The one piece of the placeholder that is worth testing without a DOM: which
|
|
272
|
+
sentence a visitor reads. PURE — takes a host (or vendor label), a category
|
|
273
|
+
name and a language, returns the finished string.
|
|
274
|
+
|
|
275
|
+
`host` is used verbatim: the core has no vendor-label table (only the SaaS
|
|
276
|
+
scanner's VENDOR_DB does, server-side), so §2's «ярлык хоста; без ярлыка —
|
|
277
|
+
содержимое с <host>» collapses to the host here. When a label lookup is
|
|
278
|
+
added to the core later, this signature already accepts it — pass the label
|
|
279
|
+
instead of the host and nothing else changes.
|
|
280
|
+
|
|
281
|
+
The category name is resolved through the SAME buildStrings() the panel
|
|
282
|
+
uses, so «Маркетинг» in the placeholder is «Маркетинг» on the switch. An
|
|
283
|
+
unknown language falls back to en, an unknown category to marketing — the
|
|
284
|
+
category a strict-mode interception is filed under. */
|
|
285
|
+
/* SPEC V1.12 §3 — «заглушки iframe — по сервису».
|
|
286
|
+
|
|
287
|
+
`subject` names what the visitor has to agree to for this frame to appear.
|
|
288
|
+
Left out, it is the category, exactly as in 0.5.7. Passed, it is the
|
|
289
|
+
SERVICE name — which is the only honest sentence in the state this wave
|
|
290
|
+
introduces: analytics granted, one service refused, the frame still held.
|
|
291
|
+
Naming the category there would read «загрузится после согласия на
|
|
292
|
+
"Аналитика"» to a visitor who has already agreed to analytics. */
|
|
293
|
+
function placeholderText(host, category, lang, subject) {
|
|
294
|
+
var table = localeTable();
|
|
295
|
+
var T2 = buildStrings(resolveLang(lang, table), table);
|
|
296
|
+
var cat = (category && T2.cat[category]) ? category : 'marketing';
|
|
297
|
+
var name = String(host || '').trim();
|
|
298
|
+
var label = (typeof subject === 'string' && subject.trim())
|
|
299
|
+
? subject.trim() : T2.cat[cat].title;
|
|
300
|
+
return T2.phText
|
|
301
|
+
.replace('{host}', name || T2.phLabel)
|
|
302
|
+
.replace('{cat}', label);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* Which service, if any, is what is actually holding this frame back? Null
|
|
306
|
+
when the frame is held by its category alone — the ordinary 0.5.7 case. */
|
|
307
|
+
function holdingService(src) {
|
|
308
|
+
var ck = api();
|
|
309
|
+
if (!ck || typeof ck._serviceForUrl !== 'function') return null;
|
|
310
|
+
try {
|
|
311
|
+
var svc = ck._serviceForUrl(src);
|
|
312
|
+
if (!svc) return null;
|
|
313
|
+
// Only when the SERVICE is the reason: with the category still denied the
|
|
314
|
+
// category is the honest thing to name, because granting the service
|
|
315
|
+
// alone would not bring the frame back.
|
|
316
|
+
if (typeof ck.allowed === 'function' && !ck.allowed(svc.category)) return null;
|
|
317
|
+
if (typeof ck.allowedService !== 'function') return null;
|
|
318
|
+
return ck.allowedService(svc.id) ? null : svc;
|
|
319
|
+
} catch (e) { return null; }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* Hostname of a blocked frame's real address, for the sentence above. The
|
|
323
|
+
data-src is whatever the page asked for, which may be protocol-relative or
|
|
324
|
+
relative, so it is resolved against the page like the core does it. */
|
|
325
|
+
function hostOf(src) {
|
|
326
|
+
var s = String(src || '');
|
|
327
|
+
try {
|
|
328
|
+
var base = (typeof location !== 'undefined' && location.href) || 'http://localhost/';
|
|
329
|
+
var h = new URL(s, base).hostname || '';
|
|
330
|
+
return h.toLowerCase().replace(/^www\./, '');
|
|
331
|
+
} catch (e) {
|
|
332
|
+
var m = /^(?:[a-z]+:)?\/\/([^/?#]+)/i.exec(s);
|
|
333
|
+
return m ? m[1].toLowerCase().replace(/:\d+$/, '').replace(/^www\./, '') : '';
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
164
337
|
/* --------------------------------------------------------------- styles */
|
|
165
338
|
|
|
166
339
|
var CSS = [
|
|
@@ -285,6 +458,28 @@
|
|
|
285
458
|
'.ck-cat__badge{font-size:12px;font-weight:500;color:var(--ck-muted);',
|
|
286
459
|
'border:1px solid var(--ck-line);border-radius:999px;padding:1px 8px}',
|
|
287
460
|
'.ck-cat__desc{margin:4px 0 0;font-size:13.5px;color:var(--ck-muted)}',
|
|
461
|
+
/* SPEC V1.12 §3 — «N сервисов · M cookie». --ck-muted, like every other
|
|
462
|
+
secondary label on the card, and it is measured for AA against the card
|
|
463
|
+
background by the same rule the description above answers to. */
|
|
464
|
+
'.ck-cat__count{font-size:12px;font-weight:500;color:var(--ck-muted)}',
|
|
465
|
+
|
|
466
|
+
/* ---- services inside a group (SPEC V1.12 §3) ---- */
|
|
467
|
+
/* Indented and rule-separated so the nesting reads without colour: a
|
|
468
|
+
service belongs to the group above it, and its own cookie table belongs
|
|
469
|
+
to it. The left border is the only decoration; everything else is
|
|
470
|
+
spacing, which survives forced-colours mode intact. */
|
|
471
|
+
'.ck-svcs{margin:12px 0 0;padding-left:12px;border-left:2px solid var(--ck-line)}',
|
|
472
|
+
'.ck-svc{padding:10px 0;border-bottom:1px solid var(--ck-line)}',
|
|
473
|
+
'.ck-svc:first-child{padding-top:2px}',
|
|
474
|
+
'.ck-svc:last-child{border-bottom:0;padding-bottom:2px}',
|
|
475
|
+
'.ck-svc__top{display:flex;gap:12px;align-items:flex-start}',
|
|
476
|
+
'.ck-svc__txt{flex:1 1 auto;min-width:0}',
|
|
477
|
+
'.ck-svc__name{font-size:14px;font-weight:600}',
|
|
478
|
+
'.ck-svc__vendor{margin:2px 0 0;font-size:12.5px;color:var(--ck-muted)}',
|
|
479
|
+
'.ck-svc__desc{margin:4px 0 0;font-size:13px;color:var(--ck-muted)}',
|
|
480
|
+
/* --ck-link, not --ck-accent: accent-coloured TEXT goes through the same
|
|
481
|
+
>= 4.5:1 rule as «Подробнее» — see the note on the `a{}` rule above. */
|
|
482
|
+
'.ck-svc__policy{display:inline-block;margin-top:4px;font-size:12.5px;color:var(--ck-link)}',
|
|
288
483
|
|
|
289
484
|
/* ---- switch ---- */
|
|
290
485
|
'.ck-switch{flex:none;width:46px;height:27px;padding:0;border-radius:999px;',
|
|
@@ -294,6 +489,11 @@
|
|
|
294
489
|
'.ck-switch[aria-checked="true"]{background:var(--ck-accent);border-color:var(--ck-accent)}',
|
|
295
490
|
'.ck-switch[aria-checked="true"]::after{left:auto;right:2px;border-color:transparent}',
|
|
296
491
|
'.ck-switch[disabled]{cursor:not-allowed;opacity:.55}',
|
|
492
|
+
/* The service switch: the same control, smaller. Still 27px of vertical
|
|
493
|
+
hit area at the row level and a real <button role="switch">, so the
|
|
494
|
+
keyboard and screen-reader behaviour is identical to the group's. */
|
|
495
|
+
'.ck-switch--sm{width:38px;height:22px}',
|
|
496
|
+
'.ck-switch--sm::after{width:16px;height:16px}',
|
|
297
497
|
|
|
298
498
|
/* ---- cookie table ---- */
|
|
299
499
|
'.ck-det{margin-top:12px}',
|
|
@@ -434,6 +634,7 @@
|
|
|
434
634
|
var LANG = 'en'; // the code T was built from, reassigned per mount
|
|
435
635
|
var nodes = {}; // banner/panel/fab refs
|
|
436
636
|
var switches = {}; // category -> button
|
|
637
|
+
var serviceSwitches = {}; // category -> [button], SPEC V1.12 §3
|
|
437
638
|
var panelOpen = false;
|
|
438
639
|
var lastFocus = null;
|
|
439
640
|
|
|
@@ -463,6 +664,92 @@
|
|
|
463
664
|
return out;
|
|
464
665
|
}
|
|
465
666
|
|
|
667
|
+
/* ------------------------------------------------------- services (§3) */
|
|
668
|
+
|
|
669
|
+
/* The services of one category, as the CORE normalised them.
|
|
670
|
+
|
|
671
|
+
Read through ConsentKit._services() rather than off config.services
|
|
672
|
+
directly: the core drops rows with `enabled: false`, a malformed id or an
|
|
673
|
+
unknown category, and the panel must list exactly the set the engine
|
|
674
|
+
blocks by. A config the core has not seen (no init(), or a core too old to
|
|
675
|
+
know about services) yields nothing, and the panel renders as it did in
|
|
676
|
+
0.5.7 — which is what «старый конфиг рендерится как раньше» asks for. */
|
|
677
|
+
function servicesFor(cat) {
|
|
678
|
+
var ck = api();
|
|
679
|
+
var list = null;
|
|
680
|
+
try {
|
|
681
|
+
if (ck && typeof ck._services === 'function') list = ck._services();
|
|
682
|
+
} catch (e) { list = null; }
|
|
683
|
+
if (!list || !Array.isArray(list)) return [];
|
|
684
|
+
var out = [];
|
|
685
|
+
for (var i = 0; i < list.length; i++) {
|
|
686
|
+
if (list[i] && list[i].category === cat) out.push(list[i]);
|
|
687
|
+
}
|
|
688
|
+
return out;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/* The service's one-line purpose, in the banner's language.
|
|
692
|
+
|
|
693
|
+
§3 asks for «одна строка purpose на языке баннера» and §2 ships
|
|
694
|
+
`purpose: { ru?, ro?, en? }`. Falls back to en, then to nothing at all —
|
|
695
|
+
an empty purpose renders no paragraph rather than the string "undefined"
|
|
696
|
+
or a stray language. LANG is the resolved banner code, so a `pt-BR`
|
|
697
|
+
banner asks for `pt` and lands on en, which is the honest answer. */
|
|
698
|
+
function servicePurpose(svc, lang) {
|
|
699
|
+
var p = (svc && svc.purpose) || {};
|
|
700
|
+
var code = String(lang == null ? LANG : lang || 'en').slice(0, 2).toLowerCase();
|
|
701
|
+
var v = p[code] || p.en;
|
|
702
|
+
return (typeof v === 'string' && v.trim()) ? v.trim() : '';
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/* The cookieTable rows this service claims, by name.
|
|
706
|
+
|
|
707
|
+
§3: «под сервисом его cookie (имя · срок · назначение)» drawn «from
|
|
708
|
+
cookieTable rows whose name is in service.cookies». A name the service
|
|
709
|
+
declares but the table does not describe is NOT invented here: the panel
|
|
710
|
+
shows what the owner wrote down, and the declaration page is where the
|
|
711
|
+
full list lives. */
|
|
712
|
+
function cookieRowsForService(rows, svc) {
|
|
713
|
+
var names = (svc && svc.cookies) || [];
|
|
714
|
+
if (!names.length) return [];
|
|
715
|
+
var out = [];
|
|
716
|
+
for (var i = 0; i < rows.length; i++) {
|
|
717
|
+
var n = rows[i] && rows[i].name;
|
|
718
|
+
if (typeof n === 'string' && names.indexOf(n) > -1) out.push(rows[i]);
|
|
719
|
+
}
|
|
720
|
+
return out;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/* SPEC V1.12 §3 — «N сервисов · M cookie» on the group header. Pure, so the
|
|
724
|
+
wording in each of the three languages is testable without a DOM.
|
|
725
|
+
|
|
726
|
+
The cookie half counts the WHOLE group, services and loose rows alike: the
|
|
727
|
+
line answers «сколько cookie в этой группе», which is the question a
|
|
728
|
+
visitor scanning the header is actually asking. */
|
|
729
|
+
function groupCountLabel(serviceCount, cookieCount, strings, lang) {
|
|
730
|
+
return plural(strings.svcCount, serviceCount, lang) + ' · ' +
|
|
731
|
+
plural(strings.ckCount, cookieCount, lang);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/* Rows left over once every service has taken its own — «Cookie в этой
|
|
735
|
+
группе (N)» keeps meaning «the rest», exactly as it did before services
|
|
736
|
+
existed. With no services at all this returns the whole list unchanged. */
|
|
737
|
+
function looseCookies(rows, svcs) {
|
|
738
|
+
if (!svcs.length) return rows;
|
|
739
|
+
var claimed = {};
|
|
740
|
+
for (var i = 0; i < svcs.length; i++) {
|
|
741
|
+
var names = svcs[i].cookies || [];
|
|
742
|
+
for (var j = 0; j < names.length; j++) claimed[names[j]] = true;
|
|
743
|
+
}
|
|
744
|
+
var out = [];
|
|
745
|
+
for (var k = 0; k < rows.length; k++) {
|
|
746
|
+
var n = rows[k] && rows[k].name;
|
|
747
|
+
if (typeof n === 'string' && claimed[n]) continue;
|
|
748
|
+
out.push(rows[k]);
|
|
749
|
+
}
|
|
750
|
+
return out;
|
|
751
|
+
}
|
|
752
|
+
|
|
466
753
|
/* ----------------------------------------------------------------- theme */
|
|
467
754
|
|
|
468
755
|
// Built-in palettes. Dark values are picked for >= 4.5:1 text contrast.
|
|
@@ -1195,11 +1482,155 @@
|
|
|
1195
1482
|
b.addEventListener('click', function () {
|
|
1196
1483
|
var on = b.getAttribute('aria-checked') === 'true';
|
|
1197
1484
|
b.setAttribute('aria-checked', on ? 'false' : 'true');
|
|
1485
|
+
/* SPEC V1.12 §3 — the group switch is the master:
|
|
1486
|
+
off -> every service of the group goes off and is blocked;
|
|
1487
|
+
on -> the services come back, EXCEPT the ones turned off by hand.
|
|
1488
|
+
The hand-set state is kept on the service switch itself (dataset.man)
|
|
1489
|
+
rather than being read back off the group, which is what lets a
|
|
1490
|
+
manual refusal survive the group being toggled off and on again. */
|
|
1491
|
+
syncGroup(cat);
|
|
1198
1492
|
});
|
|
1199
1493
|
}
|
|
1200
1494
|
return b;
|
|
1201
1495
|
}
|
|
1202
1496
|
|
|
1497
|
+
/* SPEC V1.12 §3 — one service, with its own switch, its purpose, its policy
|
|
1498
|
+
link and the cookies it sets. */
|
|
1499
|
+
function makeServiceSwitch(svc, cat) {
|
|
1500
|
+
var b = el('button', 'ck-switch ck-switch--sm');
|
|
1501
|
+
b.type = 'button';
|
|
1502
|
+
b.setAttribute('role', 'switch');
|
|
1503
|
+
b.setAttribute('aria-checked', 'false');
|
|
1504
|
+
b.dataset.svc = svc.id;
|
|
1505
|
+
b.dataset.cat = cat;
|
|
1506
|
+
// '1' once the visitor has switched this service off by hand. Read by
|
|
1507
|
+
// syncGroup() when the group comes back on, and cleared when they switch it
|
|
1508
|
+
// on again — a service the visitor re-enables is no longer «отключён вручную».
|
|
1509
|
+
b.dataset.man = '';
|
|
1510
|
+
/* A service switch is only ever clickable while its group is ON: syncGroup()
|
|
1511
|
+
disables it otherwise, and a disabled <button> fires no click. So this
|
|
1512
|
+
handler always runs with the group on, and there is no "turn the group
|
|
1513
|
+
back on too" case to handle — the visitor reaches a refused group through
|
|
1514
|
+
the group's own switch. */
|
|
1515
|
+
b.addEventListener('click', function () {
|
|
1516
|
+
var on = b.getAttribute('aria-checked') === 'true';
|
|
1517
|
+
b.setAttribute('aria-checked', on ? 'false' : 'true');
|
|
1518
|
+
// '1' = «switched off by hand». Cleared when it is switched back on, so a
|
|
1519
|
+
// re-enabled service is no longer «отключён вручную» and follows its group.
|
|
1520
|
+
b.dataset.man = on ? '1' : '';
|
|
1521
|
+
});
|
|
1522
|
+
return b;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/* Pushes the group switch's state down onto its services. Called when the
|
|
1526
|
+
group is clicked and when the panel is synced from the stored state. */
|
|
1527
|
+
function syncGroup(cat) {
|
|
1528
|
+
var on = !!(switches[cat] && switches[cat].getAttribute('aria-checked') === 'true');
|
|
1529
|
+
var list = serviceSwitches[cat] || [];
|
|
1530
|
+
for (var i = 0; i < list.length; i++) {
|
|
1531
|
+
var b = list[i];
|
|
1532
|
+
// Group off: everything off. Group on: everything on except what the
|
|
1533
|
+
// visitor turned off by hand.
|
|
1534
|
+
var want = on && b.dataset.man !== '1';
|
|
1535
|
+
b.setAttribute('aria-checked', want ? 'true' : 'false');
|
|
1536
|
+
// A service cannot be granted while its group is refused, and a switch
|
|
1537
|
+
// that looks operable but changes nothing is worse than a disabled one.
|
|
1538
|
+
b.disabled = !on;
|
|
1539
|
+
if (!on) b.setAttribute('aria-disabled', 'true');
|
|
1540
|
+
else b.removeAttribute('aria-disabled');
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function buildService(svc, cat, rows) {
|
|
1545
|
+
var wrap = el('div', 'ck-svc');
|
|
1546
|
+
var top = el('div', 'ck-svc__top');
|
|
1547
|
+
var txt = el('div', 'ck-svc__txt');
|
|
1548
|
+
|
|
1549
|
+
var nameId = 'ck-svc-' + svc.id;
|
|
1550
|
+
var name = el('div', 'ck-svc__name');
|
|
1551
|
+
var nameSpan = el('span', null, svc.name);
|
|
1552
|
+
nameSpan.id = nameId;
|
|
1553
|
+
name.appendChild(nameSpan);
|
|
1554
|
+
txt.appendChild(name);
|
|
1555
|
+
|
|
1556
|
+
if (svc.vendor) txt.appendChild(el('p', 'ck-svc__vendor', svc.vendor));
|
|
1557
|
+
|
|
1558
|
+
var descId = null;
|
|
1559
|
+
var purpose = servicePurpose(svc);
|
|
1560
|
+
if (purpose) {
|
|
1561
|
+
descId = nameId + '-desc';
|
|
1562
|
+
var p = el('p', 'ck-svc__desc', purpose);
|
|
1563
|
+
p.id = descId;
|
|
1564
|
+
txt.appendChild(p);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/* «Политика» — target=_blank rel=noopener, per §3. The URL is already
|
|
1568
|
+
http(s)-validated by the core's normalizeService(), which is where a
|
|
1569
|
+
javascript: address is dropped; nothing unvalidated reaches an href. */
|
|
1570
|
+
if (svc.privacyUrl) {
|
|
1571
|
+
var a = el('a', 'ck-svc__policy', T.svcPolicy);
|
|
1572
|
+
a.href = svc.privacyUrl;
|
|
1573
|
+
a.target = '_blank';
|
|
1574
|
+
a.rel = 'noopener noreferrer';
|
|
1575
|
+
// The link text is the same word on every row, so a screen reader needs
|
|
1576
|
+
// the service name to tell them apart.
|
|
1577
|
+
a.setAttribute('aria-label', T.svcPolicy + ' — ' + svc.name);
|
|
1578
|
+
txt.appendChild(a);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
var sw = makeServiceSwitch(svc, cat);
|
|
1582
|
+
sw.setAttribute('aria-labelledby', nameId);
|
|
1583
|
+
if (descId) sw.setAttribute('aria-describedby', descId);
|
|
1584
|
+
if (!serviceSwitches[cat]) serviceSwitches[cat] = [];
|
|
1585
|
+
serviceSwitches[cat].push(sw);
|
|
1586
|
+
|
|
1587
|
+
top.appendChild(txt);
|
|
1588
|
+
top.appendChild(sw);
|
|
1589
|
+
wrap.appendChild(top);
|
|
1590
|
+
|
|
1591
|
+
var own = cookieRowsForService(rows, svc);
|
|
1592
|
+
if (own.length) wrap.appendChild(cookieTable(own, T.svcCookies + ' (' + own.length + ')'));
|
|
1593
|
+
|
|
1594
|
+
return wrap;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/* The <details> block a group and a service both use for their cookies:
|
|
1598
|
+
same columns, same markup, one summary line apart. */
|
|
1599
|
+
function cookieTable(rows, summaryText) {
|
|
1600
|
+
var det = el('details', 'ck-det');
|
|
1601
|
+
var sum = el('summary');
|
|
1602
|
+
sum.appendChild(document.createTextNode(summaryText));
|
|
1603
|
+
det.appendChild(sum);
|
|
1604
|
+
|
|
1605
|
+
var tw = el('div', 'ck-tablewrap');
|
|
1606
|
+
var table = el('table');
|
|
1607
|
+
var thead = el('thead');
|
|
1608
|
+
var htr = el('tr');
|
|
1609
|
+
var heads = [T.colName, T.colVendor, T.colPurpose, T.colExpiry];
|
|
1610
|
+
for (var h = 0; h < heads.length; h++) {
|
|
1611
|
+
var th = el('th', null, heads[h]);
|
|
1612
|
+
th.setAttribute('scope', 'col');
|
|
1613
|
+
htr.appendChild(th);
|
|
1614
|
+
}
|
|
1615
|
+
thead.appendChild(htr);
|
|
1616
|
+
table.appendChild(thead);
|
|
1617
|
+
|
|
1618
|
+
var tbody = el('tbody');
|
|
1619
|
+
for (var r = 0; r < rows.length; r++) {
|
|
1620
|
+
var row = rows[r];
|
|
1621
|
+
var tr = el('tr');
|
|
1622
|
+
tr.appendChild(el('td', 'ck-mono', String(row.name == null ? '—' : row.name)));
|
|
1623
|
+
tr.appendChild(el('td', null, String(row.vendor == null ? '—' : row.vendor)));
|
|
1624
|
+
tr.appendChild(el('td', null, String(row.purpose == null ? '—' : row.purpose)));
|
|
1625
|
+
tr.appendChild(el('td', null, String(row.expiry == null ? '—' : row.expiry)));
|
|
1626
|
+
tbody.appendChild(tr);
|
|
1627
|
+
}
|
|
1628
|
+
table.appendChild(tbody);
|
|
1629
|
+
tw.appendChild(table);
|
|
1630
|
+
det.appendChild(tw);
|
|
1631
|
+
return det;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1203
1634
|
function buildCategory(cfg, cat) {
|
|
1204
1635
|
var meta = T.cat[cat] || { title: cat, desc: '' };
|
|
1205
1636
|
var locked = cat === 'necessary';
|
|
@@ -1221,6 +1652,21 @@
|
|
|
1221
1652
|
desc.id = descId;
|
|
1222
1653
|
txt.appendChild(desc);
|
|
1223
1654
|
|
|
1655
|
+
var rows = cookiesFor(cfg, cat);
|
|
1656
|
+
var svcs = servicesFor(cat);
|
|
1657
|
+
|
|
1658
|
+
/* SPEC V1.12 §3 — «N сервисов · M cookie» on the group header.
|
|
1659
|
+
|
|
1660
|
+
Rendered ONLY when the group actually has services. A site whose config
|
|
1661
|
+
predates 0.5.8 has none, and must look exactly as it did in 0.5.7 — not
|
|
1662
|
+
«0 сервисов · 3 cookie». The cookie half counts the whole group, services
|
|
1663
|
+
and loose rows alike: it answers «сколько cookie в этой группе», which is
|
|
1664
|
+
the question the line is there to answer. */
|
|
1665
|
+
if (svcs.length) {
|
|
1666
|
+
name.appendChild(el('span', 'ck-cat__count',
|
|
1667
|
+
groupCountLabel(svcs.length, rows.length, T, LANG)));
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1224
1670
|
var sw = makeSwitch(cat, locked);
|
|
1225
1671
|
sw.setAttribute('aria-labelledby', nameId);
|
|
1226
1672
|
sw.setAttribute('aria-describedby', descId);
|
|
@@ -1230,40 +1676,24 @@
|
|
|
1230
1676
|
top.appendChild(sw);
|
|
1231
1677
|
wrap.appendChild(top);
|
|
1232
1678
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
var table = el('table');
|
|
1242
|
-
var thead = el('thead');
|
|
1243
|
-
var htr = el('tr');
|
|
1244
|
-
var heads = [T.colName, T.colVendor, T.colPurpose, T.colExpiry];
|
|
1245
|
-
for (var h = 0; h < heads.length; h++) {
|
|
1246
|
-
var th = el('th', null, heads[h]);
|
|
1247
|
-
th.setAttribute('scope', 'col');
|
|
1248
|
-
htr.appendChild(th);
|
|
1679
|
+
if (svcs.length) {
|
|
1680
|
+
var list = el('div', 'ck-svcs');
|
|
1681
|
+
// A list, so a screen reader announces «3 items» before reading them.
|
|
1682
|
+
list.setAttribute('role', 'list');
|
|
1683
|
+
for (var s = 0; s < svcs.length; s++) {
|
|
1684
|
+
var item = buildService(svcs[s], cat, rows);
|
|
1685
|
+
item.setAttribute('role', 'listitem');
|
|
1686
|
+
list.appendChild(item);
|
|
1249
1687
|
}
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
tr.appendChild(el('td', null, String(row.purpose == null ? '—' : row.purpose)));
|
|
1260
|
-
tr.appendChild(el('td', null, String(row.expiry == null ? '—' : row.expiry)));
|
|
1261
|
-
tbody.appendChild(tr);
|
|
1262
|
-
}
|
|
1263
|
-
table.appendChild(tbody);
|
|
1264
|
-
tw.appendChild(table);
|
|
1265
|
-
det.appendChild(tw);
|
|
1266
|
-
wrap.appendChild(det);
|
|
1688
|
+
wrap.appendChild(list);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// «Cookie в этой группе (N)» keeps its old meaning: what is left once each
|
|
1692
|
+
// service has claimed its own. With no services that is the whole table and
|
|
1693
|
+
// the summary line is byte-for-byte what 0.5.7 rendered.
|
|
1694
|
+
var loose = looseCookies(rows, svcs);
|
|
1695
|
+
if (loose.length) {
|
|
1696
|
+
wrap.appendChild(cookieTable(loose, T.cookiesIn + ' (' + loose.length + ')'));
|
|
1267
1697
|
}
|
|
1268
1698
|
|
|
1269
1699
|
return wrap;
|
|
@@ -1288,12 +1718,30 @@
|
|
|
1288
1718
|
var url = str(texts.policyUrl);
|
|
1289
1719
|
if (url && !/^https?:\/\//i.test(url)) url = null;
|
|
1290
1720
|
|
|
1721
|
+
/* SPEC V1.10 §1 — the third destination: our own cookie declaration page.
|
|
1722
|
+
`declarationUrl` is server-owned (the SaaS config injects it, like
|
|
1723
|
+
`branding`); the client only reads it and never invents one. It is
|
|
1724
|
+
validated exactly like policyUrl — http(s) only, because a link the
|
|
1725
|
+
visitor is invited to click must not be able to carry javascript: — and
|
|
1726
|
+
it deliberately does NOT influence the default action: the URL-sensitive
|
|
1727
|
+
default above stays policyUrl-driven, so a site that gains a declaration
|
|
1728
|
+
address does not silently lose its «Подробнее» → политика link. */
|
|
1729
|
+
var decl = str(texts.declarationUrl);
|
|
1730
|
+
if (decl && !/^https?:\/\//i.test(decl)) decl = null;
|
|
1731
|
+
|
|
1291
1732
|
var action = texts.detailsAction;
|
|
1292
|
-
if (action !== 'policy' && action !== 'settings' && action !== 'hide') {
|
|
1733
|
+
if (action !== 'policy' && action !== 'settings' && action !== 'hide' && action !== 'declaration') {
|
|
1293
1734
|
action = url ? 'policy' : 'settings';
|
|
1294
1735
|
}
|
|
1295
1736
|
if (action === 'policy' && !url) action = 'settings';
|
|
1296
|
-
|
|
1737
|
+
// Asked for the declaration with no address to send anyone to: fall back to
|
|
1738
|
+
// opening the settings rather than rendering a dead link.
|
|
1739
|
+
if (action === 'declaration' && !decl) action = 'settings';
|
|
1740
|
+
|
|
1741
|
+
var href = null;
|
|
1742
|
+
if (action === 'policy') href = url;
|
|
1743
|
+
else if (action === 'declaration') href = decl;
|
|
1744
|
+
return { kind: action, href: href };
|
|
1297
1745
|
}
|
|
1298
1746
|
|
|
1299
1747
|
// Unknown type -> bar/bottom. Known type with an unrecognized position ->
|
|
@@ -1335,11 +1783,15 @@
|
|
|
1335
1783
|
p.appendChild(document.createTextNode(T.bannerText));
|
|
1336
1784
|
} else {
|
|
1337
1785
|
p.appendChild(document.createTextNode(T.bannerText + ' '));
|
|
1338
|
-
|
|
1786
|
+
// 'policy' and 'declaration' are the same DOM shape — an outbound link in
|
|
1787
|
+
// a new tab — and differ only in where they point (resolveDetails picked
|
|
1788
|
+
// the address). Both must be listed here: a missing branch would silently
|
|
1789
|
+
// render 'declaration' as the settings BUTTON instead of the link.
|
|
1790
|
+
if (det.kind === 'policy' || det.kind === 'declaration') {
|
|
1339
1791
|
var link = el('a', 'ck-banner__more', T.more);
|
|
1340
1792
|
link.href = det.href;
|
|
1341
1793
|
link.target = '_blank';
|
|
1342
|
-
link.rel = 'noopener'; // never hand the
|
|
1794
|
+
link.rel = 'noopener'; // never hand the linked page window.opener
|
|
1343
1795
|
p.appendChild(link);
|
|
1344
1796
|
} else {
|
|
1345
1797
|
// A control that changes what is on screen is a button, not a link:
|
|
@@ -1517,6 +1969,32 @@
|
|
|
1517
1969
|
var sw = switches[k];
|
|
1518
1970
|
out[k] = !!(sw && sw.getAttribute('aria-checked') === 'true');
|
|
1519
1971
|
}
|
|
1972
|
+
/* SPEC V1.12 §3 — «хранение: только отказы».
|
|
1973
|
+
|
|
1974
|
+
Read from `dataset.man`, NOT from `aria-checked`. The two answer different
|
|
1975
|
+
questions and only one of them is the visitor's decision:
|
|
1976
|
+
|
|
1977
|
+
aria-checked — what the switch currently SHOWS. syncGroup() forces every
|
|
1978
|
+
service of a refused group to `false`, so reading this
|
|
1979
|
+
would record «I refused all six of these» the moment the
|
|
1980
|
+
group went off.
|
|
1981
|
+
dataset.man — «the visitor switched this one off by hand». Set on click,
|
|
1982
|
+
restored from the stored record, and deliberately left
|
|
1983
|
+
alone by syncGroup().
|
|
1984
|
+
|
|
1985
|
+
Getting this backwards loses a denial: refuse Hotjar, later switch
|
|
1986
|
+
analytics off and save, and the refusal would be erased — so when
|
|
1987
|
+
analytics came back on, Hotjar would run again without ever having been
|
|
1988
|
+
re-consented to. */
|
|
1989
|
+
var svcs = {};
|
|
1990
|
+
for (var c = 0; c < OPT_IN.length; c++) {
|
|
1991
|
+
var list = serviceSwitches[OPT_IN[c]] || [];
|
|
1992
|
+
for (var j = 0; j < list.length; j++) {
|
|
1993
|
+
var b = list[j];
|
|
1994
|
+
if (b.dataset.man === '1') svcs[b.dataset.svc] = false;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
out.services = svcs;
|
|
1520
1998
|
return out;
|
|
1521
1999
|
}
|
|
1522
2000
|
|
|
@@ -1595,6 +2073,23 @@
|
|
|
1595
2073
|
sw.setAttribute('aria-checked', on ? 'true' : 'false');
|
|
1596
2074
|
}
|
|
1597
2075
|
if (switches.necessary) switches.necessary.setAttribute('aria-checked', 'true');
|
|
2076
|
+
|
|
2077
|
+
/* SPEC V1.12 §3 — restore the per-service switches from the stored denials.
|
|
2078
|
+
|
|
2079
|
+
`dataset.man` is set from the record FIRST, then syncGroup() derives what
|
|
2080
|
+
each switch shows from it. That ordering is the whole point: a visitor who
|
|
2081
|
+
denied Hotjar and left analytics off must still see Hotjar's own switch
|
|
2082
|
+
off when they turn analytics back on, and the group→service push is what
|
|
2083
|
+
would otherwise light it up again. */
|
|
2084
|
+
var denials = (state && state.services) || {};
|
|
2085
|
+
for (var c = 0; c < OPT_IN.length; c++) {
|
|
2086
|
+
var cat = OPT_IN[c];
|
|
2087
|
+
var list = serviceSwitches[cat] || [];
|
|
2088
|
+
for (var j = 0; j < list.length; j++) {
|
|
2089
|
+
list[j].dataset.man = denials[list[j].dataset.svc] === false ? '1' : '';
|
|
2090
|
+
}
|
|
2091
|
+
syncGroup(cat);
|
|
2092
|
+
}
|
|
1598
2093
|
}
|
|
1599
2094
|
|
|
1600
2095
|
// Idempotent: safe to call from ck:init, ck:change and right after our own API calls.
|
|
@@ -1607,6 +2102,333 @@
|
|
|
1607
2102
|
if (nodes.scrim) nodes.scrim.classList.toggle('ck-hidden', decided || !nodes.bannerModal);
|
|
1608
2103
|
if (nodes.fab) nodes.fab.classList.toggle('ck-hidden', !decided);
|
|
1609
2104
|
if (!panelOpen) syncSwitches(s);
|
|
2105
|
+
|
|
2106
|
+
/* §2 — a consent change is exactly when a card must go: the core's
|
|
2107
|
+
applyConsentToDom() has already put the frame's src back by the time
|
|
2108
|
+
ck:change reaches us (commit() revives before it dispatches), so the
|
|
2109
|
+
sweep sees a frame with a src and retires its placeholder. It runs on
|
|
2110
|
+
every sync, which also catches a category the visitor turned back OFF —
|
|
2111
|
+
though a frame that already loaded cannot be un-loaded, so that direction
|
|
2112
|
+
only matters for frames still held back. */
|
|
2113
|
+
try { sweepPlaceholders(); } catch (e) { /* noop */ }
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/* ------------------------------------------- blocked-embed placeholders (§2) */
|
|
2117
|
+
|
|
2118
|
+
/* SPEC V1.10 §2. When the core holds an <iframe> back — a known tracker, or
|
|
2119
|
+
any third-party frame in strict mode — it leaves it in one shape:
|
|
2120
|
+
`data-ck` + `data-src` and NO src (markBlockedIframe in ck-core.js). This
|
|
2121
|
+
module draws a block of the same size in its place, offering the visitor
|
|
2122
|
+
the one decision that would make the embed appear.
|
|
2123
|
+
|
|
2124
|
+
WHY A SIBLING, NOT A WRAPPER: sites style embeds through the parent
|
|
2125
|
+
(`.video-wrap > iframe`, grid children, aspect-ratio boxes). Wrapping the
|
|
2126
|
+
frame inserts a node into that relationship and breaks the layout it was
|
|
2127
|
+
meant to preserve. A sibling inserted before the frame inherits the same
|
|
2128
|
+
parent context, and removing it later restores the DOM exactly.
|
|
2129
|
+
|
|
2130
|
+
WHO RESTORES THE FRAME: not this file. `Element.prototype.setAttribute` is
|
|
2131
|
+
patched by the core, and only applyConsentToDom() may write a blocked
|
|
2132
|
+
frame's src — under `bypass`, through the native setter it captured before
|
|
2133
|
+
patching. So «Разрешить и показать» grants the category through the normal
|
|
2134
|
+
consent path and the CORE brings the frame back; the next sweep sees a
|
|
2135
|
+
frame that has a src again and drops the placeholder. One code path for
|
|
2136
|
+
the button, the panel's switches and «Принять всё» alike. */
|
|
2137
|
+
|
|
2138
|
+
var PH_ATTR = 'data-ck-ph'; // marks a frame this file hid
|
|
2139
|
+
var PH_DISPLAY = 'data-ck-ph-display'; // its previous inline display value
|
|
2140
|
+
var placeholders = []; // [{ frame, node }]
|
|
2141
|
+
|
|
2142
|
+
function placeholdersEnabled(cfg) {
|
|
2143
|
+
// No default in the core's DEFAULT_CONFIG, exactly like detailsAction:
|
|
2144
|
+
// absent means on, and only an explicit false opts out.
|
|
2145
|
+
try {
|
|
2146
|
+
var b = cfg && cfg.blocking;
|
|
2147
|
+
return !(b && b.placeholders === false);
|
|
2148
|
+
} catch (e) { return true; }
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
/* §2: «Не трогать фреймы display:none, 1×1, и те, что не в <body>.»
|
|
2152
|
+
Order matters — a frame this file has already hidden reads as display:none,
|
|
2153
|
+
which would make it skip its own placeholder and leave it stranded forever.
|
|
2154
|
+
Frames we marked are therefore exempted before the display test runs. */
|
|
2155
|
+
function frameEligible(frame) {
|
|
2156
|
+
try {
|
|
2157
|
+
if (!frame || !document.body) return false;
|
|
2158
|
+
if (!document.body.contains(frame)) return false;
|
|
2159
|
+
if (frame.getAttribute(PH_ATTR)) return true; // ours: already measured
|
|
2160
|
+
var cs = null;
|
|
2161
|
+
try { cs = window.getComputedStyle(frame); } catch (e) { cs = null; }
|
|
2162
|
+
if (cs && (cs.display === 'none' || cs.visibility === 'hidden')) return false;
|
|
2163
|
+
var r = null;
|
|
2164
|
+
try { r = frame.getBoundingClientRect(); } catch (e) { r = null; }
|
|
2165
|
+
// A 1×1 (or 0×0) frame is a tracking pixel dressed as an embed: there is
|
|
2166
|
+
// nothing for a visitor to watch and a card in its place would be noise.
|
|
2167
|
+
if (r && r.width <= 1 && r.height <= 1) {
|
|
2168
|
+
// Zero-sized because it is not laid out YET (a lazy tab, a collapsed
|
|
2169
|
+
// section) is indistinguishable here from a real pixel except via the
|
|
2170
|
+
// attributes, which a pixel sets to 1 and a video does not.
|
|
2171
|
+
var aw = parseInt(frame.getAttribute('width') || '0', 10);
|
|
2172
|
+
var ah = parseInt(frame.getAttribute('height') || '0', 10);
|
|
2173
|
+
if (!(aw > 1 || ah > 1)) return false;
|
|
2174
|
+
}
|
|
2175
|
+
return true;
|
|
2176
|
+
} catch (e) { return false; }
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
/* Size the card to the hole the frame left. Attributes first (an embed is
|
|
2180
|
+
nearly always `width="560" height="315"`), computed size second. Read
|
|
2181
|
+
BEFORE the frame is hidden — getComputedStyle on a display:none element
|
|
2182
|
+
reports nothing worth copying. */
|
|
2183
|
+
function frameSize(frame) {
|
|
2184
|
+
var w = '', h = '';
|
|
2185
|
+
try {
|
|
2186
|
+
var aw = frame.getAttribute('width');
|
|
2187
|
+
var ah = frame.getAttribute('height');
|
|
2188
|
+
if (aw && /^\d+$/.test(String(aw).trim())) w = String(aw).trim() + 'px';
|
|
2189
|
+
if (ah && /^\d+$/.test(String(ah).trim())) h = String(ah).trim() + 'px';
|
|
2190
|
+
if (!w || !h) {
|
|
2191
|
+
var cs = window.getComputedStyle(frame);
|
|
2192
|
+
if (!w && cs && cs.width && cs.width !== 'auto' && cs.width !== '0px') w = cs.width;
|
|
2193
|
+
if (!h && cs && cs.height && cs.height !== 'auto' && cs.height !== '0px') h = cs.height;
|
|
2194
|
+
}
|
|
2195
|
+
} catch (e) { /* fall through to the defaults in PH_CSS */ }
|
|
2196
|
+
return { width: w, height: h };
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
var PLAY_ICON =
|
|
2200
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" ' +
|
|
2201
|
+
'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">' +
|
|
2202
|
+
'<rect x="2.5" y="4.5" width="19" height="15" rx="2.5"/>' +
|
|
2203
|
+
'<path d="M10.5 9.2v5.6l4.6-2.8-4.6-2.8Z"/></svg>';
|
|
2204
|
+
|
|
2205
|
+
// Own sheet: a placeholder is its own shadow root, so it cannot borrow the
|
|
2206
|
+
// banner's. Tokens come from buildThemeCss() (theme.accent, radius, font),
|
|
2207
|
+
// which is why a placeholder repaints with the banner.
|
|
2208
|
+
var PH_CSS = [
|
|
2209
|
+
':host{all:initial;display:block;max-width:100%}',
|
|
2210
|
+
'*,*::before,*::after{box-sizing:border-box}',
|
|
2211
|
+
'.ck-ph{width:100%;height:100%;min-height:120px;max-width:100%;',
|
|
2212
|
+
'display:flex;flex-direction:column;align-items:center;justify-content:center;',
|
|
2213
|
+
'gap:10px;padding:20px;text-align:center;',
|
|
2214
|
+
'font-family:var(--ck-font);font-size:14px;line-height:1.5;color:var(--ck-ink);',
|
|
2215
|
+
'background:var(--ck-soft);border:1px solid var(--ck-line);',
|
|
2216
|
+
'border-radius:var(--ck-radius-card)}',
|
|
2217
|
+
'.ck-ph svg{width:32px;height:32px;display:block;color:var(--ck-muted);flex:none}',
|
|
2218
|
+
'.ck-ph p{margin:0;color:var(--ck-muted);max-width:44ch}',
|
|
2219
|
+
'.ck-ph__row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:center}',
|
|
2220
|
+
'.ck-ph__btn{font:inherit;font-weight:600;cursor:pointer;padding:9px 16px;',
|
|
2221
|
+
'border-radius:var(--ck-radius-btn);border:1px solid var(--ck-accent);',
|
|
2222
|
+
'background:var(--ck-accent);color:var(--ck-on-accent)}',
|
|
2223
|
+
'.ck-ph__link{font:inherit;background:none;border:0;padding:4px;cursor:pointer;',
|
|
2224
|
+
'color:var(--ck-link);text-decoration:underline}',
|
|
2225
|
+
':focus-visible{outline:2px solid var(--ck-accent);outline-offset:2px;border-radius:4px}'
|
|
2226
|
+
].join('\n');
|
|
2227
|
+
|
|
2228
|
+
function categoryEnabled(cfg, cat) {
|
|
2229
|
+
try {
|
|
2230
|
+
if (cat === 'necessary') return false; // nothing to grant
|
|
2231
|
+
var c = cfg && cfg.categories && cfg.categories[cat];
|
|
2232
|
+
return !c || c.enabled !== false;
|
|
2233
|
+
} catch (e) { return true; }
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
function buildPlaceholder(frame, cat, cfg) {
|
|
2237
|
+
var mountEl = document.createElement('div');
|
|
2238
|
+
mountEl.setAttribute('data-ck-placeholder', '1');
|
|
2239
|
+
var size = frameSize(frame);
|
|
2240
|
+
var st = mountEl.style;
|
|
2241
|
+
st.setProperty('max-width', '100%');
|
|
2242
|
+
if (size.width) st.setProperty('width', size.width);
|
|
2243
|
+
if (size.height) st.setProperty('min-height', size.height);
|
|
2244
|
+
|
|
2245
|
+
var sr = mountEl.attachShadow({ mode: 'open' });
|
|
2246
|
+
var sheet = document.createElement('style');
|
|
2247
|
+
/* buildThemeCss writes :host tokens — the same tokens, in this root, which
|
|
2248
|
+
is what makes a placeholder repaint with the banner.
|
|
2249
|
+
|
|
2250
|
+
The font gets applyTheme()'s treatment for the same reason it needs it
|
|
2251
|
+
there: `:host{all:initial}` resets the family, so a bare `inherit` in
|
|
2252
|
+
this root resolves to the UA default (Times), not to the page's type.
|
|
2253
|
+
The probed page font when it can be read, the system stack when it
|
|
2254
|
+
cannot — never a bare `inherit`. */
|
|
2255
|
+
var built = buildThemeCss(cfg);
|
|
2256
|
+
var phFont = '';
|
|
2257
|
+
if (built.font === 'inherit') {
|
|
2258
|
+
phFont = '\n:host{--ck-font:' + (resolvePageFont() || SYSTEM_FONT) + '}';
|
|
2259
|
+
}
|
|
2260
|
+
sheet.textContent = PH_CSS + '\n' + built.css + phFont;
|
|
2261
|
+
sr.appendChild(sheet);
|
|
2262
|
+
|
|
2263
|
+
var card = el('div', 'ck-ph');
|
|
2264
|
+
card.setAttribute('role', 'group');
|
|
2265
|
+
card.setAttribute('aria-label', T.phLabel);
|
|
2266
|
+
|
|
2267
|
+
var icon = document.createElement('div');
|
|
2268
|
+
icon.innerHTML = PLAY_ICON;
|
|
2269
|
+
card.appendChild(icon);
|
|
2270
|
+
|
|
2271
|
+
var frameSrcAttr = frame.getAttribute('data-src') || '';
|
|
2272
|
+
var label = hostOf(frameSrcAttr);
|
|
2273
|
+
// §3: when a refused SERVICE is what is holding this frame, the sentence
|
|
2274
|
+
// must name that service — its category is already granted.
|
|
2275
|
+
var held = holdingService(frameSrcAttr);
|
|
2276
|
+
card.appendChild(el('p', null,
|
|
2277
|
+
placeholderText(label, cat, LANG, held ? held.name : null)));
|
|
2278
|
+
|
|
2279
|
+
var row = el('div', 'ck-ph__row');
|
|
2280
|
+
/* Only offer the grant when it can actually take effect: the core's
|
|
2281
|
+
filterByConfig() drops a category the site disabled in config, so the
|
|
2282
|
+
button would consume the click and change nothing. The panel link stays
|
|
2283
|
+
either way — it is always an honest answer. */
|
|
2284
|
+
if (categoryEnabled(cfg, cat)) {
|
|
2285
|
+
var allow = el('button', 'ck-ph__btn', T.phAllow);
|
|
2286
|
+
allow.type = 'button';
|
|
2287
|
+
// SPEC V1.12 §3: «кнопка "Разрешить и показать" включает категорию и
|
|
2288
|
+
// снимает отказ по этому сервису». The frame's own address decides which
|
|
2289
|
+
// service that is — the visitor pressed the button on THIS embed.
|
|
2290
|
+
allow.addEventListener('click', function () { grantCategory(cat, frameSrcAttr); });
|
|
2291
|
+
row.appendChild(allow);
|
|
2292
|
+
}
|
|
2293
|
+
var settings = el('button', 'ck-ph__link', T.phSettings);
|
|
2294
|
+
settings.type = 'button';
|
|
2295
|
+
settings.addEventListener('click', function () { openPanel(settings); });
|
|
2296
|
+
row.appendChild(settings);
|
|
2297
|
+
card.appendChild(row);
|
|
2298
|
+
|
|
2299
|
+
sr.appendChild(card);
|
|
2300
|
+
return mountEl;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
/* «Разрешить и показать» — one category, through the normal consent path.
|
|
2304
|
+
|
|
2305
|
+
MERGE, DO NOT REPLACE: ConsentKit.accept({...}) SETS the whole opt-in set
|
|
2306
|
+
from the object it is given, so passing `{marketing:true}` alone would
|
|
2307
|
+
silently switch OFF an analytics consent the visitor had already given.
|
|
2308
|
+
The current state is read first and only the one category is flipped.
|
|
2309
|
+
|
|
2310
|
+
accept(object) files the decision as method 'custom', which is what §2 asks
|
|
2311
|
+
the journal to record, and ck-saas.js logs it off the ck:consent/ck:change
|
|
2312
|
+
the core dispatches — so there is no logging code here. The core's
|
|
2313
|
+
applyConsentToDom() restores the frame; sweepPlaceholders() then removes
|
|
2314
|
+
this card, driven by the ck:change that same commit dispatches. */
|
|
2315
|
+
function grantCategory(cat, src) {
|
|
2316
|
+
var ck = api();
|
|
2317
|
+
if (!ck || typeof ck.accept !== 'function') return;
|
|
2318
|
+
var st = safeState();
|
|
2319
|
+
var cur = st.categories || {};
|
|
2320
|
+
var next = {
|
|
2321
|
+
functional: cur.functional === true,
|
|
2322
|
+
analytics: cur.analytics === true,
|
|
2323
|
+
marketing: cur.marketing === true
|
|
2324
|
+
};
|
|
2325
|
+
if (cat === 'functional' || cat === 'analytics' || cat === 'marketing') next[cat] = true;
|
|
2326
|
+
|
|
2327
|
+
/* SPEC V1.12 §3 — clear the refusal on THIS frame's service, in the SAME
|
|
2328
|
+
accept() call as the category grant.
|
|
2329
|
+
|
|
2330
|
+
One call, not two: the core revives blocked frames inside commit(), so a
|
|
2331
|
+
denial still standing at that moment leaves this frame dead until some
|
|
2332
|
+
later consent change happens to run applyConsentToDom() again. The whole
|
|
2333
|
+
map is passed because accept() replaces it wholesale — every OTHER
|
|
2334
|
+
refusal the visitor made is copied across untouched. */
|
|
2335
|
+
var denials = denialsWithout(st.services, src);
|
|
2336
|
+
if (denials) next.services = denials;
|
|
2337
|
+
|
|
2338
|
+
try { ck.accept(next); } catch (e) { /* noop */ }
|
|
2339
|
+
syncFromState();
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
/* The stored denial map minus the service that owns `src`, or null when
|
|
2343
|
+
nothing would change (no denials, no service for that URL, or that service
|
|
2344
|
+
was not refused in the first place). */
|
|
2345
|
+
function denialsWithout(stored, src) {
|
|
2346
|
+
var ck = api();
|
|
2347
|
+
if (!ck || typeof ck._serviceForUrl !== 'function') return null;
|
|
2348
|
+
var map = {};
|
|
2349
|
+
var any = false;
|
|
2350
|
+
try {
|
|
2351
|
+
Object.keys(stored || {}).forEach(function (k) {
|
|
2352
|
+
if (stored[k] === false) { map[k] = false; any = true; }
|
|
2353
|
+
});
|
|
2354
|
+
} catch (e) { return null; }
|
|
2355
|
+
if (!any) return null;
|
|
2356
|
+
|
|
2357
|
+
var svc = null;
|
|
2358
|
+
try { svc = src ? ck._serviceForUrl(src) : null; } catch (e2) { svc = null; }
|
|
2359
|
+
if (!svc || map[svc.id] !== false) return null;
|
|
2360
|
+
delete map[svc.id];
|
|
2361
|
+
return map;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
function hideFrame(frame) {
|
|
2365
|
+
try {
|
|
2366
|
+
frame.setAttribute(PH_DISPLAY, frame.style.display || '');
|
|
2367
|
+
frame.setAttribute(PH_ATTR, '1');
|
|
2368
|
+
frame.style.display = 'none';
|
|
2369
|
+
} catch (e) { /* noop */ }
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
function restoreFrame(frame) {
|
|
2373
|
+
try {
|
|
2374
|
+
var prev = frame.getAttribute(PH_DISPLAY);
|
|
2375
|
+
frame.style.display = prev || '';
|
|
2376
|
+
frame.removeAttribute(PH_DISPLAY);
|
|
2377
|
+
frame.removeAttribute(PH_ATTR);
|
|
2378
|
+
} catch (e) { /* noop */ }
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
function dropPlaceholder(entry) {
|
|
2382
|
+
try {
|
|
2383
|
+
if (entry.node && entry.node.parentNode) entry.node.parentNode.removeChild(entry.node);
|
|
2384
|
+
} catch (e) { /* noop */ }
|
|
2385
|
+
restoreFrame(entry.frame);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function clearPlaceholders() {
|
|
2389
|
+
for (var i = 0; i < placeholders.length; i++) dropPlaceholder(placeholders[i]);
|
|
2390
|
+
placeholders = [];
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
/* Idempotent: called from mount() and from every consent change. Adds cards
|
|
2394
|
+
for frames still held back, removes them from frames the core revived. */
|
|
2395
|
+
function sweepPlaceholders(cfg) {
|
|
2396
|
+
if (typeof document === 'undefined' || !document.body) return;
|
|
2397
|
+
var c = cfg || safeConfig();
|
|
2398
|
+
|
|
2399
|
+
// Placeholders turned off after some were drawn: take them all down.
|
|
2400
|
+
if (!placeholdersEnabled(c)) { clearPlaceholders(); return; }
|
|
2401
|
+
|
|
2402
|
+
// 1. Retire cards whose frame came back (or left the document).
|
|
2403
|
+
var kept = [];
|
|
2404
|
+
for (var i = 0; i < placeholders.length; i++) {
|
|
2405
|
+
var e = placeholders[i];
|
|
2406
|
+
var revived = false;
|
|
2407
|
+
try {
|
|
2408
|
+
revived = !document.body.contains(e.frame) || !!e.frame.getAttribute('src');
|
|
2409
|
+
} catch (e2) { revived = true; }
|
|
2410
|
+
if (revived) dropPlaceholder(e); else kept.push(e);
|
|
2411
|
+
}
|
|
2412
|
+
placeholders = kept;
|
|
2413
|
+
|
|
2414
|
+
// 2. Draw cards for frames the core is holding back. The selector is the
|
|
2415
|
+
// core's own revival selector, which is why a frame the SITE allowed can
|
|
2416
|
+
// never match: an allowed frame keeps its src and is never marked.
|
|
2417
|
+
var list;
|
|
2418
|
+
try { list = document.querySelectorAll('iframe[data-ck][data-src]'); } catch (e3) { return; }
|
|
2419
|
+
for (var j = 0; j < list.length; j++) {
|
|
2420
|
+
var frame = list[j];
|
|
2421
|
+
try {
|
|
2422
|
+
if (frame.getAttribute('src')) continue; // already revived
|
|
2423
|
+
if (frame.getAttribute(PH_ATTR)) continue; // already carded
|
|
2424
|
+
if (!frameEligible(frame)) continue;
|
|
2425
|
+
var cat = frame.getAttribute('data-ck') || 'marketing';
|
|
2426
|
+
var node = buildPlaceholder(frame, cat, c);
|
|
2427
|
+
if (frame.parentNode) frame.parentNode.insertBefore(node, frame);
|
|
2428
|
+
hideFrame(frame);
|
|
2429
|
+
placeholders.push({ frame: frame, node: node });
|
|
2430
|
+
} catch (e4) { /* one bad frame must not stop the sweep */ }
|
|
2431
|
+
}
|
|
1610
2432
|
}
|
|
1611
2433
|
|
|
1612
2434
|
/* ----------------------------------------------------------------- mount */
|
|
@@ -1629,14 +2451,46 @@
|
|
|
1629
2451
|
// and mount() is one-shot. Buttons and colours are deliberately NOT
|
|
1630
2452
|
// here — they are token values and restyle in place.
|
|
1631
2453
|
resolveDetails(c).kind,
|
|
2454
|
+
/* SPEC V1.12 §3 — services are STRUCTURAL: each one adds a row with its
|
|
2455
|
+
own switch to the panel, and mount() is one-shot. Without this a SaaS
|
|
2456
|
+
config that arrives after the first mount (the second, idempotent
|
|
2457
|
+
init()) would re-run buildServices() in the core — so the engine would
|
|
2458
|
+
block per service — while the panel kept showing the service-less
|
|
2459
|
+
render, and the visitor would have no way to see or change any of it.
|
|
2460
|
+
|
|
2461
|
+
The COUNT and the ids, not the whole rows: what needs a rebuild is a
|
|
2462
|
+
service appearing, disappearing or changing identity. A reworded
|
|
2463
|
+
`purpose` is text inside an existing row and does not justify tearing
|
|
2464
|
+
the panel down. */
|
|
2465
|
+
serviceSignature(c),
|
|
1632
2466
|
brandSignature(c)
|
|
1633
2467
|
].join('|');
|
|
1634
2468
|
}
|
|
1635
2469
|
|
|
2470
|
+
function serviceSignature(cfg) {
|
|
2471
|
+
var list = cfg && cfg.services;
|
|
2472
|
+
if (!list || !Array.isArray(list) || !list.length) return '0';
|
|
2473
|
+
var ids = [];
|
|
2474
|
+
for (var i = 0; i < list.length; i++) {
|
|
2475
|
+
var s = list[i];
|
|
2476
|
+
if (!s || typeof s !== 'object' || s.enabled === false) continue;
|
|
2477
|
+
ids.push(String(s.id || '') + ':' + String(s.category || ''));
|
|
2478
|
+
}
|
|
2479
|
+
// '0' for «no services», however that came about: an absent key, an empty
|
|
2480
|
+
// array, or a list every row of which the core would drop. All three render
|
|
2481
|
+
// the same panel, so none of them may differ in the signature.
|
|
2482
|
+
if (!ids.length) return '0';
|
|
2483
|
+
return ids.length + ',' + ids.join(',');
|
|
2484
|
+
}
|
|
2485
|
+
|
|
1636
2486
|
function remount(cfg) {
|
|
1637
2487
|
mounted = false;
|
|
1638
2488
|
panelOpen = false;
|
|
1639
2489
|
lastFocus = null;
|
|
2490
|
+
// The cards belong to the render about to be replaced: their listeners close
|
|
2491
|
+
// over the old T and the old shadow root. mount() sweeps again and draws
|
|
2492
|
+
// fresh ones in the new language and theme.
|
|
2493
|
+
clearPlaceholders();
|
|
1640
2494
|
// The pending schedule belongs to the shadow root about to be rebuilt; the
|
|
1641
2495
|
// fresh mount() starts its own ladder from a clean count.
|
|
1642
2496
|
clearFontTimers();
|
|
@@ -1674,6 +2528,7 @@
|
|
|
1674
2528
|
root.appendChild(style);
|
|
1675
2529
|
|
|
1676
2530
|
switches = {};
|
|
2531
|
+
serviceSwitches = {};
|
|
1677
2532
|
nodes = {};
|
|
1678
2533
|
|
|
1679
2534
|
// Palette sheet comes after the base sheet so its :host tokens win.
|
|
@@ -1692,6 +2547,55 @@
|
|
|
1692
2547
|
// Only now: scheduleFontProbes() re-applies the theme from a timer, and
|
|
1693
2548
|
// applyTheme() is a no-op until there is a host and a themeStyle to write.
|
|
1694
2549
|
scheduleFontProbes(cfg);
|
|
2550
|
+
|
|
2551
|
+
// SPEC V1.10 §2 — заглушки вместо задержанных встраиваний. After the shadow
|
|
2552
|
+
// root exists, because a placeholder's «Настроить cookie» calls openPanel().
|
|
2553
|
+
sweepPlaceholders(cfg);
|
|
2554
|
+
|
|
2555
|
+
/* SPEC V1.10 §1 — honour a settings request that arrived before there was
|
|
2556
|
+
anything to open: either ConsentKit.openSettings() called while this file
|
|
2557
|
+
was still loading (the core latches it), or a page opened directly on
|
|
2558
|
+
`#ck-settings`. Both end in the same panel. */
|
|
2559
|
+
consumePendingOpen();
|
|
2560
|
+
openFromHash();
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
/* ---------------------------------------------------- settings deep link */
|
|
2564
|
+
|
|
2565
|
+
var SETTINGS_HASH = '#ck-settings';
|
|
2566
|
+
|
|
2567
|
+
// The core latches openSettings() calls made before ck-ui.js was parsed, so a
|
|
2568
|
+
// footer link clicked during a slow load still opens the panel once we exist.
|
|
2569
|
+
function consumePendingOpen() {
|
|
2570
|
+
var ck = api();
|
|
2571
|
+
if (!ck || !ck._pendingOpen) return;
|
|
2572
|
+
try { ck._pendingOpen = false; } catch (e) { /* noop */ }
|
|
2573
|
+
openPanel(null);
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
/* `https://site/#ck-settings` — the address the cookie declaration page links
|
|
2577
|
+
«Изменить выбор cookie» to. The hash is removed again via replaceState so a
|
|
2578
|
+
reload, a shared link or a back-navigation does not re-open the panel, and
|
|
2579
|
+
so the address bar does not keep a control fragment in it.
|
|
2580
|
+
|
|
2581
|
+
replaceState is fed pathname+search rather than '' — an empty URL argument
|
|
2582
|
+
is a no-op in some engines, which would leave the hash in place and re-open
|
|
2583
|
+
the panel on the next hashchange. Everything is guarded: a sandboxed iframe
|
|
2584
|
+
throws on replaceState, and a panel that opened is worth more than a tidy
|
|
2585
|
+
address bar. */
|
|
2586
|
+
function openFromHash() {
|
|
2587
|
+
try {
|
|
2588
|
+
if (typeof location === 'undefined' || location.hash !== SETTINGS_HASH) return;
|
|
2589
|
+
} catch (e) { return; }
|
|
2590
|
+
clearSettingsHash();
|
|
2591
|
+
openPanel(null);
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
function clearSettingsHash() {
|
|
2595
|
+
try {
|
|
2596
|
+
if (typeof history === 'undefined' || typeof history.replaceState !== 'function') return;
|
|
2597
|
+
history.replaceState(null, '', location.pathname + location.search);
|
|
2598
|
+
} catch (e) { /* noop */ }
|
|
1695
2599
|
}
|
|
1696
2600
|
|
|
1697
2601
|
/* ---------------------------------------------------------------- events */
|
|
@@ -1722,7 +2626,26 @@
|
|
|
1722
2626
|
nextProbeDelay: nextProbeDelay,
|
|
1723
2627
|
shouldReprobe: shouldReprobe,
|
|
1724
2628
|
resolveDetails: resolveDetails,
|
|
1725
|
-
buildThemeCss: buildThemeCss
|
|
2629
|
+
buildThemeCss: buildThemeCss,
|
|
2630
|
+
// SPEC V1.10 §2: which sentence a blocked embed shows. Pure, so the
|
|
2631
|
+
// wording is testable (and quotable by the cabinet) without a DOM.
|
|
2632
|
+
placeholderText: placeholderText,
|
|
2633
|
+
placeholdersEnabled: placeholdersEnabled,
|
|
2634
|
+
/* SPEC V1.12 §3 — the pure halves of the services panel, so the wording
|
|
2635
|
+
and the arithmetic are testable (and quotable by the cabinet) without a
|
|
2636
|
+
DOM: which plural form a count takes in each language, how a group
|
|
2637
|
+
header reads, and which cookieTable rows sit under which service. */
|
|
2638
|
+
plural: plural,
|
|
2639
|
+
pluralIndex: pluralIndex,
|
|
2640
|
+
buildStrings: buildStrings,
|
|
2641
|
+
localeTable: localeTable,
|
|
2642
|
+
resolveLang: resolveLang,
|
|
2643
|
+
cookieRowsForService: cookieRowsForService,
|
|
2644
|
+
looseCookies: looseCookies,
|
|
2645
|
+
servicePurpose: servicePurpose,
|
|
2646
|
+
groupCountLabel: groupCountLabel,
|
|
2647
|
+
serviceSignature: serviceSignature,
|
|
2648
|
+
signature: signature
|
|
1726
2649
|
};
|
|
1727
2650
|
// The page-font probe reads the DOM, so it is not part of the pure block —
|
|
1728
2651
|
// but the debug panel must be able to quote the family the banner painted
|
|
@@ -1768,9 +2691,29 @@
|
|
|
1768
2691
|
|
|
1769
2692
|
document.addEventListener('ck:ui:open-preferences', function () {
|
|
1770
2693
|
if (!mounted) mount(safeConfig());
|
|
2694
|
+
// mount() consumes the core's latch itself; clear it here too so a call
|
|
2695
|
+
// made while we were already mounted cannot leave a stale flag behind for
|
|
2696
|
+
// a later remount to act on.
|
|
2697
|
+
var ck = api();
|
|
2698
|
+
if (ck && ck._pendingOpen) { try { ck._pendingOpen = false; } catch (e) { /* noop */ } }
|
|
1771
2699
|
openPanel(null);
|
|
1772
2700
|
});
|
|
1773
2701
|
|
|
2702
|
+
/* SPEC V1.10 §1 — navigating to `#ck-settings` on a page that is already
|
|
2703
|
+
loaded (a footer link, or the declaration page opened in the same tab).
|
|
2704
|
+
mount() covers the other half: a page ENTERED on that hash.
|
|
2705
|
+
|
|
2706
|
+
Feature-checked rather than assumed: the SSR guard above only proves there
|
|
2707
|
+
is a `document`, and the branding suite evaluates this file against a stub
|
|
2708
|
+
window that has none of the event plumbing. A missing hashchange costs the
|
|
2709
|
+
deep link, not the banner. */
|
|
2710
|
+
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
2711
|
+
window.addEventListener('hashchange', function () {
|
|
2712
|
+
if (!mounted) mount(safeConfig());
|
|
2713
|
+
openFromHash();
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
|
|
1774
2717
|
document.addEventListener('ck:ui:close', function () {
|
|
1775
2718
|
closePanel();
|
|
1776
2719
|
});
|