PageSourceSearch

https://phillipsandco.com/wp-content/plugins/lmg-consent/assets/js/banner.js?ver=1.3.0

js phillipsandco.com collected 2026-10-08 07:24:24 UTC 11,568 bytes, 328 lines download raw bytes

1/*
2 * LMG Consent — client logic.
3 *
4 * Responsibilities:
5 *   1. Read/write consent state (cookie + localStorage, versioned).
6 *   2. Honor Global Privacy Control (GPC) as a browser-level opt-out (CPRA).
7 *   3. Show the banner on first visit; hide once a decision has been made.
8 *   4. Drive the customize modal (render categories, apply toggle state).
9 *   5. Push Google Consent Mode v2 updates (gtag('consent','update',...)).
10 *   6. Log each decision to the REST endpoint (if logging enabled).
11 *   7. Dispatch `m_consent:change` (+ `lmg_consent:change`) for integrators.
12 */
13(function () {
14  'use strict';
15
16  if (typeof window.mConsent !== 'object') return;
17  var CFG = window.mConsent;
18
19  var GCM_MAP = {
20    functional: ['functionality_storage', 'personalization_storage'],
21    analytics:  ['analytics_storage'],
22    marketing:  ['ad_storage', 'ad_user_data', 'ad_personalization']
23  };
24
25  /* ---------- Global Privacy Control ---------- */
26  // CPRA treats a GPC signal as a valid opt-out of sale/sharing. We map it to
27  // a reject of all non-essential categories.
28  function gpcEnabled() {
29    try {
30      if (navigator && navigator.globalPrivacyControl === true) return true;
31      if (window.globalPrivacyControl === true) return true;
32    } catch (e) {}
33    return false;
34  }
35
36  /* ---------- storage helpers ---------- */
37  function readCookie(name) {
38    var match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.$?*|{}()[\]\\\/+^]/g, '\\$&') + '=([^;]*)'));
39    return match ? decodeURIComponent(match[1]) : null;
40  }
41  function writeCookie(name, value, maxAgeSeconds) {
42    var parts = [
43      name + '=' + encodeURIComponent(value),
44      'path=/',
45      'max-age=' + Math.floor(maxAgeSeconds),
46      'SameSite=Lax'
47    ];
48    if (location.protocol === 'https:') parts.push('Secure');
49    document.cookie = parts.join('; ');
50  }
51
52  function readState() {
53    var raw = readCookie(CFG.cookie) || localStorage.getItem(CFG.cookie);
54    if (!raw) return null;
55    try {
56      var s = JSON.parse(raw);
57      if (!s || s.schema !== CFG.schema) return null;
58      return s;
59    } catch (e) { return null; }
60  }
61  function writeState(state) {
62    var json = JSON.stringify(state);
63    writeCookie(CFG.cookie, json, CFG.cookieTTL);
64    try { localStorage.setItem(CFG.cookie, json); } catch (e) {}
65  }
66
67  /* ---------- id generation ---------- */
68  function newConsentId() {
69    if (window.crypto && crypto.randomUUID) return crypto.randomUUID();
70    return 'mc-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
71  }
72
73  /* ---------- gtag bridge ---------- */
74  function pushConsent(categories) {
75    if (typeof window.gtag !== 'function') return;
76    var update = {};
77    Object.keys(GCM_MAP).forEach(function (cat) {
78      var val = categories[cat] ? 'granted' : 'denied';
79      GCM_MAP[cat].forEach(function (key) { update[key] = val; });
80    });
81    window.gtag('consent', 'update', update);
82  }
83
84  /* ---------- reactivate CIPA-blocked loaders ---------- */
85  // Server-side, LMG_Consent_Blocker rewrote Google loader <script> tags to
86  // type="text/plain" so they neither fetched nor ran. Once the matching
87  // category is granted, clone each into a live <script> to execute it.
88  // Changing an existing script's type does NOT run it — a fresh node must be
89  // inserted (HTML spec: "already started" scripts never re-execute).
90  function activateBlockedScripts(categories) {
91    if (!categories) return;
92    var blocked = document.querySelectorAll('script[type="text/plain"][data-lmg-consent]');
93    for (var i = 0; i < blocked.length; i++) {
94      var old = blocked[i];
95      var cat = old.getAttribute('data-lmg-consent');
96      if (!categories[cat]) continue;                       // not granted
97      if (old.getAttribute('data-lmg-activated') === '1') continue; // idempotent
98      old.setAttribute('data-lmg-activated', '1');
99
100      var s = document.createElement('script');
101      for (var a = 0; a < old.attributes.length; a++) {
102        var attr = old.attributes[a];
103        if (attr.name === 'type' ||
104            attr.name === 'data-lmg-consent' ||
105            attr.name === 'data-lmg-activated') continue;
106        s.setAttribute(attr.name, attr.value);
107      }
108      s.type = 'text/javascript';
109      if (old.textContent) s.text = old.textContent;
110      (old.parentNode || document.head || document.documentElement).insertBefore(s, old);
111    }
112  }
113
114  /* ---------- REST logging ---------- */
115  function log(action, categories, consentId) {
116    if (!CFG.logEnabled) return;
117    try {
118      fetch(CFG.restUrl, {
119        method: 'POST',
120        credentials: 'same-origin',
121        headers: {
122          'Content-Type': 'application/json',
123          'X-WP-Nonce': CFG.restNonce
124        },
125        body: JSON.stringify({
126          action: action,
127          categories: categories,
128          consent_id: consentId,
129          schema: CFG.schema
130        }),
131        keepalive: true
132      }).catch(function(){});
133    } catch (e) {}
134  }
135
136  /* ---------- DOM ---------- */
137  var root;
138
139  function show(el)   { if (el) el.removeAttribute('hidden'); }
140  function hide(el)   { if (el) el.setAttribute('hidden', ''); }
141
142  /* ---------- floating preferences button ---------- */
143  // Rendered hidden by PHP; reveal it whenever the first-visit banner is NOT on
144  // screen so the two never overlap. It always reopens the modal via the
145  // existing [data-m-consent-open] handler.
146  function floatBtn() { return document.querySelector('[data-m-consent-float]'); }
147  function showFloat() { show(floatBtn()); }
148  function hideFloat() { hide(floatBtn()); }
149
150  function buildCategoryRow(cat) {
151    var row = document.createElement('div');
152    row.className = 'm-consent__cat';
153
154    var main = document.createElement('div');
155    main.className = 'm-consent__cat-main';
156
157    var h = document.createElement('h3');
158    h.className = 'm-consent__cat-title';
159    h.textContent = cat.title;
160
161    var p = document.createElement('p');
162    p.className = 'm-consent__cat-desc';
163    p.textContent = cat.desc;
164
165    main.appendChild(h);
166    main.appendChild(p);
167
168    var label = document.createElement('label');
169    label.className = 'm-consent__toggle';
170
171    var input = document.createElement('input');
172    input.type = 'checkbox';
173    input.setAttribute('data-m-consent-cat', cat.key);
174    if (cat.required) {
175      input.checked = true;
176      input.disabled = true;
177    }
178
179    var slider = document.createElement('span');
180    slider.className = 'm-consent__toggle-slider';
181    slider.setAttribute('aria-hidden', 'true');
182
183    label.appendChild(input);
184    label.appendChild(slider);
185
186    row.appendChild(main);
187    row.appendChild(label);
188    return row;
189  }
190
191  function render() {
192    root = document.getElementById('m-consent');
193    if (!root) return;
194
195    // Re-parent to <body> so the fixed banner/modal escape any transformed or
196    // clipped ancestor. CSS `position: fixed` resolves against the nearest
197    // ancestor with a transform/filter/perspective (not the viewport), so a
198    // smooth-scroll wrapper like GSAP ScrollSmoother's `.smooth-content`
199    // (which gets a translate3d transform) would otherwise pin the banner
200    // inside the scrolling content instead of to the screen.
201    if (document.body && root.parentNode !== document.body) {
202      document.body.appendChild(root);
203    }
204
205    var body = root.querySelector('[data-m-consent-categories]');
206    if (body) {
207      while (body.firstChild) body.removeChild(body.firstChild);
208      CFG.categories.forEach(function (cat) {
209        if (!cat.enabled) return;
210        body.appendChild(buildCategoryRow(cat));
211      });
212    }
213
214    wireEvents();
215
216    var state = readState();
217    if (!state) {
218      // No prior decision. If the browser sends a GPC opt-out, honor it
219      // automatically and don't force interaction with the banner.
220      if (gpcEnabled()) {
221        save('gpc', rejectAll());
222        return;
223      }
224      show(root);
225      hideFloat(); // banner is on screen — don't overlap it
226      applyTogglesFromCategories(defaultCategories());
227    } else {
228      // Already decided — keep hidden but apply stored choices to gtag and
229      // reactivate any server-blocked loaders the visitor previously allowed.
230      pushConsent(state.categories);
231      activateBlockedScripts(state.categories);
232      showFloat(); // persistent withdrawal control
233    }
234  }
235
236  function defaultCategories() {
237    var c = {};
238    CFG.categories.forEach(function (cat) { c[cat.key] = cat.required === true; });
239    return c;
240  }
241  function fullAccept() {
242    var c = {};
243    CFG.categories.forEach(function (cat) { c[cat.key] = true; });
244    return c;
245  }
246  function rejectAll() {
247    return defaultCategories();
248  }
249  function readToggles() {
250    var c = defaultCategories();
251    root.querySelectorAll('[data-m-consent-cat]').forEach(function (input) {
252      c[input.getAttribute('data-m-consent-cat')] = !!input.checked;
253    });
254    return c;
255  }
256  function applyTogglesFromCategories(cats) {
257    root.querySelectorAll('[data-m-consent-cat]').forEach(function (input) {
258      var key = input.getAttribute('data-m-consent-cat');
259      if (!input.disabled) input.checked = !!cats[key];
260    });
261  }
262
263  function save(action, categories) {
264    var prev = readState();
265    var consentId = (prev && prev.consent_id) || newConsentId();
266    var state = {
267      schema: CFG.schema,
268      consent_id: consentId,
269      ts: new Date().toISOString(),
270      action: action,
271      categories: categories
272    };
273    writeState(state);
274    pushConsent(categories);
275    activateBlockedScripts(categories);
276    log(action, categories, consentId);
277    document.dispatchEvent(new CustomEvent('m_consent:change', { detail: state }));
278    document.dispatchEvent(new CustomEvent('lmg_consent:change', { detail: state }));
279    closeBanner();
280    closeModal();
281    showFloat(); // a decision exists now — expose the persistent control
282  }
283
284  function openModal() {
285    var modal = root.querySelector('[data-m-consent-modal]');
286    var state = readState();
287    applyTogglesFromCategories(state ? state.categories : defaultCategories());
288    show(root);
289    show(modal);
290    var first = modal.querySelector('input:not([disabled]), button');
291    if (first) first.focus();
292  }
293  function closeModal() {
294    var modal = root.querySelector('[data-m-consent-modal]');
295    hide(modal);
296    if (readState()) hide(root);
297  }
298  function closeBanner() {
299    hide(root);
300  }
301
302  function wireEvents() {
303    root.addEventListener('click', function (e) {
304      var t = e.target.closest('[data-m-consent-accept], [data-m-consent-reject], [data-m-consent-customize], [data-m-consent-save], [data-m-consent-close]');
305      if (!t) return;
306      if (t.hasAttribute('data-m-consent-accept'))    return save('accept_all', fullAccept());
307      if (t.hasAttribute('data-m-consent-reject'))    return save('reject_all', rejectAll());
308      if (t.hasAttribute('data-m-consent-customize')) return openModal();
309      if (t.hasAttribute('data-m-consent-save'))      return save('custom', readToggles());
310      if (t.hasAttribute('data-m-consent-close'))     return closeModal();
311    });
312
313    document.addEventListener('click', function (e) {
314      var t = e.target.closest('[data-m-consent-open]');
315      if (t) { e.preventDefault(); openModal(); }
316    });
317
318    document.addEventListener('keydown', function (e) {
319      if (e.key === 'Escape') closeModal();
320    });
321  }
322
323  if (document.readyState === 'loading') {
324    document.addEventListener('DOMContentLoaded', render);
325  } else {
326    render();
327  }
328})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.