@ecomconsult/consentkit 0.5.7 → 0.5.9
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 +43 -1
- package/npm/core.cjs +3 -0
- package/npm/core.mjs +4 -0
- package/npm/index.cjs +3 -0
- package/npm/index.d.ts +67 -1
- package/npm/index.mjs +4 -0
- package/npm/internal-stub.mjs +11 -1
- package/npm/react.mjs +10 -1
- package/package.json +3 -2
- package/src/ck-core.js +470 -14
- package/src/ck-debug.js +273 -3
- package/src/ck-locales.js +6 -0
- package/src/ck-saas.js +33 -0
- package/src/ck-ui.js +550 -43
package/src/ck-ui.js
CHANGED
|
@@ -32,6 +32,18 @@
|
|
|
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',
|
|
35
47
|
// SPEC V1.10 §2 — the blocked-embed placeholder. {host} is the vendor
|
|
36
48
|
// label when the database knows one and the bare host otherwise; {cat} is
|
|
37
49
|
// the localized category title, taken from cat.<name>.title below, so the
|
|
@@ -80,6 +92,11 @@
|
|
|
80
92
|
colExpiry: 'Срок',
|
|
81
93
|
floating: 'Настройки cookie',
|
|
82
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 ставит',
|
|
83
100
|
phText: 'Здесь содержимое от {host}. Оно загрузится после согласия на «{cat}».',
|
|
84
101
|
phAllow: 'Разрешить и показать',
|
|
85
102
|
phSettings: 'Настроить cookie',
|
|
@@ -119,9 +136,18 @@
|
|
|
119
136
|
// SPEC V1.10 §2. Present in ck-locales.js for ro only; every other external
|
|
120
137
|
// locale falls back to DICT.en through buildStrings(), which is what §2
|
|
121
138
|
// asks for («остальные языки — en»).
|
|
122
|
-
'phText', 'phAllow', 'phSettings', 'phLabel'
|
|
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'
|
|
123
146
|
];
|
|
124
147
|
|
|
148
|
+
// Plural-form keys, filled separately from STR_KEYS (see above).
|
|
149
|
+
var PLURAL_KEYS = ['svcCount', 'ckCount'];
|
|
150
|
+
|
|
125
151
|
// builtin(en,ru) <- window.__ckLocales, read at render time so the locales
|
|
126
152
|
// file may load in any order relative to this one.
|
|
127
153
|
function localeTable() {
|
|
@@ -153,6 +179,60 @@
|
|
|
153
179
|
return 'en';
|
|
154
180
|
}
|
|
155
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
|
+
|
|
156
236
|
// Deep two-level fill from en: a partial locale must never yield undefined,
|
|
157
237
|
// which would render the literal string "undefined".
|
|
158
238
|
function buildStrings(lang, table) {
|
|
@@ -164,6 +244,15 @@
|
|
|
164
244
|
var k = STR_KEYS[i];
|
|
165
245
|
out[k] = (typeof src[k] === 'string' && src[k]) ? src[k] : base[k];
|
|
166
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
|
+
|
|
167
256
|
out.cat = {};
|
|
168
257
|
var sc = (src.cat && typeof src.cat === 'object') ? src.cat : {};
|
|
169
258
|
for (i = 0; i < ALL_CATS.length; i++) {
|
|
@@ -193,14 +282,41 @@
|
|
|
193
282
|
uses, so «Маркетинг» in the placeholder is «Маркетинг» on the switch. An
|
|
194
283
|
unknown language falls back to en, an unknown category to marketing — the
|
|
195
284
|
category a strict-mode interception is filed under. */
|
|
196
|
-
|
|
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) {
|
|
197
294
|
var table = localeTable();
|
|
198
295
|
var T2 = buildStrings(resolveLang(lang, table), table);
|
|
199
296
|
var cat = (category && T2.cat[category]) ? category : 'marketing';
|
|
200
297
|
var name = String(host || '').trim();
|
|
298
|
+
var label = (typeof subject === 'string' && subject.trim())
|
|
299
|
+
? subject.trim() : T2.cat[cat].title;
|
|
201
300
|
return T2.phText
|
|
202
301
|
.replace('{host}', name || T2.phLabel)
|
|
203
|
-
.replace('{cat}',
|
|
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; }
|
|
204
320
|
}
|
|
205
321
|
|
|
206
322
|
/* Hostname of a blocked frame's real address, for the sentence above. The
|
|
@@ -324,9 +440,13 @@
|
|
|
324
440
|
'border:1px solid var(--ck-line);border-radius:var(--ck-radius-card);overflow:hidden}',
|
|
325
441
|
'.ck-panel__head{display:flex;align-items:flex-start;gap:16px;padding:22px 24px 14px;',
|
|
326
442
|
'border-bottom:1px solid var(--ck-line)}',
|
|
443
|
+
// The text block takes the width and the close button sits at the padding
|
|
444
|
+
// edge, flush with the switches below it: without flex:1 the button
|
|
445
|
+
// followed the text's own wrapped width and floated short of the edge.
|
|
446
|
+
'.ck-panel__head>div{flex:1 1 auto;min-width:0}',
|
|
327
447
|
'.ck-panel__head h2{margin:0 0 4px;font-size:18px;font-weight:600;letter-spacing:-.01em}',
|
|
328
448
|
'.ck-panel__head p{margin:0;font-size:14px;color:var(--ck-muted)}',
|
|
329
|
-
'.ck-x{flex:none;width:36px;height:36px;border-radius:var(--ck-radius-btn);border:1px solid var(--ck-line);',
|
|
449
|
+
'.ck-x{flex:none;margin-left:auto;width:36px;height:36px;border-radius:var(--ck-radius-btn);border:1px solid var(--ck-line);',
|
|
330
450
|
'background:transparent;display:inline-flex;align-items:center;justify-content:center;color:var(--ck-muted)}',
|
|
331
451
|
'.ck-panel__body{overflow:auto;padding:6px 24px 10px;-webkit-overflow-scrolling:touch}',
|
|
332
452
|
'.ck-panel__foot{display:flex;gap:10px;flex-wrap:wrap;padding:16px 24px;',
|
|
@@ -342,6 +462,28 @@
|
|
|
342
462
|
'.ck-cat__badge{font-size:12px;font-weight:500;color:var(--ck-muted);',
|
|
343
463
|
'border:1px solid var(--ck-line);border-radius:999px;padding:1px 8px}',
|
|
344
464
|
'.ck-cat__desc{margin:4px 0 0;font-size:13.5px;color:var(--ck-muted)}',
|
|
465
|
+
/* SPEC V1.12 §3 — «N сервисов · M cookie». --ck-muted, like every other
|
|
466
|
+
secondary label on the card, and it is measured for AA against the card
|
|
467
|
+
background by the same rule the description above answers to. */
|
|
468
|
+
'.ck-cat__count{font-size:12px;font-weight:500;color:var(--ck-muted)}',
|
|
469
|
+
|
|
470
|
+
/* ---- services inside a group (SPEC V1.12 §3) ---- */
|
|
471
|
+
/* Indented and rule-separated so the nesting reads without colour: a
|
|
472
|
+
service belongs to the group above it, and its own cookie table belongs
|
|
473
|
+
to it. The left border is the only decoration; everything else is
|
|
474
|
+
spacing, which survives forced-colours mode intact. */
|
|
475
|
+
'.ck-svcs{margin:12px 0 0;padding-left:12px;border-left:2px solid var(--ck-line)}',
|
|
476
|
+
'.ck-svc{padding:10px 0;border-bottom:1px solid var(--ck-line)}',
|
|
477
|
+
'.ck-svc:first-child{padding-top:2px}',
|
|
478
|
+
'.ck-svc:last-child{border-bottom:0;padding-bottom:2px}',
|
|
479
|
+
'.ck-svc__top{display:flex;gap:12px;align-items:flex-start}',
|
|
480
|
+
'.ck-svc__txt{flex:1 1 auto;min-width:0}',
|
|
481
|
+
'.ck-svc__name{font-size:14px;font-weight:600}',
|
|
482
|
+
'.ck-svc__vendor{margin:2px 0 0;font-size:12.5px;color:var(--ck-muted)}',
|
|
483
|
+
'.ck-svc__desc{margin:4px 0 0;font-size:13px;color:var(--ck-muted)}',
|
|
484
|
+
/* --ck-link, not --ck-accent: accent-coloured TEXT goes through the same
|
|
485
|
+
>= 4.5:1 rule as «Подробнее» — see the note on the `a{}` rule above. */
|
|
486
|
+
'.ck-svc__policy{display:inline-block;margin-top:4px;font-size:12.5px;color:var(--ck-link)}',
|
|
345
487
|
|
|
346
488
|
/* ---- switch ---- */
|
|
347
489
|
'.ck-switch{flex:none;width:46px;height:27px;padding:0;border-radius:999px;',
|
|
@@ -351,6 +493,11 @@
|
|
|
351
493
|
'.ck-switch[aria-checked="true"]{background:var(--ck-accent);border-color:var(--ck-accent)}',
|
|
352
494
|
'.ck-switch[aria-checked="true"]::after{left:auto;right:2px;border-color:transparent}',
|
|
353
495
|
'.ck-switch[disabled]{cursor:not-allowed;opacity:.55}',
|
|
496
|
+
/* The service switch: the same control, smaller. Still 27px of vertical
|
|
497
|
+
hit area at the row level and a real <button role="switch">, so the
|
|
498
|
+
keyboard and screen-reader behaviour is identical to the group's. */
|
|
499
|
+
'.ck-switch--sm{width:38px;height:22px}',
|
|
500
|
+
'.ck-switch--sm::after{width:16px;height:16px}',
|
|
354
501
|
|
|
355
502
|
/* ---- cookie table ---- */
|
|
356
503
|
'.ck-det{margin-top:12px}',
|
|
@@ -491,6 +638,7 @@
|
|
|
491
638
|
var LANG = 'en'; // the code T was built from, reassigned per mount
|
|
492
639
|
var nodes = {}; // banner/panel/fab refs
|
|
493
640
|
var switches = {}; // category -> button
|
|
641
|
+
var serviceSwitches = {}; // category -> [button], SPEC V1.12 §3
|
|
494
642
|
var panelOpen = false;
|
|
495
643
|
var lastFocus = null;
|
|
496
644
|
|
|
@@ -520,6 +668,92 @@
|
|
|
520
668
|
return out;
|
|
521
669
|
}
|
|
522
670
|
|
|
671
|
+
/* ------------------------------------------------------- services (§3) */
|
|
672
|
+
|
|
673
|
+
/* The services of one category, as the CORE normalised them.
|
|
674
|
+
|
|
675
|
+
Read through ConsentKit._services() rather than off config.services
|
|
676
|
+
directly: the core drops rows with `enabled: false`, a malformed id or an
|
|
677
|
+
unknown category, and the panel must list exactly the set the engine
|
|
678
|
+
blocks by. A config the core has not seen (no init(), or a core too old to
|
|
679
|
+
know about services) yields nothing, and the panel renders as it did in
|
|
680
|
+
0.5.7 — which is what «старый конфиг рендерится как раньше» asks for. */
|
|
681
|
+
function servicesFor(cat) {
|
|
682
|
+
var ck = api();
|
|
683
|
+
var list = null;
|
|
684
|
+
try {
|
|
685
|
+
if (ck && typeof ck._services === 'function') list = ck._services();
|
|
686
|
+
} catch (e) { list = null; }
|
|
687
|
+
if (!list || !Array.isArray(list)) return [];
|
|
688
|
+
var out = [];
|
|
689
|
+
for (var i = 0; i < list.length; i++) {
|
|
690
|
+
if (list[i] && list[i].category === cat) out.push(list[i]);
|
|
691
|
+
}
|
|
692
|
+
return out;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* The service's one-line purpose, in the banner's language.
|
|
696
|
+
|
|
697
|
+
§3 asks for «одна строка purpose на языке баннера» and §2 ships
|
|
698
|
+
`purpose: { ru?, ro?, en? }`. Falls back to en, then to nothing at all —
|
|
699
|
+
an empty purpose renders no paragraph rather than the string "undefined"
|
|
700
|
+
or a stray language. LANG is the resolved banner code, so a `pt-BR`
|
|
701
|
+
banner asks for `pt` and lands on en, which is the honest answer. */
|
|
702
|
+
function servicePurpose(svc, lang) {
|
|
703
|
+
var p = (svc && svc.purpose) || {};
|
|
704
|
+
var code = String(lang == null ? LANG : lang || 'en').slice(0, 2).toLowerCase();
|
|
705
|
+
var v = p[code] || p.en;
|
|
706
|
+
return (typeof v === 'string' && v.trim()) ? v.trim() : '';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/* The cookieTable rows this service claims, by name.
|
|
710
|
+
|
|
711
|
+
§3: «под сервисом его cookie (имя · срок · назначение)» drawn «from
|
|
712
|
+
cookieTable rows whose name is in service.cookies». A name the service
|
|
713
|
+
declares but the table does not describe is NOT invented here: the panel
|
|
714
|
+
shows what the owner wrote down, and the declaration page is where the
|
|
715
|
+
full list lives. */
|
|
716
|
+
function cookieRowsForService(rows, svc) {
|
|
717
|
+
var names = (svc && svc.cookies) || [];
|
|
718
|
+
if (!names.length) return [];
|
|
719
|
+
var out = [];
|
|
720
|
+
for (var i = 0; i < rows.length; i++) {
|
|
721
|
+
var n = rows[i] && rows[i].name;
|
|
722
|
+
if (typeof n === 'string' && names.indexOf(n) > -1) out.push(rows[i]);
|
|
723
|
+
}
|
|
724
|
+
return out;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* SPEC V1.12 §3 — «N сервисов · M cookie» on the group header. Pure, so the
|
|
728
|
+
wording in each of the three languages is testable without a DOM.
|
|
729
|
+
|
|
730
|
+
The cookie half counts the WHOLE group, services and loose rows alike: the
|
|
731
|
+
line answers «сколько cookie в этой группе», which is the question a
|
|
732
|
+
visitor scanning the header is actually asking. */
|
|
733
|
+
function groupCountLabel(serviceCount, cookieCount, strings, lang) {
|
|
734
|
+
return plural(strings.svcCount, serviceCount, lang) + ' · ' +
|
|
735
|
+
plural(strings.ckCount, cookieCount, lang);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/* Rows left over once every service has taken its own — «Cookie в этой
|
|
739
|
+
группе (N)» keeps meaning «the rest», exactly as it did before services
|
|
740
|
+
existed. With no services at all this returns the whole list unchanged. */
|
|
741
|
+
function looseCookies(rows, svcs) {
|
|
742
|
+
if (!svcs.length) return rows;
|
|
743
|
+
var claimed = {};
|
|
744
|
+
for (var i = 0; i < svcs.length; i++) {
|
|
745
|
+
var names = svcs[i].cookies || [];
|
|
746
|
+
for (var j = 0; j < names.length; j++) claimed[names[j]] = true;
|
|
747
|
+
}
|
|
748
|
+
var out = [];
|
|
749
|
+
for (var k = 0; k < rows.length; k++) {
|
|
750
|
+
var n = rows[k] && rows[k].name;
|
|
751
|
+
if (typeof n === 'string' && claimed[n]) continue;
|
|
752
|
+
out.push(rows[k]);
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
|
|
523
757
|
/* ----------------------------------------------------------------- theme */
|
|
524
758
|
|
|
525
759
|
// Built-in palettes. Dark values are picked for >= 4.5:1 text contrast.
|
|
@@ -1252,11 +1486,155 @@
|
|
|
1252
1486
|
b.addEventListener('click', function () {
|
|
1253
1487
|
var on = b.getAttribute('aria-checked') === 'true';
|
|
1254
1488
|
b.setAttribute('aria-checked', on ? 'false' : 'true');
|
|
1489
|
+
/* SPEC V1.12 §3 — the group switch is the master:
|
|
1490
|
+
off -> every service of the group goes off and is blocked;
|
|
1491
|
+
on -> the services come back, EXCEPT the ones turned off by hand.
|
|
1492
|
+
The hand-set state is kept on the service switch itself (dataset.man)
|
|
1493
|
+
rather than being read back off the group, which is what lets a
|
|
1494
|
+
manual refusal survive the group being toggled off and on again. */
|
|
1495
|
+
syncGroup(cat);
|
|
1255
1496
|
});
|
|
1256
1497
|
}
|
|
1257
1498
|
return b;
|
|
1258
1499
|
}
|
|
1259
1500
|
|
|
1501
|
+
/* SPEC V1.12 §3 — one service, with its own switch, its purpose, its policy
|
|
1502
|
+
link and the cookies it sets. */
|
|
1503
|
+
function makeServiceSwitch(svc, cat) {
|
|
1504
|
+
var b = el('button', 'ck-switch ck-switch--sm');
|
|
1505
|
+
b.type = 'button';
|
|
1506
|
+
b.setAttribute('role', 'switch');
|
|
1507
|
+
b.setAttribute('aria-checked', 'false');
|
|
1508
|
+
b.dataset.svc = svc.id;
|
|
1509
|
+
b.dataset.cat = cat;
|
|
1510
|
+
// '1' once the visitor has switched this service off by hand. Read by
|
|
1511
|
+
// syncGroup() when the group comes back on, and cleared when they switch it
|
|
1512
|
+
// on again — a service the visitor re-enables is no longer «отключён вручную».
|
|
1513
|
+
b.dataset.man = '';
|
|
1514
|
+
/* A service switch is only ever clickable while its group is ON: syncGroup()
|
|
1515
|
+
disables it otherwise, and a disabled <button> fires no click. So this
|
|
1516
|
+
handler always runs with the group on, and there is no "turn the group
|
|
1517
|
+
back on too" case to handle — the visitor reaches a refused group through
|
|
1518
|
+
the group's own switch. */
|
|
1519
|
+
b.addEventListener('click', function () {
|
|
1520
|
+
var on = b.getAttribute('aria-checked') === 'true';
|
|
1521
|
+
b.setAttribute('aria-checked', on ? 'false' : 'true');
|
|
1522
|
+
// '1' = «switched off by hand». Cleared when it is switched back on, so a
|
|
1523
|
+
// re-enabled service is no longer «отключён вручную» and follows its group.
|
|
1524
|
+
b.dataset.man = on ? '1' : '';
|
|
1525
|
+
});
|
|
1526
|
+
return b;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/* Pushes the group switch's state down onto its services. Called when the
|
|
1530
|
+
group is clicked and when the panel is synced from the stored state. */
|
|
1531
|
+
function syncGroup(cat) {
|
|
1532
|
+
var on = !!(switches[cat] && switches[cat].getAttribute('aria-checked') === 'true');
|
|
1533
|
+
var list = serviceSwitches[cat] || [];
|
|
1534
|
+
for (var i = 0; i < list.length; i++) {
|
|
1535
|
+
var b = list[i];
|
|
1536
|
+
// Group off: everything off. Group on: everything on except what the
|
|
1537
|
+
// visitor turned off by hand.
|
|
1538
|
+
var want = on && b.dataset.man !== '1';
|
|
1539
|
+
b.setAttribute('aria-checked', want ? 'true' : 'false');
|
|
1540
|
+
// A service cannot be granted while its group is refused, and a switch
|
|
1541
|
+
// that looks operable but changes nothing is worse than a disabled one.
|
|
1542
|
+
b.disabled = !on;
|
|
1543
|
+
if (!on) b.setAttribute('aria-disabled', 'true');
|
|
1544
|
+
else b.removeAttribute('aria-disabled');
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function buildService(svc, cat, rows) {
|
|
1549
|
+
var wrap = el('div', 'ck-svc');
|
|
1550
|
+
var top = el('div', 'ck-svc__top');
|
|
1551
|
+
var txt = el('div', 'ck-svc__txt');
|
|
1552
|
+
|
|
1553
|
+
var nameId = 'ck-svc-' + svc.id;
|
|
1554
|
+
var name = el('div', 'ck-svc__name');
|
|
1555
|
+
var nameSpan = el('span', null, svc.name);
|
|
1556
|
+
nameSpan.id = nameId;
|
|
1557
|
+
name.appendChild(nameSpan);
|
|
1558
|
+
txt.appendChild(name);
|
|
1559
|
+
|
|
1560
|
+
if (svc.vendor) txt.appendChild(el('p', 'ck-svc__vendor', svc.vendor));
|
|
1561
|
+
|
|
1562
|
+
var descId = null;
|
|
1563
|
+
var purpose = servicePurpose(svc);
|
|
1564
|
+
if (purpose) {
|
|
1565
|
+
descId = nameId + '-desc';
|
|
1566
|
+
var p = el('p', 'ck-svc__desc', purpose);
|
|
1567
|
+
p.id = descId;
|
|
1568
|
+
txt.appendChild(p);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/* «Политика» — target=_blank rel=noopener, per §3. The URL is already
|
|
1572
|
+
http(s)-validated by the core's normalizeService(), which is where a
|
|
1573
|
+
javascript: address is dropped; nothing unvalidated reaches an href. */
|
|
1574
|
+
if (svc.privacyUrl) {
|
|
1575
|
+
var a = el('a', 'ck-svc__policy', T.svcPolicy);
|
|
1576
|
+
a.href = svc.privacyUrl;
|
|
1577
|
+
a.target = '_blank';
|
|
1578
|
+
a.rel = 'noopener noreferrer';
|
|
1579
|
+
// The link text is the same word on every row, so a screen reader needs
|
|
1580
|
+
// the service name to tell them apart.
|
|
1581
|
+
a.setAttribute('aria-label', T.svcPolicy + ' — ' + svc.name);
|
|
1582
|
+
txt.appendChild(a);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
var sw = makeServiceSwitch(svc, cat);
|
|
1586
|
+
sw.setAttribute('aria-labelledby', nameId);
|
|
1587
|
+
if (descId) sw.setAttribute('aria-describedby', descId);
|
|
1588
|
+
if (!serviceSwitches[cat]) serviceSwitches[cat] = [];
|
|
1589
|
+
serviceSwitches[cat].push(sw);
|
|
1590
|
+
|
|
1591
|
+
top.appendChild(txt);
|
|
1592
|
+
top.appendChild(sw);
|
|
1593
|
+
wrap.appendChild(top);
|
|
1594
|
+
|
|
1595
|
+
var own = cookieRowsForService(rows, svc);
|
|
1596
|
+
if (own.length) wrap.appendChild(cookieTable(own, T.svcCookies + ' (' + own.length + ')'));
|
|
1597
|
+
|
|
1598
|
+
return wrap;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
/* The <details> block a group and a service both use for their cookies:
|
|
1602
|
+
same columns, same markup, one summary line apart. */
|
|
1603
|
+
function cookieTable(rows, summaryText) {
|
|
1604
|
+
var det = el('details', 'ck-det');
|
|
1605
|
+
var sum = el('summary');
|
|
1606
|
+
sum.appendChild(document.createTextNode(summaryText));
|
|
1607
|
+
det.appendChild(sum);
|
|
1608
|
+
|
|
1609
|
+
var tw = el('div', 'ck-tablewrap');
|
|
1610
|
+
var table = el('table');
|
|
1611
|
+
var thead = el('thead');
|
|
1612
|
+
var htr = el('tr');
|
|
1613
|
+
var heads = [T.colName, T.colVendor, T.colPurpose, T.colExpiry];
|
|
1614
|
+
for (var h = 0; h < heads.length; h++) {
|
|
1615
|
+
var th = el('th', null, heads[h]);
|
|
1616
|
+
th.setAttribute('scope', 'col');
|
|
1617
|
+
htr.appendChild(th);
|
|
1618
|
+
}
|
|
1619
|
+
thead.appendChild(htr);
|
|
1620
|
+
table.appendChild(thead);
|
|
1621
|
+
|
|
1622
|
+
var tbody = el('tbody');
|
|
1623
|
+
for (var r = 0; r < rows.length; r++) {
|
|
1624
|
+
var row = rows[r];
|
|
1625
|
+
var tr = el('tr');
|
|
1626
|
+
tr.appendChild(el('td', 'ck-mono', String(row.name == null ? '—' : row.name)));
|
|
1627
|
+
tr.appendChild(el('td', null, String(row.vendor == null ? '—' : row.vendor)));
|
|
1628
|
+
tr.appendChild(el('td', null, String(row.purpose == null ? '—' : row.purpose)));
|
|
1629
|
+
tr.appendChild(el('td', null, String(row.expiry == null ? '—' : row.expiry)));
|
|
1630
|
+
tbody.appendChild(tr);
|
|
1631
|
+
}
|
|
1632
|
+
table.appendChild(tbody);
|
|
1633
|
+
tw.appendChild(table);
|
|
1634
|
+
det.appendChild(tw);
|
|
1635
|
+
return det;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1260
1638
|
function buildCategory(cfg, cat) {
|
|
1261
1639
|
var meta = T.cat[cat] || { title: cat, desc: '' };
|
|
1262
1640
|
var locked = cat === 'necessary';
|
|
@@ -1278,6 +1656,21 @@
|
|
|
1278
1656
|
desc.id = descId;
|
|
1279
1657
|
txt.appendChild(desc);
|
|
1280
1658
|
|
|
1659
|
+
var rows = cookiesFor(cfg, cat);
|
|
1660
|
+
var svcs = servicesFor(cat);
|
|
1661
|
+
|
|
1662
|
+
/* SPEC V1.12 §3 — «N сервисов · M cookie» on the group header.
|
|
1663
|
+
|
|
1664
|
+
Rendered ONLY when the group actually has services. A site whose config
|
|
1665
|
+
predates 0.5.8 has none, and must look exactly as it did in 0.5.7 — not
|
|
1666
|
+
«0 сервисов · 3 cookie». The cookie half counts the whole group, services
|
|
1667
|
+
and loose rows alike: it answers «сколько cookie в этой группе», which is
|
|
1668
|
+
the question the line is there to answer. */
|
|
1669
|
+
if (svcs.length) {
|
|
1670
|
+
name.appendChild(el('span', 'ck-cat__count',
|
|
1671
|
+
groupCountLabel(svcs.length, rows.length, T, LANG)));
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1281
1674
|
var sw = makeSwitch(cat, locked);
|
|
1282
1675
|
sw.setAttribute('aria-labelledby', nameId);
|
|
1283
1676
|
sw.setAttribute('aria-describedby', descId);
|
|
@@ -1287,40 +1680,24 @@
|
|
|
1287
1680
|
top.appendChild(sw);
|
|
1288
1681
|
wrap.appendChild(top);
|
|
1289
1682
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
var table = el('table');
|
|
1299
|
-
var thead = el('thead');
|
|
1300
|
-
var htr = el('tr');
|
|
1301
|
-
var heads = [T.colName, T.colVendor, T.colPurpose, T.colExpiry];
|
|
1302
|
-
for (var h = 0; h < heads.length; h++) {
|
|
1303
|
-
var th = el('th', null, heads[h]);
|
|
1304
|
-
th.setAttribute('scope', 'col');
|
|
1305
|
-
htr.appendChild(th);
|
|
1306
|
-
}
|
|
1307
|
-
thead.appendChild(htr);
|
|
1308
|
-
table.appendChild(thead);
|
|
1309
|
-
|
|
1310
|
-
var tbody = el('tbody');
|
|
1311
|
-
for (var r = 0; r < rows.length; r++) {
|
|
1312
|
-
var row = rows[r];
|
|
1313
|
-
var tr = el('tr');
|
|
1314
|
-
tr.appendChild(el('td', 'ck-mono', String(row.name == null ? '—' : row.name)));
|
|
1315
|
-
tr.appendChild(el('td', null, String(row.vendor == null ? '—' : row.vendor)));
|
|
1316
|
-
tr.appendChild(el('td', null, String(row.purpose == null ? '—' : row.purpose)));
|
|
1317
|
-
tr.appendChild(el('td', null, String(row.expiry == null ? '—' : row.expiry)));
|
|
1318
|
-
tbody.appendChild(tr);
|
|
1683
|
+
if (svcs.length) {
|
|
1684
|
+
var list = el('div', 'ck-svcs');
|
|
1685
|
+
// A list, so a screen reader announces «3 items» before reading them.
|
|
1686
|
+
list.setAttribute('role', 'list');
|
|
1687
|
+
for (var s = 0; s < svcs.length; s++) {
|
|
1688
|
+
var item = buildService(svcs[s], cat, rows);
|
|
1689
|
+
item.setAttribute('role', 'listitem');
|
|
1690
|
+
list.appendChild(item);
|
|
1319
1691
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1692
|
+
wrap.appendChild(list);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// «Cookie в этой группе (N)» keeps its old meaning: what is left once each
|
|
1696
|
+
// service has claimed its own. With no services that is the whole table and
|
|
1697
|
+
// the summary line is byte-for-byte what 0.5.7 rendered.
|
|
1698
|
+
var loose = looseCookies(rows, svcs);
|
|
1699
|
+
if (loose.length) {
|
|
1700
|
+
wrap.appendChild(cookieTable(loose, T.cookiesIn + ' (' + loose.length + ')'));
|
|
1324
1701
|
}
|
|
1325
1702
|
|
|
1326
1703
|
return wrap;
|
|
@@ -1596,6 +1973,32 @@
|
|
|
1596
1973
|
var sw = switches[k];
|
|
1597
1974
|
out[k] = !!(sw && sw.getAttribute('aria-checked') === 'true');
|
|
1598
1975
|
}
|
|
1976
|
+
/* SPEC V1.12 §3 — «хранение: только отказы».
|
|
1977
|
+
|
|
1978
|
+
Read from `dataset.man`, NOT from `aria-checked`. The two answer different
|
|
1979
|
+
questions and only one of them is the visitor's decision:
|
|
1980
|
+
|
|
1981
|
+
aria-checked — what the switch currently SHOWS. syncGroup() forces every
|
|
1982
|
+
service of a refused group to `false`, so reading this
|
|
1983
|
+
would record «I refused all six of these» the moment the
|
|
1984
|
+
group went off.
|
|
1985
|
+
dataset.man — «the visitor switched this one off by hand». Set on click,
|
|
1986
|
+
restored from the stored record, and deliberately left
|
|
1987
|
+
alone by syncGroup().
|
|
1988
|
+
|
|
1989
|
+
Getting this backwards loses a denial: refuse Hotjar, later switch
|
|
1990
|
+
analytics off and save, and the refusal would be erased — so when
|
|
1991
|
+
analytics came back on, Hotjar would run again without ever having been
|
|
1992
|
+
re-consented to. */
|
|
1993
|
+
var svcs = {};
|
|
1994
|
+
for (var c = 0; c < OPT_IN.length; c++) {
|
|
1995
|
+
var list = serviceSwitches[OPT_IN[c]] || [];
|
|
1996
|
+
for (var j = 0; j < list.length; j++) {
|
|
1997
|
+
var b = list[j];
|
|
1998
|
+
if (b.dataset.man === '1') svcs[b.dataset.svc] = false;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
out.services = svcs;
|
|
1599
2002
|
return out;
|
|
1600
2003
|
}
|
|
1601
2004
|
|
|
@@ -1674,6 +2077,23 @@
|
|
|
1674
2077
|
sw.setAttribute('aria-checked', on ? 'true' : 'false');
|
|
1675
2078
|
}
|
|
1676
2079
|
if (switches.necessary) switches.necessary.setAttribute('aria-checked', 'true');
|
|
2080
|
+
|
|
2081
|
+
/* SPEC V1.12 §3 — restore the per-service switches from the stored denials.
|
|
2082
|
+
|
|
2083
|
+
`dataset.man` is set from the record FIRST, then syncGroup() derives what
|
|
2084
|
+
each switch shows from it. That ordering is the whole point: a visitor who
|
|
2085
|
+
denied Hotjar and left analytics off must still see Hotjar's own switch
|
|
2086
|
+
off when they turn analytics back on, and the group→service push is what
|
|
2087
|
+
would otherwise light it up again. */
|
|
2088
|
+
var denials = (state && state.services) || {};
|
|
2089
|
+
for (var c = 0; c < OPT_IN.length; c++) {
|
|
2090
|
+
var cat = OPT_IN[c];
|
|
2091
|
+
var list = serviceSwitches[cat] || [];
|
|
2092
|
+
for (var j = 0; j < list.length; j++) {
|
|
2093
|
+
list[j].dataset.man = denials[list[j].dataset.svc] === false ? '1' : '';
|
|
2094
|
+
}
|
|
2095
|
+
syncGroup(cat);
|
|
2096
|
+
}
|
|
1677
2097
|
}
|
|
1678
2098
|
|
|
1679
2099
|
// Idempotent: safe to call from ck:init, ck:change and right after our own API calls.
|
|
@@ -1852,8 +2272,13 @@
|
|
|
1852
2272
|
icon.innerHTML = PLAY_ICON;
|
|
1853
2273
|
card.appendChild(icon);
|
|
1854
2274
|
|
|
1855
|
-
var
|
|
1856
|
-
|
|
2275
|
+
var frameSrcAttr = frame.getAttribute('data-src') || '';
|
|
2276
|
+
var label = hostOf(frameSrcAttr);
|
|
2277
|
+
// §3: when a refused SERVICE is what is holding this frame, the sentence
|
|
2278
|
+
// must name that service — its category is already granted.
|
|
2279
|
+
var held = holdingService(frameSrcAttr);
|
|
2280
|
+
card.appendChild(el('p', null,
|
|
2281
|
+
placeholderText(label, cat, LANG, held ? held.name : null)));
|
|
1857
2282
|
|
|
1858
2283
|
var row = el('div', 'ck-ph__row');
|
|
1859
2284
|
/* Only offer the grant when it can actually take effect: the core's
|
|
@@ -1863,7 +2288,10 @@
|
|
|
1863
2288
|
if (categoryEnabled(cfg, cat)) {
|
|
1864
2289
|
var allow = el('button', 'ck-ph__btn', T.phAllow);
|
|
1865
2290
|
allow.type = 'button';
|
|
1866
|
-
|
|
2291
|
+
// SPEC V1.12 §3: «кнопка "Разрешить и показать" включает категорию и
|
|
2292
|
+
// снимает отказ по этому сервису». The frame's own address decides which
|
|
2293
|
+
// service that is — the visitor pressed the button on THIS embed.
|
|
2294
|
+
allow.addEventListener('click', function () { grantCategory(cat, frameSrcAttr); });
|
|
1867
2295
|
row.appendChild(allow);
|
|
1868
2296
|
}
|
|
1869
2297
|
var settings = el('button', 'ck-ph__link', T.phSettings);
|
|
@@ -1888,20 +2316,55 @@
|
|
|
1888
2316
|
the core dispatches — so there is no logging code here. The core's
|
|
1889
2317
|
applyConsentToDom() restores the frame; sweepPlaceholders() then removes
|
|
1890
2318
|
this card, driven by the ck:change that same commit dispatches. */
|
|
1891
|
-
function grantCategory(cat) {
|
|
2319
|
+
function grantCategory(cat, src) {
|
|
1892
2320
|
var ck = api();
|
|
1893
2321
|
if (!ck || typeof ck.accept !== 'function') return;
|
|
1894
|
-
var
|
|
2322
|
+
var st = safeState();
|
|
2323
|
+
var cur = st.categories || {};
|
|
1895
2324
|
var next = {
|
|
1896
2325
|
functional: cur.functional === true,
|
|
1897
2326
|
analytics: cur.analytics === true,
|
|
1898
2327
|
marketing: cur.marketing === true
|
|
1899
2328
|
};
|
|
1900
2329
|
if (cat === 'functional' || cat === 'analytics' || cat === 'marketing') next[cat] = true;
|
|
2330
|
+
|
|
2331
|
+
/* SPEC V1.12 §3 — clear the refusal on THIS frame's service, in the SAME
|
|
2332
|
+
accept() call as the category grant.
|
|
2333
|
+
|
|
2334
|
+
One call, not two: the core revives blocked frames inside commit(), so a
|
|
2335
|
+
denial still standing at that moment leaves this frame dead until some
|
|
2336
|
+
later consent change happens to run applyConsentToDom() again. The whole
|
|
2337
|
+
map is passed because accept() replaces it wholesale — every OTHER
|
|
2338
|
+
refusal the visitor made is copied across untouched. */
|
|
2339
|
+
var denials = denialsWithout(st.services, src);
|
|
2340
|
+
if (denials) next.services = denials;
|
|
2341
|
+
|
|
1901
2342
|
try { ck.accept(next); } catch (e) { /* noop */ }
|
|
1902
2343
|
syncFromState();
|
|
1903
2344
|
}
|
|
1904
2345
|
|
|
2346
|
+
/* The stored denial map minus the service that owns `src`, or null when
|
|
2347
|
+
nothing would change (no denials, no service for that URL, or that service
|
|
2348
|
+
was not refused in the first place). */
|
|
2349
|
+
function denialsWithout(stored, src) {
|
|
2350
|
+
var ck = api();
|
|
2351
|
+
if (!ck || typeof ck._serviceForUrl !== 'function') return null;
|
|
2352
|
+
var map = {};
|
|
2353
|
+
var any = false;
|
|
2354
|
+
try {
|
|
2355
|
+
Object.keys(stored || {}).forEach(function (k) {
|
|
2356
|
+
if (stored[k] === false) { map[k] = false; any = true; }
|
|
2357
|
+
});
|
|
2358
|
+
} catch (e) { return null; }
|
|
2359
|
+
if (!any) return null;
|
|
2360
|
+
|
|
2361
|
+
var svc = null;
|
|
2362
|
+
try { svc = src ? ck._serviceForUrl(src) : null; } catch (e2) { svc = null; }
|
|
2363
|
+
if (!svc || map[svc.id] !== false) return null;
|
|
2364
|
+
delete map[svc.id];
|
|
2365
|
+
return map;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
1905
2368
|
function hideFrame(frame) {
|
|
1906
2369
|
try {
|
|
1907
2370
|
frame.setAttribute(PH_DISPLAY, frame.style.display || '');
|
|
@@ -1992,10 +2455,38 @@
|
|
|
1992
2455
|
// and mount() is one-shot. Buttons and colours are deliberately NOT
|
|
1993
2456
|
// here — they are token values and restyle in place.
|
|
1994
2457
|
resolveDetails(c).kind,
|
|
2458
|
+
/* SPEC V1.12 §3 — services are STRUCTURAL: each one adds a row with its
|
|
2459
|
+
own switch to the panel, and mount() is one-shot. Without this a SaaS
|
|
2460
|
+
config that arrives after the first mount (the second, idempotent
|
|
2461
|
+
init()) would re-run buildServices() in the core — so the engine would
|
|
2462
|
+
block per service — while the panel kept showing the service-less
|
|
2463
|
+
render, and the visitor would have no way to see or change any of it.
|
|
2464
|
+
|
|
2465
|
+
The COUNT and the ids, not the whole rows: what needs a rebuild is a
|
|
2466
|
+
service appearing, disappearing or changing identity. A reworded
|
|
2467
|
+
`purpose` is text inside an existing row and does not justify tearing
|
|
2468
|
+
the panel down. */
|
|
2469
|
+
serviceSignature(c),
|
|
1995
2470
|
brandSignature(c)
|
|
1996
2471
|
].join('|');
|
|
1997
2472
|
}
|
|
1998
2473
|
|
|
2474
|
+
function serviceSignature(cfg) {
|
|
2475
|
+
var list = cfg && cfg.services;
|
|
2476
|
+
if (!list || !Array.isArray(list) || !list.length) return '0';
|
|
2477
|
+
var ids = [];
|
|
2478
|
+
for (var i = 0; i < list.length; i++) {
|
|
2479
|
+
var s = list[i];
|
|
2480
|
+
if (!s || typeof s !== 'object' || s.enabled === false) continue;
|
|
2481
|
+
ids.push(String(s.id || '') + ':' + String(s.category || ''));
|
|
2482
|
+
}
|
|
2483
|
+
// '0' for «no services», however that came about: an absent key, an empty
|
|
2484
|
+
// array, or a list every row of which the core would drop. All three render
|
|
2485
|
+
// the same panel, so none of them may differ in the signature.
|
|
2486
|
+
if (!ids.length) return '0';
|
|
2487
|
+
return ids.length + ',' + ids.join(',');
|
|
2488
|
+
}
|
|
2489
|
+
|
|
1999
2490
|
function remount(cfg) {
|
|
2000
2491
|
mounted = false;
|
|
2001
2492
|
panelOpen = false;
|
|
@@ -2041,6 +2532,7 @@
|
|
|
2041
2532
|
root.appendChild(style);
|
|
2042
2533
|
|
|
2043
2534
|
switches = {};
|
|
2535
|
+
serviceSwitches = {};
|
|
2044
2536
|
nodes = {};
|
|
2045
2537
|
|
|
2046
2538
|
// Palette sheet comes after the base sheet so its :host tokens win.
|
|
@@ -2142,7 +2634,22 @@
|
|
|
2142
2634
|
// SPEC V1.10 §2: which sentence a blocked embed shows. Pure, so the
|
|
2143
2635
|
// wording is testable (and quotable by the cabinet) without a DOM.
|
|
2144
2636
|
placeholderText: placeholderText,
|
|
2145
|
-
placeholdersEnabled: placeholdersEnabled
|
|
2637
|
+
placeholdersEnabled: placeholdersEnabled,
|
|
2638
|
+
/* SPEC V1.12 §3 — the pure halves of the services panel, so the wording
|
|
2639
|
+
and the arithmetic are testable (and quotable by the cabinet) without a
|
|
2640
|
+
DOM: which plural form a count takes in each language, how a group
|
|
2641
|
+
header reads, and which cookieTable rows sit under which service. */
|
|
2642
|
+
plural: plural,
|
|
2643
|
+
pluralIndex: pluralIndex,
|
|
2644
|
+
buildStrings: buildStrings,
|
|
2645
|
+
localeTable: localeTable,
|
|
2646
|
+
resolveLang: resolveLang,
|
|
2647
|
+
cookieRowsForService: cookieRowsForService,
|
|
2648
|
+
looseCookies: looseCookies,
|
|
2649
|
+
servicePurpose: servicePurpose,
|
|
2650
|
+
groupCountLabel: groupCountLabel,
|
|
2651
|
+
serviceSignature: serviceSignature,
|
|
2652
|
+
signature: signature
|
|
2146
2653
|
};
|
|
2147
2654
|
// The page-font probe reads the DOM, so it is not part of the pure block —
|
|
2148
2655
|
// but the debug panel must be able to quote the family the banner painted
|