turbograft 0.4.8 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -4
  3. data/lib/assets/javascripts/turbograft/click.js +44 -0
  4. data/lib/assets/javascripts/turbograft/component_url.js +53 -0
  5. data/lib/assets/javascripts/turbograft/csrf_token.js +23 -0
  6. data/lib/assets/javascripts/turbograft/document.js +20 -0
  7. data/lib/assets/javascripts/turbograft/initializers.js +83 -0
  8. data/lib/assets/javascripts/turbograft/link.js +58 -0
  9. data/lib/assets/javascripts/turbograft/page.js +105 -0
  10. data/lib/assets/javascripts/turbograft/remote.js +248 -0
  11. data/lib/assets/javascripts/turbograft/response.js +48 -0
  12. data/lib/assets/javascripts/turbograft/turbohead.js +159 -0
  13. data/lib/assets/javascripts/turbograft/turbolinks.js +504 -0
  14. data/lib/assets/javascripts/turbograft.js +37 -0
  15. data/lib/turbograft/version.rb +1 -1
  16. metadata +27 -27
  17. data/lib/assets/javascripts/turbograft/click.coffee +0 -34
  18. data/lib/assets/javascripts/turbograft/component_url.coffee +0 -24
  19. data/lib/assets/javascripts/turbograft/csrf_token.coffee +0 -9
  20. data/lib/assets/javascripts/turbograft/document.coffee +0 -11
  21. data/lib/assets/javascripts/turbograft/initializers.coffee +0 -67
  22. data/lib/assets/javascripts/turbograft/link.coffee +0 -41
  23. data/lib/assets/javascripts/turbograft/page.coffee +0 -77
  24. data/lib/assets/javascripts/turbograft/remote.coffee +0 -179
  25. data/lib/assets/javascripts/turbograft/response.coffee +0 -31
  26. data/lib/assets/javascripts/turbograft/turbohead.coffee +0 -142
  27. data/lib/assets/javascripts/turbograft/turbolinks.coffee +0 -361
  28. data/lib/assets/javascripts/turbograft.coffee +0 -30
@@ -1,142 +0,0 @@
1
- TRACKED_ASSET_SELECTOR = '[data-turbolinks-track]'
2
- TRACKED_ATTRIBUTE_NAME = 'turbolinksTrack'
3
- ANONYMOUS_TRACK_VALUE = 'true'
4
-
5
- scriptPromises = {}
6
- resolvePreviousRequest = null
7
-
8
- waitForCompleteDownloads = ->
9
- loadingPromises = Object.keys(scriptPromises).map (url) ->
10
- scriptPromises[url]
11
- Promise.all(loadingPromises)
12
-
13
- class TurboGraft.TurboHead
14
- constructor: (@activeDocument, @upstreamDocument) ->
15
- @activeAssets = extractTrackedAssets(@activeDocument)
16
- @upstreamAssets = extractTrackedAssets(@upstreamDocument)
17
- @newScripts = @upstreamAssets
18
- .filter(attributeMatches('nodeName', 'SCRIPT'))
19
- .filter(noAttributeMatchesIn('src', @activeAssets))
20
-
21
- @newLinks = @upstreamAssets
22
- .filter(attributeMatches('nodeName', 'LINK'))
23
- .filter(noAttributeMatchesIn('href', @activeAssets))
24
-
25
- @_testAPI: {
26
- reset: ->
27
- scriptPromises = {}
28
- resolvePreviousRequest = null
29
- }
30
-
31
- hasChangedAnonymousAssets: () ->
32
- anonymousUpstreamAssets = @upstreamAssets
33
- .filter(datasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE))
34
- anonymousActiveAssets = @activeAssets
35
- .filter(datasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE))
36
-
37
- if anonymousActiveAssets.length != anonymousUpstreamAssets.length
38
- return true
39
-
40
- noMatchingSrc = noAttributeMatchesIn('src', anonymousUpstreamAssets)
41
- noMatchingHref = noAttributeMatchesIn('href', anonymousUpstreamAssets)
42
-
43
- anonymousActiveAssets.some((node) ->
44
- noMatchingSrc(node) || noMatchingHref(node)
45
- )
46
-
47
- movingFromTrackedToUntracked: () ->
48
- @upstreamAssets.length == 0 && @activeAssets.length > 0
49
-
50
- hasNamedAssetConflicts: () ->
51
- @newScripts
52
- .concat(@newLinks)
53
- .filter(noDatasetMatches(TRACKED_ATTRIBUTE_NAME, ANONYMOUS_TRACK_VALUE))
54
- .some(datasetMatchesIn(TRACKED_ATTRIBUTE_NAME, @activeAssets))
55
-
56
- hasAssetConflicts: () ->
57
- @movingFromTrackedToUntracked() ||
58
- @hasNamedAssetConflicts() ||
59
- @hasChangedAnonymousAssets()
60
-
61
- waitForAssets: () ->
62
- resolvePreviousRequest?(isCanceled: true)
63
-
64
- new Promise((resolve) =>
65
- resolvePreviousRequest = resolve
66
- waitForCompleteDownloads()
67
- .then(@_insertNewAssets)
68
- .then(waitForCompleteDownloads)
69
- .then(resolve)
70
- )
71
-
72
- _insertNewAssets: () =>
73
- updateLinkTags(@activeDocument, @newLinks)
74
- updateScriptTags(@activeDocument, @newScripts)
75
-
76
- extractTrackedAssets = (doc) ->
77
- [].slice.call(doc.querySelectorAll(TRACKED_ASSET_SELECTOR))
78
-
79
- attributeMatches = (attribute, value) ->
80
- (node) -> node[attribute] == value
81
-
82
- attributeMatchesIn = (attribute, collection) ->
83
- (node) ->
84
- collection.some((nodeFromCollection) -> node[attribute] == nodeFromCollection[attribute])
85
-
86
- noAttributeMatchesIn = (attribute, collection) ->
87
- (node) ->
88
- !collection.some((nodeFromCollection) -> node[attribute] == nodeFromCollection[attribute])
89
-
90
- datasetMatches = (attribute, value) ->
91
- (node) -> node.dataset[attribute] == value
92
-
93
- noDatasetMatches = (attribute, value) ->
94
- (node) -> node.dataset[attribute] != value
95
-
96
- datasetMatchesIn = (attribute, collection) ->
97
- (node) ->
98
- value = node.dataset[attribute]
99
- collection.some(datasetMatches(attribute, value))
100
-
101
- noDatasetMatchesIn = (attribute, collection) ->
102
- (node) ->
103
- value = node.dataset[attribute]
104
- !collection.some(datasetMatches(attribute, value))
105
-
106
- updateLinkTags = (activeDocument, newLinks) ->
107
- # style tag load events don't work in all browsers
108
- # as such we just hope they load ¯\_(ツ)_/¯
109
- newLinks.forEach((linkNode) ->
110
- newNode = linkNode.cloneNode()
111
- activeDocument.head.appendChild(newNode)
112
- triggerEvent("page:after-link-inserted", newNode)
113
- )
114
-
115
- updateScriptTags = (activeDocument, newScripts) ->
116
- promise = Promise.resolve()
117
- newScripts.forEach (scriptNode) ->
118
- promise = promise.then(-> insertScript(activeDocument, scriptNode))
119
- promise
120
-
121
- insertScript = (activeDocument, scriptNode) ->
122
- url = scriptNode.src
123
- if scriptPromises[url]
124
- return scriptPromises[url]
125
-
126
- # Clone script tags to guarantee browser execution.
127
- newNode = activeDocument.createElement('SCRIPT')
128
- newNode.setAttribute(attr.name, attr.value) for attr in scriptNode.attributes
129
- newNode.appendChild(activeDocument.createTextNode(scriptNode.innerHTML))
130
-
131
- scriptPromises[url] = new Promise((resolve) ->
132
- onAssetEvent = (event) ->
133
- triggerEvent("page:#script-error", event) if event.type == 'error'
134
- newNode.removeEventListener('load', onAssetEvent)
135
- newNode.removeEventListener('error', onAssetEvent)
136
- resolve()
137
-
138
- newNode.addEventListener('load', onAssetEvent)
139
- newNode.addEventListener('error', onAssetEvent)
140
- activeDocument.head.appendChild(newNode)
141
- triggerEvent("page:after-script-inserted", newNode)
142
- )
@@ -1,361 +0,0 @@
1
- Response = TurboGraft.Response
2
- TurboHead = TurboGraft.TurboHead
3
- jQuery = window.jQuery
4
-
5
- xhr = null
6
- activeDocument = document
7
-
8
- installDocumentReadyPageEventTriggers = ->
9
- activeDocument.addEventListener 'DOMContentLoaded', ( ->
10
- triggerEvent 'page:change'
11
- triggerEvent 'page:update'
12
- ), true
13
-
14
- installJqueryAjaxSuccessPageUpdateTrigger = ->
15
- if typeof jQuery isnt 'undefined'
16
- jQuery(activeDocument).on 'ajaxSuccess', (event, xhr, settings) ->
17
- return unless jQuery.trim xhr.responseText
18
- triggerEvent 'page:update'
19
-
20
- # Handle bug in Firefox 26/27 where history.state is initially undefined
21
- historyStateIsDefined =
22
- window.history.state != undefined or navigator.userAgent.match /Firefox\/2[6|7]/
23
-
24
- browserSupportsPushState =
25
- window.history and window.history.pushState and window.history.replaceState and historyStateIsDefined
26
-
27
- window.triggerEvent = (name, data) ->
28
- event = activeDocument.createEvent 'Events'
29
- event.data = data if data
30
- event.initEvent name, true, true
31
- activeDocument.dispatchEvent event
32
-
33
- window.triggerEventFor = (name, node, data) ->
34
- event = activeDocument.createEvent 'Events'
35
- event.data = data if data
36
- event.initEvent name, true, true
37
- node.dispatchEvent event
38
-
39
- popCookie = (name) ->
40
- value = activeDocument.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or ''
41
- activeDocument.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
42
- value
43
-
44
- requestMethodIsSafe =
45
- popCookie('request_method') in ['GET','']
46
-
47
- browserSupportsTurbolinks = browserSupportsPushState and requestMethodIsSafe
48
-
49
- browserSupportsCustomEvents =
50
- activeDocument.addEventListener and activeDocument.createEvent
51
-
52
- if browserSupportsCustomEvents
53
- installDocumentReadyPageEventTriggers()
54
- installJqueryAjaxSuccessPageUpdateTrigger()
55
-
56
- replaceNode = (newNode, oldNode) ->
57
- replacedNode = oldNode.parentNode.replaceChild(newNode, oldNode)
58
- triggerEvent('page:after-node-removed', replacedNode)
59
-
60
- removeNode = (node) ->
61
- removedNode = node.parentNode.removeChild(node)
62
- triggerEvent('page:after-node-removed', removedNode)
63
-
64
- # TODO: triggerEvent should be accessible to all these guys
65
- # on some kind of eventbus
66
- # TODO: clean up everything above me ^
67
- # TODO: decide on the public API
68
- class window.Turbolinks
69
- currentState = null
70
- referer = null
71
-
72
- fetch = (url, options = {}) ->
73
- return if pageChangePrevented(url)
74
- url = new ComponentUrl(url)
75
-
76
- rememberReferer()
77
-
78
- fetchReplacement(url, options)
79
-
80
- isPartialReplace = (response, options) ->
81
- Boolean(
82
- options.partialReplace ||
83
- options.onlyKeys?.length ||
84
- options.exceptKeys?.length
85
- )
86
-
87
- @fullPageNavigate: (url) ->
88
- if url?
89
- url = (new ComponentUrl(url)).absolute
90
- triggerEvent('page:before-full-refresh', url: url)
91
- activeDocument.location.href = url
92
- return
93
-
94
- @pushState: (state, title, url) ->
95
- window.history.pushState(state, title, url)
96
-
97
- @replaceState: (state, title, url) ->
98
- window.history.replaceState(state, title, url)
99
-
100
- @document: (documentToUse) ->
101
- activeDocument = documentToUse if documentToUse
102
- activeDocument
103
-
104
- fetchReplacement = (url, options) ->
105
- triggerEvent 'page:fetch', url: url.absolute
106
-
107
- if xhr?
108
- # Workaround for sinon xhr.abort()
109
- # https://github.com/sinonjs/sinon/issues/432#issuecomment-216917023
110
- xhr.readyState = 0
111
- xhr.statusText = "abort"
112
- xhr.abort()
113
-
114
- xhr = new XMLHttpRequest
115
-
116
- xhr.open 'GET', url.withoutHashForIE10compatibility(), true
117
- xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
118
- xhr.setRequestHeader 'X-XHR-Referer', referer
119
- options.headers ?= {}
120
-
121
- for k,v of options.headers
122
- xhr.setRequestHeader k, v
123
-
124
- xhr.onload = ->
125
- if xhr.status >= 500
126
- Turbolinks.fullPageNavigate(url)
127
- else
128
- Turbolinks.loadPage(url, xhr, options)
129
- xhr = null
130
-
131
- xhr.onerror = ->
132
- # Workaround for sinon xhr.abort()
133
- if xhr.statusText == "abort"
134
- xhr = null
135
- return
136
- Turbolinks.fullPageNavigate(url)
137
-
138
- xhr.send()
139
-
140
- return
141
-
142
- @loadPage: (url, xhr, options = {}) ->
143
- triggerEvent 'page:receive'
144
- response = new Response(xhr, url)
145
- options.updatePushState ?= true
146
- options.partialReplace = isPartialReplace(response, options)
147
-
148
- unless upstreamDocument = response.document()
149
- triggerEvent 'page:error', xhr
150
- Turbolinks.fullPageNavigate(response.finalURL)
151
- return
152
-
153
- if options.partialReplace
154
- updateBody(upstreamDocument, response, options)
155
- return
156
-
157
- turbohead = new TurboHead(activeDocument, upstreamDocument)
158
- if turbohead.hasAssetConflicts()
159
- return Turbolinks.fullPageNavigate(response.finalURL)
160
-
161
- turbohead.waitForAssets().then((result) ->
162
- updateBody(upstreamDocument, response, options) unless result?.isCanceled
163
- )
164
-
165
- updateBody = (upstreamDocument, response, options) ->
166
- nodes = changePage(
167
- upstreamDocument.querySelector('title')?.textContent,
168
- removeNoscriptTags(upstreamDocument.querySelector('body')),
169
- CSRFToken.get(upstreamDocument).token,
170
- 'runScripts',
171
- options
172
- )
173
- reflectNewUrl(response.finalURL) if options.updatePushState
174
-
175
- Turbolinks.resetScrollPosition() unless options.partialReplace
176
-
177
- options.callback?()
178
- triggerEvent 'page:load', nodes
179
-
180
- changePage = (title, body, csrfToken, runScripts, options = {}) ->
181
- activeDocument.title = title if title
182
-
183
- if options.onlyKeys?.length
184
- nodesToRefresh = [].concat(getNodesWithRefreshAlways(), getNodesMatchingRefreshKeys(options.onlyKeys))
185
- nodes = refreshNodes(nodesToRefresh, body)
186
- setAutofocusElement() if anyAutofocusElement(nodes)
187
- return nodes
188
- else
189
- refreshNodes(getNodesWithRefreshAlways(), body)
190
- persistStaticElements(body)
191
- if options.exceptKeys?.length
192
- refreshAllExceptWithKeys(options.exceptKeys, body)
193
- else
194
- deleteRefreshNeverNodes(body)
195
-
196
- triggerEvent 'page:before-replace'
197
- replaceNode(body, activeDocument.body)
198
- CSRFToken.update csrfToken if csrfToken?
199
- setAutofocusElement()
200
- executeScriptTags() if runScripts
201
- currentState = window.history.state
202
- triggerEvent 'page:change'
203
- triggerEvent 'page:update'
204
-
205
- return
206
-
207
- getNodesMatchingRefreshKeys = (keys) ->
208
- matchingNodes = []
209
- for key in keys
210
- for node in TurboGraft.querySelectorAllTGAttribute(activeDocument, 'refresh', key)
211
- matchingNodes.push(node)
212
-
213
- return matchingNodes
214
-
215
- getNodesWithRefreshAlways = ->
216
- matchingNodes = []
217
- for node in TurboGraft.querySelectorAllTGAttribute(activeDocument, 'refresh-always')
218
- matchingNodes.push(node)
219
-
220
- return matchingNodes
221
-
222
- anyAutofocusElement = (nodes) ->
223
- for node in nodes
224
- if node.querySelectorAll('input[autofocus], textarea[autofocus]').length > 0
225
- return true
226
-
227
- false
228
-
229
- setAutofocusElement = ->
230
- autofocusElement = (list = activeDocument.querySelectorAll 'input[autofocus], textarea[autofocus]')[list.length - 1]
231
- if autofocusElement and activeDocument.activeElement isnt autofocusElement
232
- autofocusElement.focus()
233
-
234
- deleteRefreshNeverNodes = (body) ->
235
- for node in TurboGraft.querySelectorAllTGAttribute(body, 'refresh-never')
236
- removeNode(node)
237
-
238
- return
239
-
240
- refreshNodes = (allNodesToBeRefreshed, body) ->
241
- triggerEvent 'page:before-partial-replace', allNodesToBeRefreshed
242
-
243
- parentIsRefreshing = (node) ->
244
- for potentialParent in allNodesToBeRefreshed when node != potentialParent
245
- return true if potentialParent.contains(node)
246
- false
247
-
248
- refreshedNodes = []
249
- for existingNode in allNodesToBeRefreshed
250
- continue if parentIsRefreshing(existingNode)
251
-
252
- unless nodeId = existingNode.getAttribute('id')
253
- throw new Error "Turbolinks refresh: Refresh key elements must have an id."
254
-
255
- if newNode = body.querySelector("##{ nodeId }")
256
- newNode = newNode.cloneNode(true)
257
- replaceNode(newNode, existingNode)
258
-
259
- if newNode.nodeName == 'SCRIPT' && newNode.dataset.turbolinksEval != "false"
260
- executeScriptTag(newNode)
261
- else
262
- refreshedNodes.push(newNode)
263
-
264
- else if !TurboGraft.hasTGAttribute(existingNode, "refresh-always")
265
- removeNode(existingNode)
266
-
267
- refreshedNodes
268
-
269
- keepNodes = (body, allNodesToKeep) ->
270
- for existingNode in allNodesToKeep
271
- unless nodeId = existingNode.getAttribute('id')
272
- throw new Error("TurboGraft refresh: Kept nodes must have an id.")
273
-
274
- if remoteNode = body.querySelector("##{ nodeId }")
275
- replaceNode(existingNode, remoteNode)
276
-
277
- persistStaticElements = (body) ->
278
- allNodesToKeep = []
279
-
280
- nodes = TurboGraft.querySelectorAllTGAttribute(activeDocument, 'tg-static')
281
- allNodesToKeep.push(node) for node in nodes
282
-
283
- keepNodes(body, allNodesToKeep)
284
- return
285
-
286
- refreshAllExceptWithKeys = (keys, body) ->
287
- allNodesToKeep = []
288
-
289
- for key in keys
290
- for node in TurboGraft.querySelectorAllTGAttribute(activeDocument, 'refresh', key)
291
- allNodesToKeep.push(node)
292
-
293
- keepNodes(body, allNodesToKeep)
294
- return
295
-
296
- executeScriptTags = ->
297
- scripts = Array::slice.call activeDocument.body.querySelectorAll 'script:not([data-turbolinks-eval="false"])'
298
- for script in scripts when script.type in ['', 'text/javascript']
299
- executeScriptTag(script)
300
- return
301
-
302
- executeScriptTag = (script) ->
303
- copy = activeDocument.createElement 'script'
304
- copy.setAttribute attr.name, attr.value for attr in script.attributes
305
- copy.appendChild activeDocument.createTextNode script.innerHTML
306
- { parentNode, nextSibling } = script
307
- parentNode.removeChild script
308
- parentNode.insertBefore copy, nextSibling
309
- return
310
-
311
- removeNoscriptTags = (node) ->
312
- node.innerHTML = node.innerHTML.replace /<noscript[\S\s]*?<\/noscript>/ig, ''
313
- node
314
-
315
- reflectNewUrl = (url) ->
316
- if (url = new ComponentUrl url).absolute isnt referer
317
- Turbolinks.pushState { turbolinks: true, url: url.absolute }, '', url.absolute
318
- return
319
-
320
- rememberReferer = ->
321
- referer = activeDocument.location.href
322
-
323
- @rememberCurrentUrl: ->
324
- Turbolinks.replaceState { turbolinks: true, url: activeDocument.location.href }, '', activeDocument.location.href
325
-
326
- @rememberCurrentState: ->
327
- currentState = window.history.state
328
-
329
- recallScrollPosition = (page) ->
330
- window.scrollTo page.positionX, page.positionY
331
-
332
- @resetScrollPosition: ->
333
- if activeDocument.location.hash
334
- activeDocument.location.href = activeDocument.location.href
335
- else
336
- window.scrollTo 0, 0
337
-
338
- pageChangePrevented = (url) ->
339
- !triggerEvent('page:before-change', url)
340
-
341
- installHistoryChangeHandler = (event) ->
342
- if event.state?.turbolinks
343
- Turbolinks.visit event.target.location.href
344
-
345
- # Delay execution of function long enough to miss the popstate event
346
- # some browsers fire on the initial page load.
347
- bypassOnLoadPopstate = (fn) ->
348
- setTimeout fn, 500
349
-
350
- if browserSupportsTurbolinks
351
- @visit = fetch
352
- @rememberCurrentUrl()
353
- @rememberCurrentState()
354
-
355
- activeDocument.addEventListener 'click', Click.installHandlerLast, true
356
-
357
- bypassOnLoadPopstate ->
358
- window.addEventListener 'popstate', installHistoryChangeHandler, false
359
-
360
- else
361
- @visit = (url) -> activeDocument.location.href = url
@@ -1,30 +0,0 @@
1
- #= require_self
2
- #= require_tree ./turbograft
3
-
4
- window.TurboGraft ?= { handlers: {} }
5
-
6
- TurboGraft.tgAttribute = (attr) ->
7
- tgAttr = if attr[0...3] == 'tg-'
8
- "data-#{attr}"
9
- else
10
- "data-tg-#{attr}"
11
-
12
- TurboGraft.getTGAttribute = (node, attr) ->
13
- tgAttr = TurboGraft.tgAttribute(attr)
14
- node.getAttribute(tgAttr) || node.getAttribute(attr)
15
-
16
- TurboGraft.removeTGAttribute = (node, attr) ->
17
- tgAttr = TurboGraft.tgAttribute(attr)
18
- node.removeAttribute(tgAttr)
19
- node.removeAttribute(attr)
20
-
21
- TurboGraft.hasTGAttribute = (node, attr) ->
22
- tgAttr = TurboGraft.tgAttribute(attr)
23
- node.hasAttribute(tgAttr) || node.hasAttribute(attr)
24
-
25
- TurboGraft.querySelectorAllTGAttribute = (node, attr, value = null) ->
26
- tgAttr = TurboGraft.tgAttribute(attr)
27
- if value
28
- node.querySelectorAll("[#{tgAttr}=#{value}], [#{attr}=#{value}]")
29
- else
30
- node.querySelectorAll("[#{tgAttr}], [#{attr}]")