--- /dev/null
+/* InstantClick 2.1 | (C) 2014 Alexandre Dieulot | http://instantclick.io/license.html */
+var InstantClick = function(document, location) {
+ // Internal variables
+ var $currentLocationWithoutHash
+ var $urlToPreload
+ var $preloadTimer
+
+ // Preloading-related variables
+ var $history = {}
+ var $xhr
+ var $url = false
+ var $title = false
+ var $hasBody = true
+ var $body = false
+ var $timing = {}
+ var $isPreloading = false
+ var $isWaitingForCompletion = false
+
+ // Variables defined by public functions
+ var $useWhitelist
+ var $preloadOnMousedown
+ var $delayBeforePreload
+ var $eventsCallbacks = {
+ 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 triggerPageEvent(eventType) {
+ for (var i = 0; i < $eventsCallbacks[eventType].length; i++) {
+ $eventsCallbacks[eventType][i]()
+ }
+ }
+
+ function changePage(title, body, newUrl, scrollY_) {
+ var doc = document.implementation.createHTMLDocument('')
+ doc.documentElement.innerHTML = body
+ document.documentElement.replaceChild(doc.body, document.body)
+ /* We cannot just use `document.body = doc.body` as it causes Safari 5.1, 6.0,
+ and Mobile 7.0 to execute script tags directly.
+ */
+
+ var elem = document.createElement('i')
+ elem.innerHTML = title
+ document.title = elem.textContent
+
+ if (newUrl) {
+ history.pushState(null, null, newUrl)
+
+ var hashIndex = newUrl.indexOf('#')
+ var hashElem = hashIndex > -1 && document.getElementById(newUrl.substr(hashIndex + 1))
+ var offset = 0
+ if (hashElem) {
+ for (; hashElem.offsetParent; hashElem = hashElem.offsetParent) {
+ offset += hashElem.offsetTop
+ }
+ }
+ scrollTo(0, offset)
+
+ $currentLocationWithoutHash = removeHash(newUrl)
+ }
+ else {
+ scrollTo(0, scrollY_)
+ }
+
+ instantanize()
+
+ triggerPageEvent('change')
+ }
+
+ function setPreloadingAsHalted() {
+ $isPreloading = false
+ $isWaitingForCompletion = false
+ }
+
+
+ ////////// 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 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
+
+ var text = $xhr.responseText
+
+ var titleIndex = text.indexOf('<title')
+ if (titleIndex > -1) {
+ $title = text.substr(text.indexOf('>', titleIndex) + 1)
+ $title = $title.substr(0, $title.indexOf('</title'))
+ }
+
+ var bodyIndex = text.indexOf('<body')
+ if (bodyIndex > -1) {
+ $body = text.substr(bodyIndex)
+ var closingIndex = $body.indexOf('</body')
+ if (closingIndex > -1) {
+ $body = $body.substr(0, closingIndex)
+ }
+
+ var urlWithoutHash = removeHash($url)
+ $history[urlWithoutHash] = {
+ body: $body,
+ title: $title,
+ scrollY: urlWithoutHash in $history ? $history[urlWithoutHash].scrollY : 0
+ }
+ }
+ else {
+ $hasBody = false
+ }
+
+ 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 || // link to an anchor
+ ($useWhitelist ? !a.hasAttribute('data-instant') : a.hasAttribute('data-no-instant'))) {
+ continue
+ }
+ if ($preloadOnMousedown) {
+ a.addEventListener('mousedown', mousedown)
+ }
+ else {
+ a.addEventListener('mouseover', mouseover)
+ }
+ a.addEventListener('click', click)
+ }
+ if (!isInitializing) {
+ var scripts = document.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 (looks like it 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
+ $hasBody = true
+ $timing = {
+ start: +new Date
+ }
+ $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)
+ $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.
+ Two possibilities:
+ 1) He clicks on the same link again, either because it's slow
+ to load (there's no browser loading indicator with
+ InstantClick, so he might think his click hasn't registered
+ if the page isn't loading fast enough) or because he has
+ a habit of double clicking on the web;
+ 2) He clicks on another link.
+
+ In the first case, we redirect him (send him to the page the old
+ way) so that he can have the browser's loading indicator back.
+ In the second case, we redirect him because we haven't preloaded
+ that link, since we were already preloading the last one.
+
+ Determining if it's a double click might be overkill as there is
+ (hopefully) not that many people that double click on the web.
+ Fighting against the perception that the page is stuck is
+ interesting though, a seemingly good way to do that would be to
+ later incorporate a progress bar.
+ */
+
+ location.href = url
+ return
+ }
+ if (!$hasBody) {
+ location.href = $url
+ return
+ }
+ if (!$body) {
+ $isWaitingForCompletion = true
+ return
+ }
+ $history[$currentLocationWithoutHash].scrollY = pageYOffset
+ setPreloadingAsHalted()
+ changePage($title, $body, $url)
+ }
+
+
+ ////////// PUBLIC VARIABLE AND FUNCTIONS //////////
+
+
+ var supported = 'pushState' in history
+
+ function init() {
+ if ($currentLocationWithoutHash) {
+ /* Already initialized */
+ return
+ }
+ if (!supported) {
+ triggerPageEvent('change')
+ 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.outerHTML,
+ title: document.title,
+ scrollY: pageYOffset
+ }
+ $xhr = new XMLHttpRequest()
+ $xhr.addEventListener('readystatechange', readystatechange)
+
+ instantanize(true)
+
+ triggerPageEvent('change')
+
+ addEventListener('popstate', function() {
+ var loc = removeHash(location.href)
+ if (loc == $currentLocationWithoutHash) {
+ return
+ }
+ if (!(loc in $history)) {
+ location.href = location.href // Reloads the page and makes use of cache for assets, 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)
+ }
+
+ /* The debug function isn't included by default to reduce file size.
+ To enable it, add a slash at the beginning of the comment englobing
+ the debug function, and uncomment "debug: debug," in the return
+ statement below the function. */
+
+ /*
+ function debug() {
+ return {
+ currentLocationWithoutHash: $currentLocationWithoutHash,
+ history: $history,
+ xhr: $xhr,
+ url: $url,
+ title: $title,
+ hasBody: $hasBody,
+ body: $body,
+ timing: $timing,
+ isPreloading: $isPreloading,
+ isWaitingForCompletion: $isWaitingForCompletion
+ }
+ }
+ //*/
+
+
+ return {
+ // debug: debug,
+ supported: supported,
+ init: init,
+ on: on
+ }
+
+}(document, location);