├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.cfg ├── setup.py ├── test_project.py ├── tests.py ├── tox.ini └── turbolinks ├── __init__.py ├── middleware.py ├── models.py └── static └── turbolinks ├── turbolinks.js └── turbolinks.js.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | 24 | # PyInstaller 25 | # Usually these files are written by a python script from a template 26 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 27 | *.manifest 28 | *.spec 29 | 30 | # Installer logs 31 | pip-log.txt 32 | pip-delete-this-directory.txt 33 | 34 | # Unit test / coverage reports 35 | htmlcov/ 36 | .tox/ 37 | .coverage 38 | .cache 39 | nosetests.xml 40 | coverage.xml 41 | 42 | # Databases 43 | *.sqlite3 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | 4 | python: 5 | - "2.6" 6 | - "2.7" 7 | - "3.2" 8 | - "3.3" 9 | - "3.4" 10 | - "3.5" 11 | - "pypy" 12 | 13 | install: pip install tox-travis 14 | script: tox 15 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Authors 2 | ------- 3 | 4 | Dmitry Gladkov 5 | Hsiaoming Yang (author of flask-turbolinks) 6 | 7 | Contributors 8 | ------------ 9 | 10 | liuzhe 11 | Brad Pitcher 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Dmitry Gladkov and contributors. See AUTHORS for more details. 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. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include turbolinks/static *.js 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-turbolinks |Build Status| 2 | ================================ 3 | 4 | Drop-in turbolinks implementation for Django 5 | 6 | Installation 7 | ------------ 8 | 9 | ``$ pip install django-turbolinks`` 10 | 11 | Configuration 12 | ------------- 13 | 14 | 2. Add ``turbolinks.middleware.TurbolinksMiddleware`` after 15 | ``django.contrib.sessions.middleware.SessionMiddleware`` to your 16 | ``MIDDLEWARE_CLASSES`` setting. 17 | 3. Add ``turbolinks`` to your ``INSTALLED_APPS`` setting. 18 | 4. Run ``./manage.py collectstatic`` 19 | 5. Include ``/static/turbolinks/turbolinks.js`` script in your base 20 | template. 21 | 22 | .. |Build Status| image:: https://travis-ci.org/dgladkov/django-turbolinks.svg?branch=master 23 | :target: https://travis-ci.org/dgladkov/django-turbolinks 24 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | publish = test sdist bdist_wheel upload 3 | 4 | [bdist_wheel] 5 | universal = 1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import unicode_literals 3 | 4 | import ast 5 | import re 6 | from setuptools import Command, setup, find_packages 7 | 8 | 9 | _version_re = re.compile(r'__version__\s+=\s+(.*)') 10 | 11 | with open('turbolinks/__init__.py', 'rb') as f: 12 | version = str(ast.literal_eval(_version_re.search( 13 | f.read().decode('utf-8')).group(1))) 14 | 15 | 16 | class Test(Command): 17 | user_options = [] 18 | 19 | def initialize_options(self): 20 | pass 21 | 22 | def finalize_options(self): 23 | pass 24 | 25 | def run(self): 26 | from test_project import main 27 | main() 28 | 29 | 30 | setup( 31 | name='django-turbolinks', 32 | version=version, 33 | url='https://github.com/dgladkov/django-turbolinks', 34 | license='MIT', 35 | author='Dmitry Gladkov', 36 | author_email='dmitry.gladkov@gmail.com', 37 | description='Drop-in turbolinks implementation for Django', 38 | long_description=open('README.rst').read(), 39 | packages=find_packages(), 40 | include_package_data=True, 41 | zip_safe=False, 42 | install_requires=[ 43 | 'Django>=1.6', 44 | ], 45 | classifiers=[ 46 | 'Development Status :: 4 - Beta', 47 | 'Environment :: Web Environment', 48 | 'Intended Audience :: Developers', 49 | 'License :: OSI Approved :: MIT License', 50 | 'Operating System :: OS Independent', 51 | 'Programming Language :: Python', 52 | 'Programming Language :: Python :: 3', 53 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 54 | 'Topic :: Software Development :: Libraries :: Python Modules' 55 | ], 56 | cmdclass={'test': Test}, 57 | ) 58 | -------------------------------------------------------------------------------- /test_project.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import unicode_literals 3 | 4 | import django 5 | from django.conf import settings 6 | from django.conf.urls import url 7 | from django.http import HttpResponse, HttpResponseRedirect 8 | 9 | 10 | # Configuration 11 | if not settings.configured: 12 | settings.configure( 13 | DEBUG=True, 14 | ROOT_URLCONF=__name__, 15 | DATABASES={ 16 | 'default': { 17 | 'ENGINE': 'django.db.backends.sqlite3', 18 | 'NAME': ':memory:', 19 | } 20 | }, 21 | MIDDLEWARE_CLASSES=[ 22 | 'django.middleware.common.CommonMiddleware', 23 | 'django.contrib.sessions.middleware.SessionMiddleware', 24 | 'turbolinks.middleware.TurbolinksMiddleware' 25 | ], 26 | INSTALLED_APPS=[ 27 | 'django.contrib.sessions', 28 | 'turbolinks', 29 | ], 30 | ) 31 | # Django >=1.7 compatibility 32 | if hasattr(django, 'setup'): 33 | django.setup() 34 | 35 | 36 | # URL Router 37 | urlpatterns = [] 38 | 39 | 40 | def route(regex): 41 | def inner(view): 42 | urlpatterns.append(url(regex, view)) 43 | return view 44 | return inner 45 | 46 | 47 | # Views 48 | @route(r'^$') 49 | def index(request): 50 | return HttpResponse(request.META.get('HTTP_REFERER', '')) 51 | 52 | 53 | @route(r'^page/$') 54 | def page(request): 55 | return HttpResponse('page') 56 | 57 | 58 | @route(r'^redirect/$') 59 | def redirect(request): 60 | return HttpResponseRedirect('/page/') 61 | 62 | 63 | @route(r'^x-redirect/$') 64 | def x_redirect(request): 65 | return HttpResponseRedirect('http://example.com') 66 | 67 | 68 | # CLI 69 | def main(): 70 | from django.core.management import execute_from_command_line 71 | execute_from_command_line() 72 | 73 | 74 | if __name__ == '__main__': 75 | main() 76 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.test import TestCase 4 | 5 | 6 | class MainTestCase(TestCase): 7 | 8 | def test_home(self): 9 | response = self.client.get('/', HTTP_X_XHR_REFERER='/page/') 10 | self.assertEqual(response.content, b'/page/') 11 | self.assertEqual(response.cookies.get('request_method').value, 'GET') 12 | 13 | def test_redirect(self): 14 | response = self.client.get('/redirect/', HTTP_X_XHR_REFERER='/page/') 15 | self.assertEqual( 16 | self.client.session.get('_turbolinks_redirect_to'), 17 | '/page/', 18 | ) 19 | self.assertNotIn('X-XHR-Redirected-To', response) 20 | 21 | response = self.client.get('/page/', HTTP_X_XHR_REFERER='/redirect/') 22 | self.assertNotIn('_turbolinks_redirect_to', self.client.session) 23 | self.assertIn('X-XHR-Redirected-To', response) 24 | 25 | def test_cookie(self): 26 | self.client.cookies['request_method'] = 'GET' 27 | response = self.client.get('/', HTTP_X_XHR_REFERER='/page/') 28 | self.assertFalse(response.cookies) 29 | 30 | def test_x_redirect(self): 31 | response = self.client.get('/x-redirect/') 32 | self.assertEqual(response.status_code, 302) 33 | 34 | response = self.client.get('/x-redirect/', HTTP_X_XHR_REFERER='/page/') 35 | self.assertEqual(response.status_code, 403) 36 | 37 | # origin and redirect are exactly the same 38 | response = self.client.get( 39 | '/x-redirect/', 40 | HTTP_X_XHR_REFERER='http://example.com/' 41 | ) 42 | self.assertEqual(response.status_code, 302) 43 | 44 | # port differs 45 | response = self.client.get( 46 | '/x-redirect/', 47 | HTTP_X_XHR_REFERER='http://example.com:8000/' 48 | ) 49 | self.assertEqual(response.status_code, 403) 50 | 51 | # same domain, different URI 52 | response = self.client.get( 53 | '/x-redirect/', 54 | HTTP_X_XHR_REFERER='http://example.com/example/' 55 | ) 56 | self.assertEqual(response.status_code, 302) 57 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py26-django-16 4 | {py27,py32,py33,py34}-django-{17,18} 5 | {py27,py34,py35}-django-{19,master} 6 | {pypy}-django-{16,17,18,19,20} 7 | 8 | [testenv] 9 | commands = python test_project.py test [] 10 | deps = 11 | django-16: Django>=1.6,<1.7 12 | django-17: Django>=1.7,<1.8 13 | django-18: Django>=1.8,<1.9 14 | django-19: Django>=1.9,<1.10 15 | django-master: git+git://github.com/django/django.git 16 | -------------------------------------------------------------------------------- /turbolinks/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.5.1' 2 | -------------------------------------------------------------------------------- /turbolinks/middleware.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | try: 4 | from django.utils.deprecation import MiddlewareMixin 5 | except ImportError: 6 | MiddlewareMixin = object 7 | from django.http import HttpResponseForbidden 8 | from django.utils.six.moves.urllib.parse import urlparse 9 | 10 | 11 | def same_origin(current_uri, redirect_uri): 12 | a = urlparse(current_uri) 13 | if not a.scheme: 14 | return True 15 | b = urlparse(redirect_uri) 16 | return (a.scheme, a.hostname, a.port) == (b.scheme, b.hostname, b.port) 17 | 18 | 19 | class TurbolinksMiddleware(MiddlewareMixin): 20 | 21 | def process_request(self, request): 22 | referrer = request.META.get('HTTP_X_XHR_REFERER') 23 | if referrer: 24 | # overwrite referrer 25 | request.META['HTTP_REFERER'] = referrer 26 | return 27 | 28 | def process_response(self, request, response): 29 | referrer = request.META.get('HTTP_X_XHR_REFERER') 30 | if not referrer: 31 | # turbolinks not enabled 32 | return response 33 | 34 | method = request.COOKIES.get('request_method') 35 | if not method or method != request.method: 36 | response.set_cookie('request_method', request.method) 37 | 38 | if response.has_header('Location'): 39 | # this is a redirect response 40 | loc = response['Location'] 41 | request.session['_turbolinks_redirect_to'] = loc 42 | 43 | # cross domain blocker 44 | if referrer and not same_origin(loc, referrer): 45 | return HttpResponseForbidden() 46 | else: 47 | if request.session.get('_turbolinks_redirect_to'): 48 | loc = request.session.pop('_turbolinks_redirect_to') 49 | response['X-XHR-Redirected-To'] = loc 50 | return response 51 | -------------------------------------------------------------------------------- /turbolinks/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dgladkov/django-turbolinks/b5868d2ed0d5c67d5cb054c9ac3f38541978431c/turbolinks/models.py -------------------------------------------------------------------------------- /turbolinks/static/turbolinks/turbolinks.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.9.2 2 | (function() { 3 | var CSRFToken, Click, ComponentUrl, EVENTS, Link, ProgressBar, ProgressBarAPI, browserIsBuggy, browserSupportsCustomEvents, browserSupportsPushState, browserSupportsTurbolinks, cacheCurrentPage, cacheSize, changePage, clone, constrainPageCacheTo, createDocument, crossOriginRedirect, currentState, disableRequestCaching, enableTransitionCache, executeScriptTags, extractTitleAndBody, fetch, fetchHistory, fetchReplacement, findNodes, findNodesMatchingKeys, initializeTurbolinks, installDocumentReadyPageEventTriggers, installJqueryAjaxSuccessPageUpdateTrigger, loadedAssets, manuallyTriggerHashChangeForFirefox, onHistoryChange, onNodeRemoved, pageCache, pageChangePrevented, pagesCached, popCookie, processResponse, progressBar, recallScrollPosition, ref, referer, reflectNewUrl, reflectRedirectedUrl, rememberCurrentState, rememberCurrentUrl, rememberReferer, removeNoscriptTags, replace, requestCachingEnabled, requestMethodIsSafe, resetScrollPosition, setAutofocusElement, swapNodes, transitionCacheEnabled, transitionCacheFor, triggerEvent, ua, uniqueId, visit, xhr, 4 | indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, 5 | extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 6 | hasProp = {}.hasOwnProperty, 7 | slice = [].slice, 8 | bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; 9 | 10 | pageCache = {}; 11 | 12 | cacheSize = 10; 13 | 14 | transitionCacheEnabled = false; 15 | 16 | requestCachingEnabled = true; 17 | 18 | progressBar = null; 19 | 20 | currentState = null; 21 | 22 | loadedAssets = null; 23 | 24 | referer = null; 25 | 26 | xhr = null; 27 | 28 | EVENTS = { 29 | BEFORE_CHANGE: 'page:before-change', 30 | FETCH: 'page:fetch', 31 | RECEIVE: 'page:receive', 32 | CHANGE: 'page:change', 33 | UPDATE: 'page:update', 34 | LOAD: 'page:load', 35 | RESTORE: 'page:restore', 36 | BEFORE_UNLOAD: 'page:before-unload', 37 | AFTER_REMOVE: 'page:after-remove' 38 | }; 39 | 40 | fetch = function(url, options) { 41 | var cachedPage; 42 | if (options == null) { 43 | options = {}; 44 | } 45 | url = new ComponentUrl(url); 46 | rememberReferer(); 47 | cacheCurrentPage(); 48 | if (progressBar != null) { 49 | progressBar.start(); 50 | } 51 | if (transitionCacheEnabled && (cachedPage = transitionCacheFor(url.absolute))) { 52 | fetchHistory(cachedPage); 53 | options.showProgressBar = false; 54 | return fetchReplacement(url, options); 55 | } else { 56 | options.onLoadFunction = resetScrollPosition; 57 | return fetchReplacement(url, options); 58 | } 59 | }; 60 | 61 | transitionCacheFor = function(url) { 62 | var cachedPage; 63 | cachedPage = pageCache[url]; 64 | if (cachedPage && !cachedPage.transitionCacheDisabled) { 65 | return cachedPage; 66 | } 67 | }; 68 | 69 | enableTransitionCache = function(enable) { 70 | if (enable == null) { 71 | enable = true; 72 | } 73 | return transitionCacheEnabled = enable; 74 | }; 75 | 76 | disableRequestCaching = function(disable) { 77 | if (disable == null) { 78 | disable = true; 79 | } 80 | requestCachingEnabled = !disable; 81 | return disable; 82 | }; 83 | 84 | fetchReplacement = function(url, options) { 85 | if (options.cacheRequest == null) { 86 | options.cacheRequest = requestCachingEnabled; 87 | } 88 | if (options.showProgressBar == null) { 89 | options.showProgressBar = true; 90 | } 91 | triggerEvent(EVENTS.FETCH, { 92 | url: url.absolute 93 | }); 94 | if (xhr != null) { 95 | xhr.abort(); 96 | } 97 | xhr = new XMLHttpRequest; 98 | xhr.open('GET', url.formatForXHR({ 99 | cache: options.cacheRequest 100 | }), true); 101 | xhr.setRequestHeader('Accept', 'text/html, application/xhtml+xml, application/xml'); 102 | xhr.setRequestHeader('X-XHR-Referer', referer); 103 | xhr.onload = function() { 104 | var doc; 105 | triggerEvent(EVENTS.RECEIVE, { 106 | url: url.absolute 107 | }); 108 | if (doc = processResponse()) { 109 | reflectNewUrl(url); 110 | reflectRedirectedUrl(); 111 | changePage(doc, options); 112 | if (options.showProgressBar) { 113 | if (progressBar != null) { 114 | progressBar.done(); 115 | } 116 | } 117 | manuallyTriggerHashChangeForFirefox(); 118 | if (typeof options.onLoadFunction === "function") { 119 | options.onLoadFunction(); 120 | } 121 | return triggerEvent(EVENTS.LOAD); 122 | } else { 123 | if (progressBar != null) { 124 | progressBar.done(); 125 | } 126 | return document.location.href = crossOriginRedirect() || url.absolute; 127 | } 128 | }; 129 | if (progressBar && options.showProgressBar) { 130 | xhr.onprogress = (function(_this) { 131 | return function(event) { 132 | var percent; 133 | percent = event.lengthComputable ? event.loaded / event.total * 100 : progressBar.value + (100 - progressBar.value) / 10; 134 | return progressBar.advanceTo(percent); 135 | }; 136 | })(this); 137 | } 138 | xhr.onloadend = function() { 139 | return xhr = null; 140 | }; 141 | xhr.onerror = function() { 142 | return document.location.href = url.absolute; 143 | }; 144 | return xhr.send(); 145 | }; 146 | 147 | fetchHistory = function(cachedPage) { 148 | if (xhr != null) { 149 | xhr.abort(); 150 | } 151 | changePage(createDocument(cachedPage.body), { 152 | title: cachedPage.title, 153 | runScripts: false 154 | }); 155 | if (progressBar != null) { 156 | progressBar.done(); 157 | } 158 | recallScrollPosition(cachedPage); 159 | return triggerEvent(EVENTS.RESTORE); 160 | }; 161 | 162 | cacheCurrentPage = function() { 163 | var currentStateUrl; 164 | currentStateUrl = new ComponentUrl(currentState.url); 165 | pageCache[currentStateUrl.absolute] = { 166 | url: currentStateUrl.relative, 167 | body: document.body.outerHTML, 168 | title: document.title, 169 | positionY: window.pageYOffset, 170 | positionX: window.pageXOffset, 171 | cachedAt: new Date().getTime(), 172 | transitionCacheDisabled: document.querySelector('[data-no-transition-cache]') != null 173 | }; 174 | return constrainPageCacheTo(cacheSize); 175 | }; 176 | 177 | pagesCached = function(size) { 178 | if (size == null) { 179 | size = cacheSize; 180 | } 181 | if (/^[\d]+$/.test(size)) { 182 | return cacheSize = parseInt(size); 183 | } 184 | }; 185 | 186 | constrainPageCacheTo = function(limit) { 187 | var cacheTimesRecentFirst, i, key, len, pageCacheKeys, results; 188 | pageCacheKeys = Object.keys(pageCache); 189 | cacheTimesRecentFirst = pageCacheKeys.map(function(url) { 190 | return pageCache[url].cachedAt; 191 | }).sort(function(a, b) { 192 | return b - a; 193 | }); 194 | results = []; 195 | for (i = 0, len = pageCacheKeys.length; i < len; i++) { 196 | key = pageCacheKeys[i]; 197 | if (pageCache[key].cachedAt <= cacheTimesRecentFirst[limit]) { 198 | results.push(delete pageCache[key]); 199 | } 200 | } 201 | return results; 202 | }; 203 | 204 | replace = function(html, options) { 205 | if (options == null) { 206 | options = {}; 207 | } 208 | return changePage(createDocument(html), options); 209 | }; 210 | 211 | changePage = function(doc, options) { 212 | var csrfToken, existingBody, nodesToBeKept, ref, scriptsToRun, targetBody, title; 213 | ref = extractTitleAndBody(doc), title = ref[0], targetBody = ref[1], csrfToken = ref[2]; 214 | if (title == null) { 215 | title = options.title; 216 | } 217 | triggerEvent(EVENTS.BEFORE_UNLOAD); 218 | document.title = title; 219 | if (options.change) { 220 | swapNodes(targetBody, findNodes(document.body, '[data-turbolinks-temporary]'), { 221 | keep: false 222 | }); 223 | swapNodes(targetBody, findNodesMatchingKeys(document.body, options.change), { 224 | keep: false 225 | }); 226 | } else { 227 | if (!options.flush) { 228 | nodesToBeKept = findNodes(document.body, '[data-turbolinks-permanent]'); 229 | if (options.keep) { 230 | nodesToBeKept.push.apply(nodesToBeKept, findNodesMatchingKeys(document.body, options.keep)); 231 | } 232 | swapNodes(targetBody, nodesToBeKept, { 233 | keep: true 234 | }); 235 | } 236 | existingBody = document.documentElement.replaceChild(targetBody, document.body); 237 | onNodeRemoved(existingBody); 238 | if (csrfToken != null) { 239 | CSRFToken.update(csrfToken); 240 | } 241 | setAutofocusElement(); 242 | } 243 | scriptsToRun = options.runScripts === false ? 'script[data-turbolinks-eval="always"]' : 'script:not([data-turbolinks-eval="false"])'; 244 | executeScriptTags(scriptsToRun); 245 | currentState = window.history.state; 246 | triggerEvent(EVENTS.CHANGE); 247 | return triggerEvent(EVENTS.UPDATE); 248 | }; 249 | 250 | findNodes = function(body, selector) { 251 | return Array.prototype.slice.apply(body.querySelectorAll(selector)); 252 | }; 253 | 254 | findNodesMatchingKeys = function(body, keys) { 255 | var i, key, len, matchingNodes, ref; 256 | matchingNodes = []; 257 | ref = (Array.isArray(keys) ? keys : [keys]); 258 | for (i = 0, len = ref.length; i < len; i++) { 259 | key = ref[i]; 260 | matchingNodes.push.apply(matchingNodes, findNodes(body, '[id^="' + key + ':"], [id="' + key + '"]')); 261 | } 262 | return matchingNodes; 263 | }; 264 | 265 | swapNodes = function(targetBody, existingNodes, options) { 266 | var existingNode, i, len, nodeId, targetNode; 267 | for (i = 0, len = existingNodes.length; i < len; i++) { 268 | existingNode = existingNodes[i]; 269 | if (!(nodeId = existingNode.getAttribute('id'))) { 270 | throw new Error("Turbolinks partial replace: turbolinks elements must have an id."); 271 | } 272 | if (targetNode = targetBody.querySelector('[id="' + nodeId + '"]')) { 273 | if (options.keep) { 274 | existingNode.parentNode.insertBefore(existingNode.cloneNode(true), existingNode); 275 | targetBody.ownerDocument.adoptNode(existingNode); 276 | targetNode.parentNode.replaceChild(existingNode, targetNode); 277 | } else { 278 | targetNode = targetNode.cloneNode(true); 279 | existingNode.parentNode.replaceChild(targetNode, existingNode); 280 | onNodeRemoved(existingNode); 281 | } 282 | } 283 | } 284 | }; 285 | 286 | onNodeRemoved = function(node) { 287 | if (typeof jQuery !== 'undefined') { 288 | jQuery(node).remove(); 289 | } 290 | return triggerEvent(EVENTS.AFTER_REMOVE, node); 291 | }; 292 | 293 | executeScriptTags = function(selector) { 294 | var attr, copy, i, j, len, len1, nextSibling, parentNode, ref, ref1, script, scripts; 295 | scripts = document.body.querySelectorAll(selector); 296 | for (i = 0, len = scripts.length; i < len; i++) { 297 | script = scripts[i]; 298 | if (!((ref = script.type) === '' || ref === 'text/javascript')) { 299 | continue; 300 | } 301 | copy = document.createElement('script'); 302 | ref1 = script.attributes; 303 | for (j = 0, len1 = ref1.length; j < len1; j++) { 304 | attr = ref1[j]; 305 | copy.setAttribute(attr.name, attr.value); 306 | } 307 | if (!script.hasAttribute('async')) { 308 | copy.async = false; 309 | } 310 | copy.appendChild(document.createTextNode(script.innerHTML)); 311 | parentNode = script.parentNode, nextSibling = script.nextSibling; 312 | parentNode.removeChild(script); 313 | parentNode.insertBefore(copy, nextSibling); 314 | } 315 | }; 316 | 317 | removeNoscriptTags = function(node) { 318 | node.innerHTML = node.innerHTML.replace(//ig, ''); 319 | return node; 320 | }; 321 | 322 | setAutofocusElement = function() { 323 | var autofocusElement, list; 324 | autofocusElement = (list = document.querySelectorAll('input[autofocus], textarea[autofocus]'))[list.length - 1]; 325 | if (autofocusElement && document.activeElement !== autofocusElement) { 326 | return autofocusElement.focus(); 327 | } 328 | }; 329 | 330 | reflectNewUrl = function(url) { 331 | if ((url = new ComponentUrl(url)).absolute !== referer) { 332 | return window.history.pushState({ 333 | turbolinks: true, 334 | url: url.absolute 335 | }, '', url.absolute); 336 | } 337 | }; 338 | 339 | reflectRedirectedUrl = function() { 340 | var location, preservedHash; 341 | if (location = xhr.getResponseHeader('X-XHR-Redirected-To')) { 342 | location = new ComponentUrl(location); 343 | preservedHash = location.hasNoHash() ? document.location.hash : ''; 344 | return window.history.replaceState(window.history.state, '', location.href + preservedHash); 345 | } 346 | }; 347 | 348 | crossOriginRedirect = function() { 349 | var redirect; 350 | if (((redirect = xhr.getResponseHeader('Location')) != null) && (new ComponentUrl(redirect)).crossOrigin()) { 351 | return redirect; 352 | } 353 | }; 354 | 355 | rememberReferer = function() { 356 | return referer = document.location.href; 357 | }; 358 | 359 | rememberCurrentUrl = function() { 360 | return window.history.replaceState({ 361 | turbolinks: true, 362 | url: document.location.href 363 | }, '', document.location.href); 364 | }; 365 | 366 | rememberCurrentState = function() { 367 | return currentState = window.history.state; 368 | }; 369 | 370 | manuallyTriggerHashChangeForFirefox = function() { 371 | var url; 372 | if (navigator.userAgent.match(/Firefox/) && !(url = new ComponentUrl).hasNoHash()) { 373 | window.history.replaceState(currentState, '', url.withoutHash()); 374 | return document.location.hash = url.hash; 375 | } 376 | }; 377 | 378 | recallScrollPosition = function(page) { 379 | return window.scrollTo(page.positionX, page.positionY); 380 | }; 381 | 382 | resetScrollPosition = function() { 383 | if (document.location.hash) { 384 | return document.location.href = document.location.href; 385 | } else { 386 | return window.scrollTo(0, 0); 387 | } 388 | }; 389 | 390 | clone = function(original) { 391 | var copy, key, value; 392 | if ((original == null) || typeof original !== 'object') { 393 | return original; 394 | } 395 | copy = new original.constructor(); 396 | for (key in original) { 397 | value = original[key]; 398 | copy[key] = clone(value); 399 | } 400 | return copy; 401 | }; 402 | 403 | popCookie = function(name) { 404 | var ref, value; 405 | value = ((ref = document.cookie.match(new RegExp(name + "=(\\w+)"))) != null ? ref[1].toUpperCase() : void 0) || ''; 406 | document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'; 407 | return value; 408 | }; 409 | 410 | uniqueId = function() { 411 | return new Date().getTime().toString(36); 412 | }; 413 | 414 | triggerEvent = function(name, data) { 415 | var event; 416 | if (typeof Prototype !== 'undefined') { 417 | Event.fire(document, name, data, true); 418 | } 419 | event = document.createEvent('Events'); 420 | if (data) { 421 | event.data = data; 422 | } 423 | event.initEvent(name, true, true); 424 | return document.dispatchEvent(event); 425 | }; 426 | 427 | pageChangePrevented = function(url) { 428 | return !triggerEvent(EVENTS.BEFORE_CHANGE, { 429 | url: url 430 | }); 431 | }; 432 | 433 | processResponse = function() { 434 | var assetsChanged, clientOrServerError, doc, downloadingFile, extractTrackAssets, intersection, validContent; 435 | clientOrServerError = function() { 436 | var ref; 437 | return (400 <= (ref = xhr.status) && ref < 600); 438 | }; 439 | validContent = function() { 440 | var contentType; 441 | return ((contentType = xhr.getResponseHeader('Content-Type')) != null) && contentType.match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/); 442 | }; 443 | downloadingFile = function() { 444 | var disposition; 445 | return ((disposition = xhr.getResponseHeader('Content-Disposition')) != null) && disposition.match(/^attachment/); 446 | }; 447 | extractTrackAssets = function(doc) { 448 | var i, len, node, ref, results; 449 | ref = doc.querySelector('head').childNodes; 450 | results = []; 451 | for (i = 0, len = ref.length; i < len; i++) { 452 | node = ref[i]; 453 | if ((typeof node.getAttribute === "function" ? node.getAttribute('data-turbolinks-track') : void 0) != null) { 454 | results.push(node.getAttribute('src') || node.getAttribute('href')); 455 | } 456 | } 457 | return results; 458 | }; 459 | assetsChanged = function(doc) { 460 | var fetchedAssets; 461 | loadedAssets || (loadedAssets = extractTrackAssets(document)); 462 | fetchedAssets = extractTrackAssets(doc); 463 | return fetchedAssets.length !== loadedAssets.length || intersection(fetchedAssets, loadedAssets).length !== loadedAssets.length; 464 | }; 465 | intersection = function(a, b) { 466 | var i, len, ref, results, value; 467 | if (a.length > b.length) { 468 | ref = [b, a], a = ref[0], b = ref[1]; 469 | } 470 | results = []; 471 | for (i = 0, len = a.length; i < len; i++) { 472 | value = a[i]; 473 | if (indexOf.call(b, value) >= 0) { 474 | results.push(value); 475 | } 476 | } 477 | return results; 478 | }; 479 | if (!clientOrServerError() && validContent() && !downloadingFile()) { 480 | doc = createDocument(xhr.responseText); 481 | if (doc && !assetsChanged(doc)) { 482 | return doc; 483 | } 484 | } 485 | }; 486 | 487 | extractTitleAndBody = function(doc) { 488 | var title; 489 | title = doc.querySelector('title'); 490 | return [title != null ? title.textContent : void 0, removeNoscriptTags(doc.querySelector('body')), CSRFToken.get(doc).token]; 491 | }; 492 | 493 | CSRFToken = { 494 | get: function(doc) { 495 | var tag; 496 | if (doc == null) { 497 | doc = document; 498 | } 499 | return { 500 | node: tag = doc.querySelector('meta[name="csrf-token"]'), 501 | token: tag != null ? typeof tag.getAttribute === "function" ? tag.getAttribute('content') : void 0 : void 0 502 | }; 503 | }, 504 | update: function(latest) { 505 | var current; 506 | current = this.get(); 507 | if ((current.token != null) && (latest != null) && current.token !== latest) { 508 | return current.node.setAttribute('content', latest); 509 | } 510 | } 511 | }; 512 | 513 | createDocument = function(html) { 514 | var doc; 515 | doc = document.documentElement.cloneNode(); 516 | doc.innerHTML = html; 517 | doc.head = doc.querySelector('head'); 518 | doc.body = doc.querySelector('body'); 519 | return doc; 520 | }; 521 | 522 | ComponentUrl = (function() { 523 | function ComponentUrl(original1) { 524 | this.original = original1 != null ? original1 : document.location.href; 525 | if (this.original.constructor === ComponentUrl) { 526 | return this.original; 527 | } 528 | this._parse(); 529 | } 530 | 531 | ComponentUrl.prototype.withoutHash = function() { 532 | return this.href.replace(this.hash, '').replace('#', ''); 533 | }; 534 | 535 | ComponentUrl.prototype.withoutHashForIE10compatibility = function() { 536 | return this.withoutHash(); 537 | }; 538 | 539 | ComponentUrl.prototype.hasNoHash = function() { 540 | return this.hash.length === 0; 541 | }; 542 | 543 | ComponentUrl.prototype.crossOrigin = function() { 544 | return this.origin !== (new ComponentUrl).origin; 545 | }; 546 | 547 | ComponentUrl.prototype.formatForXHR = function(options) { 548 | if (options == null) { 549 | options = {}; 550 | } 551 | return (options.cache ? this : this.withAntiCacheParam()).withoutHashForIE10compatibility(); 552 | }; 553 | 554 | ComponentUrl.prototype.withAntiCacheParam = function() { 555 | return new ComponentUrl(/([?&])_=[^&]*/.test(this.absolute) ? this.absolute.replace(/([?&])_=[^&]*/, "$1_=" + (uniqueId())) : new ComponentUrl(this.absolute + (/\?/.test(this.absolute) ? "&" : "?") + ("_=" + (uniqueId())))); 556 | }; 557 | 558 | ComponentUrl.prototype._parse = function() { 559 | var ref; 560 | (this.link != null ? this.link : this.link = document.createElement('a')).href = this.original; 561 | ref = this.link, this.href = ref.href, this.protocol = ref.protocol, this.host = ref.host, this.hostname = ref.hostname, this.port = ref.port, this.pathname = ref.pathname, this.search = ref.search, this.hash = ref.hash; 562 | this.origin = [this.protocol, '//', this.hostname].join(''); 563 | if (this.port.length !== 0) { 564 | this.origin += ":" + this.port; 565 | } 566 | this.relative = [this.pathname, this.search, this.hash].join(''); 567 | return this.absolute = this.href; 568 | }; 569 | 570 | return ComponentUrl; 571 | 572 | })(); 573 | 574 | Link = (function(superClass) { 575 | extend(Link, superClass); 576 | 577 | Link.HTML_EXTENSIONS = ['html']; 578 | 579 | Link.allowExtensions = function() { 580 | var extension, extensions, i, len; 581 | extensions = 1 <= arguments.length ? slice.call(arguments, 0) : []; 582 | for (i = 0, len = extensions.length; i < len; i++) { 583 | extension = extensions[i]; 584 | Link.HTML_EXTENSIONS.push(extension); 585 | } 586 | return Link.HTML_EXTENSIONS; 587 | }; 588 | 589 | function Link(link1) { 590 | this.link = link1; 591 | if (this.link.constructor === Link) { 592 | return this.link; 593 | } 594 | this.original = this.link.href; 595 | this.originalElement = this.link; 596 | this.link = this.link.cloneNode(false); 597 | Link.__super__.constructor.apply(this, arguments); 598 | } 599 | 600 | Link.prototype.shouldIgnore = function() { 601 | return this.crossOrigin() || this._anchored() || this._nonHtml() || this._optOut() || this._target(); 602 | }; 603 | 604 | Link.prototype._anchored = function() { 605 | return (this.hash.length > 0 || this.href.charAt(this.href.length - 1) === '#') && (this.withoutHash() === (new ComponentUrl).withoutHash()); 606 | }; 607 | 608 | Link.prototype._nonHtml = function() { 609 | return this.pathname.match(/\.[a-z]+$/g) && !this.pathname.match(new RegExp("\\.(?:" + (Link.HTML_EXTENSIONS.join('|')) + ")?$", 'g')); 610 | }; 611 | 612 | Link.prototype._optOut = function() { 613 | var ignore, link; 614 | link = this.originalElement; 615 | while (!(ignore || link === document)) { 616 | ignore = link.getAttribute('data-no-turbolink') != null; 617 | link = link.parentNode; 618 | } 619 | return ignore; 620 | }; 621 | 622 | Link.prototype._target = function() { 623 | return this.link.target.length !== 0; 624 | }; 625 | 626 | return Link; 627 | 628 | })(ComponentUrl); 629 | 630 | Click = (function() { 631 | Click.installHandlerLast = function(event) { 632 | if (!event.defaultPrevented) { 633 | document.removeEventListener('click', Click.handle, false); 634 | return document.addEventListener('click', Click.handle, false); 635 | } 636 | }; 637 | 638 | Click.handle = function(event) { 639 | return new Click(event); 640 | }; 641 | 642 | function Click(event1) { 643 | this.event = event1; 644 | if (this.event.defaultPrevented) { 645 | return; 646 | } 647 | this._extractLink(); 648 | if (this._validForTurbolinks()) { 649 | if (!pageChangePrevented(this.link.absolute)) { 650 | visit(this.link.href); 651 | } 652 | this.event.preventDefault(); 653 | } 654 | } 655 | 656 | Click.prototype._extractLink = function() { 657 | var link; 658 | link = this.event.target; 659 | while (!(!link.parentNode || link.nodeName === 'A')) { 660 | link = link.parentNode; 661 | } 662 | if (link.nodeName === 'A' && link.href.length !== 0) { 663 | return this.link = new Link(link); 664 | } 665 | }; 666 | 667 | Click.prototype._validForTurbolinks = function() { 668 | return (this.link != null) && !(this.link.shouldIgnore() || this._nonStandardClick()); 669 | }; 670 | 671 | Click.prototype._nonStandardClick = function() { 672 | return this.event.which > 1 || this.event.metaKey || this.event.ctrlKey || this.event.shiftKey || this.event.altKey; 673 | }; 674 | 675 | return Click; 676 | 677 | })(); 678 | 679 | ProgressBar = (function() { 680 | var className, originalOpacity; 681 | 682 | className = 'turbolinks-progress-bar'; 683 | 684 | originalOpacity = 0.99; 685 | 686 | ProgressBar.enable = function() { 687 | return progressBar != null ? progressBar : progressBar = new ProgressBar('html'); 688 | }; 689 | 690 | ProgressBar.disable = function() { 691 | if (progressBar != null) { 692 | progressBar.uninstall(); 693 | } 694 | return progressBar = null; 695 | }; 696 | 697 | function ProgressBar(elementSelector) { 698 | this.elementSelector = elementSelector; 699 | this._trickle = bind(this._trickle, this); 700 | this._reset = bind(this._reset, this); 701 | this.value = 0; 702 | this.content = ''; 703 | this.speed = 300; 704 | this.opacity = originalOpacity; 705 | this.install(); 706 | } 707 | 708 | ProgressBar.prototype.install = function() { 709 | this.element = document.querySelector(this.elementSelector); 710 | this.element.classList.add(className); 711 | this.styleElement = document.createElement('style'); 712 | document.head.appendChild(this.styleElement); 713 | return this._updateStyle(); 714 | }; 715 | 716 | ProgressBar.prototype.uninstall = function() { 717 | this.element.classList.remove(className); 718 | return document.head.removeChild(this.styleElement); 719 | }; 720 | 721 | ProgressBar.prototype.start = function() { 722 | if (this.value > 0) { 723 | this._reset(); 724 | this._reflow(); 725 | } 726 | return this.advanceTo(5); 727 | }; 728 | 729 | ProgressBar.prototype.advanceTo = function(value) { 730 | var ref; 731 | if ((value > (ref = this.value) && ref <= 100)) { 732 | this.value = value; 733 | this._updateStyle(); 734 | if (this.value === 100) { 735 | return this._stopTrickle(); 736 | } else if (this.value > 0) { 737 | return this._startTrickle(); 738 | } 739 | } 740 | }; 741 | 742 | ProgressBar.prototype.done = function() { 743 | if (this.value > 0) { 744 | this.advanceTo(100); 745 | return this._finish(); 746 | } 747 | }; 748 | 749 | ProgressBar.prototype._finish = function() { 750 | this.fadeTimer = setTimeout((function(_this) { 751 | return function() { 752 | _this.opacity = 0; 753 | return _this._updateStyle(); 754 | }; 755 | })(this), this.speed / 2); 756 | return this.resetTimer = setTimeout(this._reset, this.speed); 757 | }; 758 | 759 | ProgressBar.prototype._reflow = function() { 760 | return this.element.offsetHeight; 761 | }; 762 | 763 | ProgressBar.prototype._reset = function() { 764 | this._stopTimers(); 765 | this.value = 0; 766 | this.opacity = originalOpacity; 767 | return this._withSpeed(0, (function(_this) { 768 | return function() { 769 | return _this._updateStyle(true); 770 | }; 771 | })(this)); 772 | }; 773 | 774 | ProgressBar.prototype._stopTimers = function() { 775 | this._stopTrickle(); 776 | clearTimeout(this.fadeTimer); 777 | return clearTimeout(this.resetTimer); 778 | }; 779 | 780 | ProgressBar.prototype._startTrickle = function() { 781 | if (this.trickleTimer) { 782 | return; 783 | } 784 | return this.trickleTimer = setTimeout(this._trickle, this.speed); 785 | }; 786 | 787 | ProgressBar.prototype._stopTrickle = function() { 788 | clearTimeout(this.trickleTimer); 789 | return delete this.trickleTimer; 790 | }; 791 | 792 | ProgressBar.prototype._trickle = function() { 793 | this.advanceTo(this.value + Math.random() / 2); 794 | return this.trickleTimer = setTimeout(this._trickle, this.speed); 795 | }; 796 | 797 | ProgressBar.prototype._withSpeed = function(speed, fn) { 798 | var originalSpeed, result; 799 | originalSpeed = this.speed; 800 | this.speed = speed; 801 | result = fn(); 802 | this.speed = originalSpeed; 803 | return result; 804 | }; 805 | 806 | ProgressBar.prototype._updateStyle = function(forceRepaint) { 807 | if (forceRepaint == null) { 808 | forceRepaint = false; 809 | } 810 | if (forceRepaint) { 811 | this._changeContentToForceRepaint(); 812 | } 813 | return this.styleElement.textContent = this._createCSSRule(); 814 | }; 815 | 816 | ProgressBar.prototype._changeContentToForceRepaint = function() { 817 | return this.content = this.content === '' ? ' ' : ''; 818 | }; 819 | 820 | ProgressBar.prototype._createCSSRule = function() { 821 | return this.elementSelector + "." + className + "::before {\n content: '" + this.content + "';\n position: fixed;\n top: 0;\n left: 0;\n z-index: 2000;\n background-color: #0076ff;\n height: 3px;\n opacity: " + this.opacity + ";\n width: " + this.value + "%;\n transition: width " + this.speed + "ms ease-out, opacity " + (this.speed / 2) + "ms ease-in;\n transform: translate3d(0,0,0);\n}"; 822 | }; 823 | 824 | return ProgressBar; 825 | 826 | })(); 827 | 828 | ProgressBarAPI = { 829 | enable: ProgressBar.enable, 830 | disable: ProgressBar.disable, 831 | start: function() { 832 | return ProgressBar.enable().start(); 833 | }, 834 | advanceTo: function(value) { 835 | return progressBar != null ? progressBar.advanceTo(value) : void 0; 836 | }, 837 | done: function() { 838 | return progressBar != null ? progressBar.done() : void 0; 839 | } 840 | }; 841 | 842 | installDocumentReadyPageEventTriggers = function() { 843 | return document.addEventListener('DOMContentLoaded', (function() { 844 | triggerEvent(EVENTS.CHANGE); 845 | return triggerEvent(EVENTS.UPDATE); 846 | }), true); 847 | }; 848 | 849 | installJqueryAjaxSuccessPageUpdateTrigger = function() { 850 | if (typeof jQuery !== 'undefined') { 851 | return jQuery(document).on('ajaxSuccess', function(event, xhr, settings) { 852 | if (!jQuery.trim(xhr.responseText)) { 853 | return; 854 | } 855 | return triggerEvent(EVENTS.UPDATE); 856 | }); 857 | } 858 | }; 859 | 860 | onHistoryChange = function(event) { 861 | var cachedPage, ref; 862 | if (((ref = event.state) != null ? ref.turbolinks : void 0) && event.state.url !== currentState.url) { 863 | if (cachedPage = pageCache[(new ComponentUrl(event.state.url)).absolute]) { 864 | cacheCurrentPage(); 865 | return fetchHistory(cachedPage); 866 | } else { 867 | return visit(event.target.location.href); 868 | } 869 | } 870 | }; 871 | 872 | initializeTurbolinks = function() { 873 | rememberCurrentUrl(); 874 | rememberCurrentState(); 875 | ProgressBar.enable(); 876 | document.addEventListener('click', Click.installHandlerLast, true); 877 | window.addEventListener('hashchange', function(event) { 878 | rememberCurrentUrl(); 879 | return rememberCurrentState(); 880 | }, false); 881 | return window.addEventListener('popstate', onHistoryChange, false); 882 | }; 883 | 884 | browserSupportsPushState = window.history && 'pushState' in window.history; 885 | 886 | ua = navigator.userAgent; 887 | 888 | browserIsBuggy = (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && ua.indexOf('Mobile Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1; 889 | 890 | requestMethodIsSafe = (ref = popCookie('request_method')) === 'GET' || ref === ''; 891 | 892 | browserSupportsTurbolinks = browserSupportsPushState && !browserIsBuggy && requestMethodIsSafe; 893 | 894 | browserSupportsCustomEvents = document.addEventListener && document.createEvent; 895 | 896 | if (browserSupportsCustomEvents) { 897 | installDocumentReadyPageEventTriggers(); 898 | installJqueryAjaxSuccessPageUpdateTrigger(); 899 | } 900 | 901 | if (browserSupportsTurbolinks) { 902 | visit = fetch; 903 | initializeTurbolinks(); 904 | } else { 905 | visit = function(url) { 906 | return document.location.href = url; 907 | }; 908 | } 909 | 910 | this.Turbolinks = { 911 | visit: visit, 912 | replace: replace, 913 | pagesCached: pagesCached, 914 | cacheCurrentPage: cacheCurrentPage, 915 | enableTransitionCache: enableTransitionCache, 916 | disableRequestCaching: disableRequestCaching, 917 | ProgressBar: ProgressBarAPI, 918 | allowLinkExtensions: Link.allowExtensions, 919 | supported: browserSupportsTurbolinks, 920 | EVENTS: clone(EVENTS) 921 | }; 922 | 923 | }).call(this); 924 | -------------------------------------------------------------------------------- /turbolinks/static/turbolinks/turbolinks.js.coffee: -------------------------------------------------------------------------------- 1 | pageCache = {} 2 | cacheSize = 10 3 | transitionCacheEnabled = false 4 | requestCachingEnabled = true 5 | progressBar = null 6 | 7 | currentState = null 8 | loadedAssets = null 9 | 10 | referer = null 11 | 12 | xhr = null 13 | 14 | EVENTS = 15 | BEFORE_CHANGE: 'page:before-change' 16 | FETCH: 'page:fetch' 17 | RECEIVE: 'page:receive' 18 | CHANGE: 'page:change' 19 | UPDATE: 'page:update' 20 | LOAD: 'page:load' 21 | RESTORE: 'page:restore' 22 | BEFORE_UNLOAD: 'page:before-unload' 23 | AFTER_REMOVE: 'page:after-remove' 24 | 25 | fetch = (url, options = {}) -> 26 | url = new ComponentUrl url 27 | 28 | rememberReferer() 29 | cacheCurrentPage() 30 | progressBar?.start() 31 | 32 | if transitionCacheEnabled and cachedPage = transitionCacheFor(url.absolute) 33 | fetchHistory cachedPage 34 | options.showProgressBar = false 35 | fetchReplacement url, options 36 | else 37 | options.onLoadFunction = resetScrollPosition 38 | fetchReplacement url, options 39 | 40 | transitionCacheFor = (url) -> 41 | cachedPage = pageCache[url] 42 | cachedPage if cachedPage and !cachedPage.transitionCacheDisabled 43 | 44 | enableTransitionCache = (enable = true) -> 45 | transitionCacheEnabled = enable 46 | 47 | disableRequestCaching = (disable = true) -> 48 | requestCachingEnabled = not disable 49 | disable 50 | 51 | fetchReplacement = (url, options) -> 52 | options.cacheRequest ?= requestCachingEnabled 53 | options.showProgressBar ?= true 54 | 55 | triggerEvent EVENTS.FETCH, url: url.absolute 56 | 57 | xhr?.abort() 58 | xhr = new XMLHttpRequest 59 | xhr.open 'GET', url.formatForXHR(cache: options.cacheRequest), true 60 | xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml' 61 | xhr.setRequestHeader 'X-XHR-Referer', referer 62 | 63 | xhr.onload = -> 64 | triggerEvent EVENTS.RECEIVE, url: url.absolute 65 | 66 | if doc = processResponse() 67 | reflectNewUrl url 68 | reflectRedirectedUrl() 69 | changePage doc, options 70 | if options.showProgressBar 71 | progressBar?.done() 72 | manuallyTriggerHashChangeForFirefox() 73 | options.onLoadFunction?() 74 | triggerEvent EVENTS.LOAD 75 | else 76 | progressBar?.done() 77 | document.location.href = crossOriginRedirect() or url.absolute 78 | 79 | if progressBar and options.showProgressBar 80 | xhr.onprogress = (event) => 81 | percent = if event.lengthComputable 82 | event.loaded / event.total * 100 83 | else 84 | progressBar.value + (100 - progressBar.value) / 10 85 | progressBar.advanceTo(percent) 86 | 87 | xhr.onloadend = -> xhr = null 88 | xhr.onerror = -> document.location.href = url.absolute 89 | 90 | xhr.send() 91 | 92 | fetchHistory = (cachedPage) -> 93 | xhr?.abort() 94 | changePage createDocument(cachedPage.body), title: cachedPage.title, runScripts: false 95 | progressBar?.done() 96 | recallScrollPosition cachedPage 97 | triggerEvent EVENTS.RESTORE 98 | 99 | cacheCurrentPage = -> 100 | currentStateUrl = new ComponentUrl currentState.url 101 | 102 | pageCache[currentStateUrl.absolute] = 103 | url: currentStateUrl.relative, 104 | body: document.body.outerHTML, 105 | title: document.title, 106 | positionY: window.pageYOffset, 107 | positionX: window.pageXOffset, 108 | cachedAt: new Date().getTime(), 109 | transitionCacheDisabled: document.querySelector('[data-no-transition-cache]')? 110 | 111 | constrainPageCacheTo cacheSize 112 | 113 | pagesCached = (size = cacheSize) -> 114 | cacheSize = parseInt(size) if /^[\d]+$/.test size 115 | 116 | constrainPageCacheTo = (limit) -> 117 | pageCacheKeys = Object.keys pageCache 118 | 119 | cacheTimesRecentFirst = pageCacheKeys.map (url) -> 120 | pageCache[url].cachedAt 121 | .sort (a, b) -> b - a 122 | 123 | for key in pageCacheKeys when pageCache[key].cachedAt <= cacheTimesRecentFirst[limit] 124 | delete pageCache[key] 125 | 126 | replace = (html, options = {}) -> 127 | changePage createDocument(html), options 128 | 129 | changePage = (doc, options) -> 130 | [title, targetBody, csrfToken] = extractTitleAndBody(doc) 131 | title ?= options.title 132 | 133 | triggerEvent EVENTS.BEFORE_UNLOAD 134 | document.title = title 135 | 136 | if options.change 137 | swapNodes(targetBody, findNodes(document.body, '[data-turbolinks-temporary]'), keep: false) 138 | swapNodes(targetBody, findNodesMatchingKeys(document.body, options.change), keep: false) 139 | else 140 | unless options.flush 141 | nodesToBeKept = findNodes(document.body, '[data-turbolinks-permanent]') 142 | nodesToBeKept.push(findNodesMatchingKeys(document.body, options.keep)...) if options.keep 143 | swapNodes(targetBody, nodesToBeKept, keep: true) 144 | 145 | existingBody = document.documentElement.replaceChild(targetBody, document.body) 146 | onNodeRemoved(existingBody) 147 | CSRFToken.update csrfToken if csrfToken? 148 | setAutofocusElement() 149 | 150 | scriptsToRun = if options.runScripts is false then 'script[data-turbolinks-eval="always"]' else 'script:not([data-turbolinks-eval="false"])' 151 | executeScriptTags(scriptsToRun) 152 | currentState = window.history.state 153 | 154 | triggerEvent EVENTS.CHANGE 155 | triggerEvent EVENTS.UPDATE 156 | 157 | findNodes = (body, selector) -> 158 | Array::slice.apply(body.querySelectorAll(selector)) 159 | 160 | findNodesMatchingKeys = (body, keys) -> 161 | matchingNodes = [] 162 | for key in (if Array.isArray(keys) then keys else [keys]) 163 | matchingNodes.push(findNodes(body, '[id^="'+key+':"], [id="'+key+'"]')...) 164 | 165 | return matchingNodes 166 | 167 | swapNodes = (targetBody, existingNodes, options) -> 168 | for existingNode in existingNodes 169 | unless nodeId = existingNode.getAttribute('id') 170 | throw new Error("Turbolinks partial replace: turbolinks elements must have an id.") 171 | 172 | if targetNode = targetBody.querySelector('[id="'+nodeId+'"]') 173 | if options.keep 174 | existingNode.parentNode.insertBefore(existingNode.cloneNode(true), existingNode) 175 | targetBody.ownerDocument.adoptNode(existingNode) 176 | targetNode.parentNode.replaceChild(existingNode, targetNode) 177 | else 178 | targetNode = targetNode.cloneNode(true) 179 | existingNode.parentNode.replaceChild(targetNode, existingNode) 180 | onNodeRemoved(existingNode) 181 | return 182 | 183 | onNodeRemoved = (node) -> 184 | if typeof jQuery isnt 'undefined' 185 | jQuery(node).remove() 186 | triggerEvent(EVENTS.AFTER_REMOVE, node) 187 | 188 | executeScriptTags = (selector) -> 189 | scripts = document.body.querySelectorAll(selector) 190 | for script in scripts when script.type in ['', 'text/javascript'] 191 | copy = document.createElement 'script' 192 | copy.setAttribute attr.name, attr.value for attr in script.attributes 193 | copy.async = false unless script.hasAttribute 'async' 194 | copy.appendChild document.createTextNode script.innerHTML 195 | { parentNode, nextSibling } = script 196 | parentNode.removeChild script 197 | parentNode.insertBefore copy, nextSibling 198 | return 199 | 200 | removeNoscriptTags = (node) -> 201 | node.innerHTML = node.innerHTML.replace //ig, '' 202 | node 203 | 204 | # Firefox bug: Doesn't autofocus fields that are inserted via JavaScript 205 | setAutofocusElement = -> 206 | autofocusElement = (list = document.querySelectorAll 'input[autofocus], textarea[autofocus]')[list.length - 1] 207 | if autofocusElement and document.activeElement isnt autofocusElement 208 | autofocusElement.focus() 209 | 210 | reflectNewUrl = (url) -> 211 | if (url = new ComponentUrl url).absolute isnt referer 212 | window.history.pushState { turbolinks: true, url: url.absolute }, '', url.absolute 213 | 214 | reflectRedirectedUrl = -> 215 | if location = xhr.getResponseHeader 'X-XHR-Redirected-To' 216 | location = new ComponentUrl location 217 | preservedHash = if location.hasNoHash() then document.location.hash else '' 218 | window.history.replaceState window.history.state, '', location.href + preservedHash 219 | 220 | crossOriginRedirect = -> 221 | redirect if (redirect = xhr.getResponseHeader('Location'))? and (new ComponentUrl(redirect)).crossOrigin() 222 | 223 | rememberReferer = -> 224 | referer = document.location.href 225 | 226 | rememberCurrentUrl = -> 227 | window.history.replaceState { turbolinks: true, url: document.location.href }, '', document.location.href 228 | 229 | rememberCurrentState = -> 230 | currentState = window.history.state 231 | 232 | # Unlike other browsers, Firefox doesn't trigger hashchange after changing the 233 | # location (via pushState) to an anchor on a different page. For example: 234 | # 235 | # /pages/one => /pages/two#with-hash 236 | # 237 | # By forcing Firefox to trigger hashchange, the rest of the code can rely on more 238 | # consistent behavior across browsers. 239 | manuallyTriggerHashChangeForFirefox = -> 240 | if navigator.userAgent.match(/Firefox/) and !(url = (new ComponentUrl)).hasNoHash() 241 | window.history.replaceState currentState, '', url.withoutHash() 242 | document.location.hash = url.hash 243 | 244 | recallScrollPosition = (page) -> 245 | window.scrollTo page.positionX, page.positionY 246 | 247 | resetScrollPosition = -> 248 | if document.location.hash 249 | document.location.href = document.location.href 250 | else 251 | window.scrollTo 0, 0 252 | 253 | clone = (original) -> 254 | return original if not original? or typeof original isnt 'object' 255 | copy = new original.constructor() 256 | copy[key] = clone value for key, value of original 257 | copy 258 | 259 | popCookie = (name) -> 260 | value = document.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or '' 261 | document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/' 262 | value 263 | 264 | uniqueId = -> 265 | new Date().getTime().toString(36) 266 | 267 | triggerEvent = (name, data) -> 268 | if typeof Prototype isnt 'undefined' 269 | Event.fire document, name, data, true 270 | 271 | event = document.createEvent 'Events' 272 | event.data = data if data 273 | event.initEvent name, true, true 274 | document.dispatchEvent event 275 | 276 | pageChangePrevented = (url) -> 277 | !triggerEvent EVENTS.BEFORE_CHANGE, url: url 278 | 279 | processResponse = -> 280 | clientOrServerError = -> 281 | 400 <= xhr.status < 600 282 | 283 | validContent = -> 284 | (contentType = xhr.getResponseHeader('Content-Type'))? and 285 | contentType.match /^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/ 286 | 287 | downloadingFile = -> 288 | (disposition = xhr.getResponseHeader('Content-Disposition'))? and 289 | disposition.match /^attachment/ 290 | 291 | extractTrackAssets = (doc) -> 292 | for node in doc.querySelector('head').childNodes when node.getAttribute?('data-turbolinks-track')? 293 | node.getAttribute('src') or node.getAttribute('href') 294 | 295 | assetsChanged = (doc) -> 296 | loadedAssets ||= extractTrackAssets document 297 | fetchedAssets = extractTrackAssets doc 298 | fetchedAssets.length isnt loadedAssets.length or intersection(fetchedAssets, loadedAssets).length isnt loadedAssets.length 299 | 300 | intersection = (a, b) -> 301 | [a, b] = [b, a] if a.length > b.length 302 | value for value in a when value in b 303 | 304 | if not clientOrServerError() and validContent() and not downloadingFile() 305 | doc = createDocument xhr.responseText 306 | if doc and !assetsChanged doc 307 | return doc 308 | 309 | extractTitleAndBody = (doc) -> 310 | title = doc.querySelector 'title' 311 | [ title?.textContent, removeNoscriptTags(doc.querySelector('body')), CSRFToken.get(doc).token ] 312 | 313 | CSRFToken = 314 | get: (doc = document) -> 315 | node: tag = doc.querySelector 'meta[name="csrf-token"]' 316 | token: tag?.getAttribute? 'content' 317 | 318 | update: (latest) -> 319 | current = @get() 320 | if current.token? and latest? and current.token isnt latest 321 | current.node.setAttribute 'content', latest 322 | 323 | createDocument = (html) -> 324 | doc = document.documentElement.cloneNode() 325 | doc.innerHTML = html 326 | doc.head = doc.querySelector 'head' 327 | doc.body = doc.querySelector 'body' 328 | doc 329 | 330 | # The ComponentUrl class converts a basic URL string into an object 331 | # that behaves similarly to document.location. 332 | # 333 | # If an instance is created from a relative URL, the current document 334 | # is used to fill in the missing attributes (protocol, host, port). 335 | class ComponentUrl 336 | constructor: (@original = document.location.href) -> 337 | return @original if @original.constructor is ComponentUrl 338 | @_parse() 339 | 340 | withoutHash: -> @href.replace(@hash, '').replace('#', '') 341 | 342 | # Intention revealing function alias 343 | withoutHashForIE10compatibility: -> @withoutHash() 344 | 345 | hasNoHash: -> @hash.length is 0 346 | 347 | crossOrigin: -> 348 | @origin isnt (new ComponentUrl).origin 349 | 350 | formatForXHR: (options = {}) -> 351 | (if options.cache then @ else @withAntiCacheParam()).withoutHashForIE10compatibility() 352 | 353 | withAntiCacheParam: -> 354 | new ComponentUrl( 355 | if /([?&])_=[^&]*/.test @absolute 356 | @absolute.replace /([?&])_=[^&]*/, "$1_=#{uniqueId()}" 357 | else 358 | new ComponentUrl(@absolute + (if /\?/.test(@absolute) then "&" else "?") + "_=#{uniqueId()}") 359 | ) 360 | 361 | _parse: -> 362 | (@link ?= document.createElement 'a').href = @original 363 | { @href, @protocol, @host, @hostname, @port, @pathname, @search, @hash } = @link 364 | @origin = [@protocol, '//', @hostname].join '' 365 | @origin += ":#{@port}" unless @port.length is 0 366 | @relative = [@pathname, @search, @hash].join '' 367 | @absolute = @href 368 | 369 | # The Link class derives from the ComponentUrl class, but is built from an 370 | # existing link element. Provides verification functionality for Turbolinks 371 | # to use in determining whether it should process the link when clicked. 372 | class Link extends ComponentUrl 373 | @HTML_EXTENSIONS: ['html'] 374 | 375 | @allowExtensions: (extensions...) -> 376 | Link.HTML_EXTENSIONS.push extension for extension in extensions 377 | Link.HTML_EXTENSIONS 378 | 379 | constructor: (@link) -> 380 | return @link if @link.constructor is Link 381 | @original = @link.href 382 | @originalElement = @link 383 | @link = @link.cloneNode false 384 | super 385 | 386 | shouldIgnore: -> 387 | @crossOrigin() or 388 | @_anchored() or 389 | @_nonHtml() or 390 | @_optOut() or 391 | @_target() 392 | 393 | _anchored: -> 394 | (@hash.length > 0 or @href.charAt(@href.length - 1) is '#') and 395 | (@withoutHash() is (new ComponentUrl).withoutHash()) 396 | 397 | _nonHtml: -> 398 | @pathname.match(/\.[a-z]+$/g) and not @pathname.match(new RegExp("\\.(?:#{Link.HTML_EXTENSIONS.join('|')})?$", 'g')) 399 | 400 | _optOut: -> 401 | link = @originalElement 402 | until ignore or link is document 403 | ignore = link.getAttribute('data-no-turbolink')? 404 | link = link.parentNode 405 | ignore 406 | 407 | _target: -> 408 | @link.target.length isnt 0 409 | 410 | 411 | # The Click class handles clicked links, verifying if Turbolinks should 412 | # take control by inspecting both the event and the link. If it should, 413 | # the page change process is initiated. If not, control is passed back 414 | # to the browser for default functionality. 415 | class Click 416 | @installHandlerLast: (event) -> 417 | unless event.defaultPrevented 418 | document.removeEventListener 'click', Click.handle, false 419 | document.addEventListener 'click', Click.handle, false 420 | 421 | @handle: (event) -> 422 | new Click event 423 | 424 | constructor: (@event) -> 425 | return if @event.defaultPrevented 426 | @_extractLink() 427 | if @_validForTurbolinks() 428 | visit @link.href unless pageChangePrevented(@link.absolute) 429 | @event.preventDefault() 430 | 431 | _extractLink: -> 432 | link = @event.target 433 | link = link.parentNode until !link.parentNode or link.nodeName is 'A' 434 | @link = new Link(link) if link.nodeName is 'A' and link.href.length isnt 0 435 | 436 | _validForTurbolinks: -> 437 | @link? and not (@link.shouldIgnore() or @_nonStandardClick()) 438 | 439 | _nonStandardClick: -> 440 | @event.which > 1 or 441 | @event.metaKey or 442 | @event.ctrlKey or 443 | @event.shiftKey or 444 | @event.altKey 445 | 446 | 447 | class ProgressBar 448 | className = 'turbolinks-progress-bar' 449 | # Setting the opacity to a value < 1 fixes a display issue in Safari 6 and 450 | # iOS 6 where the progress bar would fill the entire page. 451 | originalOpacity = 0.99 452 | 453 | @enable: -> 454 | progressBar ?= new ProgressBar 'html' 455 | 456 | @disable: -> 457 | progressBar?.uninstall() 458 | progressBar = null 459 | 460 | constructor: (@elementSelector) -> 461 | @value = 0 462 | @content = '' 463 | @speed = 300 464 | @opacity = originalOpacity 465 | @install() 466 | 467 | install: -> 468 | @element = document.querySelector(@elementSelector) 469 | @element.classList.add(className) 470 | @styleElement = document.createElement('style') 471 | document.head.appendChild(@styleElement) 472 | @_updateStyle() 473 | 474 | uninstall: -> 475 | @element.classList.remove(className) 476 | document.head.removeChild(@styleElement) 477 | 478 | start: -> 479 | if @value > 0 480 | @_reset() 481 | @_reflow() 482 | 483 | @advanceTo(5) 484 | 485 | advanceTo: (value) -> 486 | if value > @value <= 100 487 | @value = value 488 | @_updateStyle() 489 | 490 | if @value is 100 491 | @_stopTrickle() 492 | else if @value > 0 493 | @_startTrickle() 494 | 495 | done: -> 496 | if @value > 0 497 | @advanceTo(100) 498 | @_finish() 499 | 500 | _finish: -> 501 | @fadeTimer = setTimeout => 502 | @opacity = 0 503 | @_updateStyle() 504 | , @speed / 2 505 | 506 | @resetTimer = setTimeout(@_reset, @speed) 507 | 508 | _reflow: -> 509 | @element.offsetHeight 510 | 511 | _reset: => 512 | @_stopTimers() 513 | @value = 0 514 | @opacity = originalOpacity 515 | @_withSpeed(0, => @_updateStyle(true)) 516 | 517 | _stopTimers: -> 518 | @_stopTrickle() 519 | clearTimeout(@fadeTimer) 520 | clearTimeout(@resetTimer) 521 | 522 | _startTrickle: -> 523 | return if @trickleTimer 524 | @trickleTimer = setTimeout(@_trickle, @speed) 525 | 526 | _stopTrickle: -> 527 | clearTimeout(@trickleTimer) 528 | delete @trickleTimer 529 | 530 | _trickle: => 531 | @advanceTo(@value + Math.random() / 2) 532 | @trickleTimer = setTimeout(@_trickle, @speed) 533 | 534 | _withSpeed: (speed, fn) -> 535 | originalSpeed = @speed 536 | @speed = speed 537 | result = fn() 538 | @speed = originalSpeed 539 | result 540 | 541 | _updateStyle: (forceRepaint = false) -> 542 | @_changeContentToForceRepaint() if forceRepaint 543 | @styleElement.textContent = @_createCSSRule() 544 | 545 | _changeContentToForceRepaint: -> 546 | @content = if @content is '' then ' ' else '' 547 | 548 | _createCSSRule: -> 549 | """ 550 | #{@elementSelector}.#{className}::before { 551 | content: '#{@content}'; 552 | position: fixed; 553 | top: 0; 554 | left: 0; 555 | z-index: 2000; 556 | background-color: #0076ff; 557 | height: 3px; 558 | opacity: #{@opacity}; 559 | width: #{@value}%; 560 | transition: width #{@speed}ms ease-out, opacity #{@speed / 2}ms ease-in; 561 | transform: translate3d(0,0,0); 562 | } 563 | """ 564 | 565 | ProgressBarAPI = 566 | enable: ProgressBar.enable 567 | disable: ProgressBar.disable 568 | start: -> ProgressBar.enable().start() 569 | advanceTo: (value) -> progressBar?.advanceTo(value) 570 | done: -> progressBar?.done() 571 | 572 | installDocumentReadyPageEventTriggers = -> 573 | document.addEventListener 'DOMContentLoaded', ( -> 574 | triggerEvent EVENTS.CHANGE 575 | triggerEvent EVENTS.UPDATE 576 | ), true 577 | 578 | installJqueryAjaxSuccessPageUpdateTrigger = -> 579 | if typeof jQuery isnt 'undefined' 580 | jQuery(document).on 'ajaxSuccess', (event, xhr, settings) -> 581 | return unless jQuery.trim xhr.responseText 582 | triggerEvent EVENTS.UPDATE 583 | 584 | onHistoryChange = (event) -> 585 | if event.state?.turbolinks && event.state.url != currentState.url 586 | if cachedPage = pageCache[(new ComponentUrl(event.state.url)).absolute] 587 | cacheCurrentPage() 588 | fetchHistory cachedPage 589 | else 590 | visit event.target.location.href 591 | 592 | initializeTurbolinks = -> 593 | rememberCurrentUrl() 594 | rememberCurrentState() 595 | 596 | ProgressBar.enable() 597 | 598 | document.addEventListener 'click', Click.installHandlerLast, true 599 | 600 | window.addEventListener 'hashchange', (event) -> 601 | rememberCurrentUrl() 602 | rememberCurrentState() 603 | , false 604 | 605 | window.addEventListener 'popstate', onHistoryChange, false 606 | 607 | browserSupportsPushState = window.history and 'pushState' of window.history 608 | 609 | # Copied from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js 610 | ua = navigator.userAgent 611 | browserIsBuggy = 612 | (ua.indexOf('Android 2.') != -1 or ua.indexOf('Android 4.0') != -1) and 613 | ua.indexOf('Mobile Safari') != -1 and 614 | ua.indexOf('Chrome') == -1 and 615 | ua.indexOf('Windows Phone') == -1 616 | 617 | requestMethodIsSafe = popCookie('request_method') in ['GET',''] 618 | 619 | browserSupportsTurbolinks = browserSupportsPushState and !browserIsBuggy and requestMethodIsSafe 620 | 621 | browserSupportsCustomEvents = 622 | document.addEventListener and document.createEvent 623 | 624 | if browserSupportsCustomEvents 625 | installDocumentReadyPageEventTriggers() 626 | installJqueryAjaxSuccessPageUpdateTrigger() 627 | 628 | if browserSupportsTurbolinks 629 | visit = fetch 630 | initializeTurbolinks() 631 | else 632 | visit = (url) -> document.location.href = url 633 | 634 | # Public API 635 | # Turbolinks.visit(url) 636 | # Turbolinks.replace(html) 637 | # Turbolinks.pagesCached() 638 | # Turbolinks.pagesCached(20) 639 | # Turbolinks.cacheCurrentPage() 640 | # Turbolinks.enableTransitionCache() 641 | # Turbolinks.disableRequestCaching() 642 | # Turbolinks.ProgressBar.enable() 643 | # Turbolinks.ProgressBar.disable() 644 | # Turbolinks.ProgressBar.start() 645 | # Turbolinks.ProgressBar.advanceTo(80) 646 | # Turbolinks.ProgressBar.done() 647 | # Turbolinks.allowLinkExtensions('md') 648 | # Turbolinks.supported 649 | # Turbolinks.EVENTS 650 | @Turbolinks = { 651 | visit, 652 | replace, 653 | pagesCached, 654 | cacheCurrentPage, 655 | enableTransitionCache, 656 | disableRequestCaching, 657 | ProgressBar: ProgressBarAPI, 658 | allowLinkExtensions: Link.allowExtensions, 659 | supported: browserSupportsTurbolinks, 660 | EVENTS: clone(EVENTS) 661 | } 662 | --------------------------------------------------------------------------------