breezy 0.1.3 → 0.1.4

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