turbograft 0.0.5 → 0.0.6

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: 787fc4754c128412c6e357c972d57e2fa7296626
4
- data.tar.gz: 4003be73065634f0f6ac7a7bc3b7e6dd5c66d658
3
+ metadata.gz: 0c249b4ccd0c6e283c8a5bf909ebdec4541b4f4d
4
+ data.tar.gz: 16169d007d25ecc6ac97c05fb604047440a4ec78
5
5
  SHA512:
6
- metadata.gz: 9ac920f9f81d37e2b272446abe3fcf4f9423ef69dd6de0334d1cf382795359a878cfcf3096046139c00414e0782c220746946e1c79317f221506b63e75778011
7
- data.tar.gz: 5f11c59040c76045370b02fa43c23c9b720d4a166554c0cda354334ae80df6237637ebafc936d04b2b5d1ffc009d8aee6af4d83a162f9a236e62372cff5fc243
6
+ metadata.gz: 1a843ee131ba65714567e500cba001ec560ac8459316c8f146bb30de45145eb9831a1f5ac69a102a573b667ddff50ead11e2b513b300ddc0487e977888418483
7
+ data.tar.gz: 40ffcb9804a953630b2f58a4f7d909ba3616abdfcf391e3f742ff0bd181dc0e0c9e20e709732a5078b418e3f870ab6b14a7c6fe576884966a276e1c014e8b4a5
@@ -0,0 +1,37 @@
1
+ # The Click class handles clicked links, verifying if Turbolinks should
2
+ # take control by inspecting both the event and the link. If it should,
3
+ # the page change process is initiated. If not, control is passed back
4
+ # to the browser for default functionality.
5
+ class window.Click
6
+ @installHandlerLast: (event) ->
7
+ unless event.defaultPrevented
8
+ document.removeEventListener 'click', Click.handle, false
9
+ document.addEventListener 'click', Click.handle, false
10
+
11
+ @handle: (event) ->
12
+ new Click event
13
+
14
+ constructor: (@event) ->
15
+ return if @event.defaultPrevented
16
+ @_extractLink()
17
+ if @_validForTurbolinks()
18
+ Turbolinks.visit @link.href unless @_pageChangePrevented()
19
+ @event.preventDefault()
20
+
21
+ _pageChangePrevented: ->
22
+ !triggerEvent 'page:before-change' # TODO: fix this global
23
+
24
+ _extractLink: ->
25
+ link = @event.target
26
+ link = link.parentNode until !link.parentNode or link.nodeName is 'A'
27
+ @link = new Link(link) if link.nodeName is 'A' and link.href.length isnt 0
28
+
29
+ _validForTurbolinks: ->
30
+ @link? and not (@link.shouldIgnore() or @_nonStandardClick())
31
+
32
+ _nonStandardClick: ->
33
+ @event.which > 1 or
34
+ @event.metaKey or
35
+ @event.ctrlKey or
36
+ @event.shiftKey or
37
+ @event.altKey
@@ -0,0 +1,24 @@
1
+ # The ComponentUrl class converts a basic URL string into an object
2
+ # that behaves similarly to document.location.
3
+ #
4
+ # If an instance is created from a relative URL, the current document
5
+ # is used to fill in the missing attributes (protocol, host, port).
6
+ class window.ComponentUrl
7
+ constructor: (@original = document.location.href) ->
8
+ return @original if @original.constructor is ComponentUrl
9
+ @_parse()
10
+
11
+ withoutHash: -> @href.replace @hash, ''
12
+
13
+ # Intention revealing function alias
14
+ withoutHashForIE10compatibility: -> @withoutHash()
15
+
16
+ hasNoHash: -> @hash.length is 0
17
+
18
+ _parse: ->
19
+ (@link ?= document.createElement 'a').href = @original
20
+ { @href, @protocol, @host, @hostname, @port, @pathname, @search, @hash } = @link
21
+ @origin = [@protocol, '//', @hostname].join ''
22
+ @origin += ":#{@port}" unless @port.length is 0
23
+ @relative = [@pathname, @search, @hash].join ''
24
+ @absolute = @href
@@ -0,0 +1,9 @@
1
+ class window.CSRFToken
2
+ @get: (doc = document) ->
3
+ node: tag = doc.querySelector 'meta[name="csrf-token"]'
4
+ token: tag?.getAttribute? 'content'
5
+
6
+ @update: (latest) ->
7
+ current = @get()
8
+ if current.token? and latest? and current.token isnt latest
9
+ current.node.setAttribute 'content', latest
@@ -0,0 +1,16 @@
1
+ partialGraftClickHandler = (ev) ->
2
+ target = ev.target
3
+ partialGraft = target.getAttribute("partial-graft")
4
+ return unless partialGraft?
5
+ href = target.getAttribute("href")
6
+ refresh = target.getAttribute("refresh")
7
+ throw "TurboGraft developer error: href is not defined on node #{target}" if !href?
8
+ throw "TurboGraft developer error: refresh is not defined on node #{target}" if !refresh?
9
+
10
+ keys = refresh.trim().split(" ")
11
+
12
+ Page.refresh
13
+ url: href,
14
+ onlyKeys: keys
15
+
16
+ document.addEventListener 'click', partialGraftClickHandler, true
@@ -0,0 +1,41 @@
1
+ # The Link class derives from the ComponentUrl class, but is built from an
2
+ # existing link element. Provides verification functionality for Turbolinks
3
+ # to use in determining whether it should process the link when clicked.
4
+ class window.Link extends ComponentUrl
5
+ @HTML_EXTENSIONS: ['html']
6
+
7
+ @allowExtensions: (extensions...) ->
8
+ Link.HTML_EXTENSIONS.push extension for extension in extensions
9
+ Link.HTML_EXTENSIONS
10
+
11
+ constructor: (@link) ->
12
+ return @link if @link.constructor is Link
13
+ @original = @link.href
14
+ super
15
+
16
+ shouldIgnore: ->
17
+ @_crossOrigin() or
18
+ @_anchored() or
19
+ @_nonHtml() or
20
+ @_optOut() or
21
+ @_target()
22
+
23
+ _crossOrigin: ->
24
+ @origin isnt (new ComponentUrl).origin
25
+
26
+ _anchored: ->
27
+ ((@hash and @withoutHash()) is (current = new ComponentUrl).withoutHash()) or
28
+ (@href is current.href + '#')
29
+
30
+ _nonHtml: ->
31
+ @pathname.match(/\.[a-z]+$/g) and not @pathname.match(new RegExp("\\.(?:#{Link.HTML_EXTENSIONS.join('|')})?$", 'g'))
32
+
33
+ _optOut: ->
34
+ link = @link
35
+ until ignore or link is document or link is null
36
+ ignore = link.getAttribute('data-no-turbolink')?
37
+ link = link.parentNode
38
+ ignore
39
+
40
+ _target: ->
41
+ @link.target.length isnt 0
@@ -0,0 +1,25 @@
1
+ window.Page = {} if !window.Page
2
+
3
+ Page.visit = (url, opts={}) ->
4
+ if opts.reload
5
+ window.location = url
6
+ else
7
+ Turbolinks.visit(url)
8
+
9
+ Page.refresh = (options = {}, callback) ->
10
+ newUrl = if options.url
11
+ options.url
12
+ else if options.queryParams
13
+ paramString = $.param(options.queryParams)
14
+ paramString = "?#{paramString}" if paramString
15
+ location.pathname + paramString
16
+ else
17
+ location.href
18
+
19
+ if options.response
20
+ Turbolinks.loadPage null, options.response, true, callback, options.onlyKeys || []
21
+ else
22
+ Turbolinks.visit newUrl, true, options.onlyKeys || [], -> callback?()
23
+
24
+ Page.open = ->
25
+ window.open(arguments...)
@@ -0,0 +1,42 @@
1
+ class window.PageCache
2
+ storage = {}
3
+ simultaneousAdditionOffset = 0
4
+ constructor: (@cacheSize = 10) ->
5
+ storage = {}
6
+ return this
7
+
8
+ get: (key) ->
9
+ storage[key]
10
+
11
+ set: (key, value) ->
12
+ if typeof value != "object"
13
+ throw "Developer error: You must store objects in this cache"
14
+
15
+ value['cachedAt'] = new Date().getTime() + (simultaneousAdditionOffset+=1)
16
+
17
+ storage[key] = value
18
+ @constrain()
19
+
20
+ clear: ->
21
+ storage = {}
22
+
23
+ setCacheSize: (newSize) ->
24
+ if /^[\d]+$/.test(newSize)
25
+ @cacheSize = parseInt(newSize, 10)
26
+ @constrain()
27
+ else
28
+ throw "Developer error: Invalid parameter '#{newSize}' for PageCache; must be integer"
29
+
30
+ constrain: ->
31
+ pageCacheKeys = Object.keys storage
32
+
33
+ cacheTimesRecentFirst = pageCacheKeys.map (url) =>
34
+ storage[url].cachedAt
35
+ .sort (a, b) -> b - a
36
+
37
+ for key in pageCacheKeys when storage[key].cachedAt <= cacheTimesRecentFirst[@cacheSize]
38
+ triggerEvent 'page:expire', storage[key] # TODO: fix this global
39
+ delete storage[key]
40
+
41
+ length: ->
42
+ Object.keys(storage).length
@@ -0,0 +1,8 @@
1
+ #= require click
2
+ #= require component_url
3
+ #= require csrf_token
4
+ #= require link
5
+ #= require page
6
+ #= require page_cache
7
+ #= require turbolinks
8
+ #= require initializers
@@ -0,0 +1,353 @@
1
+ xhr = null
2
+
3
+ installDocumentReadyPageEventTriggers = ->
4
+ document.addEventListener 'DOMContentLoaded', ( ->
5
+ triggerEvent 'page:change'
6
+ triggerEvent 'page:update'
7
+ ), true
8
+
9
+ installJqueryAjaxSuccessPageUpdateTrigger = ->
10
+ if typeof jQuery isnt 'undefined'
11
+ jQuery(document).on 'ajaxSuccess', (event, xhr, settings) ->
12
+ return unless jQuery.trim xhr.responseText
13
+ triggerEvent 'page:update'
14
+
15
+ # Handle bug in Firefox 26/27 where history.state is initially undefined
16
+ historyStateIsDefined =
17
+ window.history.state != undefined or navigator.userAgent.match /Firefox\/2[6|7]/
18
+
19
+ browserSupportsPushState =
20
+ window.history and window.history.pushState and window.history.replaceState and historyStateIsDefined
21
+
22
+ browserIsntBuggy =
23
+ !navigator.userAgent.match /CriOS\//
24
+
25
+ window.triggerEvent = (name, data) ->
26
+ event = document.createEvent 'Events'
27
+ event.data = data if data
28
+ event.initEvent name, true, true
29
+ document.dispatchEvent event
30
+
31
+ popCookie = (name) ->
32
+ value = document.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or ''
33
+ document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
34
+ value
35
+
36
+ requestMethodIsSafe =
37
+ popCookie('request_method') in ['GET','']
38
+
39
+ browserSupportsTurbolinks = browserSupportsPushState and browserIsntBuggy and requestMethodIsSafe
40
+
41
+ browserSupportsCustomEvents =
42
+ document.addEventListener and document.createEvent
43
+
44
+ if browserSupportsCustomEvents
45
+ installDocumentReadyPageEventTriggers()
46
+ installJqueryAjaxSuccessPageUpdateTrigger()
47
+
48
+ # TODO: triggerEvent should be accessible to all these guys
49
+ # on some kind of eventbus
50
+ # TODO: clean up everything above me ^
51
+ # TODO: decide on the public API
52
+ class window.Turbolinks
53
+ createDocument = null
54
+ currentState = null
55
+ loadedAssets = null
56
+ referer = null
57
+ usePageCache = false
58
+ @pageCache = pageCache = new PageCache()
59
+
60
+ fetch = (url, partialReplace = false, replaceContents = [], callback) ->
61
+ url = new ComponentUrl url
62
+
63
+ rememberReferer()
64
+ Turbolinks.cacheCurrentPage() if usePageCache
65
+
66
+ if usePageCache and cachedPage = transitionCacheFor(url.absolute)
67
+ fetchHistory cachedPage
68
+ fetchReplacement url, partialReplace, null, replaceContents
69
+ else
70
+ fetchReplacement url, partialReplace, ->
71
+ resetScrollPosition() unless replaceContents.length
72
+ callback?()
73
+ , replaceContents
74
+
75
+ @pageCacheEnabled = ->
76
+ usePageCache
77
+
78
+ @usePageCache = (status) ->
79
+ usePageCache = status
80
+
81
+ transitionCacheFor = (url) ->
82
+ cachedPage = pageCache.get(url)
83
+ cachedPage if cachedPage and !cachedPage.transitionCacheDisabled
84
+
85
+ @pushState: (state, title, url) ->
86
+ window.history.pushState(state, title, url)
87
+
88
+ @replaceState: (state, title, url) ->
89
+ window.history.replaceState(state, title, url)
90
+
91
+ fetchReplacement = (url, partialReplace, onLoadFunction, replaceContents) ->
92
+ triggerEvent 'page:fetch', url: url.absolute
93
+
94
+ xhr?.abort()
95
+ xhr = new XMLHttpRequest
96
+ xhr.open 'GET', url.withoutHashForIE10compatibility(), true
97
+ xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
98
+ xhr.setRequestHeader 'X-XHR-Referer', referer
99
+
100
+ xhr.onload = ->
101
+ if xhr.status >= 500
102
+ document.location.href = url.absolute
103
+ else
104
+ Turbolinks.loadPage(url, xhr, partialReplace, onLoadFunction, replaceContents)
105
+
106
+ xhr.onloadend = -> xhr = null
107
+ xhr.onerror = ->
108
+ document.location.href = url.absolute
109
+
110
+ xhr.send()
111
+
112
+ return
113
+
114
+ @loadPage: (url, xhr, partialReplace = false, onLoadFunction = (->), replaceContents = []) ->
115
+ triggerEvent 'page:receive'
116
+
117
+ if doc = processResponse(xhr, partialReplace)
118
+ reflectNewUrl url
119
+ nodes = changePage(extractTitleAndBody(doc)..., partialReplace, replaceContents)
120
+ reflectRedirectedUrl(xhr)
121
+ triggerEvent 'page:load', nodes
122
+ onLoadFunction?()
123
+ else
124
+ document.location.href = url.absolute
125
+
126
+ return
127
+
128
+ fetchHistory = (cachedPage) ->
129
+ xhr?.abort()
130
+ changePage cachedPage.title, cachedPage.body, false
131
+ recallScrollPosition cachedPage
132
+ triggerEvent 'page:restore'
133
+
134
+
135
+ @cacheCurrentPage: ->
136
+ currentStateUrl = new ComponentUrl currentState.url
137
+
138
+ pageCache.set currentStateUrl.absolute,
139
+ url: currentStateUrl.relative,
140
+ body: document.body,
141
+ title: document.title,
142
+ positionY: window.pageYOffset,
143
+ positionX: window.pageXOffset,
144
+ transitionCacheDisabled: document.querySelector('[data-no-transition-cache]')?
145
+
146
+ return
147
+
148
+ changePage = (title, body, csrfToken, runScripts, partialReplace, replaceContents = []) ->
149
+ document.title = title if title
150
+ if replaceContents.length
151
+ return refreshNodesWithKeys(replaceContents, body)
152
+ else
153
+ deleteRefreshNeverNodes(body)
154
+
155
+ triggerEvent 'page:before-replace'
156
+ document.documentElement.replaceChild body, document.body
157
+ CSRFToken.update csrfToken if csrfToken?
158
+ executeScriptTags() if runScripts
159
+ currentState = window.history.state
160
+ triggerEvent 'page:change'
161
+ triggerEvent 'page:update'
162
+
163
+ return
164
+
165
+ deleteRefreshNeverNodes = (body) ->
166
+ for node in body.querySelectorAll('[refresh-never]')
167
+ node.parentNode.removeChild(node)
168
+
169
+ return
170
+
171
+ refreshNodesWithKeys = (keys, body) ->
172
+ allNodesToBeRefreshed = []
173
+ for node in document.querySelectorAll("[refresh-always]")
174
+ allNodesToBeRefreshed.push(node)
175
+
176
+ for key in keys
177
+ for node in document.querySelectorAll("[refresh=#{key}]")
178
+ allNodesToBeRefreshed.push(node)
179
+
180
+ triggerEvent 'page:before-partial-replace', allNodesToBeRefreshed
181
+
182
+ parentIsRefreshing = (node) ->
183
+ for potentialParent in allNodesToBeRefreshed when node != potentialParent
184
+ return true if potentialParent.contains(node)
185
+ false
186
+
187
+ refreshedNodes = []
188
+ for existingNode in allNodesToBeRefreshed
189
+ continue if parentIsRefreshing(existingNode)
190
+
191
+ unless nodeId = existingNode.getAttribute('id')
192
+ throw new Error "Turbolinks refresh: Refresh key elements must have an id."
193
+
194
+ if newNode = body.querySelector("##{ nodeId }")
195
+ existingNode.parentNode.replaceChild(newNode, existingNode)
196
+
197
+ if newNode.nodeName == 'SCRIPT' && newNode.getAttribute("data-turbolinks-eval") != "false"
198
+ executeScriptTag(newNode)
199
+ else
200
+ refreshedNodes.push(newNode)
201
+
202
+ else if existingNode.getAttribute("refresh-always") == null
203
+ existingNode.parentNode.removeChild(existingNode)
204
+
205
+ refreshedNodes
206
+
207
+ executeScriptTags = ->
208
+ scripts = Array::slice.call document.body.querySelectorAll 'script:not([data-turbolinks-eval="false"])'
209
+ for script in scripts when script.type in ['', 'text/javascript']
210
+ executeScriptTag(script)
211
+ return
212
+
213
+ executeScriptTag = (script) ->
214
+ copy = document.createElement 'script'
215
+ copy.setAttribute attr.name, attr.value for attr in script.attributes
216
+ copy.appendChild document.createTextNode script.innerHTML
217
+ { parentNode, nextSibling } = script
218
+ parentNode.removeChild script
219
+ parentNode.insertBefore copy, nextSibling
220
+ return
221
+
222
+ removeNoscriptTags = (node) ->
223
+ node.innerHTML = node.innerHTML.replace /<noscript[\S\s]*?<\/noscript>/ig, ''
224
+ node
225
+
226
+ reflectNewUrl = (url) ->
227
+ if (url = new ComponentUrl url).absolute isnt referer
228
+ Turbolinks.pushState { turbolinks: true, url: url.absolute }, '', url.absolute
229
+ return
230
+
231
+ reflectRedirectedUrl = (xhr) ->
232
+ if location = xhr.getResponseHeader 'X-XHR-Redirected-To'
233
+ location = new ComponentUrl location
234
+ preservedHash = if location.hasNoHash() then document.location.hash else ''
235
+ Turbolinks.replaceState currentState, '', location.href + preservedHash
236
+ return
237
+
238
+ rememberReferer = ->
239
+ referer = document.location.href
240
+
241
+ @rememberCurrentUrl: ->
242
+ Turbolinks.replaceState { turbolinks: true, url: document.location.href }, '', document.location.href
243
+
244
+ @rememberCurrentState: ->
245
+ currentState = window.history.state
246
+
247
+ recallScrollPosition = (page) ->
248
+ window.scrollTo page.positionX, page.positionY
249
+
250
+ resetScrollPosition = ->
251
+ if document.location.hash
252
+ document.location.href = document.location.href
253
+ else
254
+ window.scrollTo 0, 0
255
+
256
+ pageChangePrevented = ->
257
+ !triggerEvent 'page:before-change'
258
+
259
+ processResponse = (xhr, partial = false) ->
260
+ clientOrServerError = ->
261
+ return false if xhr.status == 422 # we want to render form validations
262
+ 400 <= xhr.status < 600
263
+
264
+ validContent = ->
265
+ xhr.getResponseHeader('Content-Type').match /^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/
266
+
267
+ extractTrackAssets = (doc) ->
268
+ for node in doc.head.childNodes when node.getAttribute?('data-turbolinks-track')?
269
+ node.getAttribute('src') or node.getAttribute('href')
270
+
271
+ assetsChanged = (doc) ->
272
+ loadedAssets ||= extractTrackAssets document
273
+ fetchedAssets = extractTrackAssets doc
274
+ fetchedAssets.length isnt loadedAssets.length or intersection(fetchedAssets, loadedAssets).length isnt loadedAssets.length
275
+
276
+ intersection = (a, b) ->
277
+ [a, b] = [b, a] if a.length > b.length
278
+ value for value in a when value in b
279
+
280
+ if !clientOrServerError() && validContent()
281
+ doc = createDocument xhr.responseText
282
+ changed = assetsChanged(doc)
283
+
284
+ if doc && (!changed || partial)
285
+ return doc
286
+
287
+ extractTitleAndBody = (doc) ->
288
+ title = doc.querySelector 'title'
289
+ [ title?.textContent, removeNoscriptTags(doc.body), CSRFToken.get(doc).token, 'runScripts' ]
290
+
291
+ installHistoryChangeHandler = (event) ->
292
+ if event.state?.turbolinks
293
+ if cachedPage = pageCache.get((new ComponentUrl(event.state.url)).absolute)
294
+ Turbolinks.cacheCurrentPage()
295
+ fetchHistory cachedPage
296
+ else
297
+ Turbolinks.visit event.target.location.href
298
+
299
+ # Delay execution of function long enough to miss the popstate event
300
+ # some browsers fire on the initial page load.
301
+ bypassOnLoadPopstate = (fn) ->
302
+ setTimeout fn, 500
303
+
304
+ browserCompatibleDocumentParser = ->
305
+ createDocumentUsingParser = (html) ->
306
+ (new DOMParser).parseFromString html, 'text/html'
307
+
308
+ createDocumentUsingDOM = (html) ->
309
+ doc = document.implementation.createHTMLDocument ''
310
+ doc.documentElement.innerHTML = html
311
+ doc
312
+
313
+ createDocumentUsingWrite = (html) ->
314
+ doc = document.implementation.createHTMLDocument ''
315
+ doc.open 'replace'
316
+ doc.write html
317
+ doc.close()
318
+ doc
319
+
320
+ # Use createDocumentUsingParser if DOMParser is defined and natively
321
+ # supports 'text/html' parsing (Firefox 12+, IE 10)
322
+ #
323
+ # Use createDocumentUsingDOM if createDocumentUsingParser throws an exception
324
+ # due to unsupported type 'text/html' (Firefox < 12, Opera)
325
+ #
326
+ # Use createDocumentUsingWrite if:
327
+ # - DOMParser isn't defined
328
+ # - createDocumentUsingParser returns null due to unsupported type 'text/html' (Chrome, Safari)
329
+ # - createDocumentUsingDOM doesn't create a valid HTML document (safeguarding against potential edge cases)
330
+ try
331
+ if window.DOMParser
332
+ testDoc = createDocumentUsingParser '<html><body><p>test'
333
+ createDocumentUsingParser
334
+ catch e
335
+ testDoc = createDocumentUsingDOM '<html><body><p>test'
336
+ createDocumentUsingDOM
337
+ finally
338
+ unless testDoc?.body?.childNodes.length is 1
339
+ return createDocumentUsingWrite
340
+
341
+ if browserSupportsTurbolinks
342
+ @visit = fetch
343
+ @rememberCurrentUrl()
344
+ @rememberCurrentState()
345
+ createDocument = browserCompatibleDocumentParser()
346
+
347
+ document.addEventListener 'click', Click.installHandlerLast, true
348
+
349
+ bypassOnLoadPopstate ->
350
+ window.addEventListener 'popstate', installHistoryChangeHandler, false
351
+
352
+ else
353
+ @visit = (url) -> document.location.href = url
@@ -1,3 +1,3 @@
1
1
  module TurboGraft
2
- VERSION = '0.0.5'
2
+ VERSION = '0.0.6'
3
3
  end