diff options
Diffstat (limited to 'static/js/instantpage.ts')
-rw-r--r-- | static/js/instantpage.ts | 292 |
1 files changed, 292 insertions, 0 deletions
diff --git a/static/js/instantpage.ts b/static/js/instantpage.ts new file mode 100644 index 0000000..7c78a6f --- /dev/null +++ b/static/js/instantpage.ts @@ -0,0 +1,292 @@ +(function () { + self.addEventListener("DOMContentLoaded", function () { + /*! instant.page v5.1.0 - (C) 2019-2020 Alexandre Dieulot - https://instant.page/license */ + + let mouseoverTimer; + let lastTouchTimestamp; + const prefetches = new Set(); + const prefetchElement = document.createElement("link"); + const isSupported = prefetchElement.relList && + prefetchElement.relList.supports && + prefetchElement.relList.supports("prefetch") && + window.IntersectionObserver && + "isIntersecting" in IntersectionObserverEntry.prototype; + const allowQueryString = "instantAllowQueryString" in document.body.dataset; + const allowExternalLinks = "instantAllowExternalLinks" in + document.body.dataset; + const useWhitelist = "instantWhitelist" in document.body.dataset; + const mousedownShortcut = "instantMousedownShortcut" in + document.body.dataset; + const DELAY_TO_NOT_BE_CONSIDERED_A_TOUCH_INITIATED_ACTION = 1111; + + let delayOnHover = 65; + let useMousedown = false; + let useMousedownOnly = false; + let useViewport = false; + + if ("instantIntensity" in document.body.dataset) { + const intensity = document.body.dataset.instantIntensity; + + if (intensity.substr(0, "mousedown".length) == "mousedown") { + useMousedown = true; + if (intensity == "mousedown-only") { + useMousedownOnly = true; + } + } else if (intensity.substr(0, "viewport".length) == "viewport") { + if ( + !(navigator.connection && + (navigator.connection.saveData || + (navigator.connection.effectiveType && + navigator.connection.effectiveType.includes("2g")))) + ) { + if (intensity == "viewport") { + /* Biggest iPhone resolution (which we want): 414 × 896 = 370944 + * Small 7" tablet resolution (which we don’t want): 600 × 1024 = 614400 + * Note that the viewport (which we check here) is smaller than the resolution due to the UI’s chrome */ + if ( + document.documentElement.clientWidth * + document.documentElement.clientHeight < 450000 + ) { + useViewport = true; + } + } else if (intensity == "viewport-all") { + useViewport = true; + } + } + } else { + const milliseconds = parseInt(intensity); + if (!isNaN(milliseconds)) { + delayOnHover = milliseconds; + } + } + } + + if (isSupported) { + const eventListenersOptions = { + capture: true, + passive: true, + }; + + if (!useMousedownOnly) { + document.addEventListener( + "touchstart", + touchstartListener, + eventListenersOptions, + ); + } + + if (!useMousedown) { + document.addEventListener( + "mouseover", + mouseoverListener, + eventListenersOptions, + ); + } else if (!mousedownShortcut) { + document.addEventListener( + "mousedown", + mousedownListener, + eventListenersOptions, + ); + } + + if (mousedownShortcut) { + document.addEventListener( + "mousedown", + mousedownShortcutListener, + eventListenersOptions, + ); + } + + if (useViewport) { + let triggeringFunction; + if (window.requestIdleCallback) { + triggeringFunction = function (callback) { + requestIdleCallback(callback, { + timeout: 1500, + }); + }; + } else { + triggeringFunction = function (callback) { + callback(); + }; + } + + triggeringFunction(function () { + const intersectionObserver = new IntersectionObserver( + function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + const linkElement = entry.target; + intersectionObserver.unobserve(linkElement); + preload(linkElement.href); + } + }); + }, + ); + + document.querySelectorAll("a").forEach(function (linkElement) { + if (isPreloadable(linkElement)) { + intersectionObserver.observe(linkElement); + } + }); + }); + } + } + + function touchstartListener(event) { + /* Chrome on Android calls mouseover before touchcancel so `lastTouchTimestamp` + * must be assigned on touchstart to be measured on mouseover. */ + lastTouchTimestamp = performance.now(); + + const linkElement = event.target.closest("a"); + + if (!isPreloadable(linkElement)) { + return; + } + + preload(linkElement.href); + } + + function mouseoverListener(event) { + if ( + performance.now() - lastTouchTimestamp < + DELAY_TO_NOT_BE_CONSIDERED_A_TOUCH_INITIATED_ACTION + ) { + return; + } + + const linkElement = event.target.closest("a"); + + if (!isPreloadable(linkElement)) { + return; + } + + linkElement.addEventListener("mouseout", mouseoutListener, { + passive: true, + }); + + mouseoverTimer = setTimeout(function () { + preload(linkElement.href); + mouseoverTimer = undefined; + }, delayOnHover); + } + + function mousedownListener(event) { + const linkElement = event.target.closest("a"); + + if (!isPreloadable(linkElement)) { + return; + } + + preload(linkElement.href); + } + + function mouseoutListener(event) { + if ( + event.relatedTarget && + event.target.closest("a") == event.relatedTarget.closest("a") + ) { + return; + } + + if (mouseoverTimer) { + clearTimeout(mouseoverTimer); + mouseoverTimer = undefined; + } + } + + function mousedownShortcutListener(event) { + if ( + performance.now() - lastTouchTimestamp < + DELAY_TO_NOT_BE_CONSIDERED_A_TOUCH_INITIATED_ACTION + ) { + return; + } + + const linkElement = event.target.closest("a"); + + if (event.which > 1 || event.metaKey || event.ctrlKey) { + return; + } + + if (!linkElement) { + return; + } + + linkElement.addEventListener("click", function (event) { + if (event.detail == 1337) { + return; + } + + event.preventDefault(); + }, { capture: true, passive: false, once: true }); + + const customEvent = new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: false, + detail: 1337, + }); + linkElement.dispatchEvent(customEvent); + } + + function isPreloadable(linkElement) { + if (!linkElement || !linkElement.href) { + return; + } + + if (useWhitelist && !("instant" in linkElement.dataset)) { + return; + } + + if ( + !allowExternalLinks && linkElement.origin != location.origin && + !("instant" in linkElement.dataset) + ) { + return; + } + + if (!["http:", "https:"].includes(linkElement.protocol)) { + return; + } + + if (linkElement.protocol == "http:" && location.protocol == "https:") { + return; + } + + if ( + !allowQueryString && linkElement.search && + !("instant" in linkElement.dataset) + ) { + return; + } + + if ( + linkElement.hash && + linkElement.pathname + linkElement.search == + location.pathname + location.search + ) { + return; + } + + if ("noInstant" in linkElement.dataset) { + return; + } + + return true; + } + + function preload(url) { + if (prefetches.has(url)) { + return; + } + + const prefetcher = document.createElement("link"); + prefetcher.rel = "prefetch"; + prefetcher.href = url; + document.head.appendChild(prefetcher); + + prefetches.add(url); + } + }); +})(); |