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