breezy 0.1.3 → 0.1.4

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: 8ce62034c621228792d356fbfaa25033f2636132
4
- data.tar.gz: 6f29e954b0b7402138e5e2c0f63736f1880c3638
3
+ metadata.gz: a241ba166d21aca45e519db2413f0fc9d47c6209
4
+ data.tar.gz: 697ff9e8f58420f08dc10d01c185f200b00a6357
5
5
  SHA512:
6
- metadata.gz: a1d54e6480b866bdd8d6bd36d1b31106a78c25c892e6101f7e1aff6a9ec8c703ef40b36555fae584806e1728302e1c01df9dba518a6b39e9eaca988763af0428
7
- data.tar.gz: 9bb3e30225f2df6796e35e3e221ddf0509ea334ae98452a79115a27326e2d72d0b7ca7aa8ab72b50389c27e91343e9df2c7735faee27976ecf538d769dfd91c1
6
+ metadata.gz: ad4a653fc55253f0ae7ca1f381f3ff34db917460a62b52cbe9dfe766335003b1b978adfb041fe8ff563e82ae0288db986d8b70c61f5d9b5272c65efefcd3f0e9
7
+ data.tar.gz: 8f10b7d1ed970640d8472991697f4ceb4883c46757831fbe89db4dfe44f61a2db5b73f05bfa6534b29655aab2b7d4d2d1aaf6eff388518a305f24c24249f81f0
data/README.md CHANGED
@@ -104,7 +104,7 @@ rails g breezy:view Posts new index
104
104
  ```
105
105
 
106
106
  # Navigation and Forms
107
- Breezy intercepts all clicks on `<a>` and all submits on `<form>` elements enabled with `data-bz-remote`. Breezy will `preventDefault` then make the same request using XMLHttpRequest with a content type of `.js`. If there's an existing request, Breezy will cancel it unless the `data-bz-async` option is used.
107
+ Breezy intercepts all clicks on `<a>` and all submits on `<form>` elements enabled with `data-bz-remote`. Breezy will `preventDefault` then make the same request using XMLHttpRequest with a content type of `.js`. If there's an existing request, Breezy will cancel it unless the `data-bz-remote-async` option is used.
108
108
 
109
109
  Once the response loads, a `breezy:load` event will be fired with the JS object that you created with BreezyTemplates. If you used the installation generator, the event will be set for you in the `<head>` element of your layout:
110
110
 
@@ -123,8 +123,8 @@ document.addEventListener('breezy:load', function(event){
123
123
  Attribute | default value | description
124
124
  -------------------|--------------------------|------------
125
125
  `data-bz-remote` | For `<a>` the default is `get`. For forms, the default is `post` if a method is not specified. | Use this to create seamless page to page transitions. Works for both links and forms. You can specify the request method by giving it a value, e.g `<a href='foobar' data-bz-remote=post>`. For forms, the request method of the form is used. `<form action=foobar method='post' data-bz-remote>`.
126
- `data-bz-async` | `false` | Fires off an async request. Responses are pushed into a queue will be evaluated in order of click or submit.
127
- `data-bz-push-state` | `true` | Captures the element's URL in the browsers history. Normally used with `data-bz-async`.
126
+ `data-bz-remote-async` | `false` | Fires off an async request. Responses are pushed into a queue will be evaluated in order of click or submit.
127
+ `data-bz-push-state` | `true` | Captures the element's URL in the browsers history. Normally used with `data-bz-remote-async`.
128
128
  `data-bz-silent` | false | To be used with the `breezy_silent?` ruby helper. Useful if you don't want to perform a redirect or render. Just return a 204, and Breezy will not fire a `breezy:load` event.
129
129
 
130
130
 
File without changes
@@ -0,0 +1,61 @@
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
+ uniqueId = ->
7
+ new Date().getTime().toString(36)
8
+
9
+ class Breezy.ComponentUrl
10
+ constructor: (@original = document.location.href) ->
11
+ return @original if @original.constructor is Breezy.ComponentUrl
12
+ @_parse()
13
+
14
+ withoutHash: -> @href.replace(@hash, '').replace('#', '')
15
+
16
+ # Intention revealing function alias
17
+ withoutHashForIE10compatibility: -> @withoutHash()
18
+
19
+ hasNoHash: -> @hash.length is 0
20
+
21
+ crossOrigin: ->
22
+ @origin isnt (new Breezy.ComponentUrl).origin
23
+
24
+ formatForXHR: (options = {}) ->
25
+ (if options.cache then @withMimeBust() else @withAntiCacheParam()).withoutHashForIE10compatibility()
26
+
27
+ withMimeBust: ->
28
+ new Breezy.ComponentUrl(
29
+ if /([?&])__=[^&]*/.test @absolute
30
+ @absolute
31
+ else
32
+ new Breezy.ComponentUrl(@withoutHash() + (if /\?/.test(@absolute) then "&" else "?") + "__=0" + @hash)
33
+ )
34
+
35
+ withAntiCacheParam: ->
36
+ new Breezy.ComponentUrl(
37
+ if /([?&])_=[^&]*/.test @absolute
38
+ @absolute.replace /([?&])_=[^&]*/, "$1_=#{uniqueId()}"
39
+ else
40
+ new Breezy.ComponentUrl(@withoutHash() + (if /\?/.test(@absolute) then "&" else "?") + "_=#{uniqueId()}" + @hash)
41
+ )
42
+
43
+ _parse: ->
44
+ (@link ?= document.createElement 'a').href = @original
45
+ { @href, @protocol, @host, @hostname, @port, @pathname, @search, @hash } = @link
46
+
47
+ if @protocol == ':'
48
+ @protocol = document.location.protocol
49
+ if @protocol == ''
50
+ @protocol = document.location.protocol
51
+ if @port == ''
52
+ @port = document.location.port
53
+ if @hostname == ''
54
+ @hostname = document.location.hostname
55
+ if @pathname == ''
56
+ @pathname = '/'
57
+
58
+ @origin = [@protocol, '//', @hostname].join ''
59
+ @origin += ":#{@port}" unless @port.length is 0
60
+ @relative = [@pathname, @search, @hash].join ''
61
+ @absolute = @href
@@ -0,0 +1,197 @@
1
+ #= require breezy/doubly_linked_list
2
+ #= require breezy/snapshot
3
+ #= require breezy/progress_bar
4
+ #= require breezy/parallel_queue
5
+ #= require breezy/component_url
6
+
7
+ PAGE_CACHE_SIZE = 20
8
+
9
+ class Breezy.Controller
10
+ constructor: ->
11
+ @atomCache = {}
12
+ @history = new Breezy.Snapshot(this)
13
+ @transitionCacheEnabled = false
14
+ @requestCachingEnabled = true
15
+
16
+ @progressBar = new Breezy.ProgressBar 'html'
17
+ @pq = new Breezy.ParallelQueue
18
+ @http = null
19
+
20
+ @history.rememberCurrentUrlAndState()
21
+
22
+ currentPage: =>
23
+ @history.currentPage
24
+
25
+ request: (url, options = {}) =>
26
+ options = Breezy.Utils.reverseMerge options,
27
+ pushState: true
28
+
29
+ url = new Breezy.ComponentUrl url
30
+ return if @pageChangePrevented(url.absolute, options.target)
31
+
32
+ if url.crossOrigin()
33
+ document.location.href = url.absolute
34
+ return
35
+
36
+ @history.cacheCurrentPage()
37
+ if @progressBar? and !options.async
38
+ @progressBar?.start()
39
+ restorePoint = @history.transitionCacheFor(url.absolute)
40
+
41
+ if @transitionCacheEnabled and restorePoint and restorePoint.transition_cache
42
+ @history.reflectNewUrl(url)
43
+ @restore(restorePoint)
44
+ options.showProgressBar = false
45
+
46
+ options.cacheRequest ?= @requestCachingEnabled
47
+ options.showProgressBar ?= true
48
+
49
+ Breezy.Utils.triggerEvent Breezy.EVENTS.FETCH, url: url.absolute, options.target
50
+
51
+ if options.async
52
+ options.showProgressBar = false
53
+ req = @createRequest(url, options)
54
+ req.onError = ->
55
+ Breezy.Utils.triggerEvent Breezy.EVENTS.ERROR, null, options.target
56
+ @pq.push(req)
57
+ req.send(options.payload)
58
+ else
59
+ @pq.drain()
60
+ @http?.abort()
61
+ @http = @createRequest(url, options)
62
+ @http.send(options.payload)
63
+
64
+ enableTransitionCache: (enable = true) =>
65
+ @transitionCacheEnabled = enable
66
+
67
+ disableRequestCaching: (disable = true) =>
68
+ @requestCachingEnabled = not disable
69
+ disable
70
+
71
+ restore: (cachedPage, options = {}) =>
72
+ @http?.abort()
73
+ @history.changePage(cachedPage, options)
74
+
75
+ @progressBar?.done()
76
+ Breezy.Utils.triggerEvent Breezy.EVENTS.RESTORE
77
+ Breezy.Utils.triggerEvent Breezy.EVENTS.LOAD, cachedPage
78
+
79
+ replace: (nextPage, options = {}) =>
80
+ Breezy.Utils.withDefaults(nextPage, @history.currentBrowserState)
81
+ @history.changePage(nextPage, options)
82
+ Breezy.Utils.triggerEvent Breezy.EVENTS.LOAD, @currentPage()
83
+
84
+ crossOriginRedirect: =>
85
+ redirect = @http.getResponseHeader('Location')
86
+ crossOrigin = (new Breezy.ComponentUrl(redirect)).crossOrigin()
87
+
88
+ if redirect? and crossOrigin
89
+ redirect
90
+
91
+ pageChangePrevented: (url, target) =>
92
+ !Breezy.Utils.triggerEvent Breezy.EVENTS.BEFORE_CHANGE, url: url, target
93
+
94
+ cache: (key, value) =>
95
+ return @atomCache[key] if value == null
96
+ @atomCache[key] ||= value
97
+
98
+ # Events
99
+ onLoadEnd: => @http = null
100
+
101
+ onLoad: (xhr, url, options) =>
102
+ Breezy.Utils.triggerEvent Breezy.EVENTS.RECEIVE, url: url.absolute, options.target
103
+ nextPage = @processResponse(xhr)
104
+ if xhr.status == 0
105
+ return
106
+
107
+ if nextPage
108
+ if options.async && url.pathname != @currentPage().pathname
109
+
110
+ unless options.ignoreSamePathConstraint
111
+ @progressBar?.done()
112
+ Breezy.Utils.warn("Async response path is different from current page path")
113
+ return
114
+
115
+ if options.pushState
116
+ @history.reflectNewUrl url
117
+
118
+ Breezy.Utils.withDefaults(nextPage, @history.currentBrowserState)
119
+
120
+ if nextPage.action != 'graft'
121
+ @history.changePage(nextPage, options)
122
+ Breezy.Utils.triggerEvent Breezy.EVENTS.LOAD, @currentPage()
123
+ else
124
+ ##clean this up
125
+ @history.graftByKeypath("data.#{nextPage.path}", nextPage.data)
126
+
127
+ if options.showProgressBar
128
+ @progressBar?.done()
129
+ @history.constrainPageCacheTo()
130
+ else
131
+ if options.async
132
+ Breezy.Utils.triggerEvent Breezy.EVENTS.ERROR, xhr, options.target
133
+ else
134
+ @progressBar?.done()
135
+ document.location.href = @crossOriginRedirect() or url.absolute
136
+
137
+ onProgress: (event) =>
138
+ @progressBar.advanceFromEvent(event)
139
+
140
+ onError: (url) =>
141
+ document.location.href = url.absolute
142
+
143
+ createRequest: (url, opts)=>
144
+ jsAccept = 'text/javascript, application/x-javascript, application/javascript'
145
+ requestMethod = opts.requestMethod || 'GET'
146
+
147
+ xhr = new XMLHttpRequest
148
+ xhr.open requestMethod, url.formatForXHR(cache: opts.cacheRequest), true
149
+ xhr.setRequestHeader 'Accept', jsAccept
150
+ xhr.setRequestHeader 'X-XHR-Referer', document.location.href
151
+ xhr.setRequestHeader 'X-Silent', opts.silent if opts.silent
152
+ xhr.setRequestHeader 'X-Requested-With', 'XMLHttpRequest'
153
+ xhr.setRequestHeader 'Content-Type', opts.contentType if opts.contentType
154
+
155
+ csrfToken = Breezy.CSRFToken.get().token
156
+ xhr.setRequestHeader('X-CSRF-Token', csrfToken) if csrfToken
157
+
158
+ if !opts.silent
159
+ xhr.onload = =>
160
+ self = ` this `
161
+ redirectedUrl = self.getResponseHeader 'X-XHR-Redirected-To'
162
+ actualUrl = redirectedUrl || url
163
+ @onLoad(self, actualUrl, opts)
164
+ else
165
+ xhr.onload = =>
166
+ @progressBar?.done()
167
+
168
+ xhr.onprogress = @onProgress if @progressBar and opts.showProgressBar
169
+ xhr.onloadend = @onLoadEnd
170
+ xhr.onerror = =>
171
+ @onError(url)
172
+ xhr
173
+
174
+ processResponse: (xhr) ->
175
+ if @hasValidResponse(xhr)
176
+ return @responseContent(xhr)
177
+
178
+ hasValidResponse: (xhr) ->
179
+ not @clientOrServerError(xhr) and @validContent(xhr) and not @downloadingFile(xhr)
180
+
181
+ responseContent: (xhr) ->
182
+ new Function("'use strict'; return " + xhr.responseText )()
183
+
184
+ clientOrServerError: (xhr) ->
185
+ 400 <= xhr.status < 600
186
+
187
+ validContent: (xhr) ->
188
+ contentType = xhr.getResponseHeader('Content-Type')
189
+ jsContent = /^(?:text\/javascript|application\/x-javascript|application\/javascript)(?:;|$)/
190
+
191
+ contentType? and contentType.match jsContent
192
+
193
+ downloadingFile: (xhr) ->
194
+ (disposition = xhr.getResponseHeader('Content-Disposition'))? and
195
+ disposition.match /^attachment/
196
+
197
+
@@ -0,0 +1,9 @@
1
+ class Breezy.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,57 @@
1
+ class Breezy.DoublyLinkedList
2
+ constructor: ->
3
+ @head = @tail = null
4
+ @length = 0
5
+
6
+ createNode: (obj) ->
7
+ return {prev: null, element: obj, next: null}
8
+
9
+ push: (obj) ->
10
+ if (@tail)
11
+ ele = @createNode(obj)
12
+ ele.prev = @tail
13
+ @tail = @tail.next = ele
14
+ @length += 1
15
+ else
16
+ @head = @tail = @createNode(obj)
17
+ @length += 1
18
+
19
+ pop: ()->
20
+ if (@tail)
21
+ element = @tail
22
+ @tail = element.prev
23
+ element.prev = null
24
+ if (@tail)
25
+ @tail.next = null
26
+ if (@head == element)
27
+ @head = null
28
+ @length -= 1
29
+
30
+ element.element
31
+ else
32
+ null
33
+
34
+ shift: ()->
35
+ if (@head)
36
+ element = @head
37
+ @head = element.next
38
+ element.next = null
39
+ if (@head)
40
+ @head.prev = null
41
+ if (@tail == element)
42
+ @tail = null
43
+ @length -= 1
44
+
45
+ element.element
46
+ else
47
+ null
48
+
49
+ unshift: (obj)->
50
+ if (@head)
51
+ ele = @createNode(obj)
52
+ ele.next = @head
53
+ @head = @head.prev = ele
54
+ @length += 1
55
+ else
56
+ @head = @tail = @createNode(obj)
57
+ @length += 1
@@ -0,0 +1,35 @@
1
+ #= require breezy/doubly_linked_list
2
+
3
+ class Breezy.ParallelQueue
4
+ constructor: ->
5
+ @dll = new Breezy.DoublyLinkedList
6
+ @active = true
7
+
8
+ push:(xhr)->
9
+ @dll.push(xhr)
10
+ xhr._originalOnLoad = xhr.onload.bind(xhr)
11
+
12
+ xhr.onload = =>
13
+ if @active
14
+ xhr._isDone = true
15
+ node = @dll.head
16
+ while(node)
17
+ qxhr = node.element
18
+ if !qxhr._isDone
19
+ node = null
20
+ else
21
+ node = node.next
22
+ @dll.shift()
23
+ qxhr._originalOnLoad()
24
+
25
+ drain: ->
26
+ @active = false
27
+ node = @dll.head
28
+ while(node)
29
+ qxhr = node.element
30
+ qxhr.abort()
31
+ qxhr._isDone = true
32
+ node = node.next
33
+ @dll = new Breezy.DoublyLinkedList
34
+ @active = true
35
+
@@ -0,0 +1,139 @@
1
+ class Breezy.ProgressBar
2
+ className = 'breezy-progress-bar'
3
+ # Setting the opacity to a value < 1 fixes a display issue in Safari 6 and
4
+ # iOS 6 where the progress bar would fill the entire page.
5
+ originalOpacity = 0.99
6
+
7
+ constructor: (@elementSelector) ->
8
+ @value = 0
9
+ @content = ''
10
+ @speed = 300
11
+ @opacity = originalOpacity
12
+ @delay = 400
13
+ @active = null
14
+ @install()
15
+
16
+ install: ->
17
+ if @active
18
+ return
19
+
20
+ @active = true
21
+ @element = document.querySelector(@elementSelector)
22
+ @element.classList.add(className)
23
+ @styleElement = document.createElement('style')
24
+ document.head.appendChild(@styleElement)
25
+ @_updateStyle()
26
+
27
+ uninstall: ->
28
+ if !@active
29
+ return
30
+
31
+ @active = false
32
+ @element.classList.remove(className)
33
+ document.head.removeChild(@styleElement)
34
+
35
+ start: ({delay} = {})->
36
+ clearTimeout(@displayTimeout)
37
+ if @delay
38
+ @display = false
39
+ @displayTimeout = setTimeout =>
40
+ @display = true
41
+ , @delay
42
+ else
43
+ @display = true
44
+
45
+ if @value > 0
46
+ @_reset()
47
+ @_reflow()
48
+
49
+ @advanceTo(5)
50
+
51
+ advanceTo: (value) ->
52
+ if value > @value <= 100
53
+ @value = value
54
+ @_updateStyle()
55
+
56
+ if @value is 100
57
+ @_stopTrickle()
58
+ else if @value > 0
59
+ @_startTrickle()
60
+
61
+ advanceFromEvent: (event) =>
62
+ percent = if event.lengthComputable
63
+ event.loaded / event.total * 100
64
+ else
65
+ @value + (100 - @value) / 10
66
+ @advanceTo(percent)
67
+
68
+ done: ->
69
+ if @value > 0
70
+ @advanceTo(100)
71
+ @_finish()
72
+
73
+ setDelay: (milliseconds) =>
74
+ @delay = milliseconds
75
+
76
+ _finish: ->
77
+ @fadeTimer = setTimeout =>
78
+ @opacity = 0
79
+ @_updateStyle()
80
+ , @speed / 2
81
+
82
+ @resetTimer = setTimeout(@_reset, @speed)
83
+
84
+ _reflow: ->
85
+ @element.offsetHeight
86
+
87
+ _reset: =>
88
+ @_stopTimers()
89
+ @value = 0
90
+ @opacity = originalOpacity
91
+ @_withSpeed(0, => @_updateStyle(true))
92
+
93
+ _stopTimers: ->
94
+ @_stopTrickle()
95
+ clearTimeout(@fadeTimer)
96
+ clearTimeout(@resetTimer)
97
+
98
+ _startTrickle: ->
99
+ return if @trickleTimer
100
+ @trickleTimer = setTimeout(@_trickle, @speed)
101
+
102
+ _stopTrickle: ->
103
+ clearTimeout(@trickleTimer)
104
+ delete @trickleTimer
105
+
106
+ _trickle: =>
107
+ @advanceTo(@value + Math.random() / 2)
108
+ @trickleTimer = setTimeout(@_trickle, @speed)
109
+
110
+ _withSpeed: (speed, fn) ->
111
+ originalSpeed = @speed
112
+ @speed = speed
113
+ result = fn()
114
+ @speed = originalSpeed
115
+ result
116
+
117
+ _updateStyle: (forceRepaint = false) ->
118
+ @_changeContentToForceRepaint() if forceRepaint
119
+ @styleElement.textContent = @_createCSSRule()
120
+
121
+ _changeContentToForceRepaint: ->
122
+ @content = if @content is '' then ' ' else ''
123
+
124
+ _createCSSRule: ->
125
+ """
126
+ #{@elementSelector}.#{className}::before {
127
+ content: '#{@content}';
128
+ position: fixed;
129
+ top: 0;
130
+ left: 0;
131
+ z-index: 2000;
132
+ background-color: #0076ff;
133
+ height: 3px;
134
+ opacity: #{@opacity};
135
+ width: #{if @display then @value else 0}%;
136
+ transition: width #{@speed}ms ease-out, opacity #{@speed / 2}ms ease-in;
137
+ transform: translate3d(0,0,0);
138
+ }
139
+ """