govuk_publishing_components 37.6.0 → 37.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/govuk_publishing_components/analytics-ga4/ga4-specialist-link-tracker.js +24 -2
- data/app/assets/javascripts/govuk_publishing_components/components/layout-super-navigation-header.js +63 -0
- data/app/assets/javascripts/govuk_publishing_components/lib/trigger-event.js +4 -0
- data/app/assets/stylesheets/govuk_publishing_components/components/_contents-list.scss +1 -1
- data/app/views/govuk_publishing_components/components/_contents_list.html.erb +2 -0
- data/app/views/govuk_publishing_components/components/_govspeak.html.erb +11 -0
- data/app/views/govuk_publishing_components/components/_layout_super_navigation_header.html.erb +3 -4
- data/app/views/govuk_publishing_components/components/docs/contents_list.yml +1 -0
- data/app/views/govuk_publishing_components/components/docs/govspeak.yml +9 -0
- data/app/views/govuk_publishing_components/components/docs/layout_super_navigation_header.yml +2 -0
- data/lib/govuk_publishing_components/version.rb +1 -1
- data/node_modules/govuk-single-consent/README.md +157 -0
- data/node_modules/govuk-single-consent/dist/singleconsent.cjs.js +419 -0
- data/node_modules/govuk-single-consent/dist/singleconsent.esm.js +417 -0
- data/node_modules/govuk-single-consent/dist/singleconsent.iife.js +431 -0
- data/node_modules/govuk-single-consent/dist/singleconsent.iife.min.js +1 -0
- data/node_modules/govuk-single-consent/package.json +56 -0
- metadata +8 -2
@@ -0,0 +1,419 @@
|
|
1
|
+
'use strict';
|
2
|
+
|
3
|
+
const DEFAULT_TIMEOUT = 10000;
|
4
|
+
function request(url, options, onSuccess, onError) {
|
5
|
+
try {
|
6
|
+
var req = new XMLHttpRequest();
|
7
|
+
var isTimeout = false;
|
8
|
+
options = options || {};
|
9
|
+
req.onreadystatechange = function () {
|
10
|
+
if (req.readyState === req.DONE) {
|
11
|
+
if (req.status >= 200 && req.status < 400) {
|
12
|
+
let jsonResponse;
|
13
|
+
try {
|
14
|
+
jsonResponse = JSON.parse(req.responseText);
|
15
|
+
onSuccess(jsonResponse);
|
16
|
+
}
|
17
|
+
catch (error) {
|
18
|
+
return onError(error);
|
19
|
+
}
|
20
|
+
}
|
21
|
+
else if (req.status === 0 && req.timeout > 0) {
|
22
|
+
// Possible timeout, waiting for ontimeout event
|
23
|
+
// Timeout will throw a status = 0 request
|
24
|
+
// onreadystatechange preempts ontimeout
|
25
|
+
// And we can't know for sure at this stage if it's a timeout
|
26
|
+
setTimeout(function () {
|
27
|
+
if (isTimeout) {
|
28
|
+
return;
|
29
|
+
}
|
30
|
+
return onError(new Error('Request to ' + url + ' failed with status: ' + req.status));
|
31
|
+
}, 500);
|
32
|
+
}
|
33
|
+
else {
|
34
|
+
return onError(new Error('Request to ' + url + ' failed with status: ' + req.status));
|
35
|
+
}
|
36
|
+
}
|
37
|
+
};
|
38
|
+
req.open(options.method || 'GET', url, true);
|
39
|
+
if (options.timeout) {
|
40
|
+
req.timeout = options.timeout;
|
41
|
+
}
|
42
|
+
else {
|
43
|
+
req.timeout = DEFAULT_TIMEOUT;
|
44
|
+
}
|
45
|
+
req.ontimeout = function () {
|
46
|
+
isTimeout = true;
|
47
|
+
return onError(new Error('Request to ' + url + ' timed out'));
|
48
|
+
};
|
49
|
+
var headers = options.headers || {};
|
50
|
+
for (var name in headers) {
|
51
|
+
req.setRequestHeader(name, headers[name]);
|
52
|
+
}
|
53
|
+
req.send(options.body || null);
|
54
|
+
}
|
55
|
+
catch (error) {
|
56
|
+
return onError(error);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
function addUrlParameter(url, name, value) {
|
60
|
+
url = parseUrl(url);
|
61
|
+
var newParam = name.concat('=', value);
|
62
|
+
var modified = false;
|
63
|
+
findByKey(name, url.params, function (index) {
|
64
|
+
url.params[index] = newParam;
|
65
|
+
modified = true;
|
66
|
+
});
|
67
|
+
if (!modified) {
|
68
|
+
url.params.push(newParam);
|
69
|
+
}
|
70
|
+
return buildUrl(url);
|
71
|
+
}
|
72
|
+
function removeUrlParameter(url, name) {
|
73
|
+
url = parseUrl(url);
|
74
|
+
findByKey(name, url.params, function (index) {
|
75
|
+
url.params.splice(index--, 1);
|
76
|
+
});
|
77
|
+
return buildUrl(url);
|
78
|
+
}
|
79
|
+
function parseUrl(url) {
|
80
|
+
var parts = url.split('?');
|
81
|
+
return {
|
82
|
+
address: parts[0],
|
83
|
+
params: (parts[1] || '').split('&').filter(Boolean),
|
84
|
+
};
|
85
|
+
}
|
86
|
+
function buildUrl(parts) {
|
87
|
+
return [parts.address, parts.params.join('&')].join('?').replace(/\??$/, '');
|
88
|
+
}
|
89
|
+
function findByKey(key, keyvals, callback) {
|
90
|
+
key += '=';
|
91
|
+
for (var index = 0; keyvals.length > index; index++) {
|
92
|
+
if (keyvals[index].trim().slice(0, key.length) === key) {
|
93
|
+
var value = keyvals[index].trim().slice(key.length);
|
94
|
+
if (callback) {
|
95
|
+
callback(index, value);
|
96
|
+
}
|
97
|
+
else {
|
98
|
+
return value;
|
99
|
+
}
|
100
|
+
}
|
101
|
+
}
|
102
|
+
}
|
103
|
+
function isCrossOrigin(link) {
|
104
|
+
return (link.protocol !== window.location.protocol ||
|
105
|
+
link.hostname !== window.location.hostname ||
|
106
|
+
(link.port || '80') !== (window.location.port || '80'));
|
107
|
+
}
|
108
|
+
function getOriginFromLink(link) {
|
109
|
+
var origin = link.protocol.concat('//', link.hostname);
|
110
|
+
if (link.port && link.port !== '80' && link.port !== '443') {
|
111
|
+
origin = origin.concat(':', link.port);
|
112
|
+
}
|
113
|
+
return origin;
|
114
|
+
}
|
115
|
+
function isBrowser() {
|
116
|
+
return typeof module === 'undefined';
|
117
|
+
}
|
118
|
+
function setCookie({ name, value, lifetime }) {
|
119
|
+
// const encodedValue = encodeURIComponent(value)
|
120
|
+
const encodedValue = value;
|
121
|
+
document.cookie = name
|
122
|
+
.concat('=', encodedValue)
|
123
|
+
.concat('; path=/', '; max-age='.concat(lifetime.toString()), document.location.protocol === 'https:' ? '; Secure' : '');
|
124
|
+
}
|
125
|
+
function getCookie(name, defaultValue) {
|
126
|
+
name += '=';
|
127
|
+
const cookies = document.cookie.split(';');
|
128
|
+
let cookie = null;
|
129
|
+
for (let i = 0; i < cookies.length; i++) {
|
130
|
+
let currentCookie = cookies[i].trim();
|
131
|
+
if (currentCookie.indexOf(name) === 0) {
|
132
|
+
cookie = currentCookie;
|
133
|
+
break;
|
134
|
+
}
|
135
|
+
}
|
136
|
+
if (cookie) {
|
137
|
+
return decodeURIComponent(cookie.trim().slice(name.length));
|
138
|
+
}
|
139
|
+
return defaultValue || null;
|
140
|
+
}
|
141
|
+
function validateConsentObject(response) {
|
142
|
+
try {
|
143
|
+
if (typeof response !== 'object' || response === null) {
|
144
|
+
return false;
|
145
|
+
}
|
146
|
+
var expectedKeys = ['essential', 'settings', 'usage', 'campaigns'];
|
147
|
+
var allKeysPresent = true;
|
148
|
+
var responseKeysCount = 0;
|
149
|
+
for (var i = 0; i < expectedKeys.length; i++) {
|
150
|
+
if (!(expectedKeys[i] in response)) {
|
151
|
+
allKeysPresent = false;
|
152
|
+
break;
|
153
|
+
}
|
154
|
+
}
|
155
|
+
var allValuesBoolean = true;
|
156
|
+
for (var key in response) {
|
157
|
+
if (response.hasOwnProperty(key)) {
|
158
|
+
responseKeysCount++;
|
159
|
+
if (typeof response[key] !== 'boolean') {
|
160
|
+
allValuesBoolean = false;
|
161
|
+
break;
|
162
|
+
}
|
163
|
+
}
|
164
|
+
}
|
165
|
+
var correctNumberOfKeys = responseKeysCount === expectedKeys.length;
|
166
|
+
}
|
167
|
+
catch (err) {
|
168
|
+
return false;
|
169
|
+
}
|
170
|
+
return allKeysPresent && allValuesBoolean && correctNumberOfKeys;
|
171
|
+
}
|
172
|
+
|
173
|
+
const COOKIE_DAYS = 365;
|
174
|
+
class GovConsentConfig {
|
175
|
+
constructor(baseUrl) {
|
176
|
+
this.uidFromCookie = findByKey(GovConsentConfig.UID_KEY, document.cookie.split(';'));
|
177
|
+
this.uidFromUrl = findByKey(GovConsentConfig.UID_KEY, parseUrl(location.href).params);
|
178
|
+
this.baseUrl = baseUrl;
|
179
|
+
}
|
180
|
+
}
|
181
|
+
GovConsentConfig.UID_KEY = 'gov_singleconsent_uid';
|
182
|
+
GovConsentConfig.CONSENTS_COOKIE_NAME = 'cookies_policy';
|
183
|
+
GovConsentConfig.PREFERENCES_SET_COOKIE_NAME = 'cookies_preferences_set';
|
184
|
+
GovConsentConfig.COOKIE_LIFETIME = COOKIE_DAYS * 24 * 60 * 60;
|
185
|
+
|
186
|
+
class ApiV1 {
|
187
|
+
constructor(baseUrl) {
|
188
|
+
this.version = 'v1';
|
189
|
+
this.baseUrl = baseUrl;
|
190
|
+
}
|
191
|
+
buildUrl(endpoint, pathParam) {
|
192
|
+
let url = `${this.baseUrl}/api/${this.version}${endpoint}`;
|
193
|
+
if (pathParam) {
|
194
|
+
url += `/${pathParam}`;
|
195
|
+
}
|
196
|
+
return url;
|
197
|
+
}
|
198
|
+
origins() {
|
199
|
+
return this.buildUrl(ApiV1.Routes.origins);
|
200
|
+
}
|
201
|
+
consents(id) {
|
202
|
+
return this.buildUrl(ApiV1.Routes.consents, id || '');
|
203
|
+
}
|
204
|
+
}
|
205
|
+
ApiV1.Routes = {
|
206
|
+
origins: '/origins',
|
207
|
+
consents: '/consent',
|
208
|
+
};
|
209
|
+
|
210
|
+
class GovSingleConsent {
|
211
|
+
constructor(consentsUpdateCallback, baseUrlOrEnv) {
|
212
|
+
/**
|
213
|
+
Initialises _GovConsent object by performing the following:
|
214
|
+
1. Removes 'uid' from URL.
|
215
|
+
2. Sets 'uid' attribute from cookie or URL.
|
216
|
+
3. Fetches consent status from API if 'uid' exists.
|
217
|
+
4. Notifies event listeners with API response.
|
218
|
+
|
219
|
+
@arg baseUrl: string - the domain of where the single consent API is. Required.
|
220
|
+
@arg consentsUpdateCallback: function(consents, consentsPreferencesSet, error) - callback when the consents are updated
|
221
|
+
"consents": this is the consents object. It has the following properties: "essential", "usage", "campaigns", "settings". Each property is a boolean.
|
222
|
+
"consentsPreferencesSet": true if the consents have been set for this user and this domain. Typically, only display the cookie banner if this is true.
|
223
|
+
"error": if an error occurred, this is the error object. Otherwise, this is null.
|
224
|
+
*/
|
225
|
+
this.cachedConsentsCookie = null;
|
226
|
+
this.validateConstructorArguments(baseUrlOrEnv, consentsUpdateCallback);
|
227
|
+
const baseUrl = this.resolveBaseUrl(baseUrlOrEnv);
|
228
|
+
this._consentsUpdateCallback = consentsUpdateCallback;
|
229
|
+
this.config = new GovConsentConfig(baseUrl);
|
230
|
+
this.urls = new ApiV1(this.config.baseUrl);
|
231
|
+
window.cachedConsentsCookie = null;
|
232
|
+
this.hideUIDParameter();
|
233
|
+
this.initialiseUIDandConsents();
|
234
|
+
}
|
235
|
+
validateConstructorArguments(baseUrlOrEnv, consentsUpdateCallback) {
|
236
|
+
if (!baseUrlOrEnv) {
|
237
|
+
throw new Error('Argument baseUrl is required');
|
238
|
+
}
|
239
|
+
if (typeof baseUrlOrEnv !== 'string') {
|
240
|
+
throw new Error('Argument baseUrl must be a string');
|
241
|
+
}
|
242
|
+
if (!consentsUpdateCallback) {
|
243
|
+
throw new Error('Argument consentsUpdateCallback is required');
|
244
|
+
}
|
245
|
+
if (typeof consentsUpdateCallback !== 'function') {
|
246
|
+
throw new Error('Argument consentsUpdateCallback must be a function');
|
247
|
+
}
|
248
|
+
}
|
249
|
+
resolveBaseUrl(baseUrlOrEnv) {
|
250
|
+
if (baseUrlOrEnv === 'staging') {
|
251
|
+
return 'https://gds-single-consent-staging.app';
|
252
|
+
}
|
253
|
+
else if (baseUrlOrEnv === 'production') {
|
254
|
+
return 'https://gds-single-consent.app';
|
255
|
+
}
|
256
|
+
// If not "staging" or "production", assume it's a custom URL
|
257
|
+
return baseUrlOrEnv;
|
258
|
+
}
|
259
|
+
initialiseUIDandConsents() {
|
260
|
+
const currentUID = this.getCurrentUID();
|
261
|
+
if (this.isNewUID(currentUID)) {
|
262
|
+
this.handleNewUID(currentUID);
|
263
|
+
}
|
264
|
+
if (this.uid) {
|
265
|
+
this.fetchAndUpdateConsents();
|
266
|
+
}
|
267
|
+
else {
|
268
|
+
this._consentsUpdateCallback(null, false, null);
|
269
|
+
}
|
270
|
+
}
|
271
|
+
handleNewUID(newUID) {
|
272
|
+
this.uid = newUID;
|
273
|
+
this.updateLinksEventHandlers(newUID);
|
274
|
+
this.setUIDCookie(newUID);
|
275
|
+
}
|
276
|
+
isNewUID(currentUID) {
|
277
|
+
return currentUID && currentUID !== this.uid;
|
278
|
+
}
|
279
|
+
fetchAndUpdateConsents() {
|
280
|
+
const consentsUrl = this.urls.consents(this.uid);
|
281
|
+
request(consentsUrl, { timeout: 1000 }, (jsonResponse) => {
|
282
|
+
if (!validateConsentObject(jsonResponse.status)) {
|
283
|
+
const error = new Error('Invalid consents object returned from the API: ' + JSON.stringify(jsonResponse));
|
284
|
+
return this.defaultToRejectAllConsents(error);
|
285
|
+
}
|
286
|
+
const consents = jsonResponse.status;
|
287
|
+
this.updateBrowserConsents(consents);
|
288
|
+
this._consentsUpdateCallback(consents, GovSingleConsent.isConsentPreferencesSet(), null);
|
289
|
+
}, (error) => this.defaultToRejectAllConsents(error));
|
290
|
+
}
|
291
|
+
getCurrentUID() {
|
292
|
+
// Get the current uid from URL or from the cookie if it exists
|
293
|
+
return this.config.uidFromUrl || this.config.uidFromCookie;
|
294
|
+
}
|
295
|
+
setConsents(consents) {
|
296
|
+
if (!consents) {
|
297
|
+
throw new Error('consents is required in GovSingleConsent.setConsents()');
|
298
|
+
}
|
299
|
+
const successCallback = (response) => {
|
300
|
+
if (!response.uid) {
|
301
|
+
throw new Error('No UID returned from the API');
|
302
|
+
}
|
303
|
+
if (this.isNewUID(response.uid)) {
|
304
|
+
this.handleNewUID(response.uid);
|
305
|
+
}
|
306
|
+
this.updateBrowserConsents(consents);
|
307
|
+
this._consentsUpdateCallback(consents, GovSingleConsent.isConsentPreferencesSet(), null);
|
308
|
+
};
|
309
|
+
const url = this.urls.consents(this.uid);
|
310
|
+
request(url, {
|
311
|
+
method: 'POST',
|
312
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
313
|
+
body: 'status='.concat(JSON.stringify(consents)),
|
314
|
+
}, successCallback, (error) => this.defaultToRejectAllConsents(error));
|
315
|
+
}
|
316
|
+
defaultToRejectAllConsents(error) {
|
317
|
+
this.updateBrowserConsents(GovSingleConsent.REJECT_ALL);
|
318
|
+
this._consentsUpdateCallback(GovSingleConsent.REJECT_ALL, GovSingleConsent.isConsentPreferencesSet(), error);
|
319
|
+
}
|
320
|
+
static getConsents() {
|
321
|
+
if (window.cachedConsentsCookie) {
|
322
|
+
return window.cachedConsentsCookie;
|
323
|
+
}
|
324
|
+
const cookieValue = getCookie(GovConsentConfig.CONSENTS_COOKIE_NAME, null);
|
325
|
+
if (cookieValue) {
|
326
|
+
return JSON.parse(cookieValue);
|
327
|
+
}
|
328
|
+
return null;
|
329
|
+
}
|
330
|
+
static hasConsentedToEssential() {
|
331
|
+
const consents = GovSingleConsent.getConsents();
|
332
|
+
return consents === null || consents === void 0 ? void 0 : consents.essential;
|
333
|
+
}
|
334
|
+
static hasConsentedToUsage() {
|
335
|
+
const consents = GovSingleConsent.getConsents();
|
336
|
+
return consents === null || consents === void 0 ? void 0 : consents.usage;
|
337
|
+
}
|
338
|
+
static hasConsentedToCampaigns() {
|
339
|
+
const consents = GovSingleConsent.getConsents();
|
340
|
+
return consents === null || consents === void 0 ? void 0 : consents.campaigns;
|
341
|
+
}
|
342
|
+
static hasConsentedToSettings() {
|
343
|
+
const consents = GovSingleConsent.getConsents();
|
344
|
+
return consents === null || consents === void 0 ? void 0 : consents.settings;
|
345
|
+
}
|
346
|
+
static isConsentPreferencesSet() {
|
347
|
+
const value = getCookie(GovConsentConfig.PREFERENCES_SET_COOKIE_NAME, null);
|
348
|
+
return value === 'true';
|
349
|
+
}
|
350
|
+
updateLinksEventHandlers(currentUID) {
|
351
|
+
request(this.urls.origins(), {},
|
352
|
+
// Update links with UID
|
353
|
+
(origins) => this.addUIDtoCrossOriginLinks(origins, currentUID), (error) => {
|
354
|
+
throw error;
|
355
|
+
});
|
356
|
+
}
|
357
|
+
addUIDtoCrossOriginLinks(origins, uid) {
|
358
|
+
/**
|
359
|
+
* Adds uid URL parameter to consent sharing links.
|
360
|
+
* Only links with known origins are updated.
|
361
|
+
*/
|
362
|
+
const links = document.querySelectorAll('a[href]');
|
363
|
+
Array.prototype.forEach.call(links, (link) => {
|
364
|
+
if (isCrossOrigin(link) &&
|
365
|
+
origins.indexOf(getOriginFromLink(link)) >= 0) {
|
366
|
+
link.addEventListener('click', (event) => {
|
367
|
+
event.target.href = addUrlParameter(event.target.href, GovConsentConfig.UID_KEY, uid);
|
368
|
+
});
|
369
|
+
}
|
370
|
+
});
|
371
|
+
}
|
372
|
+
setUIDCookie(uid) {
|
373
|
+
setCookie({
|
374
|
+
name: GovConsentConfig.UID_KEY,
|
375
|
+
value: uid,
|
376
|
+
lifetime: GovConsentConfig.COOKIE_LIFETIME,
|
377
|
+
});
|
378
|
+
}
|
379
|
+
updateBrowserConsents(consents) {
|
380
|
+
this.setConsentsCookie(consents);
|
381
|
+
this.setPreferencesSetCookie(true);
|
382
|
+
window.cachedConsentsCookie = consents;
|
383
|
+
}
|
384
|
+
setConsentsCookie(consents) {
|
385
|
+
setCookie({
|
386
|
+
name: GovConsentConfig.CONSENTS_COOKIE_NAME,
|
387
|
+
value: JSON.stringify(consents),
|
388
|
+
lifetime: GovConsentConfig.COOKIE_LIFETIME,
|
389
|
+
});
|
390
|
+
}
|
391
|
+
setPreferencesSetCookie(value) {
|
392
|
+
setCookie({
|
393
|
+
name: GovConsentConfig.PREFERENCES_SET_COOKIE_NAME,
|
394
|
+
value: value.toString(),
|
395
|
+
lifetime: GovConsentConfig.COOKIE_LIFETIME,
|
396
|
+
});
|
397
|
+
}
|
398
|
+
hideUIDParameter() {
|
399
|
+
history.replaceState(null, null, removeUrlParameter(location.href, GovConsentConfig.UID_KEY));
|
400
|
+
}
|
401
|
+
}
|
402
|
+
GovSingleConsent.ACCEPT_ALL = {
|
403
|
+
essential: true,
|
404
|
+
usage: true,
|
405
|
+
campaigns: true,
|
406
|
+
settings: true,
|
407
|
+
};
|
408
|
+
GovSingleConsent.REJECT_ALL = {
|
409
|
+
essential: true,
|
410
|
+
usage: false,
|
411
|
+
campaigns: false,
|
412
|
+
settings: false,
|
413
|
+
};
|
414
|
+
|
415
|
+
if (isBrowser()) {
|
416
|
+
window.GovSingleConsent = GovSingleConsent;
|
417
|
+
}
|
418
|
+
|
419
|
+
exports.GovSingleConsent = GovSingleConsent;
|