gitlab-turbolinks-classic 2.5.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fab1162c00d9fd06c7e9c32a5965e47f4ece1c9c
4
+ data.tar.gz: 639e8ef02772645c360cb292141f167c0a6625e8
5
+ SHA512:
6
+ metadata.gz: 535e9473c5fc40a49f6b03189d07b0ab9d27fa39def8ba9e7e84b0ddab2da90f644d0975cc28ebf80108261bc807ff066fc73784804be24ba943723faea5e063
7
+ data.tar.gz: dd9e8239d71ccb68590ae67e7e404ad27f2cf3ac0948e6f57317c4d42c4876adf79ede2a731270fb2bc5cdccd8f402aa13fbd19e097dc9f2ba82acb0457521e1
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2012-2014 David Heinemeier Hansson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,251 @@
1
+ Turbolinks Classic (GitLab fork)
2
+ ===========
3
+
4
+ Turbolinks 5 is a ground-up rewrite with a new flow, new events, but the same core idea. It's available at [turbolinks/turbolinks](https://github.com/turbolinks/turbolinks). Releases for v2.5 stalled while version 3 was in development, but valuable changes were made on the [master branch](https://github.com/turbolinks/turbolinks-classic/commits/master). This repository is a fork with some changes from Turbolinks 2/3 before the rewrite, with the potential to backport small fixes from version 5 if needed.
5
+
6
+ About Turbolinks
7
+ ----------------
8
+
9
+ Turbolinks makes following links in your web application faster. Instead of letting the browser recompile the JavaScript and CSS between each page change, it keeps the current page instance alive and replaces only the body and the title in the head. Think CGI vs persistent process.
10
+
11
+ This is similar to [pjax](https://github.com/defunkt/jquery-pjax), but instead of worrying about what element on the page to replace, and tailoring the server-side response to fit, we replace the entire body. This means that you get the bulk of the speed benefits from pjax (no recompiling of the JavaScript or CSS) without having to tailor the server-side response. It just works.
12
+
13
+ Do note that this of course means that you'll have a long-running, persistent session with maintained state. That's what's making it so fast. But it also means that you may have to pay additional care not to leak memory or otherwise bloat that long-running state. That should rarely be a problem unless you're doing something really funky, but you do have to be aware of it. Your memory leaking sins will not be swept away automatically by the cleansing page change any more.
14
+
15
+
16
+ How much faster is it really?
17
+ -----------------------------
18
+
19
+ It depends. The more CSS and JavaScript you have, the bigger the benefit of not throwing away the browser instance and recompiling all of it for every page. Just like a CGI script that says "hello world" will be fast, but a CGI script loading Rails on every request will not.
20
+
21
+ In any case, the benefit can be up to [twice as fast](https://github.com/steveklabnik/turbolinks_test/tree/all_the_assets) in apps with lots of JS and CSS. Of course, your mileage may vary, be dependent on your browser version, the moon cycle, and all other factors affecting performance testing. But at least it's a yardstick.
22
+
23
+ The best way to find out just how fast it is? Try it on your own application. It hardly takes any effort at all.
24
+
25
+
26
+ No jQuery or any other library
27
+ --------------------------------
28
+
29
+ Turbolinks is designed to be as light-weight as possible (so you won't think twice about using it even for mobile stuff). It does not require jQuery or any other library to work. But it works great _with_ the jQuery or Prototype framework, or whatever else you have.
30
+
31
+
32
+ Events
33
+ ------
34
+
35
+ With Turbolinks pages will change without a full reload, so you can't rely on `DOMContentLoaded` or `jQuery.ready()` to trigger your code. Instead Turbolinks fires events on `document` to provide hooks into the lifecycle of the page.
36
+
37
+ ***Load* a fresh version of a page from the server:**
38
+ * `page:before-change` a Turbolinks-enabled link has been clicked *(see below for more details)*
39
+ * `page:fetch` starting to fetch a new target page
40
+ * `page:receive` the page has been fetched from the server, but not yet parsed
41
+ * `page:before-unload` the page has been parsed and is about to be changed
42
+ * `page:change` the page has been changed to the new version (and on DOMContentLoaded)
43
+ * `page:update` is triggered alongside both page:change and jQuery's ajaxSuccess (if jQuery is available - otherwise you can manually trigger it when calling XMLHttpRequest in your own code)
44
+ * `page:load` is fired at the end of the loading process.
45
+
46
+ Handlers bound to the `page:before-change` event may return `false`, which will cancel the Turbolinks process.
47
+
48
+ By default, Turbolinks caches 10 of these page loads. It listens to the [popstate](https://developer.mozilla.org/en-US/docs/DOM/Manipulating_the_browser_history#The_popstate_event) event and attempts to restore page state from the cache when it's triggered. When `popstate` is fired the following process happens:
49
+
50
+ ***Restore* a cached page from the client-side cache:**
51
+ * `page:before-unload` page has been fetched from the cache and is about to be changed
52
+ * `page:change` page has changed to the cached page.
53
+ * `page:restore` is fired at the end of restore process.
54
+
55
+ The number of pages Turbolinks caches can be configured to suit your application's needs:
56
+
57
+ ```javascript
58
+ // View the current cache size
59
+ Turbolinks.pagesCached();
60
+
61
+ // Set the cache size
62
+ Turbolinks.pagesCached(20);
63
+ ```
64
+
65
+ If you need to make dynamic HTML updates in the current page and want it to be cached properly you can call:
66
+ ```javascript
67
+ Turbolinks.cacheCurrentPage();
68
+ ```
69
+
70
+ When a page is removed from the cache due to the cache reaching its size limit, the `page:expire` event is triggered. Listeners bound to this event can access the cached page object using `event.originalEvent.data`. Keys of note for this page cache object include `url`, `body`, and `title`.
71
+
72
+ To implement a client-side spinner, you could listen for `page:fetch` to start it and `page:receive` to stop it.
73
+
74
+ ```javascript
75
+ // using jQuery for simplicity
76
+
77
+ $(document).on("page:fetch", startSpinner);
78
+ $(document).on("page:receive", stopSpinner);
79
+ ```
80
+
81
+ DOM transformations that are idempotent are best. If you have transformations that are not, bind them to `page:load` (in addition to the initial page load) instead of `page:change` (as that would run them again on the cached pages):
82
+
83
+ ```javascript
84
+ // using jQuery for simplicity
85
+
86
+ $(document).on("ready page:load", nonIdempotentFunction);
87
+ ```
88
+
89
+ Transition Cache: A Speed Boost
90
+ -------------------------------
91
+
92
+ Transition Cache, added in v2.2.0, makes loading cached pages instantaneous. Once a user has visited a page, returning later to the page results in an instant load.
93
+
94
+ For example, if Page A is already cached by Turbolinks and you are on Page B, clicking a link to Page A will *immediately* display the cached copy of Page A. Turbolinks will then fetch Page A from the server and replace the cached page once the new copy is returned.
95
+
96
+ To enable Transition Cache, include the following in your javascript:
97
+ ```javascript
98
+ Turbolinks.enableTransitionCache();
99
+ ```
100
+
101
+ The one drawback is that dramatic differences in appearance between a cached copy and new copy may lead to a jarring affect for the end-user. This will be especially true for pages that have many moving parts (expandable sections, sortable tables, infinite scrolling, etc.).
102
+
103
+ If you find that a page is causing problems, you can have Turbolinks skip displaying the cached copy by adding `data-no-transition-cache` to any DOM element on the offending page.
104
+
105
+ Progress Bar
106
+ ------------
107
+
108
+ Because Turbolinks skips the traditional full page reload, browsers won't display their native progress bar when changing pages. To fill this void, Turbolinks offers an optional JavaScript-and-CSS-based progress bar to display page loading progress.
109
+
110
+ To enable the progress bar, include the following in your JavaScript:
111
+ ```javascript
112
+ Turbolinks.enableProgressBar();
113
+ ```
114
+
115
+ The progress bar is implemented on the `<html>` element's pseudo `:before` element and can be **customized** by including CSS with higher specificity than the included styles. For example:
116
+
117
+ ```css
118
+ html.turbolinks-progress-bar::before {
119
+ background-color: red !important;
120
+ height: 5px !important;
121
+ }
122
+ ```
123
+
124
+ In Turbolinks 3.0, the progress bar will be turned on by default.
125
+
126
+
127
+ Initialization
128
+ --------------
129
+
130
+ Turbolinks will be enabled **only** if the server has rendered a `GET` request.
131
+
132
+ Some examples, given a standard RESTful resource:
133
+
134
+ * `POST :create` => resource successfully created => redirect to `GET :show`
135
+ * Turbolinks **ENABLED**
136
+ * `POST :create` => resource creation failed => render `:new`
137
+ * Turbolinks **DISABLED**
138
+
139
+ **Why not all request types?** Some browsers track the request method of each page load, but triggering pushState methods don't change this value. This could lead to the situation where pressing the browser's reload button on a page that was fetched with Turbolinks would attempt a `POST` (or something other than `GET`) because the last full page load used that method.
140
+
141
+
142
+ Opting out of Turbolinks
143
+ ------------------------
144
+
145
+ By default, all internal HTML links will be funneled through Turbolinks, but you can opt out by marking links or their parent container with `data-no-turbolink`. For example, if you mark a div with `data-no-turbolink`, then all links inside of that div will be treated as regular links. If you mark the body, every link on that entire page will be treated as regular links.
146
+
147
+ ```html
148
+ <a href="/">Home (via Turbolinks)</a>
149
+ <div id="some-div" data-no-turbolink>
150
+ <a href="/">Home (without Turbolinks)</a>
151
+ </div>
152
+ ```
153
+
154
+ Note that internal links to files containing a file extension other than **.html** will automatically be opted out of Turbolinks. So links to /images/panda.gif will just work as expected. To whitelist additional file extensions to be processed by Turbolinks, use `Turbolinks.allowLinkExtensions()`.
155
+
156
+ ```javascript
157
+ Turbolinks.allowLinkExtensions(); // => ['html']
158
+ Turbolinks.allowLinkExtensions('md'); // => ['html', 'md']
159
+ Turbolinks.allowLinkExtensions('coffee', 'scss'); // => ['html', 'md', 'coffee', 'scss']
160
+ ```
161
+
162
+ Also, Turbolinks is installed as the last click handler for links. So if you install another handler that calls event.preventDefault(), Turbolinks will not run. This ensures that you can safely use Turbolinks with stuff like `data-method`, `data-remote`, or `data-confirm` from Rails.
163
+
164
+
165
+ jquery.turbolinks
166
+ -----------------
167
+
168
+ If you have a lot of existing JavaScript that binds elements on jQuery.ready(), you can pull the [jquery.turbolinks](https://github.com/kossnocorp/jquery.turbolinks) library into your project that will trigger ready() when Turbolinks triggers the `page:load` event. It may restore functionality of some libraries.
169
+
170
+ Add the gem to your project, then add the following line to your JavaScript manifest file, after `jquery.js` but before `turbolinks.js`:
171
+
172
+ ``` js
173
+ //= require jquery.turbolinks
174
+ ```
175
+
176
+ Additional details and configuration options can be found in the [jquery.turbolinks README](https://github.com/kossnocorp/jquery.turbolinks/blob/master/README.md).
177
+
178
+ Asset change detection
179
+ ----------------------
180
+
181
+ You can track certain assets, like application.js and application.css, that you want to ensure are always of the latest version inside a Turbolinks session. This is done by marking those asset links with data-turbolinks-track, like so:
182
+
183
+ ```html
184
+ <link href="/assets/application-9bd64a86adb3cd9ab3b16e9dca67a33a.css" rel="stylesheet"
185
+ type="text/css" data-turbolinks-track>
186
+ ```
187
+
188
+ If those assets change URLs (embed an md5 stamp to ensure this), the page will do a full reload instead of going through Turbolinks. This ensures that all Turbolinks sessions will always be running off your latest JavaScript and CSS.
189
+
190
+ When this happens, you'll technically be requesting the same page twice. Once through Turbolinks to detect that the assets changed, and then again when we do a full redirect to that page.
191
+
192
+
193
+ Evaluating script tags
194
+ ----------------------
195
+
196
+ Turbolinks will evaluate any script tags in pages it visits, if those tags do not have a type or if the type is text/javascript. All other script tags will be ignored.
197
+
198
+ As a rule of thumb when switching to Turbolinks, move all of your javascript tags inside the `head` and then work backwards, only moving javascript code back to the body if absolutely necessary. If you have any script tags in the body you do not want to be re-evaluated then you can set the `data-turbolinks-eval` attribute to `false`:
199
+
200
+ ```html
201
+ <script type="text/javascript" data-turbolinks-eval=false>
202
+ console.log("I'm only run once on the initial page load");
203
+ </script>
204
+ ```
205
+
206
+ Triggering a Turbolinks visit manually
207
+ ---------------------------------------
208
+
209
+ You can use `Turbolinks.visit(path)` to go to a URL through Turbolinks.
210
+
211
+ You can also use `redirect_via_turbolinks_to` in Rails to perform a redirect via Turbolinks.
212
+
213
+
214
+ Full speed for pushState browsers, graceful fallback for everything else
215
+ ------------------------------------------------------------------------
216
+
217
+ Like pjax, this naturally only works with browsers capable of pushState. But of course we fall back gracefully to full page reloads for browsers that do not support it.
218
+
219
+
220
+ Compatibility
221
+ -------------
222
+
223
+ Turbolinks is designed to work with any browser that fully supports pushState and all the related APIs. This includes Safari 6.0+ (but not Safari 5.1.x!), IE10, and latest Chromes and Firefoxes.
224
+
225
+ Do note that existing JavaScript libraries may not all be compatible with Turbolinks out of the box due to the change in instantiation cycle. You might very well have to modify them to work with Turbolinks' new set of events. For help with this, check out the [Turbolinks Compatibility](http://reed.github.io/turbolinks-compatibility) project.
226
+
227
+
228
+ Installation
229
+ ------------
230
+
231
+ 1. Add `gem 'turbolinks'` to your Gemfile.
232
+ 1. Run `bundle install`.
233
+ 1. Add `//= require turbolinks` to your Javascript manifest file (usually found at `app/assets/javascripts/application.js`). If your manifest requires both turbolinks and jQuery, make sure turbolinks is listed *after* jQuery.
234
+ 1. Restart your server and you're now using turbolinks!
235
+
236
+ Language Ports
237
+ --------------
238
+
239
+ *These projects are not affiliated with or endorsed by the Rails Turbolinks team.*
240
+
241
+ * [Flask Turbolinks](https://github.com/lepture/flask-turbolinks) (Python Flask)
242
+ * [Django Turbolinks](https://github.com/dgladkov/django-turbolinks) (Python Django)
243
+ * [ASP.NET MVC Turbolinks](https://github.com/kazimanzurrashid/aspnetmvcturbolinks)
244
+ * [PHP Turbolinks Component](https://github.com/helthe/Turbolinks) (Symfony Component)
245
+ * [PHP Turbolinks Package](https://github.com/frenzyapp/turbolinks) (Laravel Package)
246
+ * [Grails Turbolinks](http://grails.org/plugin/turbolinks) (Grails Plugin)
247
+
248
+ Credits
249
+ -------
250
+
251
+ Thanks to Chris Wanstrath for his original work on Pjax. Thanks to Sam Stephenson and Josh Peek for their additional work on Pjax and Stacker and their help with getting Turbolinks released. Thanks to David Estes and Nick Reed for handling the lion's share of post-release issues and feature requests. And thanks to everyone else who's fixed or reported an issue!
@@ -0,0 +1,579 @@
1
+ pageCache = {}
2
+ cacheSize = 10
3
+ transitionCacheEnabled = false
4
+ progressBar = null
5
+
6
+ currentState = null
7
+ loadedAssets = null
8
+
9
+ referer = null
10
+
11
+ xhr = null
12
+
13
+ EVENTS =
14
+ BEFORE_CHANGE: 'page:before-change'
15
+ FETCH: 'page:fetch'
16
+ RECEIVE: 'page:receive'
17
+ CHANGE: 'page:change'
18
+ UPDATE: 'page:update'
19
+ LOAD: 'page:load'
20
+ RESTORE: 'page:restore'
21
+ BEFORE_UNLOAD: 'page:before-unload'
22
+ EXPIRE: 'page:expire'
23
+
24
+ fetch = (url) ->
25
+ url = new ComponentUrl url
26
+
27
+ rememberReferer()
28
+ cacheCurrentPage()
29
+ progressBar?.start()
30
+
31
+ if transitionCacheEnabled and cachedPage = transitionCacheFor(url.absolute)
32
+ fetchHistory cachedPage
33
+ fetchReplacement url, null, false
34
+ else
35
+ fetchReplacement url, resetScrollPosition
36
+
37
+ transitionCacheFor = (url) ->
38
+ cachedPage = pageCache[url]
39
+ cachedPage if cachedPage and !cachedPage.transitionCacheDisabled
40
+
41
+ enableTransitionCache = (enable = true) ->
42
+ transitionCacheEnabled = enable
43
+
44
+ enableProgressBar = (enable = true) ->
45
+ return unless browserSupportsTurbolinks
46
+ if enable
47
+ progressBar ?= new ProgressBar 'html'
48
+ else
49
+ progressBar?.uninstall()
50
+ progressBar = null
51
+
52
+ fetchReplacement = (url, onLoadFunction, showProgressBar = true) ->
53
+ triggerEvent EVENTS.FETCH, url: url.absolute
54
+
55
+ xhr?.abort()
56
+ xhr = new XMLHttpRequest
57
+ xhr.open 'GET', url.withoutHashForIE10compatibility(), true
58
+ xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
59
+ xhr.setRequestHeader 'X-XHR-Referer', referer
60
+
61
+ xhr.onload = ->
62
+ triggerEvent EVENTS.RECEIVE, url: url.absolute
63
+
64
+ if doc = processResponse()
65
+ reflectNewUrl url
66
+ reflectRedirectedUrl()
67
+ changePage extractTitleAndBody(doc)...
68
+ if showProgressBar
69
+ progressBar?.done()
70
+ manuallyTriggerHashChangeForFirefox()
71
+ onLoadFunction?()
72
+ triggerEvent EVENTS.LOAD
73
+ else
74
+ progressBar?.done()
75
+ document.location.href = crossOriginRedirect() or url.absolute
76
+
77
+ if progressBar and showProgressBar
78
+ xhr.onprogress = (event) =>
79
+ percent = if event.lengthComputable
80
+ event.loaded / event.total * 100
81
+ else
82
+ progressBar.value + (100 - progressBar.value) / 10
83
+ progressBar.advanceTo(percent)
84
+
85
+ xhr.onloadend = -> xhr = null
86
+ xhr.onerror = -> document.location.href = url.absolute
87
+
88
+ xhr.send()
89
+
90
+ fetchHistory = (cachedPage) ->
91
+ xhr?.abort()
92
+ changePage cachedPage.title, cachedPage.body
93
+ progressBar?.done()
94
+ recallScrollPosition cachedPage
95
+ triggerEvent EVENTS.RESTORE
96
+
97
+
98
+ cacheCurrentPage = ->
99
+ currentStateUrl = new ComponentUrl currentState.url
100
+
101
+ pageCache[currentStateUrl.absolute] =
102
+ url: currentStateUrl.relative,
103
+ body: document.body,
104
+ title: document.title,
105
+ positionY: window.pageYOffset,
106
+ positionX: window.pageXOffset,
107
+ cachedAt: new Date().getTime(),
108
+ transitionCacheDisabled: document.querySelector('[data-no-transition-cache]')?
109
+
110
+ constrainPageCacheTo cacheSize
111
+
112
+ pagesCached = (size = cacheSize) ->
113
+ cacheSize = parseInt(size) if /^[\d]+$/.test size
114
+
115
+ constrainPageCacheTo = (limit) ->
116
+ pageCacheKeys = Object.keys pageCache
117
+
118
+ cacheTimesRecentFirst = pageCacheKeys.map (url) ->
119
+ pageCache[url].cachedAt
120
+ .sort (a, b) -> b - a
121
+
122
+ for key in pageCacheKeys when pageCache[key].cachedAt <= cacheTimesRecentFirst[limit]
123
+ triggerEvent EVENTS.EXPIRE, pageCache[key]
124
+ delete pageCache[key]
125
+
126
+ changePage = (title, body, csrfToken, runScripts) ->
127
+ triggerEvent EVENTS.BEFORE_UNLOAD
128
+ document.title = title
129
+ document.documentElement.replaceChild body, document.body
130
+ CSRFToken.update csrfToken if csrfToken?
131
+ setAutofocusElement()
132
+ executeScriptTags() if runScripts
133
+ currentState = window.history.state
134
+ triggerEvent EVENTS.CHANGE
135
+ triggerEvent EVENTS.UPDATE
136
+
137
+ executeScriptTags = ->
138
+ scripts = Array::slice.call document.body.querySelectorAll 'script:not([data-turbolinks-eval="false"])'
139
+ for script in scripts when script.type in ['', 'text/javascript']
140
+ copy = document.createElement 'script'
141
+ copy.setAttribute attr.name, attr.value for attr in script.attributes
142
+ copy.async = false unless script.hasAttribute 'async'
143
+ copy.appendChild document.createTextNode script.innerHTML
144
+ { parentNode, nextSibling } = script
145
+ parentNode.removeChild script
146
+ parentNode.insertBefore copy, nextSibling
147
+ return
148
+
149
+ removeNoscriptTags = (node) ->
150
+ node.innerHTML = node.innerHTML.replace /<noscript[\S\s]*?<\/noscript>/ig, ''
151
+ node
152
+
153
+ # Firefox bug: Doesn't autofocus fields that are inserted via JavaScript
154
+ setAutofocusElement = ->
155
+ autofocusElement = (list = document.querySelectorAll 'input[autofocus], textarea[autofocus]')[list.length - 1]
156
+ if autofocusElement and document.activeElement isnt autofocusElement
157
+ autofocusElement.focus()
158
+
159
+ reflectNewUrl = (url) ->
160
+ if (url = new ComponentUrl url).absolute isnt referer
161
+ window.history.pushState { turbolinks: true, url: url.absolute }, '', url.absolute
162
+
163
+ reflectRedirectedUrl = ->
164
+ if location = xhr.getResponseHeader 'X-XHR-Redirected-To'
165
+ location = new ComponentUrl location
166
+ preservedHash = if location.hasNoHash() then document.location.hash else ''
167
+ window.history.replaceState window.history.state, '', location.href + preservedHash
168
+
169
+ crossOriginRedirect = ->
170
+ redirect if (redirect = xhr.getResponseHeader('Location'))? and (new ComponentUrl(redirect)).crossOrigin()
171
+
172
+ rememberReferer = ->
173
+ referer = document.location.href
174
+
175
+ rememberCurrentUrl = ->
176
+ window.history.replaceState { turbolinks: true, url: document.location.href }, '', document.location.href
177
+
178
+ rememberCurrentState = ->
179
+ currentState = window.history.state
180
+
181
+ # Unlike other browsers, Firefox doesn't trigger hashchange after changing the
182
+ # location (via pushState) to an anchor on a different page. For example:
183
+ #
184
+ # /pages/one => /pages/two#with-hash
185
+ #
186
+ # By forcing Firefox to trigger hashchange, the rest of the code can rely on more
187
+ # consistent behavior across browsers.
188
+ manuallyTriggerHashChangeForFirefox = ->
189
+ if navigator.userAgent.match(/Firefox/) and !(url = (new ComponentUrl)).hasNoHash()
190
+ window.history.replaceState currentState, '', url.withoutHash()
191
+ document.location.hash = url.hash
192
+
193
+ recallScrollPosition = (page) ->
194
+ window.scrollTo page.positionX, page.positionY
195
+
196
+ resetScrollPosition = ->
197
+ if document.location.hash
198
+ document.location.href = document.location.href
199
+ else
200
+ window.scrollTo 0, 0
201
+
202
+
203
+ clone = (original) ->
204
+ return original if not original? or typeof original isnt 'object'
205
+ copy = new original.constructor()
206
+ copy[key] = clone value for key, value of original
207
+ copy
208
+
209
+ popCookie = (name) ->
210
+ value = document.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or ''
211
+ document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
212
+ value
213
+
214
+ triggerEvent = (name, data) ->
215
+ if typeof Prototype isnt 'undefined'
216
+ Event.fire document, name, data, true
217
+
218
+ event = document.createEvent 'Events'
219
+ event.data = data if data
220
+ event.initEvent name, true, true
221
+ document.dispatchEvent event
222
+
223
+ pageChangePrevented = (url) ->
224
+ !triggerEvent EVENTS.BEFORE_CHANGE, url: url
225
+
226
+ processResponse = ->
227
+ clientOrServerError = ->
228
+ 400 <= xhr.status < 600
229
+
230
+ validContent = ->
231
+ (contentType = xhr.getResponseHeader('Content-Type'))? and
232
+ contentType.match /^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/
233
+
234
+ downloadingFile = ->
235
+ (disposition = xhr.getResponseHeader('Content-Disposition'))? and
236
+ disposition.match /^attachment/
237
+
238
+ extractTrackAssets = (doc) ->
239
+ for node in doc.querySelector('head').childNodes when node.getAttribute?('data-turbolinks-track')?
240
+ node.getAttribute('src') or node.getAttribute('href')
241
+
242
+ assetsChanged = (doc) ->
243
+ loadedAssets ||= extractTrackAssets document
244
+ fetchedAssets = extractTrackAssets doc
245
+ fetchedAssets.length isnt loadedAssets.length or intersection(fetchedAssets, loadedAssets).length isnt loadedAssets.length
246
+
247
+ intersection = (a, b) ->
248
+ [a, b] = [b, a] if a.length > b.length
249
+ value for value in a when value in b
250
+
251
+ if not clientOrServerError() and validContent() and not downloadingFile()
252
+ doc = createDocument xhr.responseText
253
+ if doc and !assetsChanged doc
254
+ return doc
255
+
256
+ extractTitleAndBody = (doc) ->
257
+ title = doc.querySelector 'title'
258
+ [ title?.textContent, removeNoscriptTags(doc.querySelector('body')), CSRFToken.get(doc).token, 'runScripts' ]
259
+
260
+ CSRFToken =
261
+ get: (doc = document) ->
262
+ node: tag = doc.querySelector 'meta[name="csrf-token"]'
263
+ token: tag?.getAttribute? 'content'
264
+
265
+ update: (latest) ->
266
+ current = @get()
267
+ if current.token? and latest? and current.token isnt latest
268
+ current.node.setAttribute 'content', latest
269
+
270
+ createDocument = (html) ->
271
+ doc = document.documentElement.cloneNode()
272
+ doc.innerHTML = html
273
+ doc.head = doc.querySelector 'head'
274
+ doc.body = doc.querySelector 'body'
275
+ doc
276
+
277
+ # The ComponentUrl class converts a basic URL string into an object
278
+ # that behaves similarly to document.location.
279
+ #
280
+ # If an instance is created from a relative URL, the current document
281
+ # is used to fill in the missing attributes (protocol, host, port).
282
+ class ComponentUrl
283
+ constructor: (@original = document.location.href) ->
284
+ return @original if @original.constructor is ComponentUrl
285
+ @_parse()
286
+
287
+ withoutHash: -> @href.replace(@hash, '').replace('#', '')
288
+
289
+ # Intention revealing function alias
290
+ withoutHashForIE10compatibility: -> @withoutHash()
291
+
292
+ hasNoHash: -> @hash.length is 0
293
+
294
+ crossOrigin: ->
295
+ @origin isnt (new ComponentUrl).origin
296
+
297
+ _parse: ->
298
+ (@link ?= document.createElement 'a').href = @original
299
+ { @href, @protocol, @host, @hostname, @port, @pathname, @search, @hash } = @link
300
+ @origin = [@protocol, '//', @hostname].join ''
301
+ @origin += ":#{@port}" unless @port.length is 0
302
+ @relative = [@pathname, @search, @hash].join ''
303
+ @absolute = @href
304
+
305
+ # The Link class derives from the ComponentUrl class, but is built from an
306
+ # existing link element. Provides verification functionality for Turbolinks
307
+ # to use in determining whether it should process the link when clicked.
308
+ class Link extends ComponentUrl
309
+ @HTML_EXTENSIONS: ['html']
310
+
311
+ @allowExtensions: (extensions...) ->
312
+ Link.HTML_EXTENSIONS.push extension for extension in extensions
313
+ Link.HTML_EXTENSIONS
314
+
315
+ constructor: (@link) ->
316
+ return @link if @link.constructor is Link
317
+ @original = @link.href
318
+ @originalElement = @link
319
+ @link = @link.cloneNode false
320
+ super
321
+
322
+ shouldIgnore: ->
323
+ @crossOrigin() or
324
+ @_anchored() or
325
+ @_nonHtml() or
326
+ @_optOut() or
327
+ @_target()
328
+
329
+ _anchored: ->
330
+ (@hash.length > 0 or @href.charAt(@href.length - 1) is '#') and
331
+ (@withoutHash() is (new ComponentUrl).withoutHash())
332
+
333
+ _nonHtml: ->
334
+ @pathname.match(/\.[a-z]+$/g) and not @pathname.match(new RegExp("\\.(?:#{Link.HTML_EXTENSIONS.join('|')})?$", 'g'))
335
+
336
+ _optOut: ->
337
+ link = @originalElement
338
+ until ignore or link is document
339
+ ignore = link.getAttribute('data-no-turbolink')?
340
+ link = link.parentNode
341
+ ignore
342
+
343
+ _target: ->
344
+ @link.target.length isnt 0
345
+
346
+
347
+ # The Click class handles clicked links, verifying if Turbolinks should
348
+ # take control by inspecting both the event and the link. If it should,
349
+ # the page change process is initiated. If not, control is passed back
350
+ # to the browser for default functionality.
351
+ class Click
352
+ @installHandlerLast: (event) ->
353
+ unless event.defaultPrevented
354
+ document.removeEventListener 'click', Click.handle, false
355
+ document.addEventListener 'click', Click.handle, false
356
+
357
+ @handle: (event) ->
358
+ new Click event
359
+
360
+ constructor: (@event) ->
361
+ return if @event.defaultPrevented
362
+ @_extractLink()
363
+ if @_validForTurbolinks()
364
+ visit @link.href unless pageChangePrevented(@link.absolute)
365
+ @event.preventDefault()
366
+
367
+ _extractLink: ->
368
+ link = @event.target
369
+ link = link.parentNode until !link.parentNode or link.nodeName is 'A'
370
+ @link = new Link(link) if link.nodeName is 'A' and link.href.length isnt 0
371
+
372
+ _validForTurbolinks: ->
373
+ @link? and not (@link.shouldIgnore() or @_nonStandardClick())
374
+
375
+ _nonStandardClick: ->
376
+ @event.which > 1 or
377
+ @event.metaKey or
378
+ @event.ctrlKey or
379
+ @event.shiftKey or
380
+ @event.altKey
381
+
382
+
383
+ class ProgressBar
384
+ className = 'turbolinks-progress-bar'
385
+ # Setting the opacity to a value < 1 fixes a display issue in Safari 6 and
386
+ # iOS 6 where the progress bar would fill the entire page.
387
+ originalOpacity = 0.99
388
+
389
+ constructor: (@elementSelector) ->
390
+ @value = 0
391
+ @content = ''
392
+ @speed = 300
393
+ @opacity = originalOpacity
394
+ @install()
395
+
396
+ install: ->
397
+ @element = document.querySelector(@elementSelector)
398
+ @element.classList.add(className)
399
+ @styleElement = document.createElement('style')
400
+ document.head.appendChild(@styleElement)
401
+ @_updateStyle()
402
+
403
+ uninstall: ->
404
+ @element.classList.remove(className)
405
+ document.head.removeChild(@styleElement)
406
+
407
+ start: ->
408
+ if @value > 0
409
+ @_reset()
410
+ @_reflow()
411
+
412
+ @advanceTo(5)
413
+
414
+ advanceTo: (value) ->
415
+ if value > @value <= 100
416
+ @value = value
417
+ @_updateStyle()
418
+
419
+ if @value is 100
420
+ @_stopTrickle()
421
+ else if @value > 0
422
+ @_startTrickle()
423
+
424
+ done: ->
425
+ if @value > 0
426
+ @advanceTo(100)
427
+ @_finish()
428
+
429
+ _finish: ->
430
+ @fadeTimer = setTimeout =>
431
+ @opacity = 0
432
+ @_updateStyle()
433
+ , @speed / 2
434
+
435
+ @resetTimer = setTimeout(@_reset, @speed)
436
+
437
+ _reflow: ->
438
+ @element.offsetHeight
439
+
440
+ _reset: =>
441
+ @_stopTimers()
442
+ @value = 0
443
+ @opacity = originalOpacity
444
+ @_withSpeed(0, => @_updateStyle(true))
445
+
446
+ _stopTimers: ->
447
+ @_stopTrickle()
448
+ clearTimeout(@fadeTimer)
449
+ clearTimeout(@resetTimer)
450
+
451
+ _startTrickle: ->
452
+ return if @trickleTimer
453
+ @trickleTimer = setTimeout(@_trickle, @speed)
454
+
455
+ _stopTrickle: ->
456
+ clearTimeout(@trickleTimer)
457
+ delete @trickleTimer
458
+
459
+ _trickle: =>
460
+ @advanceTo(@value + Math.random() / 2)
461
+ @trickleTimer = setTimeout(@_trickle, @speed)
462
+
463
+ _withSpeed: (speed, fn) ->
464
+ originalSpeed = @speed
465
+ @speed = speed
466
+ result = fn()
467
+ @speed = originalSpeed
468
+ result
469
+
470
+ _updateStyle: (forceRepaint = false) ->
471
+ @_changeContentToForceRepaint() if forceRepaint
472
+ @styleElement.textContent = @_createCSSRule()
473
+
474
+ _changeContentToForceRepaint: ->
475
+ @content = if @content is '' then ' ' else ''
476
+
477
+ _createCSSRule: ->
478
+ """
479
+ #{@elementSelector}.#{className}::before {
480
+ content: '#{@content}';
481
+ position: fixed;
482
+ top: 0;
483
+ left: 0;
484
+ z-index: 2000;
485
+ background-color: #0076ff;
486
+ height: 3px;
487
+ opacity: #{@opacity};
488
+ width: #{@value}%;
489
+ transition: width #{@speed}ms ease-out, opacity #{@speed / 2}ms ease-in;
490
+ transform: translate3d(0,0,0);
491
+ }
492
+ """
493
+
494
+
495
+ # Delay execution of function long enough to miss the popstate event
496
+ # some browsers fire on the initial page load.
497
+ bypassOnLoadPopstate = (fn) ->
498
+ setTimeout fn, 500
499
+
500
+ installDocumentReadyPageEventTriggers = ->
501
+ document.addEventListener 'DOMContentLoaded', ( ->
502
+ triggerEvent EVENTS.CHANGE
503
+ triggerEvent EVENTS.UPDATE
504
+ ), true
505
+
506
+ installJqueryAjaxSuccessPageUpdateTrigger = ->
507
+ if typeof jQuery isnt 'undefined'
508
+ jQuery(document).on 'ajaxSuccess', (event, xhr, settings) ->
509
+ return unless jQuery.trim xhr.responseText
510
+ triggerEvent EVENTS.UPDATE
511
+
512
+ installHistoryChangeHandler = (event) ->
513
+ if event.state?.turbolinks
514
+ if cachedPage = pageCache[(new ComponentUrl(event.state.url)).absolute]
515
+ cacheCurrentPage()
516
+ fetchHistory cachedPage
517
+ else
518
+ visit event.target.location.href
519
+
520
+ initializeTurbolinks = ->
521
+ rememberCurrentUrl()
522
+ rememberCurrentState()
523
+
524
+ document.addEventListener 'click', Click.installHandlerLast, true
525
+
526
+ window.addEventListener 'hashchange', (event) ->
527
+ rememberCurrentUrl()
528
+ rememberCurrentState()
529
+ , false
530
+ bypassOnLoadPopstate ->
531
+ window.addEventListener 'popstate', installHistoryChangeHandler, false
532
+
533
+ # Handle bug in Firefox 26/27 where history.state is initially undefined
534
+ historyStateIsDefined =
535
+ window.history.state != undefined or navigator.userAgent.match /Firefox\/2[6|7]/
536
+
537
+ browserSupportsPushState =
538
+ window.history and window.history.pushState and window.history.replaceState and historyStateIsDefined
539
+
540
+ browserIsntBuggy =
541
+ !navigator.userAgent.match /CriOS\//
542
+
543
+ requestMethodIsSafe =
544
+ popCookie('request_method') in ['GET','']
545
+
546
+ browserSupportsTurbolinks = browserSupportsPushState and browserIsntBuggy and requestMethodIsSafe
547
+
548
+ browserSupportsCustomEvents =
549
+ document.addEventListener and document.createEvent
550
+
551
+ if browserSupportsCustomEvents
552
+ installDocumentReadyPageEventTriggers()
553
+ installJqueryAjaxSuccessPageUpdateTrigger()
554
+
555
+ if browserSupportsTurbolinks
556
+ visit = fetch
557
+ initializeTurbolinks()
558
+ else
559
+ visit = (url) -> document.location.href = url
560
+
561
+ # Public API
562
+ # Turbolinks.visit(url)
563
+ # Turbolinks.pagesCached()
564
+ # Turbolinks.pagesCached(20)
565
+ # Turbolinks.enableTransitionCache()
566
+ # Turbolinks.cacheCurrentPage()
567
+ # Turbolinks.allowLinkExtensions('md')
568
+ # Turbolinks.supported
569
+ # Turbolinks.EVENTS
570
+ @Turbolinks = {
571
+ visit,
572
+ pagesCached,
573
+ cacheCurrentPage,
574
+ enableTransitionCache,
575
+ enableProgressBar,
576
+ allowLinkExtensions: Link.allowExtensions,
577
+ supported: browserSupportsTurbolinks,
578
+ EVENTS: clone(EVENTS)
579
+ }