1/* 2 * 3 * Only the target container is ever touched, so anything outside it keeps its 4 * DOM node. That is what stops the audio player from restarting on Safari: 5 * the <audio> element is never re-parented and its script never runs twice. 6 */ 7(function (window, document) { 8 "use strict"; 9 10 var SKIP_CLASSES = ["ext-link", "no-ajaxy", "no-ajax"]; 11 var SKIP_PROTOCOLS = ["mailto:", "tel:", "javascript:", "sms:", "whatsapp:"]; 12 13 var SKELETON = 14 '<div class="router-skeleton wrap">' + 15 '<div class="router-skeleton__bar skeleton"></div>' + 16 '<div class="router-skeleton__grid">' + 17 '<div class="router-skeleton__block skeleton"></div>' + 18 '<div class="router-skeleton__block skeleton"></div>' + 19 '<div class="router-skeleton__block skeleton"></div>' + 20 '</div></div>'; 21 22 function ReqBras(options) { 23 // Single instance: this file self-starts early, and utils.js constructs it 24 // again later. Two instances would mean two click handlers. 25 if (ReqBras.instance) return ReqBras.instance; 26 27 var settings = options || {}; 28 29 this.container = settings.elements || "#pContent"; 30 this.scrolltop = settings.scrolltop !== false; 31 this.timeout = settings.timeout || 15000; 32 this.skeletonDelay = settings.skeletonDelay || 180; 33 // A theme opts out with data-skeleton="off" on <html> 34 this.skeleton = document.documentElement.dataset.skeleton !== "off"; 35 this.prefetch = settings.prefetchoff !== true; 36 37 this.loadedScripts = new Set(); 38 this.prefetched = new Set(); 39 this.pageTimers = []; 40 this.cache = new Map(); 41 this.scrollPositions = {}; 42 this.controller = null; 43 this.skeletonTimer = null; 44 this.booted = false; 45 46 this.boot(); 47 } 48 49 // Bootstrap sets scroll-behavior: smooth on the root, which turns a plain 50 // scrollTo into a half second animation. Navigation wants a cut, not a 51 // trip: "instant" wins over the sheet. 52 function jump(y) { 53 try { 54 window.scrollTo({ top: y, left: 0, behavior: "instant" }); 55 } catch (error) { 56 window.scrollTo(0, y); 57 } 58 } 59 60 // Events keep the old names so existing listeners keep working 61 ReqBras.prototype.emit = function (name, detail) { 62 [window, document].forEach(function (node) { 63 var event = document.createEvent("HTMLEvents"); 64 event.initEvent("pronto." + name, false, false); 65 event.data = detail || {}; 66 node.dispatchEvent(event); 67 }); 68 }; 69 70 ReqBras.prototype.target = function () { 71 return document.querySelector(this.container); 72 }; 73 74 ReqBras.prototype.boot = function () { 75 if (this.booted) return; 76 this.booted = true; 77 78 var self = this; 79 80 ReqBras.instance = this; 81 82 // Everything already on the page counts as loaded, so it never runs twice. 83 // Runs again on DOMContentLoaded because this file starts before the rest 84 // of the head has been parsed. 85 this.indexScripts(); 86 document.addEventListener("DOMContentLoaded", function () { self.indexScripts(); }); 87 88 if ("scrollRestoration" in window.history) { 89 window.history.scrollRestoration = "manual"; 90 } 91 92 document.addEventListener("click", function (e) { self.onClick(e); }, false); 93 window.addEventListener("popstate", function (e) { self.onPopState(e); }, false); 94 95 if (this.prefetch) { 96 document.addEventListener("mouseover", function (e) { self.onHover(e); }, { passive: true }); 97 document.addEventListener("touchstart", function (e) { self.onHover(e); }, { passive: true }); 98 } 99 100 if (window.history.replaceState) { 101 window.history.replaceState({ reqbras: true, key: this.key() }, document.title, window.location.href); 102 } 103 104 this.emit("load"); 105 }; 106 107 ReqBras.prototype.key = function () { 108 return window.location.pathname + window.location.search; 109 }; 110 111 ReqBras.prototype.indexScripts = function () { 112 var self = this; 113 document.querySelectorAll("script[src]").forEach(function (script) { 114 self.loadedScripts.add(script.src); 115 }); 116 }; 117 118 // Security: only same-origin http(s) URLs are ever fetched or injected 119 ReqBras.prototype.isInternal = function (url) { 120 try { 121 var parsed = new URL(url, window.location.href); 122 if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
123 return parsed.origin === window.location.origin; 124 } catch (err) { 125 return false; 126 } 127 }; 128 129 ReqBras.prototype.linkFrom = function (node) { 130 return node && node.closest ? node.closest("a[href]") : null; 131 }; 132 133 // Returns the URL this link should navigate to, or null when it must be left alone 134 ReqBras.prototype.routable = function (link) { 135 if (!link) return null; 136 137 var href = link.getAttribute("href"); 138 if (!href || href.charAt(0) === "#") return null; 139 140 var lower = href.toLowerCase(); 141 if (SKIP_PROTOCOLS.some(function (p) { return lower.indexOf(p) === 0; })) return null; 142 if (link.hasAttribute("download") || link.hasAttribute("data-no-ajax")) return null; 143 if (SKIP_CLASSES.some(function (c) { return link.classList.contains(c); })) return null; 144 if (link.target && link.target !== "_self") return null; 145 if (!this.isInternal(href)) return null; 146 147 return new URL(href, window.location.href).href; 148 }; 149 150 ReqBras.prototype.onHover = function (e) { 151 var link = this.linkFrom(e.target); 152 var url = this.routable(link); 153 154 if (!url || url === window.location.href || this.cache.has(url)) return; 155 156 // One try per address: a link that fails must not be fetched on every hover 157 if (this.prefetched.has(url)) return; 158 this.prefetched.add(url); 159 160 var self = this; 161 162 fetch(url, { credentials: "same-origin", headers: { "X-Requested-With": "XMLHttpRequest" } }) 163 .then(function (response) { 164 if (!response.ok) return; 165 var type = response.headers.get("content-type") || ""; 166 if (type.indexOf("text/html") === -1) return; 167 return response.text(); 168 }) 169 .then(function (html) { 170 if (!html) return; 171 172 // Small cache: enough to make the click feel instant, not to hold the site 173 if (self.cache.size > 8) self.cache.clear(); 174 self.cache.set(url, html); 175 }) 176 .catch(function () {}); 177 }; 178 179 ReqBras.prototype.onClick = function (e) { 180 if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; 181 182 var link = this.linkFrom(e.target); 183 if (!link) return; 184 185 var href = link.getAttribute("href"); 186 if (!href) return; 187 188 // Opt-out classes and other hosts open in a new tab 189 var flagged = SKIP_CLASSES.some(function (c) { return link.classList.contains(c); }); 190 var external = !this.isInternal(href); 191 var special = SKIP_PROTOCOLS.some(function (p) { return href.toLowerCase().indexOf(p) === 0; }); 192 193 if ((flagged || external) && !special && href.charAt(0) !== "#") { 194 if (!link.target && (external || link.classList.contains("ext-link"))) { 195 link.target = "_blank"; 196 link.rel = "noopener noreferrer"; 197 } 198 return; 199 } 200 201 var url = this.routable(link); 202 if (!url || !this.target()) return; 203 204 e.preventDefault(); 205 206 if (url === window.location.href) return; 207 208 this.scrollPositions[this.key()] = window.pageYOffset; 209 210 if (this.scrolltop) jump(0); 211 212 this.go(url, true); 213 }; 214 215 ReqBras.prototype.onPopState = function (e) { 216 if (!e.state || !e.state.reqbras) return; 217 this.go(window.location.href, false, e.state.key); 218 }; 219 220 ReqBras.prototype.go = function (url, push, restoreKey) { 221 var self = this; 222 223 if (this.controller) this.controller.abort(); 224 225 this.emit("request", { url: url }); 226 227 var cached = this.cache.get(url); 228 if (cached) { 229 this.cache.delete(url); 230 this.render(cached, url, push, restoreKey); 231 return; 232 } 233 234 this.showSkeleton(); 235 236 var controller = new AbortController(); 237 var timer = window.setTimeout(function () { controller.abort(); }, this.timeout); 238 this.controller = controller; 239 240 fetch(url, { 241 credentials: "same-origin", 242 redirect: "follow", 243 headers: { "X-Requested-With": "XMLHttpRequest" }, 244 signal: controller.signal 245 }) 246 .then(function (response) { 247 if (!response.ok) throw new Error("HTTP " + response.status); 248 249 var type = response.headers.get("content-type") || ""; 250 if (type.indexOf("text/html") === -1) throw new Error("Not HTML"); 251 252 // A redirect to another origin must never be injected into the page 253 if (!self.isInternal(response.url)) throw new Error("Cross-origin response"); 254 255 return response.text(); 256 }) 257 .then(function (html) { 258 window.clearTimeout(timer); 259 self.controller = null; 260 self.render(html, url, push, restoreKey); 261 }) 262 .catch(function (err) { 263 window.clearTimeout(timer); 264 self.controller = null; 265 self.hideSkeleton(); 266 267 if (err && err.name === "AbortError") return; 268 269 self.emit("error", { url: url, error: err }); 270 window.location.href = url; 271 }); 272 }; 273 274 // Only shows up when the response is actually slow, so fast clicks do not flash 275 ReqBras.prototype.showSkeleton = function () { 276 var self = this; 277 278 this.hideSkeleton(); 279 280 if (!this.skeleton) return; 281 282 this.skeletonTimer = window.setTimeout(function () { 283 var target = self.target(); 284 if (target) target.innerHTML = SKELETON; 285 }, this.skeletonDelay); 286 }; 287 288 ReqBras.prototype.hideSkeleton = function () { 289 if (!this.skeletonTimer) return; 290 window.clearTimeout(this.skeletonTimer); 291 this.skeletonTimer = null; 292 }; 293 294 ReqBras.prototype.render = function (html, url, push, restoreKey) { 295 var target = this.target(); 296 if (!target) { window.location.href = url; return; } 297 298 // DOMParser never executes anything while parsing 299 var doc = new DOMParser().parseFromString(html, "text/html"); 300 var fresh = doc.querySelector(this.container); 301 302 if (!fresh) { window.location.href = url; return; } 303 304 this.hideSkeleton(); 305 this.clearTimers(); 306 307 var title = doc.querySelector("title"); 308 if (title) document.title = title.textContent; 309 310 // A page with unbalanced markup makes the parser nest the footer, header or 311 // player inside the container. Dropping them here keeps those shells alive. 312 fresh.querySelectorAll("footer.foot, #pHeader, #pWide, #cookie-banner-lgpd, .bras-player, .bras-player-spacer").forEach(function (stray) { 313 stray.remove(); 314 }); 315 316 // Page CSS hangs on the class of the container, so it travels with the HTML 317 target.className = fresh.className; 318 target.innerHTML = fresh.innerHTML; 319 this.runScripts(target); 320 321 if (push && window.history.pushState) { 322 window.history.pushState({ reqbras: true, key: this.key() }, document.title, url); 323 } 324 325 var restore = restoreKey ? this.scrollPositions[restoreKey] : null; 326 327 if (restore != null) { 328 jump(restore); 329 } else if (this.scrolltop) { 330 jump(0); 331 } 332
333 this.emit("render", { url: url }); 334 }; 335 336 // Intervals started by page scripts are dropped on the next navigation 337 ReqBras.prototype.clearTimers = function () { 338 this.pageTimers.forEach(function (id) { window.clearInterval(id); }); 339 this.pageTimers = []; 340 }; 341 342 ReqBras.prototype.runScripts = function (container) { 343 var self = this; 344 var scripts = Array.prototype.slice.call(container.querySelectorAll("script")); 345 346 var nativeInterval = window.setInterval; 347 var nativeTimeout = window.setTimeout; 348 349 window.setInterval = function () { 350 var id = nativeInterval.apply(window, arguments); 351 self.pageTimers.push(id); 352 return id; 353 }; 354 355 window.setTimeout = function () { 356 var id = nativeTimeout.apply(window, arguments); 357 self.pageTimers.push(id); 358 return id; 359 }; 360 361 try { 362 scripts.forEach(function (script) { self.runScript(script); }); 363 } finally { 364 window.setInterval = nativeInterval; 365 window.setTimeout = nativeTimeout; 366 } 367 }; 368 369 ReqBras.prototype.runScript = function (script) { 370 var src = script.getAttribute("src"); 371 372 if (src) { 373 var absolute = new URL(src, window.location.href).href; 374 375 // Never load the same file twice: that is what duplicated the player handlers 376 if (this.loadedScripts.has(absolute)) { script.remove(); return; } 377 this.loadedScripts.add(absolute); 378 } 379 380 var fresh = document.createElement("script"); 381 382 Array.prototype.forEach.call(script.attributes, function (attr) { 383 fresh.setAttribute(attr.name, attr.value); 384 }); 385 386 if (!src) fresh.textContent = script.textContent; 387 388 script.parentNode.replaceChild(fresh, script); 389 }; 390 391 window.ReqBras = ReqBras; 392 393 // Starts as soon as this file runs, before the heavy libraries in the head. 394 // A slow page still gets working navigation from the first paint. 395 new ReqBras({ elements: "#pContent", scrolltop: true }); 396})(window, document);
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.