This repository has been archived by the owner on Jul 28, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 428
/
turbolinks.js.coffee
356 lines (278 loc) · 11 KB
/
turbolinks.js.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
pageCache = {}
cacheSize = 10
transitionCacheEnabled = false
currentState = null
loadedAssets = null
htmlExtensions = ['html']
referer = null
createDocument = null
xhr = null
fetch = (url) ->
rememberReferer()
cacheCurrentPage()
reflectNewUrl url
if transitionCacheEnabled and cachedPage = transitionCacheFor(url)
fetchHistory cachedPage
fetchReplacement url
else
fetchReplacement url, resetScrollPosition
transitionCacheFor = (url) ->
cachedPage = pageCache[url]
cachedPage if cachedPage and !cachedPage.transitionCacheDisabled
enableTransitionCache = (enable = true) ->
transitionCacheEnabled = enable
fetchReplacement = (url, onLoadFunction = =>) ->
triggerEvent 'page:fetch', url: url
xhr?.abort()
xhr = new XMLHttpRequest
xhr.open 'GET', removeHashForIE10compatiblity(url), true
xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
xhr.setRequestHeader 'X-XHR-Referer', referer
xhr.onload = ->
triggerEvent 'page:receive'
if doc = processResponse()
changePage extractTitleAndBody(doc)...
reflectRedirectedUrl()
onLoadFunction()
triggerEvent 'page:load'
else
document.location.href = url
xhr.onloadend = -> xhr = null
xhr.onerror = -> document.location.href = url
xhr.send()
fetchHistory = (cachedPage) ->
xhr?.abort()
changePage cachedPage.title, cachedPage.body
recallScrollPosition cachedPage
triggerEvent 'page:restore'
cacheCurrentPage = ->
pageCache[currentState.url] =
url: document.location.href,
body: document.body,
title: document.title,
positionY: window.pageYOffset,
positionX: window.pageXOffset,
cachedAt: new Date().getTime(),
transitionCacheDisabled: document.querySelector('[data-no-transition-cache]')?
constrainPageCacheTo cacheSize
pagesCached = (size = cacheSize) ->
cacheSize = parseInt(size) if /^[\d]+$/.test size
constrainPageCacheTo = (limit) ->
pageCacheKeys = Object.keys pageCache
cacheTimesRecentFirst = pageCacheKeys.map (url) ->
pageCache[url].cachedAt
.sort (a, b) -> b - a
for key in pageCacheKeys when pageCache[key].cachedAt <= cacheTimesRecentFirst[limit]
triggerEvent 'page:expire', pageCache[key]
delete pageCache[key]
changePage = (title, body, csrfToken, runScripts) ->
document.title = title
document.documentElement.replaceChild body, document.body
CSRFToken.update csrfToken if csrfToken?
executeScriptTags() if runScripts
currentState = window.history.state
triggerEvent 'page:change'
triggerEvent 'page:update'
executeScriptTags = ->
scripts = Array::slice.call document.body.querySelectorAll 'script:not([data-turbolinks-eval="false"])'
for script in scripts when script.type in ['', 'text/javascript']
copy = document.createElement 'script'
copy.setAttribute attr.name, attr.value for attr in script.attributes
copy.appendChild document.createTextNode script.innerHTML
{ parentNode, nextSibling } = script
parentNode.removeChild script
parentNode.insertBefore copy, nextSibling
return
removeNoscriptTags = (node) ->
node.innerHTML = node.innerHTML.replace /<noscript[\S\s]*?<\/noscript>/ig, ''
node
reflectNewUrl = (url) ->
if url isnt referer
window.history.pushState { turbolinks: true, url: url }, '', url
reflectRedirectedUrl = ->
if location = xhr.getResponseHeader 'X-XHR-Redirected-To'
preservedHash = if removeHash(location) is location then document.location.hash else ''
window.history.replaceState currentState, '', location + preservedHash
rememberReferer = ->
referer = document.location.href
rememberCurrentUrl = ->
window.history.replaceState { turbolinks: true, url: document.location.href }, '', document.location.href
rememberCurrentState = ->
currentState = window.history.state
recallScrollPosition = (page) ->
window.scrollTo page.positionX, page.positionY
resetScrollPosition = ->
if document.location.hash
document.location.href = document.location.href
else
window.scrollTo 0, 0
# Intention revealing function alias
removeHashForIE10compatiblity = (url) ->
removeHash url
removeHash = (url) ->
link = url
unless url.href?
link = document.createElement 'A'
link.href = url
link.href.replace link.hash, ''
popCookie = (name) ->
value = document.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or ''
document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
value
triggerEvent = (name, data) ->
event = document.createEvent 'Events'
event.data = data if data
event.initEvent name, true, true
document.dispatchEvent event
pageChangePrevented = ->
!triggerEvent 'page:before-change'
processResponse = ->
clientOrServerError = ->
400 <= xhr.status < 600
validContent = ->
xhr.getResponseHeader('Content-Type').match /^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/
extractTrackAssets = (doc) ->
for node in doc.head.childNodes when node.getAttribute?('data-turbolinks-track')?
node.getAttribute('src') or node.getAttribute('href')
assetsChanged = (doc) ->
loadedAssets ||= extractTrackAssets document
fetchedAssets = extractTrackAssets doc
fetchedAssets.length isnt loadedAssets.length or intersection(fetchedAssets, loadedAssets).length isnt loadedAssets.length
intersection = (a, b) ->
[a, b] = [b, a] if a.length > b.length
value for value in a when value in b
if not clientOrServerError() and validContent()
doc = createDocument xhr.responseText
if doc and !assetsChanged doc
return doc
extractTitleAndBody = (doc) ->
title = doc.querySelector 'title'
[ title?.textContent, removeNoscriptTags(doc.body), CSRFToken.get(doc).token, 'runScripts' ]
CSRFToken =
get: (doc = document) ->
node: tag = doc.querySelector 'meta[name="csrf-token"]'
token: tag?.getAttribute? 'content'
update: (latest) ->
current = @get()
if current.token? and latest? and current.token isnt latest
current.node.setAttribute 'content', latest
browserCompatibleDocumentParser = ->
createDocumentUsingParser = (html) ->
(new DOMParser).parseFromString html, 'text/html'
createDocumentUsingDOM = (html) ->
doc = document.implementation.createHTMLDocument ''
doc.documentElement.innerHTML = html
doc
createDocumentUsingWrite = (html) ->
doc = document.implementation.createHTMLDocument ''
doc.open 'replace'
doc.write html
doc.close()
doc
# Use createDocumentUsingParser if DOMParser is defined and natively
# supports 'text/html' parsing (Firefox 12+, IE 10)
#
# Use createDocumentUsingDOM if createDocumentUsingParser throws an exception
# due to unsupported type 'text/html' (Firefox < 12, Opera)
#
# Use createDocumentUsingWrite if:
# - DOMParser isn't defined
# - createDocumentUsingParser returns null due to unsupported type 'text/html' (Chrome, Safari)
# - createDocumentUsingDOM doesn't create a valid HTML document (safeguarding against potential edge cases)
try
if window.DOMParser
testDoc = createDocumentUsingParser '<html><body><p>test'
createDocumentUsingParser
catch e
testDoc = createDocumentUsingDOM '<html><body><p>test'
createDocumentUsingDOM
finally
unless testDoc?.body?.childNodes.length is 1
return createDocumentUsingWrite
installClickHandlerLast = (event) ->
unless event.defaultPrevented
document.removeEventListener 'click', handleClick, false
document.addEventListener 'click', handleClick, false
handleClick = (event) ->
unless event.defaultPrevented
link = extractLink event
if link.nodeName is 'A' and !ignoreClick(event, link)
visit link.href unless pageChangePrevented()
event.preventDefault()
extractLink = (event) ->
link = event.target
link = link.parentNode until !link.parentNode or link.nodeName is 'A'
link
crossOriginLink = (link) ->
location.protocol isnt link.protocol or location.host isnt link.host
anchoredLink = (link) ->
((link.hash and removeHash(link)) is removeHash(location)) or
(link.href is location.href + '#')
nonHtmlLink = (link) ->
url = removeHash link
url.match(/\.[a-z]+(\?.*)?$/g) and not url.match(new RegExp("\\.(?:#{htmlExtensions.join('|')})?(\\?.*)?$", 'g'))
noTurbolink = (link) ->
until ignore or link is document
ignore = link.getAttribute('data-no-turbolink')?
link = link.parentNode
ignore
targetLink = (link) ->
link.target.length isnt 0
nonStandardClick = (event) ->
event.which > 1 or event.metaKey or event.ctrlKey or event.shiftKey or event.altKey
ignoreClick = (event, link) ->
crossOriginLink(link) or anchoredLink(link) or nonHtmlLink(link) or noTurbolink(link) or targetLink(link) or nonStandardClick(event)
allowLinkExtensions = (extensions...) ->
htmlExtensions.push extension for extension in extensions
htmlExtensions
installDocumentReadyPageEventTriggers = ->
document.addEventListener 'DOMContentLoaded', ( ->
triggerEvent 'page:change'
triggerEvent 'page:update'
), true
installJqueryAjaxSuccessPageUpdateTrigger = ->
if typeof jQuery isnt 'undefined'
jQuery(document).on 'ajaxSuccess', (event, xhr, settings) ->
return unless jQuery.trim xhr.responseText
triggerEvent 'page:update'
installHistoryChangeHandler = (event) ->
if event.state?.turbolinks
if cachedPage = pageCache[event.state.url]
cacheCurrentPage()
fetchHistory cachedPage
else
visit event.target.location.href
initializeTurbolinks = ->
rememberCurrentUrl()
rememberCurrentState()
createDocument = browserCompatibleDocumentParser()
document.addEventListener 'click', installClickHandlerLast, true
window.addEventListener 'popstate', installHistoryChangeHandler, false
# Handle bug in Firefox 26/27 where history.state is initially undefined
historyStateIsDefined =
window.history.state != undefined or navigator.userAgent.match /Firefox\/2[6|7]/
browserSupportsPushState =
window.history and window.history.pushState and window.history.replaceState and historyStateIsDefined
browserIsntBuggy =
!navigator.userAgent.match /CriOS\//
requestMethodIsSafe =
popCookie('request_method') in ['GET','']
browserSupportsTurbolinks = browserSupportsPushState and browserIsntBuggy and requestMethodIsSafe
browserSupportsCustomEvents =
document.addEventListener and document.createEvent
if browserSupportsCustomEvents
installDocumentReadyPageEventTriggers()
installJqueryAjaxSuccessPageUpdateTrigger()
if browserSupportsTurbolinks
visit = fetch
initializeTurbolinks()
else
visit = (url) -> document.location.href = url
# Public API
# Turbolinks.visit(url)
# Turbolinks.pagesCached()
# Turbolinks.pagesCached(20)
# Turbolinks.enableTransitionCache()
# Turbolinks.allowLinkExtensions('md')
# Turbolinks.supported
@Turbolinks = { visit, pagesCached, enableTransitionCache, allowLinkExtensions, supported: browserSupportsTurbolinks }