turbograft 0.0.5 → 0.0.6

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