-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Arun Shenoy <[email protected]>
- Loading branch information
1 parent
1427769
commit 62999b1
Showing
1 changed file
with
87 additions
and
203 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,219 +1,103 @@ | ||
window.addEventListener( 'load', () => { | ||
/* check if the browser supports Prefetch */ | ||
const testlink = document.createElement("link"), | ||
supportsPrefetchCheck = testlink.relList && testlink.relList.supports && testlink.relList.supports("prefetch"), | ||
/* check if the user has set a reduced data usage option on the user agent or if the current connection effective type is 2g */ | ||
navigatorConnectionCheck = navigator.connection && (navigator.connection.saveData || (navigator.connection.effectiveType || "").includes("2g")), | ||
intersectionObserverCheck = window.IntersectionObserver && "isIntersecting" in IntersectionObserverEntry.prototype; | ||
|
||
if ( ! supportsPrefetchCheck || navigatorConnectionCheck ) { | ||
return; | ||
} else { | ||
class LP_APP { | ||
constructor(config) { | ||
this.config = config; | ||
this.activeOnDesktop = config.activeOnDesktop; | ||
this.behavior = config.behavior; | ||
this.hoverDelay = config.hoverDelay; | ||
this.ignoreKeywords = config.ignoreKeywords.split(','); | ||
this.instantClick = config.instantClick; | ||
this.mobileActive = config.activeOnMobile; | ||
this.isMobile = config.isMobile; | ||
this.mobileBehavior = config.mobileBehavior; | ||
this.prefetchedUrls = new Set(); | ||
this.timerIdentifier; | ||
this.eventListenerOptions = { capture: !0, passive: !0 }; | ||
} | ||
/** | ||
* Init | ||
* @returns {void} | ||
*/ | ||
init() { | ||
const isChrome = navigator.userAgent.indexOf("Chrome/") > -1, | ||
chromeVersion = isChrome && parseInt(navigator.userAgent.substring(navigator.userAgent.indexOf("Chrome/") + "Chrome/".length)); | ||
|
||
if ( isChrome && chromeVersion < 110 ) {return;} | ||
if ( this.isMobile && ! this.mobileActive ) {return;} | ||
if ( ! this.isMobile && ! this.activeOnDesktop ) {return;} | ||
|
||
if ( ! this.isMobile ) { | ||
if ( 'mouseHover' === this.behavior ) { | ||
let hoverDelay = parseInt(this.hoverDelay); | ||
hoverDelay = isNaN(hoverDelay) ? 60 : hoverDelay; | ||
document.addEventListener("mouseover", this.mouseHover.bind(this), this.eventListenerOptions); | ||
} else if ( 'mouseDown' === this.behavior ) { | ||
if ( this.instantClick ) { | ||
document.addEventListener("mousedown", this.mouseDownToClick.bind(this), this.eventListenerOptions); | ||
} else { | ||
document.addEventListener("mousedown", this.mouseDown.bind(this), this.eventListenerOptions) | ||
} | ||
} | ||
} | ||
document.addEventListener('DOMContentLoaded', () => { | ||
const testLink = document.createElement('link'); | ||
const supportsPrefetch = testLink.relList?.supports?.('prefetch'); | ||
const isDataSaver = navigator.connection?.saveData || navigator.connection?.effectiveType?.includes('2g'); | ||
const supportsIntersectionObserver = 'IntersectionObserver' in window && 'isIntersecting' in IntersectionObserverEntry.prototype; | ||
|
||
if ( this.mobileActive && this.isMobile ) { | ||
if ( 'touchstart' === this.mobileBehavior ) { | ||
document.addEventListener("touchstart", this.touchstart.bind(this), this.eventListenerOptions); | ||
} else if ( 'viewport' && intersectionObserverCheck ) { | ||
this.viewport(); | ||
} | ||
} | ||
} | ||
/** | ||
* Viewport handler | ||
* @returns {void} | ||
*/ | ||
viewport() { | ||
const io = new IntersectionObserver((e) => { | ||
e.forEach((e) => { | ||
if (e.isIntersecting) { | ||
const n = e.target; | ||
io.unobserve(n); | ||
this.canPrefetch(n) && this.prefetchIt(n.href); | ||
} | ||
}); | ||
}); | ||
let requestIdleCallback = window.requestIdleCallback || | ||
function (cb) { | ||
var start = Date.now(); | ||
return setTimeout(function () { | ||
cb({ | ||
didTimeout: false, | ||
timeRemaining: function () { | ||
return Math.max(0, 50 - (Date.now() - start)); | ||
} | ||
}); | ||
}, 1); | ||
}; | ||
requestIdleCallback( () => { | ||
return setTimeout(function () { | ||
return document.querySelectorAll("a").forEach(function (a) { | ||
return io.observe(a); | ||
}); | ||
}, 1000); | ||
}, { timeout: 1000 }); | ||
} | ||
/** | ||
* Mouse Down handler | ||
* @param {Event} e - listener event | ||
* @returns {void} | ||
*/ | ||
mouseDown(e) { | ||
const el = e.target.closest("a"); | ||
this.canPrefetch(el) && this.prefetchIt(el.href); | ||
} | ||
if (!supportsPrefetch || isDataSaver) return; | ||
|
||
/** | ||
* Mouse Down handler for instant click | ||
* @param {Event} e - listener event | ||
* @returns {void} | ||
*/ | ||
mouseDownToClick(e) { | ||
//if (performance.now() - o < r) return; | ||
const el = e.target.closest("a"); | ||
if (e.which > 1 || e.metaKey || e.ctrlKey) return; | ||
if (!el) return; | ||
el.addEventListener( | ||
"click", | ||
function (t) { | ||
'lpappinstantclick' != t.detail && t.preventDefault(); | ||
}, | ||
{ capture: !0, passive: !1, once: !0 } | ||
); | ||
const n = new MouseEvent("click", { view: window, bubbles: !0, cancelable: !1, detail: 'lpappinstantclick' }); | ||
el.dispatchEvent(n); | ||
} | ||
class LinkPrefetcher { | ||
constructor(config) { | ||
Object.assign(this, { | ||
activeOnDesktop: config.activeOnDesktop, | ||
behavior: config.behavior, | ||
hoverDelay: parseInt(config.hoverDelay) || 60, | ||
ignoreKeywords: config.ignoreKeywords.split(','), | ||
instantClick: config.instantClick, | ||
mobileActive: config.activeOnMobile, | ||
isMobile: config.isMobile, | ||
mobileBehavior: config.mobileBehavior, | ||
prefetchedUrls: new Set(), | ||
eventOptions: { capture: true, passive: true }, | ||
timerId: null, | ||
}); | ||
} | ||
|
||
touchstart(e) { | ||
const el = e.target.closest("a"); | ||
this.canPrefetch(el) && this.prefetchIt(el.href); | ||
} | ||
init() { | ||
const isChrome = navigator.userAgent.includes('Chrome/'); | ||
const chromeVersion = isChrome && parseInt(navigator.userAgent.split('Chrome/')[1]); | ||
|
||
/** | ||
* Clean Timers | ||
* @param {Event} t - listener event | ||
* @returns {void} | ||
*/ | ||
clean(t) { | ||
if ( t.relatedTarget && t.target.closest("a") == t.relatedTarget.closest("a") || this.timerIdentifier ) { | ||
clearTimeout( this.timerIdentifier ); | ||
this.timerIdentifier = void(0); | ||
} | ||
if (isChrome && chromeVersion < 110) return; | ||
if ((this.isMobile && !this.mobileActive) || (!this.isMobile && !this.activeOnDesktop)) return; | ||
|
||
if (!this.isMobile) { | ||
this.behavior === 'mouseHover' && document.addEventListener('mouseover', this.handleHover.bind(this), this.eventOptions); | ||
this.behavior === 'mouseDown' && document.addEventListener('mousedown', this.instantClick ? this.handleInstantClick.bind(this) : this.handleMouseDown.bind(this), this.eventOptions); | ||
} | ||
|
||
/** | ||
* Mouse hover function | ||
* @param {Event} e - listener event | ||
* @returns {void} | ||
*/ | ||
mouseHover(e) { | ||
if ( !("closest" in e.target) ) return; | ||
const link = e.target.closest("a"); | ||
if ( this.canPrefetch( link ) ) { | ||
link.addEventListener("mouseout", this.clean.bind(this), { passive: !0 }); | ||
this.timerIdentifier = setTimeout(()=> { | ||
this.prefetchIt( link.href ); | ||
this.timerIdentifier = void(0); | ||
}, this.hoverDelay); | ||
} | ||
if (this.isMobile && this.mobileBehavior === 'viewport' && supportsIntersectionObserver) { | ||
this.setupViewportObserver(); | ||
} | ||
|
||
/** | ||
* Can the url be prefetched or not | ||
* @param {Element} el - link element | ||
* @returns {boolean} - if it can be prefetched | ||
*/ | ||
canPrefetch( el ) { | ||
if ( el && el.href ) { | ||
/* it has been just prefetched before */ | ||
if (this.prefetchedUrls.has(el.href)) { | ||
return false; | ||
} | ||
} | ||
|
||
/* avoid if it is the same url as the actual location */ | ||
if ( el.href.replace(/\/$/, "") !== location.origin.replace(/\/$/, "") && el.href.replace(/\/$/, "") !== location.href.replace(/\/$/, "") ) { | ||
return true; | ||
setupViewportObserver() { | ||
const observer = new IntersectionObserver((entries) => { | ||
entries.forEach((entry) => { | ||
if (entry.isIntersecting) { | ||
observer.unobserve(entry.target); | ||
this.prefetchIfEligible(entry.target.href); | ||
} | ||
}); | ||
}); | ||
|
||
/* checking exclusions */ | ||
const exclude = this.ignoreKeywords.filter( k => { | ||
if ( el.href.indexOf (k) > -1) { | ||
return k; | ||
} | ||
}) | ||
if ( exclude.length > 0 ) { return false; } | ||
|
||
} | ||
|
||
return false; | ||
} | ||
const idleCallback = window.requestIdleCallback || ((cb) => setTimeout(cb, 1)); | ||
idleCallback(() => setTimeout(() => document.querySelectorAll('a').forEach((a) => observer.observe(a)), 1000)); | ||
} | ||
|
||
/** | ||
* Append link rel=prefetch to the head | ||
* @param {string} url - url to prefetch | ||
* @returns {void} | ||
*/ | ||
prefetchIt(url) { | ||
const toPrefechLink = document.createElement("link"); | ||
handleMouseDown(event) { | ||
const el = event.target.closest('a'); | ||
this.prefetchIfEligible(el?.href); | ||
} | ||
|
||
toPrefechLink.rel = "prefetch"; | ||
toPrefechLink.href = url; | ||
toPrefechLink.as = "document"; | ||
handleInstantClick(event) { | ||
const el = event.target.closest('a'); | ||
if (!el || event.which > 1 || event.metaKey || event.ctrlKey) return; | ||
el.addEventListener('click', (e) => e.detail !== 'instantClick' && e.preventDefault(), { capture: true, passive: false, once: true }); | ||
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: false, detail: 'instantClick' })); | ||
} | ||
|
||
document.head.appendChild(toPrefechLink); | ||
this.prefetchedUrls.add(url); | ||
} | ||
handleHover(event) { | ||
const link = event.target.closest('a'); | ||
if (!link || !this.prefetchIfEligible(link.href)) return; | ||
link.addEventListener('mouseout', () => clearTimeout(this.timerId), this.eventOptions); | ||
this.timerId = setTimeout(() => this.prefetchIt(link.href), this.hoverDelay); | ||
} | ||
|
||
prefetchIfEligible(url) { | ||
if (!url || this.prefetchedUrls.has(url)) return false; | ||
if (url.replace(/\/$/, '') === location.href.replace(/\/$/, '') || this.ignoreKeywords.some((k) => url.includes(k))) return false; | ||
return true; | ||
} | ||
|
||
prefetchIt(url) { | ||
const link = document.createElement('link'); | ||
link.rel = 'prefetch'; | ||
link.href = url; | ||
document.head.appendChild(link); | ||
this.prefetchedUrls.add(url); | ||
} | ||
/* | ||
default config: | ||
'activeOnDesktop' => true, | ||
'behavior' =>'mouseHover', | ||
'hoverDelay' => 60, | ||
'instantClick' => true , | ||
'activeOnMobile' => true , | ||
'mobileBehavior' => 'viewport', | ||
'ignoreKeywords' =>'wp-admin,#,?', | ||
*/ | ||
const lpapp = new LP_APP( window.LP_CONFIG ); | ||
lpapp.init(); | ||
} | ||
|
||
const config = window.LP_CONFIG || { | ||
activeOnDesktop: true, | ||
behavior: 'mouseHover', | ||
hoverDelay: 60, | ||
instantClick: true, | ||
activeOnMobile: true, | ||
mobileBehavior: 'viewport', | ||
ignoreKeywords: 'wp-admin,#,?', | ||
}; | ||
|
||
new LinkPrefetcher(config).init(); | ||
}); | ||
|