e5e4e3447d344675e1e27631a6ea2fc83a78c362
[odoo/odoo.git] / addons / website_instantclick / static / lib / instantclick / instantclick.js
1 /* InstantClick 2.1 | (C) 2014 Alexandre Dieulot | http://instantclick.io/license.html */
2 var InstantClick = function(document, location) {
3         // Internal variables
4         var $currentLocationWithoutHash
5         var $urlToPreload
6         var $preloadTimer
7
8         // Preloading-related variables
9         var $history = {}
10         var $xhr
11         var $url = false
12         var $title = false
13         var $hasBody = true
14         var $body = false
15         var $timing = {}
16         var $isPreloading = false
17         var $isWaitingForCompletion = false
18
19         // Variables defined by public functions
20         var $useWhitelist
21         var $preloadOnMousedown
22         var $delayBeforePreload
23         var $eventsCallbacks = {
24                 change: []
25         }
26
27
28         ////////// HELPERS //////////
29
30
31         function removeHash(url) {
32                 var index = url.indexOf('#')
33                 if (index < 0) {
34                         return url
35                 }
36                 return url.substr(0, index)
37         }
38
39         function getLinkTarget(target) {
40                 while (target.nodeName != 'A') {
41                         target = target.parentNode
42                 }
43                 return target
44         }
45
46         function triggerPageEvent(eventType) {
47                 for (var i = 0; i < $eventsCallbacks[eventType].length; i++) {
48                         $eventsCallbacks[eventType][i]()
49                 }
50         }
51
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.
58                 */
59
60                 var elem = document.createElement('i')
61                 elem.innerHTML = title
62                 document.title = elem.textContent
63
64                 if (newUrl) {
65                         history.pushState(null, null, newUrl)
66
67                         var hashIndex = newUrl.indexOf('#')
68                         var hashElem = hashIndex > -1 && document.getElementById(newUrl.substr(hashIndex + 1))
69                         var offset = 0
70                         if (hashElem) {
71                                 for (; hashElem.offsetParent; hashElem = hashElem.offsetParent) {
72                                         offset += hashElem.offsetTop
73                                 }
74                         }
75                         scrollTo(0, offset)
76
77                         $currentLocationWithoutHash = removeHash(newUrl)
78                 }
79                 else {
80                         scrollTo(0, scrollY_)
81                 }
82
83                 instantanize()
84
85                 triggerPageEvent('change')
86         }
87
88         function setPreloadingAsHalted() {
89                 $isPreloading = false
90                 $isWaitingForCompletion = false
91         }
92
93
94         ////////// EVENT HANDLERS //////////
95
96
97         function mousedown(e) {
98                 preload(getLinkTarget(e.target).href)
99         }
100
101         function mouseover(e) {
102                 var a = getLinkTarget(e.target)
103                 a.addEventListener('mouseout', mouseout)
104                 if (!$delayBeforePreload) {
105                         preload(a.href)
106                 }
107                 else {
108                         $urlToPreload = a.href
109                         $preloadTimer = setTimeout(preload, $delayBeforePreload)
110                 }
111         }
112
113         function click(e) {
114                 if (e.which > 1 || e.metaKey || e.ctrlKey) { // Opening in new tab
115                         return
116                 }
117                 e.preventDefault()
118                 display(getLinkTarget(e.target).href)
119         }
120
121         function mouseout() {
122                 if ($preloadTimer) {
123                         clearTimeout($preloadTimer)
124                         $preloadTimer = false
125                         return
126                 }
127
128                 if (!$isPreloading || $isWaitingForCompletion) {
129                         return
130                 }
131                 $xhr.abort()
132                 setPreloadingAsHalted()
133         }
134
135         function readystatechange() {
136                 if ($xhr.readyState < 4) {
137                         return
138                 }
139                 if ($xhr.status == 0) {
140                         /* Request aborted */
141                         return
142                 }
143
144                 $timing.ready = +new Date - $timing.start
145
146                 var text = $xhr.responseText
147
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'))
152                 }
153
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)
160                         }
161
162                         var urlWithoutHash = removeHash($url)
163                         $history[urlWithoutHash] = {
164                                 body: $body,
165                                 title: $title,
166                                 scrollY: urlWithoutHash in $history ? $history[urlWithoutHash].scrollY : 0
167                         }
168                 }
169                 else {
170                         $hasBody = false
171                 }
172
173                 if ($isWaitingForCompletion) {
174                         $isWaitingForCompletion = false
175                         display($url)
176                 }
177         }
178
179
180         ////////// MAIN FUNCTIONS //////////
181
182
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--) {
186                         a = as[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'))) {
192                                 continue
193                         }
194                         if ($preloadOnMousedown) {
195                                 a.addEventListener('mousedown', mousedown)
196                         }
197                         else {
198                                 a.addEventListener('mouseover', mouseover)
199                         }
200                         a.addEventListener('click', click)
201                 }
202                 if (!isInitializing) {
203                         var scripts = document.getElementsByTagName('script'), script, copy, parentNode, nextSibling
204                         for (i = 0, j = scripts.length; i < j; i++) {
205                                 script = scripts[i]
206                                 if (script.hasAttribute('data-no-instant')) {
207                                         continue
208                                 }
209                                 copy = document.createElement('script')
210                                 if (script.src) {
211                                         copy.src = script.src
212                                 }
213                                 if (script.innerHTML) {
214                                         copy.innerHTML = script.innerHTML
215                                 }
216                                 parentNode = script.parentNode
217                                 nextSibling = script.nextSibling
218                                 parentNode.removeChild(script)
219                                 parentNode.insertBefore(copy, nextSibling)
220                         }
221                 }
222         }
223
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.
229
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
235
236                            - Chrome: triggers when cursor moved
237                            - Opera 12.16: triggers when cursor moved
238
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.
241                         */
242
243                         return
244                 }
245                 if ($preloadTimer) {
246                         $clearTimeout($preloadTimer)
247                         $preloadTimer = false
248                 }
249
250                 if (!url) {
251                         url = $urlToPreload
252                 }
253
254                 if ($isPreloading && (url == $url || $isWaitingForCompletion)) {
255                         return
256                 }
257                 $isPreloading = true
258                 $isWaitingForCompletion = false
259
260                 $url = url
261                 $body = false
262                 $hasBody = true
263                 $timing = {
264                         start: +new Date
265                 }
266                 $xhr.open('GET', url)
267                 $xhr.send()
268         }
269
270         function display(url) {
271                 if (!('display' in $timing)) {
272                         $timing.display = +new Date - $timing.start
273                 }
274                 if ($preloadTimer) {
275                         /* Happens when there’s a delay before preloading and that delay
276                            hasn't expired (preloading didn't kick in).
277                         */
278
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.
282                                 */
283
284                                 location.href = url
285                                 return
286                         }
287                         preload(url)
288                         $isWaitingForCompletion = true
289                         return
290                 }
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.)
302
303                            If the page is waiting for completion, the user clicked twice
304                            while the page was preloading.
305                            Two possibilities:
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.
312
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.
317
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.
323                         */
324
325                         location.href = url
326                         return
327                 }
328                 if (!$hasBody) {
329                         location.href = $url
330                         return
331                 }
332                 if (!$body) {
333                         $isWaitingForCompletion = true
334                         return
335                 }
336                 $history[$currentLocationWithoutHash].scrollY = pageYOffset
337                 setPreloadingAsHalted()
338                 changePage($title, $body, $url)
339         }
340
341
342         ////////// PUBLIC VARIABLE AND FUNCTIONS //////////
343
344
345         var supported = 'pushState' in history
346
347         function init() {
348                 if ($currentLocationWithoutHash) {
349                         /* Already initialized */
350                         return
351                 }
352                 if (!supported) {
353                         triggerPageEvent('change')
354                         return
355                 }
356                 for (var i = arguments.length - 1; i >= 0; i--) {
357                         var arg = arguments[i]
358                         if (arg === true) {
359                                 $useWhitelist = true
360                         }
361                         else if (arg == 'mousedown') {
362                                 $preloadOnMousedown = true
363                         }
364                         else if (typeof arg == 'number') {
365                                 $delayBeforePreload = arg
366                         }
367                 }
368                 $currentLocationWithoutHash = removeHash(location.href)
369                 $history[$currentLocationWithoutHash] = {
370                         body: document.body.outerHTML,
371                         title: document.title,
372                         scrollY: pageYOffset
373                 }
374                 $xhr = new XMLHttpRequest()
375                 $xhr.addEventListener('readystatechange', readystatechange)
376
377                 instantanize(true)
378
379                 triggerPageEvent('change')
380
381                 addEventListener('popstate', function() {
382                         var loc = removeHash(location.href)
383                         if (loc == $currentLocationWithoutHash) {
384                                 return
385                         }
386                         if (!(loc in $history)) {
387                                 location.href = location.href // Reloads the page and makes use of cache for assets, unlike location.reload()
388                                 return
389                         }
390                         $history[$currentLocationWithoutHash].scrollY = pageYOffset
391                         $currentLocationWithoutHash = loc
392                         changePage($history[loc].title, $history[loc].body, false, $history[loc].scrollY)
393                 })
394         }
395
396         function on(eventType, callback) {
397                 $eventsCallbacks[eventType].push(callback)
398         }
399
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. */
404
405         /*
406         function debug() {
407                 return {
408                         currentLocationWithoutHash: $currentLocationWithoutHash,
409                         history: $history,
410                         xhr: $xhr,
411                         url: $url,
412                         title: $title,
413                         hasBody: $hasBody,
414                         body: $body,
415                         timing: $timing,
416                         isPreloading: $isPreloading,
417                         isWaitingForCompletion: $isWaitingForCompletion
418                 }
419         }
420         //*/
421
422
423         return {
424                 // debug: debug,
425                 supported: supported,
426                 init: init,
427                 on: on
428         }
429
430 }(document, location);