wp-plugin-cloudflare-cache/assets/js/instantpage.js

336 lines
12 KiB
JavaScript
Raw Permalink Normal View History

2024-03-27 15:58:02 -04:00
/* instant.page v5.2.0 - (C) 2019-2023 Alexandre Dieulot - https://instant.page/license */
/* Custom modified version created for Super page Cache for Cloudflare plugin - Saumya Majumder */
let _chromiumMajorVersionInUserAgent = null
, _allowQueryString
, _allowExternalLinks
, _useWhitelist
, _delayOnHover = 65
, _lastTouchTimestamp
, _mouseoverTimer
, _preloadedList = new Set()
const DELAY_TO_NOT_BE_CONSIDERED_A_TOUCH_INITIATED_ACTION = 1111
init()
function init() {
const isSupported = document.createElement('link').relList.supports('prefetch')
// instant.page is meant to be loaded with <script type=module>
// (though sometimes webmasters load it as a regular script).
// So its normally executed (and must not cause JavaScript errors) in:
// - Chromium 61+
// - Gecko in Firefox 60+
// - WebKit in Safari 10.1+ (iOS 10.3+, macOS 10.10+)
//
// The check above used to check for IntersectionObserverEntry.isIntersecting
// but module scripts support implies this compatibility — except in Safari
// 10.112.0, but this prefetch check takes care of it.
if (!isSupported) {
return
}
const handleVaryAcceptHeader = 'instantVaryAccept' in document.body.dataset || 'Shopify' in window
// The `Vary: Accept` header when received in Chromium 79109 makes prefetches
// unusable, as Chromium used to send a different `Accept` header.
// Its applied on all Shopify sites by default, as Shopify is very popular
// and is the main source of this problem.
// `window.Shopify` only exists on “classic” Shopify sites. Those using
// Hydrogen (Remix SPA) arent concerned.
const chromiumUserAgentIndex = navigator.userAgent.indexOf('Chrome/')
if (chromiumUserAgentIndex > -1) {
_chromiumMajorVersionInUserAgent = parseInt(navigator.userAgent.substring(chromiumUserAgentIndex + 'Chrome/'.length))
}
// The user agent client hints API is a theoretically more reliable way to
// get Chromiums version… but its not available in Samsung Internet 20.
// It also requires a secure context, which would make debugging harder,
// and is only available in recent Chromium versions.
// In practice, Chromium browsers never shy from announcing "Chrome" in
// their regular user agent string, as that maximizes their compatibility.
if (handleVaryAcceptHeader && _chromiumMajorVersionInUserAgent && _chromiumMajorVersionInUserAgent < 110) {
return
}
const mousedownShortcut = 'instantMousedownShortcut' in document.body.dataset
_allowQueryString = 'instantAllowQueryString' in document.body.dataset
_allowExternalLinks = 'instantAllowExternalLinks' in document.body.dataset
_useWhitelist = 'instantWhitelist' in document.body.dataset
const eventListenersOptions = {
capture: true,
passive: true,
}
let useMousedown = false
let useMousedownOnly = false
let useViewport = false
if ('instantIntensity' in document.body.dataset) {
const intensity = document.body.dataset.instantIntensity
if (intensity.startsWith('mousedown')) {
useMousedown = true
if (intensity == 'mousedown-only') {
useMousedownOnly = true
}
}
else if (intensity.startsWith('viewport')) {
const isNavigatorConnectionSaveDataEnabled = navigator.connection && navigator.connection.saveData
const isNavigatorConnectionLike2g = navigator.connection && navigator.connection.effectiveType && navigator.connection.effectiveType.includes('2g')
if (!isNavigatorConnectionSaveDataEnabled && !isNavigatorConnectionLike2g) {
if (intensity == "viewport") {
if (document.documentElement.clientWidth * document.documentElement.clientHeight < 450000) {
useViewport = true
// Smartphones are the most likely to have a slow connection, and
// their small screen size limits the number of links (and thus
// server load).
//
// Foldable phones (being expensive as of 2023), tablets and PCs
// generally have a decent connection, and a big screen displaying
// more links that would put more load on the server.
//
// iPhone 14 Pro Max (want): 430×932 = 400760
// Samsung Galaxy S22 Ultra with display size set to 80% (want):
// 450×965 = 434250
// Small tablet (dont want): 600×960 = 576000
// Those number are virtual screen size, the viewport (used for
// the check above) will be smaller with the browsers interface.
}
}
else if (intensity == "viewport-all") {
useViewport = true
}
}
}
else {
const milliseconds = parseInt(intensity)
if (!isNaN(milliseconds)) {
_delayOnHover = milliseconds
}
}
}
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 requestIdleCallbackOrFallback = window.requestIdleCallback
// Safari has no support as of 16.3: https://webkit.org/b/164193
if (!requestIdleCallbackOrFallback) {
requestIdleCallbackOrFallback = (callback) => {
callback()
// A smarter fallback like setTimeout is not used because devices that
// may eventually be eligible to a Safari version supporting prefetch
// will be very powerful.
// The weakest devices that could be eligible are the 2017 iPad and
// the 2016 MacBook.
}
}
requestIdleCallbackOrFallback(function observeIntersection() {
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const anchorElement = entry.target
intersectionObserver.unobserve(anchorElement)
preload(anchorElement.href)
}
})
})
document.querySelectorAll('a').forEach((anchorElement) => {
if (isPreloadable(anchorElement)) {
intersectionObserver.observe(anchorElement)
}
})
}, {
timeout: 1500,
})
}
}
function touchstartListener(event) {
_lastTouchTimestamp = performance.now()
// Chrome on Android triggers mouseover before touchcancel, so
// `_lastTouchTimestamp` must be assigned on touchstart to be measured
// on mouseover.
const anchorElement = event.target.closest('a')
if (!isPreloadable(anchorElement)) {
return
}
preload(anchorElement.href, 'high')
}
function mouseoverListener(event) {
if (performance.now() - _lastTouchTimestamp < DELAY_TO_NOT_BE_CONSIDERED_A_TOUCH_INITIATED_ACTION) {
return
}
if (!('closest' in event.target)) {
return
// Without this check sometimes an error “event.target.closest is not a function” is thrown, for unknown reasons
// That error denotes that `event.target` isnt undefined. My best guess is that its the Document.
//
// Details could be gleaned from throwing such an error:
//throw new TypeError(`instant.page non-element event target: timeStamp=${~~event.timeStamp}, type=${event.type}, typeof=${typeof event.target}, nodeType=${event.target.nodeType}, nodeName=${event.target.nodeName}, viewport=${innerWidth}x${innerHeight}, coords=${event.clientX}x${event.clientY}, scroll=${scrollX}x${scrollY}`)
}
const anchorElement = event.target.closest('a')
if (!isPreloadable(anchorElement)) {
return
}
anchorElement.addEventListener('mouseout', mouseoutListener, {passive: true})
_mouseoverTimer = setTimeout(() => {
preload(anchorElement.href, 'high')
_mouseoverTimer = undefined
}, _delayOnHover)
}
function mousedownListener(event) {
const anchorElement = event.target.closest('a')
if (!isPreloadable(anchorElement)) {
return
}
preload(anchorElement.href, 'high')
}
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 anchorElement = event.target.closest('a')
if (event.which > 1 || event.metaKey || event.ctrlKey) {
return
}
if (!anchorElement) {
return
}
anchorElement.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})
anchorElement.dispatchEvent(customEvent)
}
function isPreloadable(anchorElement) {
if (!anchorElement || !anchorElement.href) {
return
}
if (_useWhitelist && !('instant' in anchorElement.dataset)) {
return
}
if (anchorElement.origin != location.origin) {
let allowed = _allowExternalLinks || 'instant' in anchorElement.dataset
if (!allowed || !_chromiumMajorVersionInUserAgent) {
// Chromium-only: see comment on “restrictive prefetch”
return
}
}
if (!['http:', 'https:'].includes(anchorElement.protocol)) {
return
}
if (anchorElement.protocol == 'http:' && location.protocol == 'https:') {
return
}
if (!_allowQueryString && anchorElement.search && !('instant' in anchorElement.dataset)) {
return
}
if (anchorElement.hash && anchorElement.pathname + anchorElement.search == location.pathname + location.search) {
return
}
if ('noInstant' in anchorElement.dataset) {
return
}
return true
}
function preload(url, fetchPriority = 'auto') {
if (_preloadedList.has(url)) {
return
}
// Custom check added by Super Page Cache for Cloudflare plugin
if( typeof swcfpc_can_url_be_prefetched === 'function' && swcfpc_can_url_be_prefetched(url.split("#")[0]) === false ) {
return
}
const linkElement = document.createElement('link')
linkElement.rel = 'prefetch'
linkElement.href = url
linkElement.fetchPriority = fetchPriority
// By default, a prefetch is loaded with a low priority.
// When theres a fair chance that this prefetch is going to be used in the
// near term (= after a touch/mouse event), giving it a high priority helps
// make the page load faster in case there are other resources loading.
// Prioritizing it implicitly means deprioritizing every other resource
// thats loading on the page. Due to HTML documents usually being much
// smaller than other resources (notably images and JavaScript), and
// prefetches happening once the initial page is sufficiently loaded,
// this theft of bandwidth should rarely be detrimental.
linkElement.as = 'document'
// as=document is Chromium-only and allows cross-origin prefetches to be
// usable for navigation. They call it “restrictive prefetch” and intend
// to remove it: https://crbug.com/1352371
//
// This document from the Chrome team dated 2022-08-10
// https://docs.google.com/document/d/1x232KJUIwIf-k08vpNfV85sVCRHkAxldfuIA5KOqi6M
// claims (I havent tested) that data- and battery-saver modes as well as
// the setting to disable preloading do not disable restrictive prefetch,
// unlike regular prefetch. Thats good for prefetching on a touch/mouse
// event, but might be bad when prefetching every link in the viewport.
document.head.appendChild(linkElement)
_preloadedList.add(url)
}