add instantclick source code
This commit is contained in:
parent
bf8b09ed0e
commit
0d1dec9d55
|
@ -1,13 +1,757 @@
|
||||||
/* InstantClick 3.1.0 | (C) 2014 Alexandre Dieulot | http://instantclick.io/license */
|
/* InstantClick 3.1.0 | (C) 2014 Alexandre Dieulot | http://instantclick.io/license */
|
||||||
var InstantClick=function(d,e){function w(a){var b=a.indexOf("#");return 0>b?a:a.substr(0,b)}function z(a){for(;a&&"A"!=a.nodeName;)a=a.parentNode;return a}function A(a){var b=e.protocol+"//"+e.host;if(!(b=a.target||a.hasAttribute("download")||0!=a.href.indexOf(b+"/")||-1<a.href.indexOf("#")&&w(a.href)==k)){if(J){a:{do{if(!a.hasAttribute)break;if(a.hasAttribute("data-no-instant"))break;if(a.hasAttribute("data-instant")){a=!0;break a}}while(a=a.parentNode);a=!1}a=!a}else a:{do{if(!a.hasAttribute)break;
|
|
||||||
if(a.hasAttribute("data-instant"))break;if(a.hasAttribute("data-no-instant")){a=!0;break a}}while(a=a.parentNode);a=!1}b=a}return b?!1:!0}function t(a,b,c,g){for(var d=!1,e=0;e<B[a].length;e++)if("receive"==a){var f=B[a][e](b,c,g);f&&("body"in f&&(c=f.body),"title"in f&&(g=f.title),d=f)}else B[a][e](b,c,g);return d}function K(a,b,c,g){d.documentElement.replaceChild(b,d.body);if(c){history.pushState(null,null,c);b=c.indexOf("#");b=-1<b&&d.getElementById(c.substr(b+1));g=0;if(b)for(;b.offsetParent;)g+=
|
var InstantClick = function(document, location) {
|
||||||
b.offsetTop,b=b.offsetParent;scrollTo(0,g);k=w(c)}else scrollTo(0,g);d.title=S&&d.title==a?a+String.fromCharCode(160):a;L();C.done();t("change",!1);a=d.createEvent("HTMLEvents");a.initEvent("instantclick:newpage",!0,!0);dispatchEvent(a)}function M(a){G>+new Date-500||(a=z(a.target))&&A(a)&&x(a.href)}function N(a){G>+new Date-500||(a=z(a.target))&&A(a)&&(a.addEventListener("mouseout",T),H?(O=a.href,l=setTimeout(x,H)):x(a.href))}function U(a){G=+new Date;(a=z(a.target))&&A(a)&&(D?a.removeEventListener("mousedown",
|
// Internal variables
|
||||||
M):a.removeEventListener("mouseover",N),x(a.href))}function V(a){var b=z(a.target);!b||!A(b)||1<a.which||a.metaKey||a.ctrlKey||(a.preventDefault(),P(b.href))}function T(){l?(clearTimeout(l),l=!1):v&&!m&&(p.abort(),m=v=!1)}function W(){if(!(4>p.readyState)&&0!=p.status){q.ready=+new Date-q.start;if(p.getResponseHeader("Content-Type").match(/\/(x|ht|xht)ml/)){var a=d.implementation.createHTMLDocument("");a.documentElement.innerHTML=p.responseText.replace(/<noscript[\s\S]+<\/noscript>/gi,"");y=a.title;
|
var $ua = navigator.userAgent,
|
||||||
u=a.body;var b=t("receive",r,u,y);b&&("body"in b&&(u=b.body),"title"in b&&(y=b.title));b=w(r);h[b]={body:u,title:y,scrollY:b in h?h[b].scrollY:0};for(var a=a.head.children,b=0,c,g=a.length-1;0<=g;g--)if(c=a[g],c.hasAttribute("data-instant-track")){c=c.getAttribute("href")||c.getAttribute("src")||c.innerHTML;for(var e=E.length-1;0<=e;e--)E[e]==c&&b++}b!=E.length&&(F=!0)}else F=!0;m&&(m=!1,P(r))}}function L(a){d.body.addEventListener("touchstart",U,!0);D?d.body.addEventListener("mousedown",M,!0):d.body.addEventListener("mouseover",
|
$isChromeForIOS = $ua.indexOf(' CriOS/') > -1,
|
||||||
N,!0);d.body.addEventListener("click",V,!0);if(!a){a=d.body.getElementsByTagName("script");var b,c,g,e;i=0;for(j=a.length;i<j;i++)b=a[i],b.hasAttribute("data-no-instant")||(c=d.createElement("script"),b.src&&(c.src=b.src),b.innerHTML&&(c.innerHTML=b.innerHTML),g=b.parentNode,e=b.nextSibling,g.removeChild(b),g.insertBefore(c,e))}}function x(a){!D&&"display"in q&&100>+new Date-(q.start+q.display)||(l&&(clearTimeout(l),l=!1),a||(a=O),v&&(a==r||m))||(v=!0,m=!1,r=a,F=u=!1,q={start:+new Date},t("fetch"),
|
$hasTouch = 'createTouch' in document,
|
||||||
p.open("GET",a),p.send())}function P(a){"display"in q||(q.display=+new Date-q.start);l||!v?l&&r&&r!=a?e.href=a:(x(a),C.start(0,!0),t("wait"),m=!0):m?e.href=a:F?e.href=r:u?(h[k].scrollY=pageYOffset,m=v=!1,K(y,u,r)):(C.start(0,!0),t("wait"),m=!0)}var I=navigator.userAgent,S=-1<I.indexOf(" CriOS/"),Q="createTouch"in d,k,O,l,G,h={},p,r=!1,y=!1,F=!1,u=!1,q={},v=!1,m=!1,E=[],J,D,H,B={fetch:[],receive:[],wait:[],change:[]},C=function(){function a(a,e){n=a;d.getElementById(f.id)&&d.body.removeChild(f);f.style.opacity=
|
$currentLocationWithoutHash,
|
||||||
"1";d.getElementById(f.id)&&d.body.removeChild(f);g();e&&setTimeout(b,0);clearTimeout(l);l=setTimeout(c,500)}function b(){n=10;g()}function c(){n+=1+2*Math.random();98<=n?n=98:l=setTimeout(c,500);g()}function g(){h.style[k]="translate("+n+"%)";d.getElementById(f.id)||d.body.appendChild(f)}function e(){d.getElementById(f.id)?(clearTimeout(l),n=100,g(),f.style.opacity="0"):(a(100==n?0:n),setTimeout(e,0))}function m(){f.style.left=pageXOffset+"px";f.style.width=innerWidth+"px";f.style.top=pageYOffset+
|
$urlToPreload,
|
||||||
"px";var a="orientation"in window&&90==Math.abs(orientation);f.style[k]="scaleY("+innerWidth/screen[a?"height":"width"]*2+")"}var f,h,k,n,l;return{init:function(){f=d.createElement("div");f.id="instantclick";h=d.createElement("div");h.id="instantclick-bar";h.className="instantclick-bar";f.appendChild(h);var a=["Webkit","Moz","O"];k="transform";if(!(k in h.style))for(var b=0;3>b;b++)a[b]+"Transform"in h.style&&(k=a[b]+"Transform");var c="transition";if(!(c in h.style))for(b=0;3>b;b++)a[b]+"Transition"in
|
$preloadTimer,
|
||||||
h.style&&(c="-"+a[b].toLowerCase()+"-"+c);a=d.createElement("style");a.innerHTML="#instantclick{position:"+(Q?"absolute":"fixed")+";top:0;left:0;width:100%;pointer-events:none;z-index:2147483647;"+c+":opacity .25s .1s}.instantclick-bar{background:#29d;width:100%;margin-left:-100%;height:2px;"+c+":all .25s}";d.head.appendChild(a);Q&&(m(),addEventListener("resize",m),addEventListener("scroll",m))},start:a,done:e}}(),R="pushState"in history&&(!I.match("Android")||I.match("Chrome/"))&&"file:"!=e.protocol;
|
$lastTouchTimestamp,
|
||||||
return{supported:R,init:function(){if(!k)if(R){for(var a=arguments.length-1;0<=a;a--){var b=arguments[a];!0===b?J=!0:"mousedown"==b?D=!0:"number"==typeof b&&(H=b)}k=w(e.href);h[k]={body:d.body,title:d.title,scrollY:pageYOffset};for(var b=d.head.children,c,a=b.length-1;0<=a;a--)c=b[a],c.hasAttribute("data-instant-track")&&(c=c.getAttribute("href")||c.getAttribute("src")||c.innerHTML,E.push(c));p=new XMLHttpRequest;p.addEventListener("readystatechange",W);L(!0);C.init();t("change",!0);addEventListener("popstate",
|
|
||||||
function(){var a=w(e.href);a!=k&&(a in h?(h[k].scrollY=pageYOffset,k=a,K(h[a].title,h[a].body,!1,h[a].scrollY)):e.href=e.href)})}else t("change",!0)},on:function(a,b){B[a].push(b)}}}(document,location);
|
// Preloading-related variables
|
||||||
|
$history = {},
|
||||||
|
$xhr,
|
||||||
|
$url = false,
|
||||||
|
$title = false,
|
||||||
|
$mustRedirect = false,
|
||||||
|
$body = false,
|
||||||
|
$timing = {},
|
||||||
|
$isPreloading = false,
|
||||||
|
$isWaitingForCompletion = false,
|
||||||
|
$trackedAssets = [],
|
||||||
|
|
||||||
|
// Variables defined by public functions
|
||||||
|
$useWhitelist,
|
||||||
|
$preloadOnMousedown,
|
||||||
|
$delayBeforePreload,
|
||||||
|
$eventsCallbacks = {
|
||||||
|
fetch: [],
|
||||||
|
receive: [],
|
||||||
|
wait: [],
|
||||||
|
change: []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
////////// HELPERS //////////
|
||||||
|
|
||||||
|
|
||||||
|
function removeHash(url) {
|
||||||
|
var index = url.indexOf('#')
|
||||||
|
if (index < 0) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
return url.substr(0, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLinkTarget(target) {
|
||||||
|
while (target && target.nodeName != 'A') {
|
||||||
|
target = target.parentNode
|
||||||
|
}
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBlacklisted(elem) {
|
||||||
|
do {
|
||||||
|
if (!elem.hasAttribute) { // Parent of <html>
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (elem.hasAttribute('data-instant')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (elem.hasAttribute('data-no-instant')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (elem = elem.parentNode);
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWhitelisted(elem) {
|
||||||
|
do {
|
||||||
|
if (!elem.hasAttribute) { // Parent of <html>
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (elem.hasAttribute('data-no-instant')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (elem.hasAttribute('data-instant')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (elem = elem.parentNode);
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPreloadable(a) {
|
||||||
|
var domain = location.protocol + '//' + location.host
|
||||||
|
|
||||||
|
if (a.target // target="_blank" etc.
|
||||||
|
|| a.hasAttribute('download')
|
||||||
|
|| a.href.indexOf(domain + '/') != 0 // Another domain, or no href attribute
|
||||||
|
|| (a.href.indexOf('#') > -1
|
||||||
|
&& removeHash(a.href) == $currentLocationWithoutHash) // Anchor
|
||||||
|
|| ($useWhitelist
|
||||||
|
? !isWhitelisted(a)
|
||||||
|
: isBlacklisted(a))
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerPageEvent(eventType, arg1, arg2, arg3) {
|
||||||
|
var returnValue = false
|
||||||
|
for (var i = 0; i < $eventsCallbacks[eventType].length; i++) {
|
||||||
|
if (eventType == 'receive') {
|
||||||
|
var altered = $eventsCallbacks[eventType][i](arg1, arg2, arg3)
|
||||||
|
if (altered) {
|
||||||
|
/* Update args for the next iteration of the loop. */
|
||||||
|
if ('body' in altered) {
|
||||||
|
arg2 = altered.body
|
||||||
|
}
|
||||||
|
if ('title' in altered) {
|
||||||
|
arg3 = altered.title
|
||||||
|
}
|
||||||
|
|
||||||
|
returnValue = altered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$eventsCallbacks[eventType][i](arg1, arg2, arg3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePage(title, body, newUrl, scrollY) {
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (newUrl) {
|
||||||
|
history.pushState(null, null, newUrl)
|
||||||
|
|
||||||
|
var hashIndex = newUrl.indexOf('#'),
|
||||||
|
hashElem = hashIndex > -1
|
||||||
|
&& document.getElementById(newUrl.substr(hashIndex + 1)),
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
if (hashElem) {
|
||||||
|
while (hashElem.offsetParent) {
|
||||||
|
offset += hashElem.offsetTop
|
||||||
|
|
||||||
|
hashElem = hashElem.offsetParent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scrollTo(0, offset)
|
||||||
|
|
||||||
|
$currentLocationWithoutHash = removeHash(newUrl)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
scrollTo(0, scrollY)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isChromeForIOS && document.title == title) {
|
||||||
|
/* Chrome for iOS:
|
||||||
|
*
|
||||||
|
* 1. Removes title on pushState, so the title needs to be set after.
|
||||||
|
*
|
||||||
|
* 2. Will not set the title if it’s identical when trimmed, so
|
||||||
|
* appending a space won't do, but a non-breaking space works.
|
||||||
|
*/
|
||||||
|
document.title = title + String.fromCharCode(160)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
instantanize()
|
||||||
|
bar.done()
|
||||||
|
triggerPageEvent('change', false)
|
||||||
|
|
||||||
|
// Real event, useful for combining userscripts, but only for that so it’s undocumented.
|
||||||
|
var userscriptEvent = document.createEvent('HTMLEvents')
|
||||||
|
userscriptEvent.initEvent('instantclick:newpage', true, true)
|
||||||
|
dispatchEvent(userscriptEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
////////// EVENT HANDLERS //////////
|
||||||
|
|
||||||
|
|
||||||
|
function mousedown(e) {
|
||||||
|
if ($lastTouchTimestamp > (+new Date - 500)) {
|
||||||
|
return // Otherwise, click doesn’t fire
|
||||||
|
}
|
||||||
|
|
||||||
|
var a = getLinkTarget(e.target)
|
||||||
|
|
||||||
|
if (!a || !isPreloadable(a)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
preload(a.href)
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseover(e) {
|
||||||
|
if ($lastTouchTimestamp > (+new Date - 500)) {
|
||||||
|
return // Otherwise, click doesn’t fire
|
||||||
|
}
|
||||||
|
|
||||||
|
var a = getLinkTarget(e.target)
|
||||||
|
|
||||||
|
if (!a || !isPreloadable(a)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.addEventListener('mouseout', mouseout)
|
||||||
|
|
||||||
|
if (!$delayBeforePreload) {
|
||||||
|
preload(a.href)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$urlToPreload = a.href
|
||||||
|
$preloadTimer = setTimeout(preload, $delayBeforePreload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function touchstart(e) {
|
||||||
|
$lastTouchTimestamp = +new Date
|
||||||
|
|
||||||
|
var a = getLinkTarget(e.target)
|
||||||
|
|
||||||
|
if (!a || !isPreloadable(a)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($preloadOnMousedown) {
|
||||||
|
a.removeEventListener('mousedown', mousedown)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
a.removeEventListener('mouseover', mouseover)
|
||||||
|
}
|
||||||
|
preload(a.href)
|
||||||
|
}
|
||||||
|
|
||||||
|
function click(e) {
|
||||||
|
var a = getLinkTarget(e.target)
|
||||||
|
|
||||||
|
if (!a || !isPreloadable(a)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.which > 1 || e.metaKey || e.ctrlKey) { // Opening in new tab
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
display(a.href)
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseout() {
|
||||||
|
if ($preloadTimer) {
|
||||||
|
clearTimeout($preloadTimer)
|
||||||
|
$preloadTimer = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$isPreloading || $isWaitingForCompletion) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$xhr.abort()
|
||||||
|
setPreloadingAsHalted()
|
||||||
|
}
|
||||||
|
|
||||||
|
function readystatechange() {
|
||||||
|
if ($xhr.readyState < 4) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ($xhr.status == 0) {
|
||||||
|
/* Request aborted */
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$timing.ready = +new Date - $timing.start
|
||||||
|
|
||||||
|
if ($xhr.getResponseHeader('Content-Type').match(/\/(x|ht|xht)ml/)) {
|
||||||
|
var doc = document.implementation.createHTMLDocument('')
|
||||||
|
doc.documentElement.innerHTML = removeNoscriptTags($xhr.responseText)
|
||||||
|
$title = doc.title
|
||||||
|
$body = doc.body
|
||||||
|
|
||||||
|
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,
|
||||||
|
scrollY: urlWithoutHash in $history ? $history[urlWithoutHash].scrollY : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var elems = doc.head.children,
|
||||||
|
found = 0,
|
||||||
|
elem,
|
||||||
|
data
|
||||||
|
|
||||||
|
for (var i = elems.length - 1; i >= 0; i--) {
|
||||||
|
elem = elems[i]
|
||||||
|
if (elem.hasAttribute('data-instant-track')) {
|
||||||
|
data = elem.getAttribute('href') || elem.getAttribute('src') || elem.innerHTML
|
||||||
|
for (var j = $trackedAssets.length - 1; j >= 0; j--) {
|
||||||
|
if ($trackedAssets[j] == data) {
|
||||||
|
found++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found != $trackedAssets.length) {
|
||||||
|
$mustRedirect = true // Assets have changed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$mustRedirect = true // Not an HTML document
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isWaitingForCompletion) {
|
||||||
|
$isWaitingForCompletion = false
|
||||||
|
display($url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
////////// MAIN FUNCTIONS //////////
|
||||||
|
|
||||||
|
|
||||||
|
function instantanize(isInitializing) {
|
||||||
|
document.body.addEventListener('touchstart', touchstart, true)
|
||||||
|
if ($preloadOnMousedown) {
|
||||||
|
document.body.addEventListener('mousedown', mousedown, true)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.body.addEventListener('mouseover', mouseover, true)
|
||||||
|
}
|
||||||
|
document.body.addEventListener('click', click, true)
|
||||||
|
|
||||||
|
if (!isInitializing) {
|
||||||
|
var scripts = document.body.getElementsByTagName('script'),
|
||||||
|
script,
|
||||||
|
copy,
|
||||||
|
parentNode,
|
||||||
|
nextSibling
|
||||||
|
|
||||||
|
for (i = 0, j = scripts.length; i < j; i++) {
|
||||||
|
script = scripts[i]
|
||||||
|
if (script.hasAttribute('data-no-instant')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
copy = document.createElement('script')
|
||||||
|
if (script.src) {
|
||||||
|
copy.src = script.src
|
||||||
|
}
|
||||||
|
if (script.innerHTML) {
|
||||||
|
copy.innerHTML = script.innerHTML
|
||||||
|
}
|
||||||
|
parentNode = script.parentNode
|
||||||
|
nextSibling = script.nextSibling
|
||||||
|
parentNode.removeChild(script)
|
||||||
|
parentNode.insertBefore(copy, nextSibling)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function preload(url) {
|
||||||
|
if (!$preloadOnMousedown
|
||||||
|
&& 'display' in $timing
|
||||||
|
&& +new Date - ($timing.start + $timing.display) < 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 behavior 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 not start preloading if last display
|
||||||
|
occurred less than 100 ms ago. If they happen to click on the link,
|
||||||
|
they will be redirected.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ($preloadTimer) {
|
||||||
|
clearTimeout($preloadTimer)
|
||||||
|
$preloadTimer = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
url = $urlToPreload
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($isPreloading && (url == $url || $isWaitingForCompletion)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$isPreloading = true
|
||||||
|
$isWaitingForCompletion = false
|
||||||
|
|
||||||
|
$url = url
|
||||||
|
$body = false
|
||||||
|
$mustRedirect = false
|
||||||
|
$timing = {
|
||||||
|
start: +new Date
|
||||||
|
}
|
||||||
|
triggerPageEvent('fetch')
|
||||||
|
$xhr.open('GET', url)
|
||||||
|
$xhr.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
function display(url) {
|
||||||
|
if (!('display' in $timing)) {
|
||||||
|
$timing.display = +new Date - $timing.start
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
Safari/Chrome will trigger mousedown, mouseover, click (and others),
|
||||||
|
but when that happens we ignore mousedown/mouseover (otherwise click
|
||||||
|
doesn’t fire). Maybe there’s a way to make the click event fire, but
|
||||||
|
that’s not worth it as mousedown/over happen just 1ms before click
|
||||||
|
in this situation.
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
location.href = url
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
preload(url)
|
||||||
|
bar.start(0, true)
|
||||||
|
triggerPageEvent('wait')
|
||||||
|
$isWaitingForCompletion = true // Must be set *after* calling `preload`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ($isWaitingForCompletion) {
|
||||||
|
/* The user clicked on a link while a page 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.
|
||||||
|
*/
|
||||||
|
location.href = url
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ($mustRedirect) {
|
||||||
|
location.href = $url
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!$body) {
|
||||||
|
bar.start(0, true)
|
||||||
|
triggerPageEvent('wait')
|
||||||
|
$isWaitingForCompletion = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$history[$currentLocationWithoutHash].scrollY = pageYOffset
|
||||||
|
setPreloadingAsHalted()
|
||||||
|
changePage($title, $body, $url)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
////////// PROGRESS BAR FUNCTIONS //////////
|
||||||
|
|
||||||
|
|
||||||
|
var bar = function() {
|
||||||
|
var $barContainer,
|
||||||
|
$barElement,
|
||||||
|
$barTransformProperty,
|
||||||
|
$barProgress,
|
||||||
|
$barTimer
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
$barContainer = document.createElement('div')
|
||||||
|
$barContainer.id = 'instantclick'
|
||||||
|
$barElement = document.createElement('div')
|
||||||
|
$barElement.id = 'instantclick-bar'
|
||||||
|
$barElement.className = 'instantclick-bar'
|
||||||
|
$barContainer.appendChild($barElement)
|
||||||
|
|
||||||
|
var vendors = ['Webkit', 'Moz', 'O']
|
||||||
|
|
||||||
|
$barTransformProperty = 'transform'
|
||||||
|
if (!($barTransformProperty in $barElement.style)) {
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
if (vendors[i] + 'Transform' in $barElement.style) {
|
||||||
|
$barTransformProperty = vendors[i] + 'Transform'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var transitionProperty = 'transition'
|
||||||
|
if (!(transitionProperty in $barElement.style)) {
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
if (vendors[i] + 'Transition' in $barElement.style) {
|
||||||
|
transitionProperty = '-' + vendors[i].toLowerCase() + '-' + transitionProperty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var style = document.createElement('style')
|
||||||
|
style.innerHTML = '#instantclick{position:' + ($hasTouch ? 'absolute' : 'fixed') + ';top:0;left:0;width:100%;pointer-events:none;z-index:2147483647;' + transitionProperty + ':opacity .25s .1s}'
|
||||||
|
+ '.instantclick-bar{background:#29d;width:100%;margin-left:-100%;height:2px;' + transitionProperty + ':all .25s}'
|
||||||
|
/* We set the bar's background in `.instantclick-bar` so that it can be
|
||||||
|
overriden in CSS with `#instantclick-bar`, as IDs have higher priority.
|
||||||
|
*/
|
||||||
|
document.head.appendChild(style)
|
||||||
|
|
||||||
|
if ($hasTouch) {
|
||||||
|
updatePositionAndScale()
|
||||||
|
addEventListener('resize', updatePositionAndScale)
|
||||||
|
addEventListener('scroll', updatePositionAndScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function start(at, jump) {
|
||||||
|
$barProgress = at
|
||||||
|
if (document.getElementById($barContainer.id)) {
|
||||||
|
document.body.removeChild($barContainer)
|
||||||
|
}
|
||||||
|
$barContainer.style.opacity = '1'
|
||||||
|
if (document.getElementById($barContainer.id)) {
|
||||||
|
document.body.removeChild($barContainer)
|
||||||
|
/* So there's no CSS animation if already done once and it goes from 1 to 0 */
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
if (jump) {
|
||||||
|
setTimeout(jumpStart, 0)
|
||||||
|
/* Must be done in a timer, otherwise the CSS animation doesn't happen. */
|
||||||
|
}
|
||||||
|
clearTimeout($barTimer)
|
||||||
|
$barTimer = setTimeout(inc, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
function jumpStart() {
|
||||||
|
$barProgress = 10
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
function inc() {
|
||||||
|
$barProgress += 1 + (Math.random() * 2)
|
||||||
|
if ($barProgress >= 98) {
|
||||||
|
$barProgress = 98
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$barTimer = setTimeout(inc, 500)
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
$barElement.style[$barTransformProperty] = 'translate(' + $barProgress + '%)'
|
||||||
|
if (!document.getElementById($barContainer.id)) {
|
||||||
|
document.body.appendChild($barContainer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function done() {
|
||||||
|
if (document.getElementById($barContainer.id)) {
|
||||||
|
clearTimeout($barTimer)
|
||||||
|
$barProgress = 100
|
||||||
|
update()
|
||||||
|
$barContainer.style.opacity = '0'
|
||||||
|
/* If you're debugging, setting this to 0.5 is handy. */
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The bar container hasn't been appended: It's a new page. */
|
||||||
|
start($barProgress == 100 ? 0 : $barProgress)
|
||||||
|
/* $barProgress is 100 on popstate, usually. */
|
||||||
|
setTimeout(done, 0)
|
||||||
|
/* Must be done in a timer, otherwise the CSS animation doesn't happen. */
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePositionAndScale() {
|
||||||
|
/* Adapted from code by Sam Stephenson and Mislav Marohnić
|
||||||
|
http://signalvnoise.com/posts/2407
|
||||||
|
*/
|
||||||
|
|
||||||
|
$barContainer.style.left = pageXOffset + 'px'
|
||||||
|
$barContainer.style.width = innerWidth + 'px'
|
||||||
|
$barContainer.style.top = pageYOffset + 'px'
|
||||||
|
|
||||||
|
var landscape = 'orientation' in window && Math.abs(orientation) == 90,
|
||||||
|
scaleY = innerWidth / screen[landscape ? 'height' : 'width'] * 2
|
||||||
|
/* We multiply the size by 2 because the progress bar is harder
|
||||||
|
to notice on a mobile device.
|
||||||
|
*/
|
||||||
|
$barContainer.style[$barTransformProperty] = 'scaleY(' + scaleY + ')'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init,
|
||||||
|
start: start,
|
||||||
|
done: done
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
|
||||||
|
////////// PUBLIC VARIABLE AND FUNCTIONS //////////
|
||||||
|
|
||||||
|
var supported = 'pushState' in history
|
||||||
|
&& (!$ua.match('Android') || $ua.match('Chrome/'))
|
||||||
|
&& location.protocol != "file:"
|
||||||
|
|
||||||
|
/* The state of Android's AOSP browsers:
|
||||||
|
|
||||||
|
2.3.7: pushState appears to work correctly, but
|
||||||
|
`doc.documentElement.innerHTML = body` is buggy.
|
||||||
|
See details here: http://stackoverflow.com/q/21918564
|
||||||
|
Not an issue anymore, but it may fail where 3.0 do, this needs
|
||||||
|
testing again.
|
||||||
|
|
||||||
|
3.0: pushState appears to work correctly (though the URL bar is only
|
||||||
|
updated on focus), but
|
||||||
|
`document.documentElement.replaceChild(doc.body, document.body)`
|
||||||
|
throws DOMException: WRONG_DOCUMENT_ERR.
|
||||||
|
|
||||||
|
4.0.2: Doesn't support pushState.
|
||||||
|
|
||||||
|
4.0.4,
|
||||||
|
4.1.1,
|
||||||
|
4.2,
|
||||||
|
4.3: pushState is here, but it doesn't update the URL bar.
|
||||||
|
(Great logic there.)
|
||||||
|
|
||||||
|
4.4: Works correctly. Claims to be 'Chrome/30.0.0.0'.
|
||||||
|
|
||||||
|
All androids tested with Android SDK's Emulator.
|
||||||
|
Version numbers are from the browser's user agent.
|
||||||
|
|
||||||
|
Because of this mess, the only whitelisted browser on Android is Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if ($currentLocationWithoutHash) {
|
||||||
|
/* Already initialized */
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!supported) {
|
||||||
|
triggerPageEvent('change', true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (var i = arguments.length - 1; i >= 0; i--) {
|
||||||
|
var arg = arguments[i]
|
||||||
|
if (arg === true) {
|
||||||
|
$useWhitelist = true
|
||||||
|
}
|
||||||
|
else if (arg == 'mousedown') {
|
||||||
|
$preloadOnMousedown = true
|
||||||
|
}
|
||||||
|
else if (typeof arg == 'number') {
|
||||||
|
$delayBeforePreload = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$currentLocationWithoutHash = removeHash(location.href)
|
||||||
|
$history[$currentLocationWithoutHash] = {
|
||||||
|
body: document.body,
|
||||||
|
title: document.title,
|
||||||
|
scrollY: pageYOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
var elems = document.head.children,
|
||||||
|
elem,
|
||||||
|
data
|
||||||
|
for (var i = elems.length - 1; i >= 0; i--) {
|
||||||
|
elem = elems[i]
|
||||||
|
if (elem.hasAttribute('data-instant-track')) {
|
||||||
|
data = elem.getAttribute('href') || elem.getAttribute('src') || elem.innerHTML
|
||||||
|
/* We can't use just `elem.href` and `elem.src` because we can't
|
||||||
|
retrieve `href`s and `src`s from the Ajax response.
|
||||||
|
*/
|
||||||
|
$trackedAssets.push(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$xhr = new XMLHttpRequest()
|
||||||
|
$xhr.addEventListener('readystatechange', readystatechange)
|
||||||
|
|
||||||
|
instantanize(true)
|
||||||
|
|
||||||
|
bar.init()
|
||||||
|
|
||||||
|
triggerPageEvent('change', true)
|
||||||
|
|
||||||
|
addEventListener('popstate', function() {
|
||||||
|
var loc = removeHash(location.href)
|
||||||
|
if (loc == $currentLocationWithoutHash) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(loc in $history)) {
|
||||||
|
location.href = location.href
|
||||||
|
/* Reloads the page while using cache for scripts, styles and images,
|
||||||
|
unlike `location.reload()` */
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$history[$currentLocationWithoutHash].scrollY = pageYOffset
|
||||||
|
$currentLocationWithoutHash = loc
|
||||||
|
changePage($history[loc].title, $history[loc].body, false, $history[loc].scrollY)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function on(eventType, callback) {
|
||||||
|
$eventsCallbacks[eventType].push(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
supported: supported,
|
||||||
|
init: init,
|
||||||
|
on: on
|
||||||
|
}
|
||||||
|
|
||||||
|
}(document, location);
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
/* InstantClick 3.1.0 | (C) 2014 Alexandre Dieulot | http://instantclick.io/license */
|
||||||
|
var InstantClick=function(d,e){function w(a){var b=a.indexOf("#");return 0>b?a:a.substr(0,b)}function z(a){for(;a&&"A"!=a.nodeName;)a=a.parentNode;return a}function A(a){var b=e.protocol+"//"+e.host;if(!(b=a.target||a.hasAttribute("download")||0!=a.href.indexOf(b+"/")||-1<a.href.indexOf("#")&&w(a.href)==k)){if(J){a:{do{if(!a.hasAttribute)break;if(a.hasAttribute("data-no-instant"))break;if(a.hasAttribute("data-instant")){a=!0;break a}}while(a=a.parentNode);a=!1}a=!a}else a:{do{if(!a.hasAttribute)break;
|
||||||
|
if(a.hasAttribute("data-instant"))break;if(a.hasAttribute("data-no-instant")){a=!0;break a}}while(a=a.parentNode);a=!1}b=a}return b?!1:!0}function t(a,b,c,g){for(var d=!1,e=0;e<B[a].length;e++)if("receive"==a){var f=B[a][e](b,c,g);f&&("body"in f&&(c=f.body),"title"in f&&(g=f.title),d=f)}else B[a][e](b,c,g);return d}function K(a,b,c,g){d.documentElement.replaceChild(b,d.body);if(c){history.pushState(null,null,c);b=c.indexOf("#");b=-1<b&&d.getElementById(c.substr(b+1));g=0;if(b)for(;b.offsetParent;)g+=
|
||||||
|
b.offsetTop,b=b.offsetParent;scrollTo(0,g);k=w(c)}else scrollTo(0,g);d.title=S&&d.title==a?a+String.fromCharCode(160):a;L();C.done();t("change",!1);a=d.createEvent("HTMLEvents");a.initEvent("instantclick:newpage",!0,!0);dispatchEvent(a)}function M(a){G>+new Date-500||(a=z(a.target))&&A(a)&&x(a.href)}function N(a){G>+new Date-500||(a=z(a.target))&&A(a)&&(a.addEventListener("mouseout",T),H?(O=a.href,l=setTimeout(x,H)):x(a.href))}function U(a){G=+new Date;(a=z(a.target))&&A(a)&&(D?a.removeEventListener("mousedown",
|
||||||
|
M):a.removeEventListener("mouseover",N),x(a.href))}function V(a){var b=z(a.target);!b||!A(b)||1<a.which||a.metaKey||a.ctrlKey||(a.preventDefault(),P(b.href))}function T(){l?(clearTimeout(l),l=!1):v&&!m&&(p.abort(),m=v=!1)}function W(){if(!(4>p.readyState)&&0!=p.status){q.ready=+new Date-q.start;if(p.getResponseHeader("Content-Type").match(/\/(x|ht|xht)ml/)){var a=d.implementation.createHTMLDocument("");a.documentElement.innerHTML=p.responseText.replace(/<noscript[\s\S]+<\/noscript>/gi,"");y=a.title;
|
||||||
|
u=a.body;var b=t("receive",r,u,y);b&&("body"in b&&(u=b.body),"title"in b&&(y=b.title));b=w(r);h[b]={body:u,title:y,scrollY:b in h?h[b].scrollY:0};for(var a=a.head.children,b=0,c,g=a.length-1;0<=g;g--)if(c=a[g],c.hasAttribute("data-instant-track")){c=c.getAttribute("href")||c.getAttribute("src")||c.innerHTML;for(var e=E.length-1;0<=e;e--)E[e]==c&&b++}b!=E.length&&(F=!0)}else F=!0;m&&(m=!1,P(r))}}function L(a){d.body.addEventListener("touchstart",U,!0);D?d.body.addEventListener("mousedown",M,!0):d.body.addEventListener("mouseover",
|
||||||
|
N,!0);d.body.addEventListener("click",V,!0);if(!a){a=d.body.getElementsByTagName("script");var b,c,g,e;i=0;for(j=a.length;i<j;i++)b=a[i],b.hasAttribute("data-no-instant")||(c=d.createElement("script"),b.src&&(c.src=b.src),b.innerHTML&&(c.innerHTML=b.innerHTML),g=b.parentNode,e=b.nextSibling,g.removeChild(b),g.insertBefore(c,e))}}function x(a){!D&&"display"in q&&100>+new Date-(q.start+q.display)||(l&&(clearTimeout(l),l=!1),a||(a=O),v&&(a==r||m))||(v=!0,m=!1,r=a,F=u=!1,q={start:+new Date},t("fetch"),
|
||||||
|
p.open("GET",a),p.send())}function P(a){"display"in q||(q.display=+new Date-q.start);l||!v?l&&r&&r!=a?e.href=a:(x(a),C.start(0,!0),t("wait"),m=!0):m?e.href=a:F?e.href=r:u?(h[k].scrollY=pageYOffset,m=v=!1,K(y,u,r)):(C.start(0,!0),t("wait"),m=!0)}var I=navigator.userAgent,S=-1<I.indexOf(" CriOS/"),Q="createTouch"in d,k,O,l,G,h={},p,r=!1,y=!1,F=!1,u=!1,q={},v=!1,m=!1,E=[],J,D,H,B={fetch:[],receive:[],wait:[],change:[]},C=function(){function a(a,e){n=a;d.getElementById(f.id)&&d.body.removeChild(f);f.style.opacity=
|
||||||
|
"1";d.getElementById(f.id)&&d.body.removeChild(f);g();e&&setTimeout(b,0);clearTimeout(l);l=setTimeout(c,500)}function b(){n=10;g()}function c(){n+=1+2*Math.random();98<=n?n=98:l=setTimeout(c,500);g()}function g(){h.style[k]="translate("+n+"%)";d.getElementById(f.id)||d.body.appendChild(f)}function e(){d.getElementById(f.id)?(clearTimeout(l),n=100,g(),f.style.opacity="0"):(a(100==n?0:n),setTimeout(e,0))}function m(){f.style.left=pageXOffset+"px";f.style.width=innerWidth+"px";f.style.top=pageYOffset+
|
||||||
|
"px";var a="orientation"in window&&90==Math.abs(orientation);f.style[k]="scaleY("+innerWidth/screen[a?"height":"width"]*2+")"}var f,h,k,n,l;return{init:function(){f=d.createElement("div");f.id="instantclick";h=d.createElement("div");h.id="instantclick-bar";h.className="instantclick-bar";f.appendChild(h);var a=["Webkit","Moz","O"];k="transform";if(!(k in h.style))for(var b=0;3>b;b++)a[b]+"Transform"in h.style&&(k=a[b]+"Transform");var c="transition";if(!(c in h.style))for(b=0;3>b;b++)a[b]+"Transition"in
|
||||||
|
h.style&&(c="-"+a[b].toLowerCase()+"-"+c);a=d.createElement("style");a.innerHTML="#instantclick{position:"+(Q?"absolute":"fixed")+";top:0;left:0;width:100%;pointer-events:none;z-index:2147483647;"+c+":opacity .25s .1s}.instantclick-bar{background:#29d;width:100%;margin-left:-100%;height:2px;"+c+":all .25s}";d.head.appendChild(a);Q&&(m(),addEventListener("resize",m),addEventListener("scroll",m))},start:a,done:e}}(),R="pushState"in history&&(!I.match("Android")||I.match("Chrome/"))&&"file:"!=e.protocol;
|
||||||
|
return{supported:R,init:function(){if(!k)if(R){for(var a=arguments.length-1;0<=a;a--){var b=arguments[a];!0===b?J=!0:"mousedown"==b?D=!0:"number"==typeof b&&(H=b)}k=w(e.href);h[k]={body:d.body,title:d.title,scrollY:pageYOffset};for(var b=d.head.children,c,a=b.length-1;0<=a;a--)c=b[a],c.hasAttribute("data-instant-track")&&(c=c.getAttribute("href")||c.getAttribute("src")||c.innerHTML,E.push(c));p=new XMLHttpRequest;p.addEventListener("readystatechange",W);L(!0);C.init();t("change",!0);addEventListener("popstate",
|
||||||
|
function(){var a=w(e.href);a!=k&&(a in h?(h[k].scrollY=pageYOffset,k=a,K(h[a].title,h[a].body,!1,h[a].scrollY)):e.href=e.href)})}else t("change",!0)},on:function(a,b){B[a].push(b)}}}(document,location);
|
Loading…
Reference in New Issue