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.
@@ -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
+ }