turbograft 0.2.3 → 0.3.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.
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