1 /* InstantClick 2.1 | (C) 2014 Alexandre Dieulot | http://instantclick.io/license.html */
2 var InstantClick = function(document, location) {
4 var $currentLocationWithoutHash
8 // Preloading-related variables
16 var $isPreloading = false
17 var $isWaitingForCompletion = false
19 // Variables defined by public functions
21 var $preloadOnMousedown
22 var $delayBeforePreload
23 var $eventsCallbacks = {
28 ////////// HELPERS //////////
31 function removeHash(url) {
32 var index = url.indexOf('#')
36 return url.substr(0, index)
39 function getLinkTarget(target) {
40 while (target.nodeName != 'A') {
41 target = target.parentNode
46 function triggerPageEvent(eventType) {
47 for (var i = 0; i < $eventsCallbacks[eventType].length; i++) {
48 $eventsCallbacks[eventType][i]()
52 function changePage(title, body, newUrl, scrollY_) {
53 var doc = document.implementation.createHTMLDocument('')
54 doc.documentElement.innerHTML = body
55 document.documentElement.replaceChild(doc.body, document.body)
56 /* We cannot just use `document.body = doc.body` as it causes Safari 5.1, 6.0,
57 and Mobile 7.0 to execute script tags directly.
60 var elem = document.createElement('i')
61 elem.innerHTML = title
62 document.title = elem.textContent
65 history.pushState(null, null, newUrl)
67 var hashIndex = newUrl.indexOf('#')
68 var hashElem = hashIndex > -1 && document.getElementById(newUrl.substr(hashIndex + 1))
71 for (; hashElem.offsetParent; hashElem = hashElem.offsetParent) {
72 offset += hashElem.offsetTop
77 $currentLocationWithoutHash = removeHash(newUrl)
85 triggerPageEvent('change')
88 function setPreloadingAsHalted() {
90 $isWaitingForCompletion = false
94 ////////// EVENT HANDLERS //////////
97 function mousedown(e) {
98 preload(getLinkTarget(e.target).href)
101 function mouseover(e) {
102 var a = getLinkTarget(e.target)
103 a.addEventListener('mouseout', mouseout)
104 if (!$delayBeforePreload) {
108 $urlToPreload = a.href
109 $preloadTimer = setTimeout(preload, $delayBeforePreload)
114 if (e.which > 1 || e.metaKey || e.ctrlKey) { // Opening in new tab
118 display(getLinkTarget(e.target).href)
121 function mouseout() {
123 clearTimeout($preloadTimer)
124 $preloadTimer = false
128 if (!$isPreloading || $isWaitingForCompletion) {
132 setPreloadingAsHalted()
135 function readystatechange() {
136 if ($xhr.readyState < 4) {
139 if ($xhr.status == 0) {
140 /* Request aborted */
144 $timing.ready = +new Date - $timing.start
146 var text = $xhr.responseText
148 var titleIndex = text.indexOf('<title')
149 if (titleIndex > -1) {
150 $title = text.substr(text.indexOf('>', titleIndex) + 1)
151 $title = $title.substr(0, $title.indexOf('</title'))
154 var bodyIndex = text.indexOf('<body')
155 if (bodyIndex > -1) {
156 $body = text.substr(bodyIndex)
157 var closingIndex = $body.indexOf('</body')
158 if (closingIndex > -1) {
159 $body = $body.substr(0, closingIndex)
162 var urlWithoutHash = removeHash($url)
163 $history[urlWithoutHash] = {
166 scrollY: urlWithoutHash in $history ? $history[urlWithoutHash].scrollY : 0
173 if ($isWaitingForCompletion) {
174 $isWaitingForCompletion = false
180 ////////// MAIN FUNCTIONS //////////
183 function instantanize(isInitializing) {
184 var as = document.getElementsByTagName('a'), a, domain = location.protocol + '//' + location.host
185 for (var i = as.length - 1; i >= 0; i--) {
187 if (a.target || // target="_blank" etc.
188 a.hasAttribute('download') ||
189 a.href.indexOf(domain + '/') != 0 || // another domain (or no href attribute)
190 a.href.indexOf('#') > -1 && removeHash(a.href) == $currentLocationWithoutHash || // link to an anchor
191 ($useWhitelist ? !a.hasAttribute('data-instant') : a.hasAttribute('data-no-instant'))) {
194 if ($preloadOnMousedown) {
195 a.addEventListener('mousedown', mousedown)
198 a.addEventListener('mouseover', mouseover)
200 a.addEventListener('click', click)
202 if (!isInitializing) {
203 var scripts = document.getElementsByTagName('script'), script, copy, parentNode, nextSibling
204 for (i = 0, j = scripts.length; i < j; i++) {
206 if (script.hasAttribute('data-no-instant')) {
209 copy = document.createElement('script')
211 copy.src = script.src
213 if (script.innerHTML) {
214 copy.innerHTML = script.innerHTML
216 parentNode = script.parentNode
217 nextSibling = script.nextSibling
218 parentNode.removeChild(script)
219 parentNode.insertBefore(copy, nextSibling)
224 function preload(url) {
225 if (!$preloadOnMousedown && 'display' in $timing && +new Date - ($timing.start + $timing.display) < 100) {
226 /* After a page is displayed, if the user's cursor happens to be above a link
227 a mouseover event will be in most browsers triggered automatically, and in
228 other browsers it will be triggered when the user moves his mouse by 1px.
230 Here are the behavior I noticed, all on Windows:
231 - Safari 5.1: auto-triggers after 0 ms
232 - IE 11: auto-triggers after 30-80 ms (looks like it depends on page's size)
233 - Firefox: auto-triggers after 10 ms
234 - Opera 18: auto-triggers after 10 ms
236 - Chrome: triggers when cursor moved
237 - Opera 12.16: triggers when cursor moved
239 To remedy to this, we do not start preloading if last display occurred less than
240 100 ms ago. If they happen to click on the link, they will be redirected.
246 $clearTimeout($preloadTimer)
247 $preloadTimer = false
254 if ($isPreloading && (url == $url || $isWaitingForCompletion)) {
258 $isWaitingForCompletion = false
266 $xhr.open('GET', url)
270 function display(url) {
271 if (!('display' in $timing)) {
272 $timing.display = +new Date - $timing.start
275 /* Happens when there’s a delay before preloading and that delay
276 hasn't expired (preloading didn't kick in).
279 if ($url && $url != url) {
280 /* Happens when the user clicks on a link before preloading
281 kicks in while another link is already preloading.
288 $isWaitingForCompletion = true
291 if (!$isPreloading || $isWaitingForCompletion) {
292 /* If the page isn't preloaded, it likely means
293 the user has focused on a link (with his Tab
294 key) and then pressed Return, which triggered a click.
295 Because very few people do this, it isn't worth handling this
296 case and preloading on focus (also, focusing on a link
297 doesn't mean it's likely that you'll "click" on it), so we just
298 redirect them when they "click".
299 It could also mean the user hovered over a link less than 100 ms
300 after a page display, thus we didn't start the preload (see
301 comments in `preload()` for the rationale behind this.)
303 If the page is waiting for completion, the user clicked twice
304 while the page was preloading.
306 1) He clicks on the same link again, either because it's slow
307 to load (there's no browser loading indicator with
308 InstantClick, so he might think his click hasn't registered
309 if the page isn't loading fast enough) or because he has
310 a habit of double clicking on the web;
311 2) He clicks on another link.
313 In the first case, we redirect him (send him to the page the old
314 way) so that he can have the browser's loading indicator back.
315 In the second case, we redirect him because we haven't preloaded
316 that link, since we were already preloading the last one.
318 Determining if it's a double click might be overkill as there is
319 (hopefully) not that many people that double click on the web.
320 Fighting against the perception that the page is stuck is
321 interesting though, a seemingly good way to do that would be to
322 later incorporate a progress bar.
333 $isWaitingForCompletion = true
336 $history[$currentLocationWithoutHash].scrollY = pageYOffset
337 setPreloadingAsHalted()
338 changePage($title, $body, $url)
342 ////////// PUBLIC VARIABLE AND FUNCTIONS //////////
345 var supported = 'pushState' in history
348 if ($currentLocationWithoutHash) {
349 /* Already initialized */
353 triggerPageEvent('change')
356 for (var i = arguments.length - 1; i >= 0; i--) {
357 var arg = arguments[i]
361 else if (arg == 'mousedown') {
362 $preloadOnMousedown = true
364 else if (typeof arg == 'number') {
365 $delayBeforePreload = arg
368 $currentLocationWithoutHash = removeHash(location.href)
369 $history[$currentLocationWithoutHash] = {
370 body: document.body.outerHTML,
371 title: document.title,
374 $xhr = new XMLHttpRequest()
375 $xhr.addEventListener('readystatechange', readystatechange)
379 triggerPageEvent('change')
381 addEventListener('popstate', function() {
382 var loc = removeHash(location.href)
383 if (loc == $currentLocationWithoutHash) {
386 if (!(loc in $history)) {
387 location.href = location.href // Reloads the page and makes use of cache for assets, unlike location.reload()
390 $history[$currentLocationWithoutHash].scrollY = pageYOffset
391 $currentLocationWithoutHash = loc
392 changePage($history[loc].title, $history[loc].body, false, $history[loc].scrollY)
396 function on(eventType, callback) {
397 $eventsCallbacks[eventType].push(callback)
400 /* The debug function isn't included by default to reduce file size.
401 To enable it, add a slash at the beginning of the comment englobing
402 the debug function, and uncomment "debug: debug," in the return
403 statement below the function. */
408 currentLocationWithoutHash: $currentLocationWithoutHash,
416 isPreloading: $isPreloading,
417 isWaitingForCompletion: $isWaitingForCompletion
425 supported: supported,
430 }(document, location);