Last active
January 14, 2022 10:49
-
-
Save phacks/9be4a4ceb27e51f60f7670f28e7f5280 to your computer and use it in GitHub Desktop.
InstantClick with Rails — https://dev.to/phacks/the-rocky-road-to-implementing-link-prefetching-in-rails-oo0
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
/* InstantClick 3.1.0 | (C) 2014-2017 Alexandre Dieulot | http://instantclick.io/license */ | |
var instantclick, | |
InstantClick = (instantclick = (function(document, location, $userAgent) { | |
// Internal variables | |
var $currentLocationWithoutHash, | |
$urlToPreload, | |
$preloadTimer, | |
$lastTouchTimestamp, | |
$hasBeenInitialized, | |
$touchEndedWithoutClickTimer, | |
$lastUsedTimeoutId = 0, | |
// Preloading-related variables | |
$history = {}, | |
$fetchedBodies = {}, | |
$xhr, | |
$url = false, | |
$title = false, | |
$isContentTypeNotHTML, | |
$areTrackedElementsDifferent, | |
$body = false, | |
$lastDisplayTimestamp = 0, | |
$isPreloading = false, | |
$isWaitingForCompletion = false, | |
$gotANetworkError = false, | |
$trackedElementsData = [], | |
// Variables defined by public functions | |
$preloadOnMousedown, | |
$delayBeforePreload = 65, | |
$eventsCallbacks = { | |
preload: [], | |
receive: [], | |
wait: [], | |
change: [], | |
restore: [], | |
exit: [], | |
}, | |
$timers = {}, | |
$currentPageXhrs = [], | |
$windowEventListeners = {}, | |
$delegatedEvents = {}; | |
////////// POLYFILL ////////// | |
// Needed for `addEvent` | |
if (!Element.prototype.matches) { | |
Element.prototype.matches = | |
Element.prototype.webkitMatchesSelector || | |
Element.prototype.msMatchesSelector || | |
function(selector) { | |
var matches = document.querySelectorAll(selector); | |
for (var i = 0; i < matches.length; i++) { | |
if (matches[i] == this) { | |
return true; | |
} | |
} | |
return false; | |
}; | |
} | |
////////// HELPERS ////////// | |
function removeHash(url) { | |
var index = url.indexOf("#"); | |
if (index == -1) { | |
return url; | |
} | |
return url.substr(0, index); | |
} | |
function getParentLinkElement(element) { | |
while (element && element.nodeName != "A") { | |
element = element.parentNode; | |
} | |
// `element` will be null if no link element is found | |
return element; | |
} | |
function isBlacklisted(element) { | |
do { | |
if (!element.hasAttribute) { | |
// Parent of <html> | |
break; | |
} | |
if (element.hasAttribute("data-instant")) { | |
return false; | |
} | |
if (element.hasAttribute("data-no-instant")) { | |
return true; | |
} | |
} while ((element = element.parentNode)); | |
return false; | |
} | |
function isPreloadable(linkElement) { | |
var domain = location.protocol + "//" + location.host; | |
if ( | |
linkElement.target || // target="_blank" etc. | |
linkElement.hasAttribute("download") || | |
linkElement.href.indexOf(domain + "/") != 0 || // Another domain, or no href attribute | |
(linkElement.href.indexOf("#") > -1 && | |
removeHash(linkElement.href) == $currentLocationWithoutHash) || // Anchor | |
isBlacklisted(linkElement) | |
) { | |
return false; | |
} | |
return true; | |
} | |
function triggerPageEvent(eventType) { | |
var argumentsToApply = Array.prototype.slice.call(arguments, 1), | |
returnValue = false; | |
for (var i = 0; i < $eventsCallbacks[eventType].length; i++) { | |
if (eventType == "receive") { | |
var altered = $eventsCallbacks[eventType][i].apply( | |
window, | |
argumentsToApply | |
); | |
if (altered) { | |
// Update arguments for the next iteration of the loop. | |
if ("body" in altered) { | |
argumentsToApply[1] = altered.body; | |
} | |
if ("title" in altered) { | |
argumentsToApply[2] = altered.title; | |
} | |
returnValue = altered; | |
} | |
} else { | |
$eventsCallbacks[eventType][i].apply(window, argumentsToApply); | |
} | |
} | |
return returnValue; | |
} | |
function changePage(title, body, urlToPush, scrollPosition) { | |
abortCurrentPageXhrs(); | |
document.documentElement.replaceChild(body, document.body); | |
// We cannot just use `document.body = doc.body`, it causes Safari (tested | |
// 5.1, 6.0 and Mobile 7.0) to execute script tags directly. | |
document.title = title; | |
removeExpiredKeys("force"); | |
if (urlToPush) { | |
addOrRemoveWindowEventListeners("remove"); | |
if (urlToPush != location.href) { | |
history.pushState(null, null, urlToPush); | |
if ($userAgent.indexOf(" CriOS/") > -1) { | |
// Chrome for iOS: | |
// | |
// 1. Removes title in tab on pushState, so it needs to be set after. | |
// | |
// 2. Will not set the title if it's identical after trimming, so we | |
// add a non-breaking space. | |
if (document.title == title) { | |
document.title = title + String.fromCharCode(160); | |
} else { | |
document.title = title; | |
} | |
} | |
} | |
var hashIndex = urlToPush.indexOf("#"), | |
offsetElement = | |
hashIndex > -1 && | |
document.getElementById(urlToPush.substr(hashIndex + 1)), | |
offset = 0; | |
if (offsetElement) { | |
while (offsetElement.offsetParent) { | |
offset += offsetElement.offsetTop; | |
offsetElement = offsetElement.offsetParent; | |
} | |
} | |
if ("requestAnimationFrame" in window) { | |
// Safari on macOS doesn't immediately visually change the page on | |
// `document.documentElement.replaceChild`, so if `scrollTo` is called | |
// without `requestAnimationFrame` it often scrolls before the page | |
// is displayed. | |
requestAnimationFrame(function() { | |
scrollTo(0, offset); | |
}); | |
} else { | |
scrollTo(0, offset); | |
// Safari on macOS scrolls before the page is visually changed, but | |
// adding `requestAnimationFrame` doesn't fix it in this case. | |
} | |
clearCurrentPageTimeouts(); | |
$currentLocationWithoutHash = removeHash(urlToPush); | |
if ($currentLocationWithoutHash in $windowEventListeners) { | |
$windowEventListeners[$currentLocationWithoutHash] = []; | |
} | |
$timers[$currentLocationWithoutHash] = {}; | |
applyScriptElements(function(element) { | |
return !element.hasAttribute("data-instant-track"); | |
}); | |
triggerPageEvent("change", false); | |
} else { | |
// On popstate, browsers scroll by themselves, but at least Firefox | |
// scrolls BEFORE popstate is fired and thus before we can replace the | |
// page. If the page before popstate is too short the user won't be | |
// scrolled at the right position as a result. We need to scroll again. | |
scrollTo(0, scrollPosition); | |
// iOS's gesture to go back by swiping from the left edge of the screen | |
// will start a preloading if the user touches a link, it needs to be | |
// cancelled otherwise the page behind the touched link will be | |
// displayed. | |
$xhr.abort(); | |
setPreloadingAsHalted(); | |
applyScriptElements(function(element) { | |
return element.hasAttribute("data-instant-restore"); | |
}); | |
restoreTimers(); | |
triggerPageEvent("restore"); | |
} | |
} | |
function setPreloadingAsHalted() { | |
$isPreloading = false; | |
$isWaitingForCompletion = false; | |
} | |
function removeNoscriptTags(html) { | |
// Must be done on text, not on a node's innerHTML, otherwise strange | |
// things happen with implicitly closed elements (see the Noscript test). | |
return html.replace(/<noscript[\s\S]+?<\/noscript>/gi, ""); | |
} | |
function abortCurrentPageXhrs() { | |
for (var i = 0; i < $currentPageXhrs.length; i++) { | |
if ( | |
typeof $currentPageXhrs[i] == "object" && | |
"abort" in $currentPageXhrs[i] | |
) { | |
$currentPageXhrs[i].instantclickAbort = true; | |
$currentPageXhrs[i].abort(); | |
} | |
} | |
$currentPageXhrs = []; | |
} | |
function clearCurrentPageTimeouts() { | |
for (var i in $timers[$currentLocationWithoutHash]) { | |
var timeout = $timers[$currentLocationWithoutHash][i]; | |
window.clearTimeout(timeout.realId); | |
timeout.delayLeft = timeout.delay - +new Date() + timeout.timestamp; | |
} | |
} | |
function restoreTimers() { | |
for (var i in $timers[$currentLocationWithoutHash]) { | |
if (!("delayLeft" in $timers[$currentLocationWithoutHash][i])) { | |
continue; | |
} | |
var args = [ | |
$timers[$currentLocationWithoutHash][i].callback, | |
$timers[$currentLocationWithoutHash][i].delayLeft, | |
]; | |
for ( | |
var j = 0; | |
j < $timers[$currentLocationWithoutHash][i].params.length; | |
j++ | |
) { | |
args.push($timers[$currentLocationWithoutHash][i].params[j]); | |
} | |
addTimer( | |
args, | |
$timers[$currentLocationWithoutHash][i].isRepeating, | |
$timers[$currentLocationWithoutHash][i].delay | |
); | |
delete $timers[$currentLocationWithoutHash][i]; | |
} | |
} | |
function handleTouchendWithoutClick() { | |
$xhr.abort(); | |
setPreloadingAsHalted(); | |
} | |
function addOrRemoveWindowEventListeners(addOrRemove) { | |
if ($currentLocationWithoutHash in $windowEventListeners) { | |
for ( | |
var i = 0; | |
i < $windowEventListeners[$currentLocationWithoutHash].length; | |
i++ | |
) { | |
window[addOrRemove + "EventListener"].apply( | |
window, | |
$windowEventListeners[$currentLocationWithoutHash][i] | |
); | |
} | |
} | |
} | |
function applyScriptElements(condition) { | |
var scriptElementsInDOM = document.body.getElementsByTagName("script"), | |
scriptElementsToCopy = [], | |
originalElement, | |
copyElement, | |
parentNode, | |
nextSibling, | |
i; | |
// `scriptElementsInDOM` will change during the copy of scripts if | |
// a script add or delete script elements, so we need to put script | |
// elements in an array to loop through them correctly. | |
for (i = 0; i < scriptElementsInDOM.length; i++) { | |
scriptElementsToCopy.push(scriptElementsInDOM[i]); | |
} | |
for (i = 0; i < scriptElementsToCopy.length; i++) { | |
originalElement = scriptElementsToCopy[i]; | |
if (!originalElement) { | |
// Might have disappeared, see previous comment | |
continue; | |
} | |
if (!condition(originalElement)) { | |
continue; | |
} | |
copyElement = document.createElement("script"); | |
for (var j = 0; j < originalElement.attributes.length; j++) { | |
copyElement.setAttribute( | |
originalElement.attributes[j].name, | |
originalElement.attributes[j].value | |
); | |
} | |
copyElement.textContent = originalElement.textContent; | |
parentNode = originalElement.parentNode; | |
nextSibling = originalElement.nextSibling; | |
parentNode.removeChild(originalElement); | |
parentNode.insertBefore(copyElement, nextSibling); | |
} | |
} | |
function addTrackedElements() { | |
var trackedElements = document.querySelectorAll("[data-instant-track]"), | |
element, | |
elementData; | |
for (var i = 0; i < trackedElements.length; i++) { | |
element = trackedElements[i]; | |
elementData = | |
element.getAttribute("href") || | |
element.getAttribute("src") || | |
element.textContent; | |
// We can't use just `element.href` and `element.src` because we can't | |
// retrieve `href`s and `src`s from the Ajax response. | |
$trackedElementsData.push(elementData); | |
} | |
} | |
function addTimer(args, isRepeating, realDelay) { | |
var callback = args[0], | |
delay = args[1], | |
params = [].slice.call(args, 2), | |
timestamp = +new Date(); | |
$lastUsedTimeoutId++; | |
var id = $lastUsedTimeoutId; | |
var callbackModified; | |
if (isRepeating) { | |
callbackModified = function(args2) { | |
callback(args2); | |
delete $timers[$currentLocationWithoutHash][id]; | |
args[0] = callback; | |
args[1] = delay; | |
addTimer(args, true); | |
}; | |
} else { | |
callbackModified = function(args2) { | |
callback(args2); | |
delete $timers[$currentLocationWithoutHash][id]; | |
}; | |
} | |
args[0] = callbackModified; | |
if (realDelay != undefined) { | |
timestamp += delay - realDelay; | |
delay = realDelay; | |
} | |
var realId = window.setTimeout.apply(window, args); | |
$timers[$currentLocationWithoutHash][id] = { | |
realId: realId, | |
timestamp: timestamp, | |
callback: callback, | |
delay: delay, | |
params: params, | |
isRepeating: isRepeating, | |
}; | |
return -id; | |
} | |
////////// EVENT LISTENERS ////////// | |
function mousedownListener(event) { | |
var linkElement = getParentLinkElement(event.target); | |
if (!linkElement || !isPreloadable(linkElement)) { | |
return; | |
} | |
preload(linkElement.href); | |
} | |
function mouseoverListener(event) { | |
if ($lastTouchTimestamp > +new Date() - 500) { | |
// On a touch device, if the content of the page change on mouseover | |
// click is never fired and the user will need to tap a second time. | |
// https://developer.apple.com/library/content/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html#//apple_ref/doc/uid/TP40006511-SW4 | |
// | |
// Content change could happen in the `preload` event, so we stop there. | |
return; | |
} | |
if (+new Date() - $lastDisplayTimestamp < 100) { | |
// After a page is displayed, if the user's cursor happens to be above | |
// a link a mouseover event will be in most browsers triggered | |
// automatically, and in other browsers it will be triggered when the | |
// user moves his mouse by 1px. | |
// | |
// Here are the behaviors I noticed, all on Windows: | |
// - Safari 5.1: auto-triggers after 0 ms | |
// - IE 11: auto-triggers after 30-80 ms (depends on page's size?) | |
// - Firefox: auto-triggers after 10 ms | |
// - Opera 18: auto-triggers after 10 ms | |
// | |
// - Chrome: triggers when cursor moved | |
// - Opera 12.16: triggers when cursor moved | |
// | |
// To remedy to this, we do nothing if the last display occurred less | |
// than 100 ms ago. | |
return; | |
} | |
var linkElement = getParentLinkElement(event.target); | |
if (!linkElement) { | |
return; | |
} | |
if (linkElement == getParentLinkElement(event.relatedTarget)) { | |
// Happens when mouseout-ing and mouseover-ing child elements of the same link element | |
return; | |
} | |
if (!isPreloadable(linkElement)) { | |
return; | |
} | |
linkElement.addEventListener("mouseout", mouseoutListener); | |
if (!$isWaitingForCompletion) { | |
$urlToPreload = linkElement.href; | |
$preloadTimer = setTimeout(preload, $delayBeforePreload); | |
} | |
} | |
function touchstartListener(event) { | |
$lastTouchTimestamp = +new Date(); | |
var linkElement = getParentLinkElement(event.target); | |
if (!linkElement || !isPreloadable(linkElement)) { | |
return; | |
} | |
if ($touchEndedWithoutClickTimer) { | |
clearTimeout($touchEndedWithoutClickTimer); | |
$touchEndedWithoutClickTimer = false; | |
} | |
linkElement.addEventListener("touchend", touchendAndTouchcancelListener); | |
linkElement.addEventListener( | |
"touchcancel", | |
touchendAndTouchcancelListener | |
); | |
preload(linkElement.href); | |
} | |
function clickListenerPrelude() { | |
// Makes clickListener be fired after everyone else, so that we can respect | |
// event.preventDefault. | |
document.addEventListener("click", clickListener); | |
} | |
function clickListener(event) { | |
document.removeEventListener("click", clickListener); | |
if ($touchEndedWithoutClickTimer) { | |
clearTimeout($touchEndedWithoutClickTimer); | |
$touchEndedWithoutClickTimer = false; | |
} | |
if (event.defaultPrevented) { | |
return; | |
} | |
var linkElement = getParentLinkElement(event.target); | |
if (!linkElement || !isPreloadable(linkElement)) { | |
return; | |
} | |
// Check if it's opening in a new tab | |
if ( | |
event.button != 0 || // Chrome < 55 fires a click event when the middle mouse button is pressed | |
event.metaKey || | |
event.ctrlKey | |
) { | |
return; | |
} | |
event.preventDefault(); | |
display(linkElement.href); | |
} | |
function mouseoutListener(event) { | |
if ( | |
getParentLinkElement(event.target) == | |
getParentLinkElement(event.relatedTarget) | |
) { | |
// Happens when mouseout-ing and mouseover-ing child elements of the same link element, | |
// we don't want to stop preloading then. | |
return; | |
} | |
if ($preloadTimer) { | |
clearTimeout($preloadTimer); | |
$preloadTimer = false; | |
return; | |
} | |
if (!$isPreloading || $isWaitingForCompletion) { | |
return; | |
} | |
$xhr.abort(); | |
setPreloadingAsHalted(); | |
} | |
function touchendAndTouchcancelListener(event) { | |
if (!$isPreloading || $isWaitingForCompletion) { | |
return; | |
} | |
$touchEndedWithoutClickTimer = setTimeout( | |
handleTouchendWithoutClick, | |
500 | |
); | |
} | |
function readystatechangeListener() { | |
if ($xhr.readyState == 2) { | |
// headers received | |
var contentType = $xhr.getResponseHeader("Content-Type"); | |
if (!contentType || !/^text\/html/i.test(contentType)) { | |
$isContentTypeNotHTML = true; | |
} | |
} | |
if ($xhr.readyState < 4) { | |
return; | |
} | |
if ($xhr.status == 0) { | |
// Request error/timeout/abort | |
$gotANetworkError = true; | |
if ($isWaitingForCompletion) { | |
triggerPageEvent("exit", $url, "network error"); | |
location.href = $url; | |
} | |
return; | |
} | |
if ($isContentTypeNotHTML) { | |
if ($isWaitingForCompletion) { | |
triggerPageEvent("exit", $url, "non-html content-type"); | |
location.href = $url; | |
} | |
return; | |
} | |
var doc = document.implementation.createHTMLDocument(""); | |
doc.documentElement.innerHTML = removeNoscriptTags($xhr.responseText); | |
$title = doc.title; | |
$body = doc.body; | |
$fetchedBodies[$url] = { body: $body, title: $title }; | |
var alteredOnReceive = triggerPageEvent("receive", $url, $body, $title); | |
if (alteredOnReceive) { | |
if ("body" in alteredOnReceive) { | |
$body = alteredOnReceive.body; | |
} | |
if ("title" in alteredOnReceive) { | |
$title = alteredOnReceive.title; | |
} | |
} | |
var urlWithoutHash = removeHash($url); | |
$history[urlWithoutHash] = { | |
body: $body, | |
title: $title, | |
scrollPosition: | |
urlWithoutHash in $history | |
? $history[urlWithoutHash].scrollPosition | |
: 0, | |
}; | |
var trackedElements = doc.querySelectorAll("[data-instant-track]"), | |
element, | |
elementData; | |
if (trackedElements.length != $trackedElementsData.length) { | |
$areTrackedElementsDifferent = true; | |
} else { | |
for (var i = 0; i < trackedElements.length; i++) { | |
element = trackedElements[i]; | |
elementData = | |
element.getAttribute("href") || | |
element.getAttribute("src") || | |
element.textContent; | |
if ($trackedElementsData.indexOf(elementData) == -1) { | |
$areTrackedElementsDifferent = true; | |
} | |
} | |
} | |
if ($isWaitingForCompletion) { | |
$isWaitingForCompletion = false; | |
display($url); | |
} | |
} | |
function popstateListener() { | |
var loc = removeHash(location.href); | |
if (loc == $currentLocationWithoutHash) { | |
return; | |
} | |
if ($isWaitingForCompletion) { | |
setPreloadingAsHalted(); | |
$xhr.abort(); | |
} | |
if (!(loc in $history)) { | |
triggerPageEvent("exit", location.href, "not in history"); | |
if (loc == location.href) { | |
// no location.hash | |
location.href = location.href; | |
// Reloads the page while using cache for scripts, styles and images, | |
// unlike `location.reload()` | |
} else { | |
// When there's a hash, `location.href = location.href` won't reload | |
// the page (but will trigger a popstate event, thus causing an infinite | |
// loop), so we need to call `location.reload()` | |
location.reload(); | |
} | |
return; | |
} | |
$history[$currentLocationWithoutHash].scrollPosition = pageYOffset; | |
clearCurrentPageTimeouts(); | |
addOrRemoveWindowEventListeners("remove"); | |
$currentLocationWithoutHash = loc; | |
changePage( | |
$history[loc].title, | |
$history[loc].body, | |
false, | |
$history[loc].scrollPosition | |
); | |
addOrRemoveWindowEventListeners("add"); | |
} | |
////////// MAIN FUNCTIONS ////////// | |
function preload(url) { | |
if ($preloadTimer) { | |
clearTimeout($preloadTimer); | |
$preloadTimer = false; | |
} | |
if (!url) { | |
url = $urlToPreload; | |
} | |
if ($isPreloading && (url == $url || $isWaitingForCompletion)) { | |
return; | |
} | |
$isPreloading = true; | |
$isWaitingForCompletion = false; | |
$url = url; | |
$body = false; | |
$isContentTypeNotHTML = false; | |
$gotANetworkError = false; | |
$areTrackedElementsDifferent = false; | |
triggerPageEvent("preload"); | |
removeExpiredKeys(); | |
if (!$fetchedBodies[$url]) { | |
$xhr.open("GET", $url); | |
$xhr.timeout = 90000; // Must be set after `open()` with IE | |
$xhr.send(); | |
} | |
} | |
function removeExpiredKeys(option) { | |
if (Object.keys($fetchedBodies).length > 13 || option == "force") { | |
$fetchedBodies = {}; | |
} | |
} | |
function display(url) { | |
if ($fetchedBodies[url]) { | |
var $body = $fetchedBodies[url]["body"]; | |
var $title = $fetchedBodies[url]["title"]; | |
} | |
$lastDisplayTimestamp = +new Date(); | |
if ($preloadTimer || !$isPreloading) { | |
// $preloadTimer: | |
// Happens when there's a delay before preloading and that delay | |
// hasn't expired (preloading didn't kick in). | |
// | |
// !$isPreloading: | |
// A link has been clicked, and preloading hasn't been initiated. | |
// It happens with touch devices when a user taps *near* the link, | |
// causing `touchstart` not to be fired. Safari/Chrome will trigger | |
// `mouseover`, `mousedown`, `click` (and others), but when that happens | |
// we do nothing in `mouseover` as it may cause `click` not to fire (see | |
// comment in `mouseoverListener`). | |
// | |
// It also happens when a user uses his keyboard to navigate (with Tab | |
// and Return), and possibly in other non-mainstream ways to navigate | |
// a website. | |
if ($preloadTimer && $url && $url != url) { | |
// Happens when the user clicks on a link before preloading | |
// kicks in while another link is already preloading. | |
triggerPageEvent( | |
"exit", | |
url, | |
"click occured while preloading planned" | |
); | |
location.href = url; | |
return; | |
} | |
preload(url); | |
triggerPageEvent("wait"); | |
$isWaitingForCompletion = true; // Must be set *after* calling `preload` | |
return; | |
} | |
if ($isWaitingForCompletion) { | |
// The user clicked on a link while a page to display was preloading. | |
// Either on the same link or on another link. If it's the same link | |
// something might have gone wrong (or he could have double clicked, we | |
// don't handle that case), so we send him to the page without pjax. | |
// If it's another link, it hasn't been preloaded, so we redirect the | |
// user to it. | |
triggerPageEvent( | |
"exit", | |
url, | |
"clicked on a link while waiting for another page to display" | |
); | |
location.href = url; | |
return; | |
} | |
if ($isContentTypeNotHTML) { | |
triggerPageEvent("exit", $url, "non-html content-type"); | |
location.href = $url; | |
return; | |
} | |
if ($gotANetworkError) { | |
triggerPageEvent("exit", $url, "network error"); | |
location.href = $url; | |
return; | |
} | |
if ($areTrackedElementsDifferent) { | |
triggerPageEvent("exit", $url, "different assets"); | |
location.href = $url; | |
return; | |
} | |
if (!$body) { | |
triggerPageEvent("wait"); | |
$isWaitingForCompletion = true; | |
return; | |
} | |
$history[$currentLocationWithoutHash].scrollPosition = pageYOffset; | |
setPreloadingAsHalted(); | |
changePage($title, $body, $url); | |
} | |
////////// PUBLIC VARIABLE AND FUNCTIONS ////////// | |
var supported = false; | |
if ("pushState" in history && location.protocol != "file:") { | |
supported = true; | |
var indexOfAndroid = $userAgent.indexOf("Android "); | |
if (indexOfAndroid > -1) { | |
// The stock browser in Android 4.0.3 through 4.3.1 supports pushState, | |
// though it doesn't update the address bar. | |
// | |
// More problematic is that it has a bug on `popstate` when coming back | |
// from a page not displayed through InstantClick: `location.href` is | |
// undefined and `location.reload()` doesn't work. | |
// | |
// Android < 4.4 is therefore blacklisted, unless it's a browser known | |
// not to have that latter bug. | |
var androidVersion = parseFloat( | |
$userAgent.substr(indexOfAndroid + "Android ".length) | |
); | |
if (androidVersion < 4.4) { | |
supported = false; | |
if (androidVersion >= 4) { | |
var whitelistedBrowsersUserAgentsOnAndroid4 = [ | |
/ Chrome\//, // Chrome, Opera, Puffin, QQ, Yandex | |
/ UCBrowser\//, | |
/ Firefox\//, | |
/ Windows Phone /, // WP 8.1+ pretends to be Android | |
]; | |
for ( | |
var i = 0; | |
i < whitelistedBrowsersUserAgentsOnAndroid4.length; | |
i++ | |
) { | |
if (whitelistedBrowsersUserAgentsOnAndroid4[i].test($userAgent)) { | |
supported = true; | |
break; | |
} | |
} | |
} | |
} | |
} | |
} | |
function init(preloadingMode) { | |
if (!supported) { | |
triggerPageEvent("change", true); | |
return; | |
} | |
if ($hasBeenInitialized) { | |
return; | |
} | |
$hasBeenInitialized = true; | |
if (preloadingMode == "mousedown") { | |
$preloadOnMousedown = true; | |
} else if (typeof preloadingMode == "number") { | |
$delayBeforePreload = preloadingMode; | |
} | |
$currentLocationWithoutHash = removeHash(location.href); | |
$timers[$currentLocationWithoutHash] = {}; | |
$history[$currentLocationWithoutHash] = { | |
body: document.body, | |
title: document.title, | |
scrollPosition: pageYOffset, | |
}; | |
if (document.readyState == "loading") { | |
document.addEventListener("DOMContentLoaded", addTrackedElements); | |
} else { | |
addTrackedElements(); | |
} | |
$xhr = new XMLHttpRequest(); | |
$xhr.addEventListener("readystatechange", readystatechangeListener); | |
document.addEventListener("touchstart", touchstartListener, true); | |
if ($preloadOnMousedown) { | |
document.addEventListener("mousedown", mousedownListener, true); | |
} else { | |
document.addEventListener("mouseover", mouseoverListener, true); | |
} | |
document.addEventListener("click", clickListenerPrelude, true); | |
addEventListener("popstate", popstateListener); | |
} | |
function on(eventType, callback) { | |
$eventsCallbacks[eventType].push(callback); | |
if (eventType == "change") { | |
callback(!$lastDisplayTimestamp); | |
} | |
} | |
function setTimeout() { | |
return addTimer(arguments, false); | |
} | |
function setInterval() { | |
return addTimer(arguments, true); | |
} | |
function clearTimeout(id) { | |
id = -id; | |
for (var loc in $timers) { | |
if (id in $timers[loc]) { | |
window.clearTimeout($timers[loc][id].realId); | |
delete $timers[loc][id]; | |
} | |
} | |
} | |
function xhr(xhr) { | |
$currentPageXhrs.push(xhr); | |
} | |
function addPageEvent() { | |
if (!($currentLocationWithoutHash in $windowEventListeners)) { | |
$windowEventListeners[$currentLocationWithoutHash] = []; | |
} | |
$windowEventListeners[$currentLocationWithoutHash].push(arguments); | |
addEventListener.apply(window, arguments); | |
} | |
function removePageEvent() { | |
if (!($currentLocationWithoutHash in $windowEventListeners)) { | |
return; | |
} | |
firstLoop: for ( | |
var i = 0; | |
i < $windowEventListeners[$currentLocationWithoutHash].length; | |
i++ | |
) { | |
if ( | |
arguments.length != | |
$windowEventListeners[$currentLocationWithoutHash][i].length | |
) { | |
continue; | |
} | |
for ( | |
var j = 0; | |
j < $windowEventListeners[$currentLocationWithoutHash][i].length; | |
j++ | |
) { | |
if ( | |
arguments[j] != | |
$windowEventListeners[$currentLocationWithoutHash][i][j] | |
) { | |
continue firstLoop; | |
} | |
} | |
$windowEventListeners[$currentLocationWithoutHash].splice(i, 1); | |
} | |
} | |
function addEvent(selector, type, listener) { | |
if (!(type in $delegatedEvents)) { | |
$delegatedEvents[type] = {}; | |
document.addEventListener( | |
type, | |
function(event) { | |
var element = event.target; | |
event.originalStopPropagation = event.stopPropagation; | |
event.stopPropagation = function() { | |
this.isPropagationStopped = true; | |
this.originalStopPropagation(); | |
}; | |
while (element && element.nodeType == 1) { | |
for (var selector in $delegatedEvents[type]) { | |
if (element.matches(selector)) { | |
for ( | |
var i = 0; | |
i < $delegatedEvents[type][selector].length; | |
i++ | |
) { | |
$delegatedEvents[type][selector][i].call(element, event); | |
} | |
if (event.isPropagationStopped) { | |
return; | |
} | |
break; | |
} | |
} | |
element = element.parentNode; | |
} | |
}, | |
false | |
); // Third parameter isn't optional in Firefox < 6 | |
if (type == "click" && /iP(?:hone|ad|od)/.test($userAgent)) { | |
// Force Mobile Safari to trigger the click event on document by adding a pointer cursor to body | |
var styleElement = document.createElement("style"); | |
styleElement.setAttribute("instantclick-mobile-safari-cursor", ""); // So that this style element doesn't surprise developers in the browser DOM inspector. | |
styleElement.textContent = "body { cursor: pointer !important; }"; | |
document.head.appendChild(styleElement); | |
} | |
} | |
if (!(selector in $delegatedEvents[type])) { | |
$delegatedEvents[type][selector] = []; | |
} | |
// Run removeEvent beforehand so that it can't be added twice | |
removeEvent(selector, type, listener); | |
$delegatedEvents[type][selector].push(listener); | |
} | |
function removeEvent(selector, type, listener) { | |
var index = $delegatedEvents[type][selector].indexOf(listener); | |
if (index > -1) { | |
$delegatedEvents[type][selector].splice(index, 1); | |
} | |
} | |
function go(url) { | |
var link = document.createElement("a"); | |
link.href = url; | |
document.body.appendChild(link); | |
link.click(); | |
} | |
//////////////////// | |
return { | |
supported: supported, | |
init: init, | |
on: on, | |
setTimeout: setTimeout, | |
setInterval: setInterval, | |
clearTimeout: clearTimeout, | |
xhr: xhr, | |
addPageEvent: addPageEvent, | |
removePageEvent: removePageEvent, | |
addEvent: addEvent, | |
removeEvent: removeEvent, | |
go: go, | |
}; | |
})(document, location, navigator.userAgent)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
NVM it works! thanks :)