rstacruz-turbolinks 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +537 -0
- data/lib/assets/javascripts/turbolinks.coffee +733 -0
- data/lib/turbolinks.rb +64 -0
- data/lib/turbolinks/cookies.rb +15 -0
- data/lib/turbolinks/redirection.rb +77 -0
- data/lib/turbolinks/version.rb +3 -0
- data/lib/turbolinks/x_domain_blocker.rb +22 -0
- data/lib/turbolinks/xhr_headers.rb +44 -0
- data/lib/turbolinks/xhr_redirect.rb +30 -0
- data/lib/turbolinks/xhr_url_for.rb +23 -0
- data/test/attachment.html +5 -0
- data/test/config.ru +65 -0
- data/test/dummy.gif +0 -0
- data/test/form.html +17 -0
- data/test/index.html +53 -0
- data/test/manifest.appcache +10 -0
- data/test/offline.html +19 -0
- data/test/other.html +26 -0
- data/test/partial1.html +34 -0
- data/test/partial2.html +26 -0
- data/test/partial3.html +28 -0
- data/test/redirect1.html +16 -0
- data/test/redirect2.html +13 -0
- data/test/reload.html +18 -0
- data/test/withoutextension +26 -0
- metadata +113 -0
@@ -0,0 +1,733 @@
|
|
1
|
+
pageCache = {}
|
2
|
+
cacheSize = 10
|
3
|
+
transitionCacheEnabled = false
|
4
|
+
requestCachingEnabled = true
|
5
|
+
progressBar = null
|
6
|
+
progressBarDelay = 400
|
7
|
+
|
8
|
+
currentState = null
|
9
|
+
loadedAssets = null
|
10
|
+
|
11
|
+
referer = null
|
12
|
+
|
13
|
+
xhr = null
|
14
|
+
|
15
|
+
EVENTS =
|
16
|
+
BEFORE_CHANGE: 'page:before-change'
|
17
|
+
FETCH: 'page:fetch'
|
18
|
+
RECEIVE: 'page:receive'
|
19
|
+
CHANGE: 'page:change'
|
20
|
+
UPDATE: 'page:update'
|
21
|
+
LOAD: 'page:load'
|
22
|
+
PARTIAL_LOAD: 'page:partial-load'
|
23
|
+
RESTORE: 'page:restore'
|
24
|
+
BEFORE_UNLOAD: 'page:before-unload'
|
25
|
+
AFTER_REMOVE: 'page:after-remove'
|
26
|
+
|
27
|
+
isPartialReplacement = (options) ->
|
28
|
+
options.change or options.append or options.prepend
|
29
|
+
|
30
|
+
fetch = (url, options = {}) ->
|
31
|
+
url = new ComponentUrl url
|
32
|
+
|
33
|
+
return if pageChangePrevented(url.absolute)
|
34
|
+
|
35
|
+
if url.crossOrigin()
|
36
|
+
document.location.href = url.absolute
|
37
|
+
return
|
38
|
+
|
39
|
+
if isPartialReplacement(options) or options.keep
|
40
|
+
removeCurrentPageFromCache()
|
41
|
+
else
|
42
|
+
cacheCurrentPage()
|
43
|
+
|
44
|
+
rememberReferer()
|
45
|
+
progressBar?.start(delay: progressBarDelay)
|
46
|
+
|
47
|
+
if transitionCacheEnabled and !isPartialReplacement(options) and cachedPage = transitionCacheFor(url.absolute)
|
48
|
+
reflectNewUrl(url)
|
49
|
+
fetchHistory cachedPage
|
50
|
+
options.showProgressBar = false
|
51
|
+
options.scroll = false
|
52
|
+
else
|
53
|
+
options.scroll ?= false if isPartialReplacement(options) and !url.hash
|
54
|
+
|
55
|
+
fetchReplacement url, options
|
56
|
+
|
57
|
+
transitionCacheFor = (url) ->
|
58
|
+
return if url is currentState.url
|
59
|
+
cachedPage = pageCache[url]
|
60
|
+
cachedPage if cachedPage and !cachedPage.transitionCacheDisabled
|
61
|
+
|
62
|
+
enableTransitionCache = (enable = true) ->
|
63
|
+
transitionCacheEnabled = enable
|
64
|
+
|
65
|
+
disableRequestCaching = (disable = true) ->
|
66
|
+
requestCachingEnabled = not disable
|
67
|
+
disable
|
68
|
+
|
69
|
+
fetchReplacement = (url, options) ->
|
70
|
+
options.cacheRequest ?= requestCachingEnabled
|
71
|
+
options.showProgressBar ?= true
|
72
|
+
|
73
|
+
triggerEvent EVENTS.FETCH, url: url.absolute
|
74
|
+
|
75
|
+
xhr?.abort()
|
76
|
+
xhr = new XMLHttpRequest
|
77
|
+
xhr.open 'GET', url.formatForXHR(cache: options.cacheRequest), true
|
78
|
+
xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
|
79
|
+
xhr.setRequestHeader 'X-XHR-Referer', referer
|
80
|
+
|
81
|
+
xhr.onload = ->
|
82
|
+
triggerEvent EVENTS.RECEIVE, url: url.absolute
|
83
|
+
|
84
|
+
if doc = processResponse()
|
85
|
+
reflectNewUrl url
|
86
|
+
reflectRedirectedUrl()
|
87
|
+
loadedNodes = changePage extractTitleAndBody(doc)..., options
|
88
|
+
if options.showProgressBar
|
89
|
+
progressBar?.done()
|
90
|
+
updateScrollPosition(options.scroll)
|
91
|
+
triggerEvent (if isPartialReplacement(options) then EVENTS.PARTIAL_LOAD else EVENTS.LOAD), loadedNodes
|
92
|
+
constrainPageCacheTo(cacheSize)
|
93
|
+
else
|
94
|
+
progressBar?.done()
|
95
|
+
document.location.href = crossOriginRedirect() or url.absolute
|
96
|
+
|
97
|
+
if progressBar and options.showProgressBar
|
98
|
+
xhr.onprogress = (event) =>
|
99
|
+
percent = if event.lengthComputable
|
100
|
+
event.loaded / event.total * 100
|
101
|
+
else
|
102
|
+
progressBar.value + (100 - progressBar.value) / 10
|
103
|
+
progressBar.advanceTo(percent)
|
104
|
+
|
105
|
+
xhr.onloadend = -> xhr = null
|
106
|
+
xhr.onerror = -> document.location.href = url.absolute
|
107
|
+
|
108
|
+
xhr.send()
|
109
|
+
|
110
|
+
fetchHistory = (cachedPage, options = {}) ->
|
111
|
+
xhr?.abort()
|
112
|
+
changePage cachedPage.title, cachedPage.body, null, runScripts: false
|
113
|
+
progressBar?.done()
|
114
|
+
updateScrollPosition(options.scroll)
|
115
|
+
triggerEvent EVENTS.RESTORE
|
116
|
+
|
117
|
+
cacheCurrentPage = ->
|
118
|
+
currentStateUrl = new ComponentUrl currentState.url
|
119
|
+
|
120
|
+
pageCache[currentStateUrl.absolute] =
|
121
|
+
url: currentStateUrl.relative,
|
122
|
+
body: document.body,
|
123
|
+
title: document.title,
|
124
|
+
positionY: window.pageYOffset,
|
125
|
+
positionX: window.pageXOffset,
|
126
|
+
cachedAt: new Date().getTime(),
|
127
|
+
transitionCacheDisabled: document.querySelector('[data-no-transition-cache]')?
|
128
|
+
|
129
|
+
removeCurrentPageFromCache = ->
|
130
|
+
delete pageCache[new ComponentUrl(currentState.url).absolute]
|
131
|
+
|
132
|
+
pagesCached = (size = cacheSize) ->
|
133
|
+
cacheSize = parseInt(size) if /^[\d]+$/.test size
|
134
|
+
|
135
|
+
constrainPageCacheTo = (limit) ->
|
136
|
+
pageCacheKeys = Object.keys pageCache
|
137
|
+
|
138
|
+
cacheTimesRecentFirst = pageCacheKeys.map (url) ->
|
139
|
+
pageCache[url].cachedAt
|
140
|
+
.sort (a, b) -> b - a
|
141
|
+
|
142
|
+
for key in pageCacheKeys when pageCache[key].cachedAt <= cacheTimesRecentFirst[limit]
|
143
|
+
onNodeRemoved(pageCache[key].body)
|
144
|
+
delete pageCache[key]
|
145
|
+
|
146
|
+
replace = (html, options = {}) ->
|
147
|
+
loadedNodes = changePage extractTitleAndBody(createDocument(html))..., options
|
148
|
+
triggerEvent (if isPartialReplacement(options) then EVENTS.PARTIAL_LOAD else EVENTS.LOAD), loadedNodes
|
149
|
+
|
150
|
+
changePage = (title, body, csrfToken, options) ->
|
151
|
+
title = options.title ? title
|
152
|
+
currentBody = document.body
|
153
|
+
|
154
|
+
if isPartialReplacement(options)
|
155
|
+
nodesToAppend = findNodesMatchingKeys(currentBody, options.append) if options.append
|
156
|
+
nodesToPrepend = findNodesMatchingKeys(currentBody, options.prepend) if options.prepend
|
157
|
+
|
158
|
+
nodesToReplace = findNodes(currentBody, '[data-turbolinks-temporary]')
|
159
|
+
nodesToReplace = nodesToReplace.concat findNodesMatchingKeys(currentBody, options.change) if options.change
|
160
|
+
|
161
|
+
nodesToChange = [].concat(nodesToAppend || [], nodesToPrepend || [], nodesToReplace || [])
|
162
|
+
nodesToChange = removeDuplicates(nodesToChange)
|
163
|
+
else
|
164
|
+
nodesToChange = [currentBody]
|
165
|
+
|
166
|
+
triggerEvent EVENTS.BEFORE_UNLOAD, nodesToChange
|
167
|
+
document.title = title if title isnt false
|
168
|
+
|
169
|
+
if isPartialReplacement(options)
|
170
|
+
appendedNodes = swapNodes(body, nodesToAppend, keep: false, append: true) if nodesToAppend
|
171
|
+
prependedNodes = swapNodes(body, nodesToPrepend, keep: false, prepend: true) if nodesToPrepend
|
172
|
+
replacedNodes = swapNodes(body, nodesToReplace, keep: false) if nodesToReplace
|
173
|
+
|
174
|
+
changedNodes = [].concat(appendedNodes || [], prependedNodes || [], replacedNodes || [])
|
175
|
+
changedNodes = removeDuplicates(changedNodes)
|
176
|
+
else
|
177
|
+
unless options.flush
|
178
|
+
nodesToKeep = findNodes(currentBody, '[data-turbolinks-permanent]')
|
179
|
+
nodesToKeep.push(findNodesMatchingKeys(currentBody, options.keep)...) if options.keep
|
180
|
+
swapNodes(body, removeDuplicates(nodesToKeep), keep: true)
|
181
|
+
|
182
|
+
document.body = body
|
183
|
+
CSRFToken.update csrfToken if csrfToken?
|
184
|
+
setAutofocusElement()
|
185
|
+
changedNodes = [body]
|
186
|
+
|
187
|
+
executeScriptTags(getScriptsToRun(changedNodes, options.runScripts))
|
188
|
+
currentState = window.history.state
|
189
|
+
|
190
|
+
triggerEvent EVENTS.CHANGE, changedNodes
|
191
|
+
triggerEvent EVENTS.UPDATE
|
192
|
+
return changedNodes
|
193
|
+
|
194
|
+
findNodes = (body, selector) ->
|
195
|
+
Array::slice.apply(body.querySelectorAll(selector))
|
196
|
+
|
197
|
+
findNodesMatchingKeys = (body, keys) ->
|
198
|
+
matchingNodes = []
|
199
|
+
for key in (if Array.isArray(keys) then keys else [keys])
|
200
|
+
matchingNodes.push(findNodes(body, '[id^="'+key+':"], [id="'+key+'"]')...)
|
201
|
+
|
202
|
+
return matchingNodes
|
203
|
+
|
204
|
+
swapNodes = (targetBody, existingNodes, options) ->
|
205
|
+
changedNodes = []
|
206
|
+
for existingNode in existingNodes
|
207
|
+
unless nodeId = existingNode.getAttribute('id')
|
208
|
+
throw new Error("Turbolinks partial replace: turbolinks elements must have an id.")
|
209
|
+
|
210
|
+
if targetNode = targetBody.querySelector('[id="'+nodeId+'"]')
|
211
|
+
if options.keep
|
212
|
+
existingNode.parentNode.insertBefore(existingNode.cloneNode(true), existingNode)
|
213
|
+
existingNode = targetNode.ownerDocument.adoptNode(existingNode)
|
214
|
+
targetNode.parentNode.replaceChild(existingNode, targetNode)
|
215
|
+
else
|
216
|
+
if options.append or options.prepend
|
217
|
+
firstChild = existingNode.firstChild
|
218
|
+
|
219
|
+
childNodes = Array::slice.call targetNode.childNodes, 0 # a copy has to be made since the list is mutated while processing
|
220
|
+
|
221
|
+
for childNode in childNodes
|
222
|
+
if !firstChild or options.append # when the parent node is empty, there is no difference between appending and prepending
|
223
|
+
existingNode.appendChild(childNode)
|
224
|
+
else if options.prepend
|
225
|
+
existingNode.insertBefore(childNode, firstChild)
|
226
|
+
|
227
|
+
changedNodes.push(existingNode)
|
228
|
+
else
|
229
|
+
existingNode.parentNode.replaceChild(targetNode, existingNode)
|
230
|
+
onNodeRemoved(existingNode)
|
231
|
+
changedNodes.push(targetNode)
|
232
|
+
|
233
|
+
return changedNodes
|
234
|
+
|
235
|
+
onNodeRemoved = (node) ->
|
236
|
+
if typeof jQuery isnt 'undefined'
|
237
|
+
jQuery(node).remove()
|
238
|
+
triggerEvent(EVENTS.AFTER_REMOVE, node)
|
239
|
+
|
240
|
+
getScriptsToRun = (changedNodes, runScripts) ->
|
241
|
+
selector = if runScripts is false then 'script[data-turbolinks-eval="always"]' else 'script:not([data-turbolinks-eval="false"])'
|
242
|
+
script for script in document.body.querySelectorAll(selector) when isEvalAlways(script) or (nestedWithinNodeList(changedNodes, script) and not withinPermanent(script))
|
243
|
+
|
244
|
+
isEvalAlways = (script) ->
|
245
|
+
script.getAttribute('data-turbolinks-eval') is 'always'
|
246
|
+
|
247
|
+
withinPermanent = (element) ->
|
248
|
+
while element?
|
249
|
+
return true if element.hasAttribute?('data-turbolinks-permanent')
|
250
|
+
element = element.parentNode
|
251
|
+
|
252
|
+
return false
|
253
|
+
|
254
|
+
nestedWithinNodeList = (nodeList, element) ->
|
255
|
+
while element?
|
256
|
+
return true if element in nodeList
|
257
|
+
element = element.parentNode
|
258
|
+
|
259
|
+
return false
|
260
|
+
|
261
|
+
executeScriptTags = (scripts) ->
|
262
|
+
for script in scripts when script.type in ['', 'text/javascript']
|
263
|
+
copy = document.createElement 'script'
|
264
|
+
copy.setAttribute attr.name, attr.value for attr in script.attributes
|
265
|
+
copy.async = false unless script.hasAttribute 'async'
|
266
|
+
copy.appendChild document.createTextNode script.innerHTML
|
267
|
+
{ parentNode, nextSibling } = script
|
268
|
+
parentNode.removeChild script
|
269
|
+
parentNode.insertBefore copy, nextSibling
|
270
|
+
return
|
271
|
+
|
272
|
+
# Firefox bug: Doesn't autofocus fields that are inserted via JavaScript
|
273
|
+
setAutofocusElement = ->
|
274
|
+
autofocusElement = (list = document.querySelectorAll 'input[autofocus], textarea[autofocus]')[list.length - 1]
|
275
|
+
if autofocusElement and document.activeElement isnt autofocusElement
|
276
|
+
autofocusElement.focus()
|
277
|
+
|
278
|
+
reflectNewUrl = (url) ->
|
279
|
+
if (url = new ComponentUrl url).absolute not in [referer, document.location.href]
|
280
|
+
window.history.pushState { turbolinks: true, url: url.absolute }, '', url.absolute
|
281
|
+
|
282
|
+
reflectRedirectedUrl = ->
|
283
|
+
if location = xhr.getResponseHeader 'X-XHR-Redirected-To'
|
284
|
+
location = new ComponentUrl location
|
285
|
+
preservedHash = if location.hasNoHash() then document.location.hash else ''
|
286
|
+
window.history.replaceState window.history.state, '', location.href + preservedHash
|
287
|
+
|
288
|
+
crossOriginRedirect = ->
|
289
|
+
redirect if (redirect = xhr.getResponseHeader('Location'))? and (new ComponentUrl(redirect)).crossOrigin()
|
290
|
+
|
291
|
+
rememberReferer = ->
|
292
|
+
referer = document.location.href
|
293
|
+
|
294
|
+
rememberCurrentUrlAndState = ->
|
295
|
+
window.history.replaceState { turbolinks: true, url: document.location.href }, '', document.location.href
|
296
|
+
currentState = window.history.state
|
297
|
+
|
298
|
+
updateScrollPosition = (position) ->
|
299
|
+
if Array.isArray(position)
|
300
|
+
window.scrollTo position[0], position[1]
|
301
|
+
else if position isnt false
|
302
|
+
if document.location.hash
|
303
|
+
document.location.href = document.location.href
|
304
|
+
rememberCurrentUrlAndState()
|
305
|
+
else
|
306
|
+
window.scrollTo 0, 0
|
307
|
+
|
308
|
+
clone = (original) ->
|
309
|
+
return original if not original? or typeof original isnt 'object'
|
310
|
+
copy = new original.constructor()
|
311
|
+
copy[key] = clone value for key, value of original
|
312
|
+
copy
|
313
|
+
|
314
|
+
removeDuplicates = (array) ->
|
315
|
+
result = []
|
316
|
+
result.push(obj) for obj in array when result.indexOf(obj) is -1
|
317
|
+
result
|
318
|
+
|
319
|
+
popCookie = (name) ->
|
320
|
+
value = document.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or ''
|
321
|
+
document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
|
322
|
+
value
|
323
|
+
|
324
|
+
uniqueId = ->
|
325
|
+
new Date().getTime().toString(36)
|
326
|
+
|
327
|
+
triggerEvent = (name, data) ->
|
328
|
+
if typeof Prototype isnt 'undefined'
|
329
|
+
Event.fire document, name, data, true
|
330
|
+
|
331
|
+
event = document.createEvent 'Events'
|
332
|
+
event.data = data if data
|
333
|
+
event.initEvent name, true, true
|
334
|
+
document.dispatchEvent event
|
335
|
+
|
336
|
+
pageChangePrevented = (url) ->
|
337
|
+
!triggerEvent EVENTS.BEFORE_CHANGE, url: url
|
338
|
+
|
339
|
+
processResponse = ->
|
340
|
+
clientOrServerError = ->
|
341
|
+
400 <= xhr.status < 600
|
342
|
+
|
343
|
+
validContent = ->
|
344
|
+
(contentType = xhr.getResponseHeader('Content-Type'))? and
|
345
|
+
contentType.match /^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/
|
346
|
+
|
347
|
+
downloadingFile = ->
|
348
|
+
(disposition = xhr.getResponseHeader('Content-Disposition'))? and
|
349
|
+
disposition.match /^attachment/
|
350
|
+
|
351
|
+
extractTrackAssets = (doc) ->
|
352
|
+
for node in doc.querySelector('head').childNodes when node.getAttribute?('data-turbolinks-track')?
|
353
|
+
node.getAttribute('src') or node.getAttribute('href')
|
354
|
+
|
355
|
+
assetsChanged = (doc) ->
|
356
|
+
loadedAssets ||= extractTrackAssets document
|
357
|
+
fetchedAssets = extractTrackAssets doc
|
358
|
+
fetchedAssets.length isnt loadedAssets.length or intersection(fetchedAssets, loadedAssets).length isnt loadedAssets.length
|
359
|
+
|
360
|
+
intersection = (a, b) ->
|
361
|
+
[a, b] = [b, a] if a.length > b.length
|
362
|
+
value for value in a when value in b
|
363
|
+
|
364
|
+
if not clientOrServerError() and validContent() and not downloadingFile()
|
365
|
+
doc = createDocument xhr.responseText
|
366
|
+
if doc and !assetsChanged doc
|
367
|
+
return doc
|
368
|
+
|
369
|
+
extractTitleAndBody = (doc) ->
|
370
|
+
title = doc.querySelector 'title'
|
371
|
+
[ title?.textContent, doc.querySelector('body'), CSRFToken.get(doc).token ]
|
372
|
+
|
373
|
+
CSRFToken =
|
374
|
+
get: (doc = document) ->
|
375
|
+
node: tag = doc.querySelector 'meta[name="csrf-token"]'
|
376
|
+
token: tag?.getAttribute? 'content'
|
377
|
+
|
378
|
+
update: (latest) ->
|
379
|
+
current = @get()
|
380
|
+
if current.token? and latest? and current.token isnt latest
|
381
|
+
current.node.setAttribute 'content', latest
|
382
|
+
|
383
|
+
createDocument = (html) ->
|
384
|
+
if /<(html|body)/i.test(html)
|
385
|
+
doc = document.documentElement.cloneNode()
|
386
|
+
doc.innerHTML = html
|
387
|
+
else
|
388
|
+
doc = document.documentElement.cloneNode(true)
|
389
|
+
doc.querySelector('body').innerHTML = html
|
390
|
+
doc.head = doc.querySelector('head')
|
391
|
+
doc.body = doc.querySelector('body')
|
392
|
+
doc
|
393
|
+
|
394
|
+
# The ComponentUrl class converts a basic URL string into an object
|
395
|
+
# that behaves similarly to document.location.
|
396
|
+
#
|
397
|
+
# If an instance is created from a relative URL, the current document
|
398
|
+
# is used to fill in the missing attributes (protocol, host, port).
|
399
|
+
class ComponentUrl
|
400
|
+
constructor: (@original = document.location.href) ->
|
401
|
+
return @original if @original.constructor is ComponentUrl
|
402
|
+
@_parse()
|
403
|
+
|
404
|
+
withoutHash: -> @href.replace(@hash, '').replace('#', '')
|
405
|
+
|
406
|
+
# Intention revealing function alias
|
407
|
+
withoutHashForIE10compatibility: -> @withoutHash()
|
408
|
+
|
409
|
+
hasNoHash: -> @hash.length is 0
|
410
|
+
|
411
|
+
crossOrigin: ->
|
412
|
+
@origin isnt (new ComponentUrl).origin
|
413
|
+
|
414
|
+
formatForXHR: (options = {}) ->
|
415
|
+
(if options.cache then @ else @withAntiCacheParam()).withoutHashForIE10compatibility()
|
416
|
+
|
417
|
+
withAntiCacheParam: ->
|
418
|
+
new ComponentUrl(
|
419
|
+
if /([?&])_=[^&]*/.test @absolute
|
420
|
+
@absolute.replace /([?&])_=[^&]*/, "$1_=#{uniqueId()}"
|
421
|
+
else
|
422
|
+
new ComponentUrl(@absolute + (if /\?/.test(@absolute) then "&" else "?") + "_=#{uniqueId()}")
|
423
|
+
)
|
424
|
+
|
425
|
+
_parse: ->
|
426
|
+
(@link ?= document.createElement 'a').href = @original
|
427
|
+
{ @href, @protocol, @host, @hostname, @port, @pathname, @search, @hash } = @link
|
428
|
+
@origin = [@protocol, '//', @hostname].join ''
|
429
|
+
@origin += ":#{@port}" unless @port.length is 0
|
430
|
+
@relative = [@pathname, @search, @hash].join ''
|
431
|
+
@absolute = @href
|
432
|
+
|
433
|
+
# The Link class derives from the ComponentUrl class, but is built from an
|
434
|
+
# existing link element. Provides verification functionality for Turbolinks
|
435
|
+
# to use in determining whether it should process the link when clicked.
|
436
|
+
class Link extends ComponentUrl
|
437
|
+
@HTML_EXTENSIONS: ['html']
|
438
|
+
|
439
|
+
@allowExtensions: (extensions...) ->
|
440
|
+
Link.HTML_EXTENSIONS.push extension for extension in extensions
|
441
|
+
Link.HTML_EXTENSIONS
|
442
|
+
|
443
|
+
constructor: (@link) ->
|
444
|
+
return @link if @link.constructor is Link
|
445
|
+
@original = @link.href
|
446
|
+
@originalElement = @link
|
447
|
+
@link = @link.cloneNode false
|
448
|
+
super
|
449
|
+
|
450
|
+
shouldIgnore: ->
|
451
|
+
@crossOrigin() or
|
452
|
+
@_anchored() or
|
453
|
+
@_nonHtml() or
|
454
|
+
@_optOut() or
|
455
|
+
@_target()
|
456
|
+
|
457
|
+
_anchored: ->
|
458
|
+
(@hash.length > 0 or @href.charAt(@href.length - 1) is '#') and
|
459
|
+
(@withoutHash() is (new ComponentUrl).withoutHash())
|
460
|
+
|
461
|
+
_nonHtml: ->
|
462
|
+
@pathname.match(/\.[a-z]+$/g) and not @pathname.match(new RegExp("\\.(?:#{Link.HTML_EXTENSIONS.join('|')})?$", 'g'))
|
463
|
+
|
464
|
+
_optOut: ->
|
465
|
+
link = @originalElement
|
466
|
+
until ignore or link is document
|
467
|
+
ignore = link.getAttribute('data-no-turbolink')?
|
468
|
+
link = link.parentNode
|
469
|
+
ignore
|
470
|
+
|
471
|
+
_target: ->
|
472
|
+
@link.target.length isnt 0
|
473
|
+
|
474
|
+
|
475
|
+
# The Click class handles clicked links, verifying if Turbolinks should
|
476
|
+
# take control by inspecting both the event and the link. If it should,
|
477
|
+
# the page change process is initiated. If not, control is passed back
|
478
|
+
# to the browser for default functionality.
|
479
|
+
class Click
|
480
|
+
@installHandlerLast: (event) ->
|
481
|
+
unless event.defaultPrevented
|
482
|
+
document.removeEventListener 'click', Click.handle, false
|
483
|
+
document.addEventListener 'click', Click.handle, false
|
484
|
+
|
485
|
+
@handle: (event) ->
|
486
|
+
new Click event
|
487
|
+
|
488
|
+
constructor: (@event) ->
|
489
|
+
return if @event.defaultPrevented
|
490
|
+
@_extractLink()
|
491
|
+
if @_validForTurbolinks()
|
492
|
+
visit @link.href
|
493
|
+
@event.preventDefault()
|
494
|
+
|
495
|
+
_extractLink: ->
|
496
|
+
link = @event.target
|
497
|
+
link = link.parentNode until !link.parentNode or link.nodeName is 'A'
|
498
|
+
@link = new Link(link) if link.nodeName is 'A' and link.href.length isnt 0
|
499
|
+
|
500
|
+
_validForTurbolinks: ->
|
501
|
+
@link? and not (@link.shouldIgnore() or @_nonStandardClick())
|
502
|
+
|
503
|
+
_nonStandardClick: ->
|
504
|
+
@event.which > 1 or
|
505
|
+
@event.metaKey or
|
506
|
+
@event.ctrlKey or
|
507
|
+
@event.shiftKey or
|
508
|
+
@event.altKey
|
509
|
+
|
510
|
+
|
511
|
+
class ProgressBar
|
512
|
+
className = 'turbolinks-progress-bar'
|
513
|
+
# Setting the opacity to a value < 1 fixes a display issue in Safari 6 and
|
514
|
+
# iOS 6 where the progress bar would fill the entire page.
|
515
|
+
originalOpacity = 0.99
|
516
|
+
|
517
|
+
@enable: ->
|
518
|
+
progressBar ?= new ProgressBar 'html'
|
519
|
+
|
520
|
+
@disable: ->
|
521
|
+
progressBar?.uninstall()
|
522
|
+
progressBar = null
|
523
|
+
|
524
|
+
constructor: (@elementSelector) ->
|
525
|
+
@value = 0
|
526
|
+
@content = ''
|
527
|
+
@speed = 300
|
528
|
+
@opacity = originalOpacity
|
529
|
+
@install()
|
530
|
+
|
531
|
+
install: ->
|
532
|
+
@element = document.querySelector(@elementSelector)
|
533
|
+
@element.classList.add(className)
|
534
|
+
@styleElement = document.createElement('style')
|
535
|
+
document.head.appendChild(@styleElement)
|
536
|
+
@_updateStyle()
|
537
|
+
|
538
|
+
uninstall: ->
|
539
|
+
@element.classList.remove(className)
|
540
|
+
document.head.removeChild(@styleElement)
|
541
|
+
|
542
|
+
start: ({delay} = {})->
|
543
|
+
clearTimeout(@displayTimeout)
|
544
|
+
if delay
|
545
|
+
@display = false
|
546
|
+
@displayTimeout = setTimeout =>
|
547
|
+
@display = true
|
548
|
+
, delay
|
549
|
+
else
|
550
|
+
@display = true
|
551
|
+
|
552
|
+
if @value > 0
|
553
|
+
@_reset()
|
554
|
+
@_reflow()
|
555
|
+
|
556
|
+
@advanceTo(5)
|
557
|
+
|
558
|
+
advanceTo: (value) ->
|
559
|
+
if value > @value <= 100
|
560
|
+
@value = value
|
561
|
+
@_updateStyle()
|
562
|
+
|
563
|
+
if @value is 100
|
564
|
+
@_stopTrickle()
|
565
|
+
else if @value > 0
|
566
|
+
@_startTrickle()
|
567
|
+
|
568
|
+
done: ->
|
569
|
+
if @value > 0
|
570
|
+
@advanceTo(100)
|
571
|
+
@_finish()
|
572
|
+
|
573
|
+
_finish: ->
|
574
|
+
@fadeTimer = setTimeout =>
|
575
|
+
@opacity = 0
|
576
|
+
@_updateStyle()
|
577
|
+
, @speed / 2
|
578
|
+
|
579
|
+
@resetTimer = setTimeout(@_reset, @speed)
|
580
|
+
|
581
|
+
_reflow: ->
|
582
|
+
@element.offsetHeight
|
583
|
+
|
584
|
+
_reset: =>
|
585
|
+
@_stopTimers()
|
586
|
+
@value = 0
|
587
|
+
@opacity = originalOpacity
|
588
|
+
@_withSpeed(0, => @_updateStyle(true))
|
589
|
+
|
590
|
+
_stopTimers: ->
|
591
|
+
@_stopTrickle()
|
592
|
+
clearTimeout(@fadeTimer)
|
593
|
+
clearTimeout(@resetTimer)
|
594
|
+
|
595
|
+
_startTrickle: ->
|
596
|
+
return if @trickleTimer
|
597
|
+
@trickleTimer = setTimeout(@_trickle, @speed)
|
598
|
+
|
599
|
+
_stopTrickle: ->
|
600
|
+
clearTimeout(@trickleTimer)
|
601
|
+
delete @trickleTimer
|
602
|
+
|
603
|
+
_trickle: =>
|
604
|
+
@advanceTo(@value + Math.random() / 2)
|
605
|
+
@trickleTimer = setTimeout(@_trickle, @speed)
|
606
|
+
|
607
|
+
_withSpeed: (speed, fn) ->
|
608
|
+
originalSpeed = @speed
|
609
|
+
@speed = speed
|
610
|
+
result = fn()
|
611
|
+
@speed = originalSpeed
|
612
|
+
result
|
613
|
+
|
614
|
+
_updateStyle: (forceRepaint = false) ->
|
615
|
+
@_changeContentToForceRepaint() if forceRepaint
|
616
|
+
@styleElement.textContent = @_createCSSRule()
|
617
|
+
|
618
|
+
_changeContentToForceRepaint: ->
|
619
|
+
@content = if @content is '' then ' ' else ''
|
620
|
+
|
621
|
+
_createCSSRule: ->
|
622
|
+
"""
|
623
|
+
#{@elementSelector}.#{className}::before {
|
624
|
+
content: '#{@content}';
|
625
|
+
position: fixed;
|
626
|
+
top: 0;
|
627
|
+
left: 0;
|
628
|
+
z-index: 2000;
|
629
|
+
background-color: #0076ff;
|
630
|
+
height: 3px;
|
631
|
+
opacity: #{@opacity};
|
632
|
+
width: #{if @display then @value else 0}%;
|
633
|
+
transition: width #{@speed}ms ease-out, opacity #{@speed / 2}ms ease-in;
|
634
|
+
transform: translate3d(0,0,0);
|
635
|
+
}
|
636
|
+
"""
|
637
|
+
|
638
|
+
ProgressBarAPI =
|
639
|
+
enable: ProgressBar.enable
|
640
|
+
disable: ProgressBar.disable
|
641
|
+
setDelay: (value) -> progressBarDelay = value
|
642
|
+
start: (options) -> ProgressBar.enable().start(options)
|
643
|
+
advanceTo: (value) -> progressBar?.advanceTo(value)
|
644
|
+
done: -> progressBar?.done()
|
645
|
+
|
646
|
+
installDocumentReadyPageEventTriggers = ->
|
647
|
+
document.addEventListener 'DOMContentLoaded', ( ->
|
648
|
+
triggerEvent EVENTS.CHANGE, [document.body]
|
649
|
+
triggerEvent EVENTS.UPDATE
|
650
|
+
), true
|
651
|
+
|
652
|
+
installJqueryAjaxSuccessPageUpdateTrigger = ->
|
653
|
+
if typeof jQuery isnt 'undefined'
|
654
|
+
jQuery(document).on 'ajaxSuccess', (event, xhr, settings) ->
|
655
|
+
return unless jQuery.trim xhr.responseText
|
656
|
+
triggerEvent EVENTS.UPDATE
|
657
|
+
|
658
|
+
onHistoryChange = (event) ->
|
659
|
+
if event.state?.turbolinks && event.state.url != currentState.url
|
660
|
+
previousUrl = new ComponentUrl(currentState.url)
|
661
|
+
newUrl = new ComponentUrl(event.state.url)
|
662
|
+
|
663
|
+
if newUrl.withoutHash() is previousUrl.withoutHash()
|
664
|
+
updateScrollPosition()
|
665
|
+
else if cachedPage = pageCache[newUrl.absolute]
|
666
|
+
cacheCurrentPage()
|
667
|
+
fetchHistory cachedPage, scroll: [cachedPage.positionX, cachedPage.positionY]
|
668
|
+
else
|
669
|
+
visit event.target.location.href
|
670
|
+
|
671
|
+
initializeTurbolinks = ->
|
672
|
+
rememberCurrentUrlAndState()
|
673
|
+
ProgressBar.enable()
|
674
|
+
|
675
|
+
document.addEventListener 'click', Click.installHandlerLast, true
|
676
|
+
window.addEventListener 'hashchange', rememberCurrentUrlAndState, false
|
677
|
+
window.addEventListener 'popstate', onHistoryChange, false
|
678
|
+
|
679
|
+
browserSupportsPushState = window.history and 'pushState' of window.history and 'state' of window.history
|
680
|
+
|
681
|
+
# Copied from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js
|
682
|
+
ua = navigator.userAgent
|
683
|
+
browserIsBuggy =
|
684
|
+
(ua.indexOf('Android 2.') != -1 or ua.indexOf('Android 4.0') != -1) and
|
685
|
+
ua.indexOf('Mobile Safari') != -1 and
|
686
|
+
ua.indexOf('Chrome') == -1 and
|
687
|
+
ua.indexOf('Windows Phone') == -1
|
688
|
+
|
689
|
+
requestMethodIsSafe = popCookie('request_method') in ['GET','']
|
690
|
+
|
691
|
+
browserSupportsTurbolinks = browserSupportsPushState and !browserIsBuggy and requestMethodIsSafe
|
692
|
+
|
693
|
+
browserSupportsCustomEvents =
|
694
|
+
document.addEventListener and document.createEvent
|
695
|
+
|
696
|
+
if browserSupportsCustomEvents
|
697
|
+
installDocumentReadyPageEventTriggers()
|
698
|
+
installJqueryAjaxSuccessPageUpdateTrigger()
|
699
|
+
|
700
|
+
if browserSupportsTurbolinks
|
701
|
+
visit = fetch
|
702
|
+
initializeTurbolinks()
|
703
|
+
else
|
704
|
+
visit = (url = document.location.href) -> document.location.href = url
|
705
|
+
|
706
|
+
# Public API
|
707
|
+
# Turbolinks.visit(url)
|
708
|
+
# Turbolinks.replace(html)
|
709
|
+
# Turbolinks.pagesCached()
|
710
|
+
# Turbolinks.pagesCached(20)
|
711
|
+
# Turbolinks.cacheCurrentPage()
|
712
|
+
# Turbolinks.enableTransitionCache()
|
713
|
+
# Turbolinks.disableRequestCaching()
|
714
|
+
# Turbolinks.ProgressBar.enable()
|
715
|
+
# Turbolinks.ProgressBar.disable()
|
716
|
+
# Turbolinks.ProgressBar.start()
|
717
|
+
# Turbolinks.ProgressBar.advanceTo(80)
|
718
|
+
# Turbolinks.ProgressBar.done()
|
719
|
+
# Turbolinks.allowLinkExtensions('md')
|
720
|
+
# Turbolinks.supported
|
721
|
+
# Turbolinks.EVENTS
|
722
|
+
@Turbolinks = {
|
723
|
+
visit,
|
724
|
+
replace,
|
725
|
+
pagesCached,
|
726
|
+
cacheCurrentPage,
|
727
|
+
enableTransitionCache,
|
728
|
+
disableRequestCaching,
|
729
|
+
ProgressBar: ProgressBarAPI,
|
730
|
+
allowLinkExtensions: Link.allowExtensions,
|
731
|
+
supported: browserSupportsTurbolinks,
|
732
|
+
EVENTS: clone(EVENTS)
|
733
|
+
}
|