* Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ const exposed = {}; if (location.search) { var a = document.createElement("a"); a.href = location.href; a.search = ""; history.replaceState(null, null, a.href); } function tweet_(url) { open( "https://twitter.com/intent/tweet?url=" + encodeURIComponent(url), "_blank" ); } function tweet(anchor) { tweet_(anchor.getAttribute("href")); } expose("tweet", tweet); function share(anchor) { var url = anchor.getAttribute("href"); event.preventDefault(); if (navigator.share) { navigator.share({ url: url, }); } else if (navigator.clipboard) { navigator.clipboard.writeText(url); message("Article URL copied to clipboard."); } else { tweet_(url); } } expose("share", share); function message(msg) { var dialog = document.getElementById("message"); dialog.textContent = msg; dialog.setAttribute("open", ""); setTimeout(function () { dialog.removeAttribute("open"); }, 3000); } function prefetch(e) { if (e.target.tagName != "A") { return; } if (e.target.origin != location.origin) { return; } var l = document.createElement("link"); l.rel = "prefetch"; l.href = e.target.href; document.head.appendChild(l); } document.documentElement.addEventListener("mouseover", prefetch, { capture: true, passive: true, }); document.documentElement.addEventListener("touchstart", prefetch, { capture: true, passive: true, }); const GA_ID = document.documentElement.getAttribute("ga-id"); window.ga = window.ga || function () { if (!GA_ID) { return; } (ga.q = ga.q || []).push(arguments); }; ga.l = +new Date(); ga("create", GA_ID, "auto"); ga("set", "transport", "beacon"); var timeout = setTimeout( (onload = function () { clearTimeout(timeout); ga("send", "pageview"); }), 1000 ); var ref = +new Date(); function ping(event) { var now = +new Date(); if (now - ref < 1000) { return; } ga("send", { hitType: "event", eventCategory: "page", eventAction: event.type, eventLabel: Math.round((now - ref) / 1000), }); ref = now; } addEventListener("pagehide", ping); addEventListener("visibilitychange", ping); addEventListener( "click", function (e) { var button = e.target.closest("button"); if (!button) { return; } ga("send", { hitType: "event", eventCategory: "button", eventAction: button.getAttribute("aria-label") || button.textContent, }); }, true ); var selectionTimeout; addEventListener( "selectionchange", function () { clearTimeout(selectionTimeout); var text = String(document.getSelection()).trim(); if (text.split(/[\s\n\r]+/).length < 3) { return; } selectionTimeout = setTimeout(function () { ga("send", { hitType: "event", eventCategory: "selection", eventAction: text, }); }, 2000); }, true ); if (window.ResizeObserver && document.querySelector("header nav #nav")) { var progress = document.getElementById("reading-progress"); var timeOfLastScroll = 0; var requestedAniFrame = false; function scroll() { if (!requestedAniFrame) { requestAnimationFrame(updateProgress); requestedAniFrame = true; } timeOfLastScroll = Date.now(); } addEventListener("scroll", scroll); var winHeight = 1000; var bottom = 10000; function updateProgress() { requestedAniFrame = false; var percent = Math.min( (document.scrollingElement.scrollTop / (bottom - winHeight)) * 100, 100 ); progress.style.transform = `translate(-${100 - percent}vw, 0)`; if (Date.now() - timeOfLastScroll < 3000) { requestAnimationFrame(updateProgress); requestedAniFrame = true; } } new ResizeObserver(() => { bottom = document.scrollingElement.scrollTop + document.querySelector("#comments,footer").getBoundingClientRect().top; winHeight = window.innerHeight; scroll(); }).observe(document.body); } function expose(name, fn) { exposed[name] = fn; } addEventListener("click", (e) => { const handler = e.target.closest("[on-click]"); if (!handler) { return; } e.preventDefault(); const name = handler.getAttribute("on-click"); const fn = exposed[name]; if (!fn) { throw new Error("Unknown handler" + name); } fn(handler); }); // There is a race condition here if an image loads faster than this JS file. But // - that is unlikely // - it only means potentially more costly layouts for that image. // - And so it isn't worth the querySelectorAll it would cost to synchronously check // load state. document.body.addEventListener( "load", (e) => { if (e.target.tagName != "IMG") { return; } // Ensure the browser doesn't try to draw the placeholder when the real image is present. e.target.style.backgroundImage = "none"; }, /* capture */ "true" ); https://kai.im/preloader Danke an Daniel Abromeit @der_abro