Skip to content

Commit

Permalink
Update assets/js/linkPrefetch.js
Browse files Browse the repository at this point in the history
Co-authored-by: Arun Shenoy <[email protected]>
  • Loading branch information
geckod22 and arunshenoy99 authored Nov 18, 2024
1 parent 1427769 commit 62999b1
Showing 1 changed file with 87 additions and 203 deletions.
290 changes: 87 additions & 203 deletions assets/js/linkPrefetch.js
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();
});

0 comments on commit 62999b1

Please sign in to comment.