turbolinks 2.5.3 → 5.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.
@@ -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
- }
@@ -1,15 +0,0 @@
1
- module Turbolinks
2
- # For non-GET requests, sets a request_method cookie containing
3
- # the request method of the current request. The Turbolinks script
4
- # will not initialize if this cookie is set.
5
- module Cookies
6
- private
7
- def set_request_method_cookie
8
- if request.get?
9
- cookies.delete(:request_method)
10
- else
11
- cookies[:request_method] = request.request_method
12
- end
13
- end
14
- end
15
- end
@@ -1,21 +0,0 @@
1
- module Turbolinks
2
- # Changes the response status to 403 Forbidden if all of these conditions are true:
3
- # - The current request originated from Turbolinks
4
- # - The request is being redirected to a different domain
5
- module XDomainBlocker
6
- private
7
- def same_origin?(a, b)
8
- a = URI.parse URI.escape(a)
9
- b = URI.parse URI.escape(b)
10
- [a.scheme, a.host, a.port] == [b.scheme, b.host, b.port]
11
- end
12
-
13
- def abort_xdomain_redirect
14
- to_uri = response.headers['Location'] || ""
15
- current = request.headers['X-XHR-Referer'] || ""
16
- unless to_uri.blank? || current.blank? || same_origin?(current, to_uri)
17
- self.status = 403
18
- end
19
- end
20
- end
21
- end
@@ -1,46 +0,0 @@
1
- module Turbolinks
2
- # Intercepts calls to _compute_redirect_to_location (used by redirect_to) for two purposes.
3
- #
4
- # 1. Corrects the behavior of redirect_to with the :back option by using the X-XHR-Referer
5
- # request header instead of the standard Referer request header.
6
- #
7
- # 2. Stores the return value (the redirect target url) to persist through to the redirect
8
- # request, where it will be used to set the X-XHR-Redirected-To response header. The
9
- # Turbolinks script will detect the header and use replaceState to reflect the redirected
10
- # url.
11
- module XHRHeaders
12
- extend ActiveSupport::Concern
13
-
14
- def _compute_redirect_to_location(*args)
15
- options, request = _normalize_redirect_params(args)
16
-
17
- store_for_turbolinks begin
18
- if options == :back && request.headers["X-XHR-Referer"]
19
- super(*[(request if args.length == 2), request.headers["X-XHR-Referer"]].compact)
20
- else
21
- super(*args)
22
- end
23
- end
24
- end
25
-
26
- private
27
- def store_for_turbolinks(url)
28
- session[:_turbolinks_redirect_to] = url if session && request.headers["X-XHR-Referer"]
29
- url
30
- end
31
-
32
- def set_xhr_redirected_to
33
- if session && session[:_turbolinks_redirect_to]
34
- response.headers['X-XHR-Redirected-To'] = session.delete :_turbolinks_redirect_to
35
- end
36
- end
37
-
38
- # Ensure backwards compatibility
39
- # Rails < 4.2: _compute_redirect_to_location(options)
40
- # Rails >= 4.2: _compute_redirect_to_location(request, options)
41
- def _normalize_redirect_params(args)
42
- options, req = args.reverse
43
- [options, req || request]
44
- end
45
- end
46
- end
@@ -1,15 +0,0 @@
1
- module Turbolinks
2
- # Corrects the behavior of url_for (and link_to, which uses url_for) with the :back
3
- # option by using the X-XHR-Referer request header instead of the standard Referer
4
- # request header.
5
- module XHRUrlFor
6
- def self.included(base)
7
- base.alias_method_chain :url_for, :xhr_referer
8
- end
9
-
10
- def url_for_with_xhr_referer(options = {})
11
- options = (controller.request.headers["X-XHR-Referer"] || options) if options == :back
12
- url_for_without_xhr_referer options
13
- end
14
- end
15
- end