turbolinks 2.5.4 → 5.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,558 +0,0 @@
1
- pageCache = {}
2
- cacheSize = 10
3
- transitionCacheEnabled = false
4
- progressBar = null
5
-
6
- currentState = null
7
- loadedAssets = null
8
-
9
- referer = null
10
-
11
- xhr = null
12
-
13
- EVENTS =
14
- BEFORE_CHANGE: 'page:before-change'
15
- FETCH: 'page:fetch'
16
- RECEIVE: 'page:receive'
17
- CHANGE: 'page:change'
18
- UPDATE: 'page:update'
19
- LOAD: 'page:load'
20
- RESTORE: 'page:restore'
21
- BEFORE_UNLOAD: 'page:before-unload'
22
- EXPIRE: 'page:expire'
23
-
24
- fetch = (url) ->
25
- url = new ComponentUrl url
26
-
27
- rememberReferer()
28
- cacheCurrentPage()
29
- progressBar?.start()
30
-
31
- if transitionCacheEnabled and cachedPage = transitionCacheFor(url.absolute)
32
- fetchHistory cachedPage
33
- fetchReplacement url, null, false
34
- else
35
- fetchReplacement url, resetScrollPosition
36
-
37
- transitionCacheFor = (url) ->
38
- cachedPage = pageCache[url]
39
- cachedPage if cachedPage and !cachedPage.transitionCacheDisabled
40
-
41
- enableTransitionCache = (enable = true) ->
42
- transitionCacheEnabled = enable
43
-
44
- enableProgressBar = (enable = true) ->
45
- return unless browserSupportsTurbolinks
46
- if enable
47
- progressBar ?= new ProgressBar 'html'
48
- else
49
- progressBar?.uninstall()
50
- progressBar = null
51
-
52
- fetchReplacement = (url, onLoadFunction, showProgressBar = true) ->
53
- triggerEvent EVENTS.FETCH, url: url.absolute
54
-
55
- xhr?.abort()
56
- xhr = new XMLHttpRequest
57
- xhr.open 'GET', url.withoutHashForIE10compatibility(), true
58
- xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
59
- xhr.setRequestHeader 'X-XHR-Referer', referer
60
-
61
- xhr.onload = ->
62
- triggerEvent EVENTS.RECEIVE, url: url.absolute
63
-
64
- if doc = processResponse()
65
- reflectNewUrl url
66
- reflectRedirectedUrl()
67
- changePage extractTitleAndBody(doc)...
68
- manuallyTriggerHashChangeForFirefox()
69
- onLoadFunction?()
70
- triggerEvent EVENTS.LOAD
71
- else
72
- document.location.href = crossOriginRedirect() or url.absolute
73
-
74
- if progressBar and showProgressBar
75
- xhr.onprogress = (event) =>
76
- percent = if event.lengthComputable
77
- event.loaded / event.total * 100
78
- else
79
- progressBar.value + (100 - progressBar.value) / 10
80
- progressBar.advanceTo(percent)
81
-
82
- xhr.onloadend = -> xhr = null
83
- xhr.onerror = -> document.location.href = url.absolute
84
-
85
- xhr.send()
86
-
87
- fetchHistory = (cachedPage) ->
88
- xhr?.abort()
89
- changePage cachedPage.title, cachedPage.body
90
- recallScrollPosition cachedPage
91
- triggerEvent EVENTS.RESTORE
92
-
93
-
94
- cacheCurrentPage = ->
95
- currentStateUrl = new ComponentUrl currentState.url
96
-
97
- pageCache[currentStateUrl.absolute] =
98
- url: currentStateUrl.relative,
99
- body: document.body,
100
- title: document.title,
101
- positionY: window.pageYOffset,
102
- positionX: window.pageXOffset,
103
- cachedAt: new Date().getTime(),
104
- transitionCacheDisabled: document.querySelector('[data-no-transition-cache]')?
105
-
106
- constrainPageCacheTo cacheSize
107
-
108
- pagesCached = (size = cacheSize) ->
109
- cacheSize = parseInt(size) if /^[\d]+$/.test size
110
-
111
- constrainPageCacheTo = (limit) ->
112
- pageCacheKeys = Object.keys pageCache
113
-
114
- cacheTimesRecentFirst = pageCacheKeys.map (url) ->
115
- pageCache[url].cachedAt
116
- .sort (a, b) -> b - a
117
-
118
- for key in pageCacheKeys when pageCache[key].cachedAt <= cacheTimesRecentFirst[limit]
119
- triggerEvent EVENTS.EXPIRE, pageCache[key]
120
- delete pageCache[key]
121
-
122
- changePage = (title, body, csrfToken, runScripts) ->
123
- triggerEvent EVENTS.BEFORE_UNLOAD
124
- document.title = title
125
- document.documentElement.replaceChild body, document.body
126
- CSRFToken.update csrfToken if csrfToken?
127
- setAutofocusElement()
128
- executeScriptTags() if runScripts
129
- currentState = window.history.state
130
- progressBar?.done()
131
- triggerEvent EVENTS.CHANGE
132
- triggerEvent EVENTS.UPDATE
133
-
134
- executeScriptTags = ->
135
- scripts = Array::slice.call document.body.querySelectorAll 'script:not([data-turbolinks-eval="false"])'
136
- for script in scripts when script.type in ['', 'text/javascript']
137
- copy = document.createElement 'script'
138
- copy.setAttribute attr.name, attr.value for attr in script.attributes
139
- copy.async = false unless script.hasAttribute 'async'
140
- copy.appendChild document.createTextNode script.innerHTML
141
- { parentNode, nextSibling } = script
142
- parentNode.removeChild script
143
- parentNode.insertBefore copy, nextSibling
144
- return
145
-
146
- removeNoscriptTags = (node) ->
147
- node.innerHTML = node.innerHTML.replace /<noscript[\S\s]*?<\/noscript>/ig, ''
148
- node
149
-
150
- # Firefox bug: Doesn't autofocus fields that are inserted via JavaScript
151
- setAutofocusElement = ->
152
- autofocusElement = (list = document.querySelectorAll 'input[autofocus], textarea[autofocus]')[list.length - 1]
153
- if autofocusElement and document.activeElement isnt autofocusElement
154
- autofocusElement.focus()
155
-
156
- reflectNewUrl = (url) ->
157
- if (url = new ComponentUrl url).absolute isnt referer
158
- window.history.pushState { turbolinks: true, url: url.absolute }, '', url.absolute
159
-
160
- reflectRedirectedUrl = ->
161
- if location = xhr.getResponseHeader 'X-XHR-Redirected-To'
162
- location = new ComponentUrl location
163
- preservedHash = if location.hasNoHash() then document.location.hash else ''
164
- window.history.replaceState window.history.state, '', location.href + preservedHash
165
-
166
- crossOriginRedirect = ->
167
- redirect if (redirect = xhr.getResponseHeader('Location'))? and (new ComponentUrl(redirect)).crossOrigin()
168
-
169
- rememberReferer = ->
170
- referer = document.location.href
171
-
172
- rememberCurrentUrl = ->
173
- window.history.replaceState { turbolinks: true, url: document.location.href }, '', document.location.href
174
-
175
- rememberCurrentState = ->
176
- currentState = window.history.state
177
-
178
- # Unlike other browsers, Firefox doesn't trigger hashchange after changing the
179
- # location (via pushState) to an anchor on a different page. For example:
180
- #
181
- # /pages/one => /pages/two#with-hash
182
- #
183
- # By forcing Firefox to trigger hashchange, the rest of the code can rely on more
184
- # consistent behavior across browsers.
185
- manuallyTriggerHashChangeForFirefox = ->
186
- if navigator.userAgent.match(/Firefox/) and !(url = (new ComponentUrl)).hasNoHash()
187
- window.history.replaceState currentState, '', url.withoutHash()
188
- document.location.hash = url.hash
189
-
190
- recallScrollPosition = (page) ->
191
- window.scrollTo page.positionX, page.positionY
192
-
193
- resetScrollPosition = ->
194
- if document.location.hash
195
- document.location.href = document.location.href
196
- else
197
- window.scrollTo 0, 0
198
-
199
-
200
- clone = (original) ->
201
- return original if not original? or typeof original isnt 'object'
202
- copy = new original.constructor()
203
- copy[key] = clone value for key, value of original
204
- copy
205
-
206
- popCookie = (name) ->
207
- value = document.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or ''
208
- document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
209
- value
210
-
211
- triggerEvent = (name, data) ->
212
- if typeof Prototype isnt 'undefined'
213
- Event.fire document, name, data, true
214
-
215
- event = document.createEvent 'Events'
216
- event.data = data if data
217
- event.initEvent name, true, true
218
- document.dispatchEvent event
219
-
220
- pageChangePrevented = (url) ->
221
- !triggerEvent EVENTS.BEFORE_CHANGE, url: url
222
-
223
- processResponse = ->
224
- clientOrServerError = ->
225
- 400 <= xhr.status < 600
226
-
227
- validContent = ->
228
- (contentType = xhr.getResponseHeader('Content-Type'))? and
229
- contentType.match /^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/
230
-
231
- extractTrackAssets = (doc) ->
232
- for node in doc.querySelector('head').childNodes when node.getAttribute?('data-turbolinks-track')?
233
- node.getAttribute('src') or node.getAttribute('href')
234
-
235
- assetsChanged = (doc) ->
236
- loadedAssets ||= extractTrackAssets document
237
- fetchedAssets = extractTrackAssets doc
238
- fetchedAssets.length isnt loadedAssets.length or intersection(fetchedAssets, loadedAssets).length isnt loadedAssets.length
239
-
240
- intersection = (a, b) ->
241
- [a, b] = [b, a] if a.length > b.length
242
- value for value in a when value in b
243
-
244
- if not clientOrServerError() and validContent()
245
- doc = createDocument xhr.responseText
246
- if doc and !assetsChanged doc
247
- return doc
248
-
249
- extractTitleAndBody = (doc) ->
250
- title = doc.querySelector 'title'
251
- [ title?.textContent, removeNoscriptTags(doc.querySelector('body')), CSRFToken.get(doc).token, 'runScripts' ]
252
-
253
- CSRFToken =
254
- get: (doc = document) ->
255
- node: tag = doc.querySelector 'meta[name="csrf-token"]'
256
- token: tag?.getAttribute? 'content'
257
-
258
- update: (latest) ->
259
- current = @get()
260
- if current.token? and latest? and current.token isnt latest
261
- current.node.setAttribute 'content', latest
262
-
263
- createDocument = (html) ->
264
- doc = document.documentElement.cloneNode()
265
- doc.innerHTML = html
266
- doc.head = doc.querySelector 'head'
267
- doc.body = doc.querySelector 'body'
268
- doc
269
-
270
- # The ComponentUrl class converts a basic URL string into an object
271
- # that behaves similarly to document.location.
272
- #
273
- # If an instance is created from a relative URL, the current document
274
- # is used to fill in the missing attributes (protocol, host, port).
275
- class ComponentUrl
276
- constructor: (@original = document.location.href) ->
277
- return @original if @original.constructor is ComponentUrl
278
- @_parse()
279
-
280
- withoutHash: -> @href.replace(@hash, '').replace('#', '')
281
-
282
- # Intention revealing function alias
283
- withoutHashForIE10compatibility: -> @withoutHash()
284
-
285
- hasNoHash: -> @hash.length is 0
286
-
287
- crossOrigin: ->
288
- @origin isnt (new ComponentUrl).origin
289
-
290
- _parse: ->
291
- (@link ?= document.createElement 'a').href = @original
292
- { @href, @protocol, @host, @hostname, @port, @pathname, @search, @hash } = @link
293
- @origin = [@protocol, '//', @hostname].join ''
294
- @origin += ":#{@port}" unless @port.length is 0
295
- @relative = [@pathname, @search, @hash].join ''
296
- @absolute = @href
297
-
298
- # The Link class derives from the ComponentUrl class, but is built from an
299
- # existing link element. Provides verification functionality for Turbolinks
300
- # to use in determining whether it should process the link when clicked.
301
- class Link extends ComponentUrl
302
- @HTML_EXTENSIONS: ['html']
303
-
304
- @allowExtensions: (extensions...) ->
305
- Link.HTML_EXTENSIONS.push extension for extension in extensions
306
- Link.HTML_EXTENSIONS
307
-
308
- constructor: (@link) ->
309
- return @link if @link.constructor is Link
310
- @original = @link.href
311
- @originalElement = @link
312
- @link = @link.cloneNode false
313
- super
314
-
315
- shouldIgnore: ->
316
- @crossOrigin() or
317
- @_anchored() or
318
- @_nonHtml() or
319
- @_optOut() or
320
- @_target()
321
-
322
- _anchored: ->
323
- (@hash.length > 0 or @href.charAt(@href.length - 1) is '#') and
324
- (@withoutHash() is (new ComponentUrl).withoutHash())
325
-
326
- _nonHtml: ->
327
- @pathname.match(/\.[a-z]+$/g) and not @pathname.match(new RegExp("\\.(?:#{Link.HTML_EXTENSIONS.join('|')})?$", 'g'))
328
-
329
- _optOut: ->
330
- link = @originalElement
331
- until ignore or link is document
332
- ignore = link.getAttribute('data-no-turbolink')?
333
- link = link.parentNode
334
- ignore
335
-
336
- _target: ->
337
- @link.target.length isnt 0
338
-
339
-
340
- # The Click class handles clicked links, verifying if Turbolinks should
341
- # take control by inspecting both the event and the link. If it should,
342
- # the page change process is initiated. If not, control is passed back
343
- # to the browser for default functionality.
344
- class Click
345
- @installHandlerLast: (event) ->
346
- unless event.defaultPrevented
347
- document.removeEventListener 'click', Click.handle, false
348
- document.addEventListener 'click', Click.handle, false
349
-
350
- @handle: (event) ->
351
- new Click event
352
-
353
- constructor: (@event) ->
354
- return if @event.defaultPrevented
355
- @_extractLink()
356
- if @_validForTurbolinks()
357
- visit @link.href unless pageChangePrevented(@link.absolute)
358
- @event.preventDefault()
359
-
360
- _extractLink: ->
361
- link = @event.target
362
- link = link.parentNode until !link.parentNode or link.nodeName is 'A'
363
- @link = new Link(link) if link.nodeName is 'A' and link.href.length isnt 0
364
-
365
- _validForTurbolinks: ->
366
- @link? and not (@link.shouldIgnore() or @_nonStandardClick())
367
-
368
- _nonStandardClick: ->
369
- @event.which > 1 or
370
- @event.metaKey or
371
- @event.ctrlKey or
372
- @event.shiftKey or
373
- @event.altKey
374
-
375
-
376
- class ProgressBar
377
- className = 'turbolinks-progress-bar'
378
-
379
- constructor: (@elementSelector) ->
380
- @value = 0
381
- @content = ''
382
- @speed = 300
383
- # Setting the opacity to a value < 1 fixes a display issue in Safari 6 and
384
- # iOS 6 where the progress bar would fill the entire page.
385
- @opacity = 0.99
386
- @install()
387
-
388
- install: ->
389
- @element = document.querySelector(@elementSelector)
390
- @element.classList.add(className)
391
- @styleElement = document.createElement('style')
392
- document.head.appendChild(@styleElement)
393
- @_updateStyle()
394
-
395
- uninstall: ->
396
- @element.classList.remove(className)
397
- document.head.removeChild(@styleElement)
398
-
399
- start: ->
400
- @advanceTo(5)
401
-
402
- advanceTo: (value) ->
403
- if value > @value <= 100
404
- @value = value
405
- @_updateStyle()
406
-
407
- if @value is 100
408
- @_stopTrickle()
409
- else if @value > 0
410
- @_startTrickle()
411
-
412
- done: ->
413
- if @value > 0
414
- @advanceTo(100)
415
- @_reset()
416
-
417
- _reset: ->
418
- originalOpacity = @opacity
419
-
420
- setTimeout =>
421
- @opacity = 0
422
- @_updateStyle()
423
- , @speed / 2
424
-
425
- setTimeout =>
426
- @value = 0
427
- @opacity = originalOpacity
428
- @_withSpeed(0, => @_updateStyle(true))
429
- , @speed
430
-
431
- _startTrickle: ->
432
- return if @trickling
433
- @trickling = true
434
- setTimeout(@_trickle, @speed)
435
-
436
- _stopTrickle: ->
437
- delete @trickling
438
-
439
- _trickle: =>
440
- return unless @trickling
441
- @advanceTo(@value + Math.random() / 2)
442
- setTimeout(@_trickle, @speed)
443
-
444
- _withSpeed: (speed, fn) ->
445
- originalSpeed = @speed
446
- @speed = speed
447
- result = fn()
448
- @speed = originalSpeed
449
- result
450
-
451
- _updateStyle: (forceRepaint = false) ->
452
- @_changeContentToForceRepaint() if forceRepaint
453
- @styleElement.textContent = @_createCSSRule()
454
-
455
- _changeContentToForceRepaint: ->
456
- @content = if @content is '' then ' ' else ''
457
-
458
- _createCSSRule: ->
459
- """
460
- #{@elementSelector}.#{className}::before {
461
- content: '#{@content}';
462
- position: fixed;
463
- top: 0;
464
- left: 0;
465
- z-index: 2000;
466
- background-color: #0076ff;
467
- height: 3px;
468
- opacity: #{@opacity};
469
- width: #{@value}%;
470
- transition: width #{@speed}ms ease-out, opacity #{@speed / 2}ms ease-in;
471
- transform: translate3d(0,0,0);
472
- }
473
- """
474
-
475
-
476
- # Delay execution of function long enough to miss the popstate event
477
- # some browsers fire on the initial page load.
478
- bypassOnLoadPopstate = (fn) ->
479
- setTimeout fn, 500
480
-
481
- installDocumentReadyPageEventTriggers = ->
482
- document.addEventListener 'DOMContentLoaded', ( ->
483
- triggerEvent EVENTS.CHANGE
484
- triggerEvent EVENTS.UPDATE
485
- ), true
486
-
487
- installJqueryAjaxSuccessPageUpdateTrigger = ->
488
- if typeof jQuery isnt 'undefined'
489
- jQuery(document).on 'ajaxSuccess', (event, xhr, settings) ->
490
- return unless jQuery.trim xhr.responseText
491
- triggerEvent EVENTS.UPDATE
492
-
493
- installHistoryChangeHandler = (event) ->
494
- if event.state?.turbolinks
495
- if cachedPage = pageCache[(new ComponentUrl(event.state.url)).absolute]
496
- cacheCurrentPage()
497
- fetchHistory cachedPage
498
- else
499
- visit event.target.location.href
500
-
501
- initializeTurbolinks = ->
502
- rememberCurrentUrl()
503
- rememberCurrentState()
504
-
505
- document.addEventListener 'click', Click.installHandlerLast, true
506
-
507
- window.addEventListener 'hashchange', (event) ->
508
- rememberCurrentUrl()
509
- rememberCurrentState()
510
- , false
511
- bypassOnLoadPopstate ->
512
- window.addEventListener 'popstate', installHistoryChangeHandler, false
513
-
514
- # Handle bug in Firefox 26/27 where history.state is initially undefined
515
- historyStateIsDefined =
516
- window.history.state != undefined or navigator.userAgent.match /Firefox\/2[6|7]/
517
-
518
- browserSupportsPushState =
519
- window.history and window.history.pushState and window.history.replaceState and historyStateIsDefined
520
-
521
- browserIsntBuggy =
522
- !navigator.userAgent.match /CriOS\//
523
-
524
- requestMethodIsSafe =
525
- popCookie('request_method') in ['GET','']
526
-
527
- browserSupportsTurbolinks = browserSupportsPushState and browserIsntBuggy and requestMethodIsSafe
528
-
529
- browserSupportsCustomEvents =
530
- document.addEventListener and document.createEvent
531
-
532
- if browserSupportsCustomEvents
533
- installDocumentReadyPageEventTriggers()
534
- installJqueryAjaxSuccessPageUpdateTrigger()
535
-
536
- if browserSupportsTurbolinks
537
- visit = fetch
538
- initializeTurbolinks()
539
- else
540
- visit = (url) -> document.location.href = url
541
-
542
- # Public API
543
- # Turbolinks.visit(url)
544
- # Turbolinks.pagesCached()
545
- # Turbolinks.pagesCached(20)
546
- # Turbolinks.enableTransitionCache()
547
- # Turbolinks.allowLinkExtensions('md')
548
- # Turbolinks.supported
549
- # Turbolinks.EVENTS
550
- @Turbolinks = {
551
- visit,
552
- pagesCached,
553
- enableTransitionCache,
554
- enableProgressBar,
555
- allowLinkExtensions: Link.allowExtensions,
556
- supported: browserSupportsTurbolinks,
557
- EVENTS: clone(EVENTS)
558
- }