rstacruz-turbolinks 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }