turbograft 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ff1c8c57bee7bd53d6ce6466aca2f56a4bb7141b
4
- data.tar.gz: c935fccb4ec7075510e6e0b8d6aa23a284b1cd08
3
+ metadata.gz: b5a814c49eaff1fbe7cf0bb02f67ef8d96fe8ff9
4
+ data.tar.gz: dd94aabbd02817b9ce92f99f87651daa94975176
5
5
  SHA512:
6
- metadata.gz: a8e1fe39216d6d6f67724497546dd9ddd946fa6745408c3f4e8c4f60ffa2b29a1447446f86847675af890c500e8ffd12d0853007c70b46b62316a16223696dc7
7
- data.tar.gz: ef845801b2eacdb8a32d9790eb525b64025a08eca8105a538484001bccc61d69d3027d2ec71645110665238bf4d33c799ec35a7b2e612925701fd6037875a0cb
6
+ metadata.gz: 3cfa4e748a8278ab568aac8b985eaa46df40fbf6ca063785fd66be1408ad55221d146683d0edeeba1f047a1ede182dc45f000353639886d1ffc27df958320168
7
+ data.tar.gz: 262fa76bf7948cd31f889e881c3b63d8a88a41d697a8d1fc37ac06d5db306905fa3508512791b0435b2e95eb18eda1662ec6f1d7b278c61d3ef2132723992c16
data/README.md CHANGED
@@ -188,5 +188,6 @@ document.addEventListener 'page:after-node-removed', (event) ->
188
188
 
189
189
  ## Testing
190
190
 
191
- - `./server` and visit http://localhost:3000/teaspoon to run the JS test suite
191
+ - `./server` and visit http://localhost:3000/teaspoon to run the JS test suite in the browser
192
+ - `bundle exec teaspoon` will run the JS test suite from the command line. Uses Selenium by default, but can be configured using the TEASPOON_DRIVER environment variable
192
193
  - `bundle exec rake test` to run the browser test suite
@@ -0,0 +1,215 @@
1
+ class window.TurboHead
2
+ constructor: (@activeDocument, @upstreamDocument) ->
3
+
4
+ update: (successCallback, failureCallback) ->
5
+ activeAssets = extractTrackedAssets(@activeDocument)
6
+ upstreamAssets = extractTrackedAssets(@upstreamDocument)
7
+ {activeScripts, newScripts} = processScripts(activeAssets, upstreamAssets)
8
+
9
+ if hasScriptConflict(activeScripts, newScripts)
10
+ return failureCallback()
11
+
12
+ updateLinkTags(activeAssets, upstreamAssets)
13
+ updateScriptTags(@activeDocument, newScripts, successCallback)
14
+
15
+ updateLinkTags = (activeAssets, upstreamAssets) ->
16
+ activeLinks = activeAssets.filter(filterForNodeType('LINK'))
17
+ upstreamLinks = upstreamAssets.filter(filterForNodeType('LINK'))
18
+ remainingActiveLinks = removeStaleLinks(activeLinks, upstreamLinks)
19
+ reorderedActiveLinks = reorderActiveLinks(remainingActiveLinks, upstreamLinks)
20
+ insertNewLinks(reorderedActiveLinks, upstreamLinks)
21
+
22
+ updateScriptTags = (activeDocument, newScripts, callback) ->
23
+ asyncSeries(
24
+ newScripts.map((scriptNode) -> insertScriptTask(activeDocument, scriptNode)),
25
+ callback
26
+ )
27
+
28
+ extractTrackedAssets = (doc) ->
29
+ for node in doc.head.children when node.dataset.turbolinksTrack?
30
+ node
31
+
32
+ filterForNodeType = (nodeType) ->
33
+ (node) -> node.nodeName == nodeType
34
+
35
+ hasScriptConflict = (activeScripts, newScripts) ->
36
+ hasExistingScriptAssetName = (upstreamNode) ->
37
+ activeScripts.some (activeNode) ->
38
+ upstreamNode.dataset.turbolinksTrackScriptAs == activeNode.dataset.turbolinksTrackScriptAs
39
+
40
+ newScripts.some(hasExistingScriptAssetName)
41
+
42
+ asyncSeries = (tasks, callback) ->
43
+ return callback() if tasks.length == 0
44
+ task = tasks.shift()
45
+ task(-> asyncSeries(tasks, callback))
46
+
47
+ insertScriptTask = (activeDocument, scriptNode) ->
48
+ # We need to clone script tags in order to ensure that the browser executes them.
49
+ newNode = activeDocument.createElement('SCRIPT')
50
+ newNode.setAttribute(attr.name, attr.value) for attr in scriptNode.attributes
51
+ newNode.appendChild(activeDocument.createTextNode(scriptNode.innerHTML))
52
+
53
+ return (done) ->
54
+ onScriptEvent = (event) ->
55
+ triggerEvent('page:script-error', event) if event.type == 'error'
56
+ newNode.removeEventListener('load', onScriptEvent)
57
+ newNode.removeEventListener('error', onScriptEvent)
58
+ done()
59
+ newNode.addEventListener('load', onScriptEvent)
60
+ newNode.addEventListener('error', onScriptEvent)
61
+ activeDocument.head.appendChild(newNode)
62
+ triggerEvent('page:after-script-inserted', newNode)
63
+
64
+ processScripts = (activeAssets, upstreamAssets) ->
65
+ activeScripts = activeAssets.filter(filterForNodeType('SCRIPT'))
66
+ upstreamScripts = upstreamAssets.filter(filterForNodeType('SCRIPT'))
67
+ hasNewSrc = (upstreamNode) ->
68
+ activeScripts.every (activeNode) ->
69
+ upstreamNode.src != activeNode.src
70
+
71
+ newScripts = upstreamScripts.filter(hasNewSrc)
72
+
73
+ {activeScripts, newScripts}
74
+
75
+ removeStaleLinks = (activeLinks, upstreamLinks) ->
76
+ isStaleLink = (link) ->
77
+ upstreamLinks.every (upstreamLink) ->
78
+ upstreamLink.href != link.href
79
+
80
+ staleLinks = activeLinks.filter(isStaleLink)
81
+
82
+ for staleLink in staleLinks
83
+ removedLink = document.head.removeChild(staleLink)
84
+ triggerEvent('page:after-link-removed', removedLink)
85
+
86
+ activeLinks.filter((link) -> !isStaleLink(link))
87
+
88
+ reorderAlreadyExists = (link1, link2, reorders) ->
89
+ reorders.some (reorderPair) ->
90
+ link1 in reorderPair && link2 in reorderPair
91
+
92
+ generateReorderGraph = (activeLinks, upstreamLinks) ->
93
+ reorders = []
94
+ for activeLink1 in activeLinks
95
+ for activeLink2 in activeLinks
96
+ continue if activeLink1.href == activeLink2.href
97
+ continue if reorderAlreadyExists(activeLink1, activeLink2, reorders)
98
+
99
+ upstreamLink1 = upstreamLinks.filter((link) -> link.href == activeLink1.href)[0]
100
+ upstreamLink2 = upstreamLinks.filter((link) -> link.href == activeLink2.href)[0]
101
+
102
+ orderHasChanged =
103
+ (activeLinks.indexOf(activeLink1) < activeLinks.indexOf(activeLink2)) !=
104
+ (upstreamLinks.indexOf(upstreamLink1) < upstreamLinks.indexOf(upstreamLink2))
105
+
106
+ reorders.push([activeLink1, activeLink2]) if orderHasChanged
107
+ reorders
108
+
109
+ nextMove = (activeLinks, reorders) ->
110
+ changesAssociatedTo = (link) ->
111
+ reorders.filter (reorderPair) ->
112
+ link in reorderPair
113
+
114
+ linksSortedByMovePriority = activeLinks
115
+ .slice()
116
+ .sort (link1, link2) ->
117
+ changesAssociatedTo(link2).length - changesAssociatedTo(link1).length
118
+
119
+ linkToMove = linksSortedByMovePriority[0]
120
+
121
+ linksToPassBy = changesAssociatedTo(linkToMove).map (reorderPair) ->
122
+ (reorderPair.filter (link) -> link.href != linkToMove.href)[0]
123
+
124
+ {linkToMove, linksToPassBy}
125
+
126
+ reorderActiveLinks = (activeLinks, upstreamLinks) ->
127
+ activeLinksCopy = activeLinks.slice()
128
+ pendingReorders = generateReorderGraph(activeLinksCopy, upstreamLinks)
129
+
130
+ removeReorder = (link1, link2) ->
131
+ reorderToRemove = (pendingReorders.filter (reorderPair) ->
132
+ link1 in reorderPair && link2 in reorderPair)[0]
133
+ indexToRemove = pendingReorders.indexOf(reorderToRemove)
134
+ pendingReorders.splice(indexToRemove, 1)
135
+
136
+ addNewReorder = (link1, link2) ->
137
+ pendingReorders.push [link1, link2]
138
+
139
+ markReorderAsFinished = (linkToMove, linkToPass, remainingLinksToPass) ->
140
+ removeReorder(linkToMove, linkToPass)
141
+ removalIndex = remainingLinksToPass.indexOf(linkToPass)
142
+ remainingLinksToPass.splice(removalIndex, 1)
143
+
144
+ removeLink = (linkToRemove, indexOfLink) ->
145
+ removedLink = document.head.removeChild(linkToRemove)
146
+ triggerEvent('page:after-link-removed', removedLink)
147
+ activeLinksCopy.splice(indexOfLink, 1)
148
+
149
+ performMove = (linkToMove, linksToPassBy) ->
150
+ moveDirection = if activeLinksCopy.indexOf(linkToMove) > activeLinksCopy.indexOf(linksToPassBy[0]) then 'UP' else 'DOWN'
151
+ startIndex = activeLinksCopy.indexOf(linkToMove)
152
+
153
+ switch moveDirection
154
+ when 'UP'
155
+ for i in [(startIndex - 1)..0]
156
+ currentLink = activeLinksCopy[i]
157
+ if currentLink in linksToPassBy
158
+ markReorderAsFinished(linkToMove, currentLink, linksToPassBy)
159
+
160
+ if linksToPassBy.length == 0
161
+ removeLink(linkToMove, startIndex)
162
+
163
+ document.head.insertBefore(linkToMove, activeLinksCopy[i])
164
+ activeLinksCopy.splice(i, 0, linkToMove)
165
+ triggerEvent('page:after-link-inserted', linkToMove)
166
+ return
167
+ else
168
+ addNewReorder(linkToMove, currentLink, pendingReorders)
169
+ when 'DOWN'
170
+ for i in [(startIndex + 1)...activeLinksCopy.length]
171
+ currentLink = activeLinksCopy[i]
172
+ if currentLink in linksToPassBy
173
+ markReorderAsFinished(linkToMove, currentLink, linksToPassBy)
174
+
175
+ if linksToPassBy.length == 0
176
+ removeLink(linkToMove, startIndex)
177
+
178
+ targetIndex = i - 1
179
+ if targetIndex == activeLinksCopy.length - 1
180
+ document.head.appendChild(linkToMove)
181
+ activeLinksCopy.push(linkToMove)
182
+ else
183
+ document.head.insertBefore(linkToMove, activeLinksCopy[targetIndex + 1])
184
+ activeLinksCopy.splice(targetIndex + 1, 0, linkToMove)
185
+ triggerEvent('page:after-link-inserted', linkToMove)
186
+ return
187
+ else
188
+ addNewReorder(linkToMove, currentLink, pendingReorders)
189
+
190
+ while pendingReorders.length > 0
191
+ {linkToMove, linksToPassBy} = nextMove(activeLinksCopy, pendingReorders)
192
+ performMove(linkToMove, linksToPassBy)
193
+
194
+ activeLinksCopy
195
+
196
+ insertNewLinks = (activeLinks, upstreamLinks) ->
197
+ isNewLink = (link) ->
198
+ activeLinks.every (activeLink) ->
199
+ activeLink.href != link.href
200
+
201
+ upstreamLinks
202
+ .filter(isNewLink)
203
+ .reverse() # This is because we can't insert before a sibling that hasn't been inserted yet.
204
+ .forEach (newUpstreamLink) ->
205
+ index = upstreamLinks.indexOf(newUpstreamLink)
206
+ newActiveLink = newUpstreamLink.cloneNode()
207
+ if index == upstreamLinks.length - 1
208
+ document.head.appendChild(newActiveLink)
209
+ activeLinks.push(newActiveLink)
210
+ else
211
+ targetIndex = activeLinks.indexOf((activeLinks.filter (link) ->
212
+ link.href == upstreamLinks[index + 1].href)[0])
213
+ document.head.insertBefore(newActiveLink, activeLinks[targetIndex])
214
+ activeLinks.splice(targetIndex, 0, newActiveLink)
215
+ triggerEvent('page:after-link-inserted', newActiveLink)
@@ -63,7 +63,6 @@ removeNode = (node) ->
63
63
  class window.Turbolinks
64
64
  createDocument = null
65
65
  currentState = null
66
- loadedAssets = null
67
66
  referer = null
68
67
 
69
68
  fetch = (url, options = {}) ->
@@ -80,6 +79,10 @@ class window.Turbolinks
80
79
 
81
80
  fetchReplacement url, options
82
81
 
82
+ @fullPageNavigate: (url) ->
83
+ triggerEvent('page:before-full-refresh', url: url)
84
+ document.location.href = url
85
+
83
86
  @pushState: (state, title, url) ->
84
87
  window.history.pushState(state, title, url)
85
88
 
@@ -89,12 +92,18 @@ class window.Turbolinks
89
92
  fetchReplacement = (url, options) ->
90
93
  triggerEvent 'page:fetch', url: url.absolute
91
94
 
92
- xhr?.abort()
95
+ if xhr?
96
+ # Workaround for sinon xhr.abort()
97
+ # https://github.com/sinonjs/sinon/issues/432#issuecomment-216917023
98
+ xhr.readyState = 0
99
+ xhr.statusText = "abort"
100
+ xhr.abort()
101
+
93
102
  xhr = new XMLHttpRequest
103
+
94
104
  xhr.open 'GET', url.withoutHashForIE10compatibility(), true
95
105
  xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
96
106
  xhr.setRequestHeader 'X-XHR-Referer', referer
97
-
98
107
  options.headers ?= {}
99
108
 
100
109
  for k,v of options.headers
@@ -102,13 +111,17 @@ class window.Turbolinks
102
111
 
103
112
  xhr.onload = ->
104
113
  if xhr.status >= 500
105
- document.location.href = url.absolute
114
+ Turbolinks.fullPageNavigate(url.absolute)
106
115
  else
107
116
  Turbolinks.loadPage(url, xhr, options)
117
+ xhr = null
108
118
 
109
- xhr.onloadend = -> xhr = null
110
- xhr.onerror = ->
111
- document.location.href = url.absolute
119
+ xhr.onerror = ->
120
+ # Workaround for sinon xhr.abort()
121
+ if xhr.statusText == "abort"
122
+ xhr = null
123
+ return
124
+ Turbolinks.fullPageNavigate(url.absolute)
112
125
 
113
126
  xhr.send()
114
127
 
@@ -117,17 +130,27 @@ class window.Turbolinks
117
130
  @loadPage: (url, xhr, options = {}) ->
118
131
  triggerEvent 'page:receive'
119
132
  options.updatePushState ?= true
120
-
121
- if doc = processResponse(xhr, options.partialReplace)
133
+ if upstreamDocument = processResponse(xhr, options.partialReplace)
122
134
  reflectNewUrl url if options.updatePushState
123
- nodes = changePage(extractTitleAndBody(doc)..., options)
124
- reflectRedirectedUrl(xhr) if options.updatePushState
125
- triggerEvent 'page:load', nodes
126
- options.onLoadFunction?()
127
- else
128
- document.location.href = url.absolute
129
135
 
130
- return
136
+ new TurboHead(document, upstreamDocument).update(
137
+ onHeadUpdateSuccess = ->
138
+ nodes = changePage(
139
+ upstreamDocument.querySelector('title')?.textContent,
140
+ removeNoscriptTags(upstreamDocument.querySelector('body')),
141
+ CSRFToken.get(upstreamDocument).token,
142
+ 'runScripts',
143
+ options
144
+ )
145
+ reflectRedirectedUrl(xhr) if options.updatePushState
146
+ options.onLoadFunction?()
147
+ triggerEvent 'page:load', nodes
148
+ ,
149
+ onHeadUpdateError = ->
150
+ Turbolinks.fullPageNavigate(url.absolute)
151
+ )
152
+ else
153
+ Turbolinks.fullPageNavigate(url.absolute)
131
154
 
132
155
  changePage = (title, body, csrfToken, runScripts, options = {}) ->
133
156
  document.title = title if title
@@ -210,7 +233,7 @@ class window.Turbolinks
210
233
  newNode = newNode.cloneNode(true)
211
234
  replaceNode(newNode, existingNode)
212
235
 
213
- if newNode.nodeName == 'SCRIPT' && newNode.getAttribute("data-turbolinks-eval") != "false"
236
+ if newNode.nodeName == 'SCRIPT' && newNode.dataset.turbolinksEval != "false"
214
237
  executeScriptTag(newNode)
215
238
  else
216
239
  refreshedNodes.push(newNode)
@@ -307,29 +330,9 @@ class window.Turbolinks
307
330
  validContent = ->
308
331
  xhr.getResponseHeader('Content-Type').match /^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/
309
332
 
310
- extractTrackAssets = (doc) ->
311
- for node in doc.querySelector('head').childNodes when node.getAttribute?('data-turbolinks-track')?
312
- node.getAttribute('src') or node.getAttribute('href')
313
-
314
- assetsChanged = (doc) ->
315
- loadedAssets ||= extractTrackAssets document
316
- fetchedAssets = extractTrackAssets doc
317
- fetchedAssets.length isnt loadedAssets.length or intersection(fetchedAssets, loadedAssets).length isnt loadedAssets.length
318
-
319
- intersection = (a, b) ->
320
- [a, b] = [b, a] if a.length > b.length
321
- value for value in a when value in b
322
-
323
333
  if !clientOrServerError() && validContent()
324
- doc = createDocument xhr.responseText
325
- changed = assetsChanged(doc)
326
-
327
- if doc && (!changed || partial)
328
- return doc
329
-
330
- extractTitleAndBody = (doc) ->
331
- title = doc.querySelector 'title'
332
- [ title?.textContent, removeNoscriptTags(doc.querySelector('body')), CSRFToken.get(doc).token, 'runScripts' ]
334
+ upstreamDocument = createDocument(xhr.responseText)
335
+ return upstreamDocument
333
336
 
334
337
  installHistoryChangeHandler = (event) ->
335
338
  if event.state?.turbolinks
@@ -1,3 +1,3 @@
1
1
  module TurboGraft
2
- VERSION = '0.2.3'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -3,13 +3,9 @@ module TurboGraft
3
3
  # option by using the X-XHR-Referer request header instead of the standard Referer
4
4
  # request header.
5
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 = {})
6
+ def url_for(options = {})
11
7
  options = (controller.request.headers["X-XHR-Referer"] || options) if options == :back
12
- url_for_without_xhr_referer options
8
+ super(options)
13
9
  end
14
10
  end
15
11
  end
data/lib/turbograft.rb CHANGED
@@ -21,8 +21,8 @@ module TurboGraft
21
21
  Config.controllers.each do |klass|
22
22
  klass.constantize.class_eval do
23
23
  include XHRHeaders, Cookies, XDomainBlocker, Redirection
24
- before_filter :set_xhr_redirected_to, :set_request_method_cookie
25
- after_filter :abort_xdomain_redirect
24
+ before_action :set_xhr_redirected_to, :set_request_method_cookie
25
+ after_action :abort_xdomain_redirect
26
26
  end
27
27
  end
28
28
 
@@ -36,7 +36,7 @@ module TurboGraft
36
36
 
37
37
  ActiveSupport.on_load(:action_view) do
38
38
  (ActionView::RoutingUrlFor rescue ActionView::Helpers::UrlHelper).module_eval do
39
- include XHRUrlFor
39
+ prepend XHRUrlFor
40
40
  end
41
41
  end unless RUBY_VERSION =~ /^1\.8/
42
42
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbograft
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kristian Plettenberg-Dussault
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2016-06-23 00:00:00.000000000 Z
16
+ date: 2016-08-24 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: coffee-rails
@@ -228,8 +228,8 @@ files:
228
228
  - lib/assets/javascripts/turbograft/link.coffee
229
229
  - lib/assets/javascripts/turbograft/page.coffee
230
230
  - lib/assets/javascripts/turbograft/remote.coffee
231
+ - lib/assets/javascripts/turbograft/turbohead.coffee
231
232
  - lib/assets/javascripts/turbograft/turbolinks.coffee
232
- - lib/turbograft.js
233
233
  - lib/turbograft.rb
234
234
  - lib/turbograft/cookies.rb
235
235
  - lib/turbograft/redirection.rb
@@ -257,7 +257,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
257
257
  version: '0'
258
258
  requirements: []
259
259
  rubyforge_project:
260
- rubygems_version: 2.2.2
260
+ rubygems_version: 2.5.1
261
261
  signing_key:
262
262
  specification_version: 4
263
263
  summary: turbolinks with partial page replacement