-/* 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);