Last active
August 29, 2015 14:11
-
-
Save thibaudcolas/1c6ea5c6b87b623aa217 to your computer and use it in GitHub Desktop.
UMD-wrapped InstantClick, bundled with browserify for the Web. Test (beware broken links between the two files): https://rawgit.com/ThibWeb/1c6ea5c6b87b623aa217/raw/caca7eadff5f5ee319dac62c9a94d847cf22e931/instantclick-browserify-1.html
This file contains hidden or 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
| <!doctype html> | |
| <html class="no-js" lang=""> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
| <title>#1 Hello world!</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| </head> | |
| <body> | |
| <p>#1 Hello world!</p> | |
| <a href="https://rawgit.com/ThibWeb/1c6ea5c6b87b623aa217/raw/fc030eba3d6635183343e8f13f3f4a8f0c8625e7/instantclick-browserify-2.html">Link to page #2</a> | |
| <script> | |
| (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ | |
| /* InstantClick 3.0.1 | (C) 2014 Alexandre Dieulot | http://instantclick.io/license */ | |
| (function(root, factory) { | |
| if (typeof define === 'function' && define.amd) { | |
| define(['InstantClick'], factory(root.document, root.location)); | |
| } else if (typeof exports === 'object') { | |
| module.exports = factory(root.document, root.location); | |
| } else { | |
| root.InstantClick = factory(root.document, root.location); | |
| } | |
| }(window || this, function(document, location) { | |
| // Internal variables | |
| var $ua = navigator.userAgent, | |
| $isChromeForIOS = $ua.indexOf(' CriOS/') > -1, | |
| $hasTouch = 'createTouch' in document, | |
| $currentLocationWithoutHash, | |
| $urlToPreload, | |
| $preloadTimer, | |
| // 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.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 triggerPageEvent(eventType, arg1) { | |
| for (var i = 0; i < $eventsCallbacks[eventType].length; i++) { | |
| $eventsCallbacks[eventType][i](arg1) | |
| } | |
| /* The `change` event takes one boolean argument: "isInitialLoad" */ | |
| } | |
| 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) | |
| } | |
| 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) { | |
| preload(getLinkTarget(e.target).href) | |
| } | |
| function mouseover(e) { | |
| var a = getLinkTarget(e.target) | |
| a.addEventListener('mouseout', mouseout) | |
| if (!$delayBeforePreload) { | |
| preload(a.href) | |
| } | |
| else { | |
| $urlToPreload = a.href | |
| $preloadTimer = setTimeout(preload, $delayBeforePreload) | |
| } | |
| } | |
| function touchstart(e) { | |
| var a = getLinkTarget(e.target) | |
| if ($preloadOnMousedown) { | |
| a.removeEventListener('mousedown', mousedown) | |
| } | |
| else { | |
| a.removeEventListener('mouseover', mouseover) | |
| } | |
| preload(a.href) | |
| } | |
| function click(e) { | |
| if (e.which > 1 || e.metaKey || e.ctrlKey) { // Opening in new tab | |
| return | |
| } | |
| e.preventDefault() | |
| display(getLinkTarget(e.target).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 | |
| triggerPageEvent('receive') | |
| 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 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) { | |
| var as = document.getElementsByTagName('a'), | |
| a, | |
| domain = location.protocol + '//' + location.host | |
| for (var i = as.length - 1; i >= 0; i--) { | |
| a = as[i] | |
| 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)) | |
| ) { | |
| continue | |
| } | |
| a.addEventListener('touchstart', touchstart) | |
| if ($preloadOnMousedown) { | |
| a.addEventListener('mousedown', mousedown) | |
| } | |
| else { | |
| a.addEventListener('mouseover', mouseover) | |
| } | |
| a.addEventListener('click', click) | |
| } | |
| 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) { | |
| /* Happens when there’s a delay before preloading and that delay | |
| hasn't expired (preloading didn't kick in). | |
| */ | |
| if ($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 | |
| return | |
| } | |
| if (!$isPreloading || $isWaitingForCompletion) { | |
| /* If the page isn't preloaded, it likely means the user has focused | |
| on a link (with his Tab key) and then pressed Return, which | |
| triggered a click. | |
| Because very few people do this, it isn't worth handling this case | |
| and preloading on focus (also, focusing on a link doesn't mean it's | |
| likely that you'll "click" on it), so we just redirect them when | |
| they "click". | |
| It could also mean the user hovered over a link less than 100 ms | |
| after a page display, thus we didn't start the preload (see | |
| comments in `preload()` for the rationale behind this.) | |
| If the page is waiting for completion, the user clicked twice while | |
| the 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), so we send him to the page the old way. | |
| If it's another link, it hasn't been preloaded, so we redirect the | |
| user the old way. | |
| */ | |
| 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 | |
| } | |
| })); | |
| },{}],2:[function(require,module,exports){ | |
| var InstantClick = require('./instantclick'); | |
| console.log(InstantClick); | |
| InstantClick.init(); | |
| },{"./instantclick":1}]},{},[2]); | |
| </script> | |
| </body> | |
| </html> |
This file contains hidden or 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
| <!doctype html> | |
| <html class="no-js" lang=""> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
| <title>#2 Hello world!</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| </head> | |
| <body> | |
| <p>#2 Hello world!</p> | |
| <a href="https://rawgit.com/ThibWeb/1c6ea5c6b87b623aa217/raw/e78fd22e04859624f510702df5e6e13373e81d82/instantclick-browserify-1.html">Link to page #1</a> | |
| <script> | |
| (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ | |
| /* InstantClick 3.0.1 | (C) 2014 Alexandre Dieulot | http://instantclick.io/license */ | |
| (function(root, factory) { | |
| if (typeof define === 'function' && define.amd) { | |
| define(['InstantClick'], factory(root.document, root.location)); | |
| } else if (typeof exports === 'object') { | |
| module.exports = factory(root.document, root.location); | |
| } else { | |
| root.InstantClick = factory(root.document, root.location); | |
| } | |
| }(window || this, function(document, location) { | |
| // Internal variables | |
| var $ua = navigator.userAgent, | |
| $isChromeForIOS = $ua.indexOf(' CriOS/') > -1, | |
| $hasTouch = 'createTouch' in document, | |
| $currentLocationWithoutHash, | |
| $urlToPreload, | |
| $preloadTimer, | |
| // 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.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 triggerPageEvent(eventType, arg1) { | |
| for (var i = 0; i < $eventsCallbacks[eventType].length; i++) { | |
| $eventsCallbacks[eventType][i](arg1) | |
| } | |
| /* The `change` event takes one boolean argument: "isInitialLoad" */ | |
| } | |
| 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) | |
| } | |
| 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) { | |
| preload(getLinkTarget(e.target).href) | |
| } | |
| function mouseover(e) { | |
| var a = getLinkTarget(e.target) | |
| a.addEventListener('mouseout', mouseout) | |
| if (!$delayBeforePreload) { | |
| preload(a.href) | |
| } | |
| else { | |
| $urlToPreload = a.href | |
| $preloadTimer = setTimeout(preload, $delayBeforePreload) | |
| } | |
| } | |
| function touchstart(e) { | |
| var a = getLinkTarget(e.target) | |
| if ($preloadOnMousedown) { | |
| a.removeEventListener('mousedown', mousedown) | |
| } | |
| else { | |
| a.removeEventListener('mouseover', mouseover) | |
| } | |
| preload(a.href) | |
| } | |
| function click(e) { | |
| if (e.which > 1 || e.metaKey || e.ctrlKey) { // Opening in new tab | |
| return | |
| } | |
| e.preventDefault() | |
| display(getLinkTarget(e.target).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 | |
| triggerPageEvent('receive') | |
| 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 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) { | |
| var as = document.getElementsByTagName('a'), | |
| a, | |
| domain = location.protocol + '//' + location.host | |
| for (var i = as.length - 1; i >= 0; i--) { | |
| a = as[i] | |
| 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)) | |
| ) { | |
| continue | |
| } | |
| a.addEventListener('touchstart', touchstart) | |
| if ($preloadOnMousedown) { | |
| a.addEventListener('mousedown', mousedown) | |
| } | |
| else { | |
| a.addEventListener('mouseover', mouseover) | |
| } | |
| a.addEventListener('click', click) | |
| } | |
| 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) { | |
| /* Happens when there’s a delay before preloading and that delay | |
| hasn't expired (preloading didn't kick in). | |
| */ | |
| if ($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 | |
| return | |
| } | |
| if (!$isPreloading || $isWaitingForCompletion) { | |
| /* If the page isn't preloaded, it likely means the user has focused | |
| on a link (with his Tab key) and then pressed Return, which | |
| triggered a click. | |
| Because very few people do this, it isn't worth handling this case | |
| and preloading on focus (also, focusing on a link doesn't mean it's | |
| likely that you'll "click" on it), so we just redirect them when | |
| they "click". | |
| It could also mean the user hovered over a link less than 100 ms | |
| after a page display, thus we didn't start the preload (see | |
| comments in `preload()` for the rationale behind this.) | |
| If the page is waiting for completion, the user clicked twice while | |
| the 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), so we send him to the page the old way. | |
| If it's another link, it hasn't been preloaded, so we redirect the | |
| user the old way. | |
| */ | |
| 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 | |
| } | |
| })); | |
| },{}],2:[function(require,module,exports){ | |
| var InstantClick = require('./instantclick'); | |
| console.log(InstantClick); | |
| InstantClick.init(); | |
| },{"./instantclick":1}]},{},[2]); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment