turbolinks 2.5.4 → 5.0.0.beta1

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
- }