PageSourceSearch

https://www.rousingthekop.com/wp-content/plugins/grv-identity/assets/js/grv-android-banner.js?ver=1789477035

js rousingthekop.com collected 2026-09-24 21:42:51 UTC 6,570 bytes, 185 lines download raw bytes

1/**
2 * Play Store install banner — Android detection and dismissal.
3 *
4 * The markup ships hidden for every mobile visitor so page caching can't pin an
5 * Android-only banner into a shared cache entry; this decides whether to reveal
6 * it on the actual device.
7 */
8(function () {
9  'use strict';
10
11  var banner = document.getElementById('grv-android-banner');
12  if (!banner) return;
13
14  var ua = navigator.userAgent || '';
15
16  // Android phones and tablets, but not Windows Phone, which used to put
17  // "Android" in its UA string for compatibility.
18  var isAndroid = /Android/i.test(ua) && !/Windows Phone/i.test(ua);
19  if (!isAndroid) return;
20
21  // Already installed and launched from the home screen — don't advertise it.
22  var standalone = window.matchMedia && window.matchMedia('(display-mode: standalone)').matches;
23  if (standalone || navigator.standalone) return;
24
25  var key = banner.getAttribute('data-dismiss-key') || 'grv_android_banner_dismissed_v1';
26  var days = parseInt(banner.getAttribute('data-dismiss-days'), 10);
27  if (!days || days < 0) days = 30;
28
29  // localStorage throws in some privacy modes, so every access is guarded and
30  // failure just means the banner shows.
31  function stored(name) {
32    try { return window.localStorage.getItem(name); } catch (e) { return null; }
33  }
34
35  var until = parseInt(stored(key), 10);
36  if (until && Date.now() < until) return;
37
38  var root = document.documentElement;
39
40  /**
41   * Publish the measured height so the stylesheet can make room for the banner.
42   * Everything that moves reads --grv-android-banner-h, so setting it here is
43   * enough — we never touch padding or top values directly from JS.
44   *
45   * Measuring beats the 56px CSS fallback because the title and subtext wrap
46   * differently across devices and font sizes.
47   */
48  function measure() {
49    var h = banner.offsetHeight;
50    if (h) {
51      root.style.setProperty('--grv-android-banner-h', h + 'px');
52    }
53  }
54
55  /**
56   * True while a modal owns the screen. The stylesheet hides the banner in this
57   * state; this mirrors the same condition so measurement can be skipped.
58   */
59  function modalOpen() {
60    if (root.classList.contains('grv-modal-open')) return true;
61
62    // The stylesheet also hides the banner for overlays that never set that
63    // class, matched structurally with :has(). Mirror the same selectors so the
64    // reserved space collapses for them too — otherwise the inline height set
65    // by measure() would keep a blank strip above a full-screen overlay.
66    try {
67      return !!document.querySelector(
68        '.grv-modal-overlay[style*="display: flex"],' +
69        '.grv-tp-overlay:not([hidden]),' +
70        '.search-form.open,' +
71        '.grv-hides-app-banner'
72      );
73    } catch (e) {
74      return false;
75    }
76  }
77
78  /**
79   * The stylesheet hides the banner while a modal is open and zeroes
80   * --grv-android-banner-h with it. An inline height set here would win over
81   * that (inline style beats a stylesheet rule), so the reserved space would
82   * survive the banner being hidden and push the modal down by an empty strip.
83   *
84   * Dropping the inline value while a modal is open hands control back to the
85   * stylesheet; reapplying on close restores the measured height.
86   */
87  /* A subtree observer fires for unrelated attribute churn too (lazy images,
88     ad slots, the theme's own scripts), and each call runs a querySelector.
89     Coalescing to one check per frame keeps that off the critical path — the
90     banner only needs to settle by the next paint, not on every mutation. */
91  var syncQueued = false;
92
93  function queueSync() {
94    if (syncQueued) return;
95    syncQueued = true;
96
97    var run = window.requestAnimationFrame || function (fn) { return setTimeout(fn, 16); };
98    run(function () {
99      syncQueued = false;
100      syncForModal();
101    });
102  }
103
104  function syncForModal() {
105    if (banner.hidden) return;
106
107    if (modalOpen()) {
108      root.style.removeProperty('--grv-android-banner-h');
109    } else {
110      measure();
111    }
112  }
113
114  /**
115   * Watch the class modal/core.js toggles on <html>. A class change carries no
116   * event of its own, so this observes the attribute directly rather than
117   * relying on every modal to announce itself.
118   */
119  function watchModal() {
120    if (typeof MutationObserver !== 'function') return;
121
122    // Watch the whole document rather than just <html>: the class lands on
123    // <html> for the grv-identity modals, but on the overlay element itself for
124    // the theme's search panel, and on <body> for twentytwenty. Subtree covers
125    // all three without needing to know which fires.
126    new MutationObserver(queueSync).observe(document.documentElement, {
127      attributes: true,
128      attributeFilter: ['class', 'style', 'hidden'],
129      subtree: true
130    });
131  }
132
133  function show() {
134    banner.hidden = false;
135    root.classList.add('grv-android-banner-open');
136    document.body.classList.add('grv-android-banner-open');
137    measure();
138  }
139
140  function hide() {
141    banner.hidden = true;
142    root.classList.remove('grv-android-banner-open');
143    document.body.classList.remove('grv-android-banner-open');
144    // Drop the inline override so the property falls back to the 0px in the
145    // stylesheet and every offset collapses with it.
146    root.style.removeProperty('--grv-android-banner-h');
147  }
148
149  /**
150   * The banner can change height after the first measurement — rotating the
151   * device changes how the text wraps, and a late webfont shifts the line box.
152   * Re-measure so the reserved space keeps matching the banner.
153   */
154  function watchHeight() {
155    if (typeof ResizeObserver === 'function') {
156      new ResizeObserver(function () {
157        if (!banner.hidden && !modalOpen()) measure();
158      }).observe(banner);
159      return;
160    }
161
162    // Older Chrome on Android — orientationchange is the case that matters.
163    window.addEventListener('resize', function () {
164      if (!banner.hidden && !modalOpen()) measure();
165    });
166  }
167
168  banner.querySelector('.grv-android-banner__close').addEventListener('click', function () {
169    hide();
170    try {
171      window.localStorage.setItem(key, String(Date.now() + days * 86400000));
172    } catch (e) { /* private mode — banner returns next visit */ }
173  });
174
175  // Tapping through to the Play Store counts as dealt with.
176  banner.querySelector('.grv-android-banner__cta').addEventListener('click', function () {
177    try {
178      window.localStorage.setItem(key, String(Date.now() + days * 86400000));
179    } catch (e) {}
180  });
181
182  show();
183  watchHeight();
184  watchModal();
185})();

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.