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.
@@ -0,0 +1,136 @@
1
+ class Breezy.Remote
2
+ SUPPORTED_METHODS = ['GET', 'PUT', 'POST', 'DELETE', 'PATCH']
3
+ FALLBACK_LINK_METHOD = 'GET'
4
+ FALLBACK_FORM_METHOD = 'POST'
5
+
6
+ constructor: (target, opts={})->
7
+ @target = target
8
+ @payload = ''
9
+ @contentType = null
10
+ @setRequestType(target)
11
+ @async = @getBZAttribute(target, 'bz-remote-async') || false
12
+ @pushState = !(@getBZAttribute(target, 'bz-push-state') == 'false')
13
+ @httpUrl = target.getAttribute('href') || target.getAttribute('action')
14
+ @silent = @getBZAttribute(target, 'bz-silent') || false
15
+ @setPayload(target)
16
+
17
+ toOptions: =>
18
+ requestMethod: @actualRequestType
19
+ payload: @payload
20
+ contentType: @contentType
21
+ silent: @silent
22
+ target: @target
23
+ async: @async
24
+ pushState: @pushState
25
+
26
+ setRequestType: (target)=>
27
+ if target.tagName == 'A'
28
+ @httpRequestType = @getBZAttribute(target, 'bz-remote')
29
+ @httpRequestType ?= ''
30
+ @httpRequestType = @httpRequestType.toUpperCase()
31
+
32
+ if @httpRequestType not in SUPPORTED_METHODS
33
+ @httpRequestType = FALLBACK_LINK_METHOD
34
+
35
+ if target.tagName == 'FORM'
36
+ formActionMethod = target.getAttribute('method')
37
+ @httpRequestType = formActionMethod || @getBZAttribute(target, 'bz-remote')
38
+ @httpRequestType ?= ''
39
+ @httpRequestType = @httpRequestType.toUpperCase()
40
+
41
+ if @httpRequestType not in SUPPORTED_METHODS
42
+ @httpRequestType = FALLBACK_FORM_METHOD
43
+
44
+ @actualRequestType = if @httpRequestType == 'GET' then 'GET' else 'POST'
45
+
46
+ setPayload: (target)=>
47
+ if target.tagName == 'FORM'
48
+ @payload = @nativeEncodeForm(target)
49
+
50
+ if @payload not instanceof FormData
51
+ if @payload.indexOf("_method") == -1 && @httpRequestType && @actualRequestType != 'GET'
52
+ @contentType = "application/x-www-form-urlencoded; charset=UTF-8"
53
+ @payload = @formAppend(@payload, "_method", @httpRequestType)
54
+ else
55
+ if !target.querySelector('[name=_method]') && @httpRequestType not in ['GET', 'POST']
56
+ @payload.append("_method", @httpRequestType)
57
+
58
+ isValid: =>
59
+ @isValidLink() || @isValidForm()
60
+
61
+ isValidLink: =>
62
+ if @target.tagName != 'A'
63
+ return false
64
+
65
+ @hasBZAttribute(@target, 'bz-remote')
66
+
67
+ isValidForm: =>
68
+ if @target.tagName != 'FORM'
69
+ return false
70
+ @hasBZAttribute(@target, 'bz-remote') &&
71
+ @target.getAttribute('action')?
72
+
73
+ formAppend: (uriEncoded, key, value) ->
74
+ uriEncoded += "&" if uriEncoded.length
75
+ uriEncoded += "#{encodeURIComponent(key)}=#{encodeURIComponent(value)}"
76
+
77
+ formDataAppend: (formData, input) ->
78
+ if input.type == 'file'
79
+ for file in input.files
80
+ formData.append(input.name, file)
81
+ else
82
+ formData.append(input.name, input.value)
83
+ formData
84
+
85
+ nativeEncodeForm: (form) ->
86
+ formData = new FormData
87
+ @iterateOverFormInputs form, (input) =>
88
+ formData = @formDataAppend(formData, input)
89
+ formData
90
+
91
+ iterateOverFormInputs: (form, callback) ->
92
+ inputs = @enabledInputs(form)
93
+ for input in inputs
94
+ inputEnabled = !input.disabled
95
+ radioOrCheck = (input.type == 'checkbox' || input.type == 'radio')
96
+
97
+ if inputEnabled && input.name
98
+ if (radioOrCheck && input.checked) || !radioOrCheck
99
+ callback(input)
100
+
101
+ enabledInputs: (form) ->
102
+ selector = "input:not([type='reset']):not([type='button']):not([type='submit']):not([type='image']), select, textarea"
103
+ inputs = Array::slice.call(form.querySelectorAll(selector))
104
+ disabledNodes = Array::slice.call(@querySelectorAllBZAttribute(form, 'bz-remote-noserialize'))
105
+
106
+ return inputs unless disabledNodes.length
107
+
108
+ disabledInputs = disabledNodes
109
+ for node in disabledNodes
110
+ disabledInputs = disabledInputs.concat(Array::slice.call(node.querySelectorAll(selector)))
111
+
112
+ enabledInputs = []
113
+ for input in inputs when disabledInputs.indexOf(input) < 0
114
+ enabledInputs.push(input)
115
+ enabledInputs
116
+
117
+ bzAttribute: (attr) ->
118
+ bzAttr = if attr[0...3] == 'bz-'
119
+ "data-#{attr}"
120
+ else
121
+ "data-bz-#{attr}"
122
+
123
+ getBZAttribute: (node, attr) ->
124
+ bzAttr = @bzAttribute(attr)
125
+ (node.getAttribute(bzAttr) || node.getAttribute(attr))
126
+
127
+ querySelectorAllBZAttribute: (node, attr, value = null) ->
128
+ bzAttr = @bzAttribute(attr)
129
+ if value
130
+ node.querySelectorAll("[#{bzAttr}=#{value}], [#{attr}=#{value}]")
131
+ else
132
+ node.querySelectorAll("[#{bzAttr}], [#{attr}]")
133
+
134
+ hasBZAttribute: (node, attr) ->
135
+ bzAttr = @bzAttribute(attr)
136
+ node.getAttribute(bzAttr)? || node.getAttribute(attr)?
@@ -0,0 +1,100 @@
1
+ #= require breezy/component_url
2
+ #= require breezy/csrf_token
3
+ #= require breezy/utils
4
+
5
+ class Breezy.Snapshot
6
+ constructor: (@controller) ->
7
+ @pageCache = {}
8
+ @currentBrowserState = null
9
+ @pageCacheSize = 10
10
+ @currentPage = null
11
+ @loadedAssets= null
12
+
13
+ onHistoryChange: (event) =>
14
+ if event.state?.breezy && event.state.url != @currentBrowserState.url
15
+ previousUrl = new Breezy.ComponentUrl(@currentBrowserState.url)
16
+ newUrl = new Breezy.ComponentUrl(event.state.url)
17
+
18
+ if restorePoint = @pageCache[newUrl.absolute]
19
+ @cacheCurrentPage()
20
+ @currentPage = restorePoint
21
+ @controller.restore(@currentPage)
22
+ else
23
+ @controller.request event.target.location.href
24
+
25
+ constrainPageCacheTo: (limit = @pageCacheSize) =>
26
+ pageCacheKeys = Object.keys @pageCache
27
+
28
+ cacheTimesRecentFirst = pageCacheKeys.map (url) =>
29
+ @pageCache[url].cachedAt
30
+ .sort (a, b) -> b - a
31
+
32
+ for key in pageCacheKeys when @pageCache[key].cachedAt <= cacheTimesRecentFirst[limit]
33
+ delete @pageCache[key]
34
+
35
+ transitionCacheFor: (url) =>
36
+ return if url is @currentBrowserState.url
37
+ cachedPage = @pageCache[url]
38
+ cachedPage if cachedPage and !cachedPage.transitionCacheDisabled
39
+
40
+ pagesCached: (size = @pageCacheSize) =>
41
+ @pageCacheSize = parseInt(size) if /^[\d]+$/.test size
42
+
43
+ cacheCurrentPage: =>
44
+ return unless @currentPage
45
+ currentUrl = new Breezy.ComponentUrl @currentBrowserState.url
46
+
47
+ Breezy.Utils.merge @currentPage,
48
+ cachedAt: new Date().getTime()
49
+ positionY: window.pageYOffset
50
+ positionX: window.pageXOffset
51
+ url: currentUrl.relative
52
+ pathname: currentUrl.pathname
53
+ transition_cache: true
54
+
55
+ @pageCache[currentUrl.absolute] = @currentPage
56
+
57
+ rememberCurrentUrlAndState: =>
58
+ window.history.replaceState { breezy: true, url: document.location.href }, '', document.location.href
59
+ @currentBrowserState = window.history.state
60
+
61
+ removeParamFromUrl: (url, parameter) =>
62
+ return url
63
+ .replace(new RegExp('^([^#]*\?)(([^#]*)&)?' + parameter + '(\=[^&#]*)?(&|#|$)' ), '$1$3$5')
64
+ .replace(/^([^#]*)((\?)&|\?(#|$))/,'$1$3$4')
65
+
66
+ reflectNewUrl: (url) =>
67
+ if (url = new Breezy.ComponentUrl url).absolute != document.location.href
68
+ preservedHash = if url.hasNoHash() then document.location.hash else ''
69
+ fullUrl = url.absolute + preservedHash
70
+ fullUrl = @removeParamFromUrl(fullUrl, '_breezy_filter')
71
+ fullUrl = @removeParamFromUrl(fullUrl, '__')
72
+
73
+ window.history.pushState { breezy: true, url: url.absolute + preservedHash }, '', fullUrl
74
+
75
+ updateCurrentBrowserState: =>
76
+ @currentBrowserState = window.history.state
77
+
78
+ changePage: (nextPage, options) =>
79
+ if @currentPage and @assetsChanged(nextPage)
80
+ document.location.reload()
81
+ return
82
+
83
+ @currentPage = nextPage
84
+ @currentPage.title = options.title ? @currentPage.title
85
+ document.title = @currentPage.title if @currentPage.title isnt false
86
+
87
+ Breezy.CSRFToken.update @currentPage.csrf_token if @currentPage.csrf_token?
88
+ @updateCurrentBrowserState()
89
+
90
+ assetsChanged: (nextPage) =>
91
+ @loadedAssets ||= @currentPage.assets
92
+ fetchedAssets = nextPage.assets
93
+ fetchedAssets.length isnt @loadedAssets.length or Breezy.Utils.intersection(fetchedAssets, @loadedAssets).length isnt @loadedAssets.length
94
+
95
+ graftByKeypath: (keypath, node, opts={})=>
96
+ for k, v in @pageCache
97
+ @history.pageCache[k] = Breezy.Utils.graftByKeypath(keypath, node, v, opts)
98
+
99
+ @currentPage = Breezy.Utils.graftByKeypath(keypath, node, @currentPage, opts)
100
+ Breezy.Utils.triggerEvent Breezy.EVENTS.LOAD, @currentPage
@@ -0,0 +1,60 @@
1
+ #= require breezy/controller
2
+ #= require breezy/remote
3
+ #= require breezy/utils
4
+
5
+ EVENTS =
6
+ BEFORE_CHANGE: 'breezy:click'
7
+ ERROR: 'breezy:request-error'
8
+ FETCH: 'breezy:request-start'
9
+ RECEIVE: 'breezy:request-end'
10
+ LOAD: 'breezy:load'
11
+ RESTORE: 'breezy:restore'
12
+
13
+ controller = new Breezy.Controller
14
+ progressBar = controller.progressBar
15
+
16
+ ProgressBarAPI =
17
+ enable: ->
18
+ progressBar.install()
19
+ disable: ->
20
+ progressBar.uninstall()
21
+ setDelay: (value) -> progressBar.setDelay(value)
22
+ start: (options) -> progressBar.start(options)
23
+ advanceTo: (value) -> progressBar.advanceTo(value)
24
+ done: -> progressBar.done()
25
+
26
+ remoteHandler = (ev) ->
27
+ target = ev.target
28
+ remote = new Breezy.Remote(target)
29
+ return unless remote.isValid()
30
+ ev.preventDefault()
31
+ controller.request remote.httpUrl, remote.toOptions()
32
+
33
+ browserSupportsCustomEvents =
34
+ document.addEventListener and document.createEvent
35
+
36
+ initializeBreezy = ->
37
+ ProgressBarAPI.enable()
38
+ window.addEventListener 'hashchange', controller.history.rememberCurrentUrlAndState, false
39
+ window.addEventListener 'popstate', controller.history.onHistoryChange, false
40
+ Breezy.Utils.documentListenerForLinks 'click', remoteHandler
41
+ document.addEventListener "submit", remoteHandler
42
+
43
+ if Breezy.Utils.browserSupportsBreezy()
44
+ visit = controller.request
45
+ initializeBreezy()
46
+ else
47
+ visit = (url = document.location.href) -> document.location.href = url
48
+
49
+ Breezy.controller = controller
50
+ Breezy.graftByKeypath = controller.history.graftByKeypath
51
+ Breezy.visit = visit
52
+ Breezy.replace = controller.replace
53
+ Breezy.cache = controller.cache
54
+ Breezy.pagesCached = controller.history.pagesCached
55
+ Breezy.enableTransitionCache = controller.enableTransitionCache
56
+ Breezy.disableRequestCaching = controller.disableRequestCaching
57
+ Breezy.ProgressBar = ProgressBarAPI
58
+ Breezy.supported = Breezy.Utils.browserSupportsBreezy()
59
+ Breezy.EVENTS = Breezy.Utils.clone(EVENTS)
60
+ Breezy.currentPage = controller.currentPage
@@ -0,0 +1,174 @@
1
+ reverseMerge = (dest, obj) ->
2
+ for k, v of obj
3
+ dest[k] = v if !dest.hasOwnProperty(k)
4
+ dest
5
+
6
+ merge = (dest, obj) ->
7
+ for k, v of obj
8
+ dest[k] = v
9
+ dest
10
+
11
+ clone = (original) ->
12
+ return original if not original? or typeof original isnt 'object'
13
+ copy = new original.constructor()
14
+ copy[key] = clone value for key, value of original
15
+ copy
16
+
17
+ withDefaults = (page, state) =>
18
+ currentUrl = new Breezy.ComponentUrl state.url
19
+
20
+ reverseMerge page,
21
+ url: currentUrl.relative
22
+ pathname: currentUrl.pathname
23
+ cachedAt: new Date().getTime()
24
+ assets: []
25
+ data: {}
26
+ title: ''
27
+ positionY: 0
28
+ positionX: 0
29
+ csrf_token: null
30
+
31
+ browserIsBuggy = () ->
32
+ # Copied from https://github.com/Modernizr/Modernizr/blob/master/feature-detects/history.js
33
+ ua = navigator.userAgent
34
+ (ua.indexOf('Android 2.') != -1 or ua.indexOf('Android 4.0') != -1) and
35
+ ua.indexOf('Mobile Safari') != -1 and
36
+ ua.indexOf('Chrome') == -1 and
37
+ ua.indexOf('Windows Phone') == -1
38
+
39
+ browserSupportsPushState = () ->
40
+ window.history and 'pushState' of window.history and 'state' of window.history
41
+
42
+ popCookie = (name) ->
43
+ value = document.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or ''
44
+ document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
45
+ value
46
+
47
+ requestMethodIsSafe = -> popCookie('request_method') in ['GET','']
48
+
49
+ browserSupportsBreezy = ->
50
+ browserSupportsPushState() and !browserIsBuggy() and requestMethodIsSafe()
51
+
52
+ intersection = (a, b) ->
53
+ [a, b] = [b, a] if a.length > b.length
54
+ value for value in a when value in b
55
+
56
+
57
+ triggerEvent = (name, data, target = document) =>
58
+ event = document.createEvent 'Events'
59
+ event.data = data if data
60
+ event.initEvent name, true, true
61
+ target.dispatchEvent event
62
+
63
+ documentListenerForLinks = (eventType, handler) ->
64
+ document.addEventListener eventType, (ev) ->
65
+ target = ev.target
66
+ while target != document && target?
67
+ if target.nodeName == "A"
68
+ isNodeDisabled = target.getAttribute('disabled')
69
+ ev.preventDefault() if target.getAttribute('disabled')
70
+ unless isNodeDisabled
71
+ handler(ev)
72
+ return
73
+
74
+ target = target.parentNode
75
+
76
+ isObject = (val) ->
77
+ Object.prototype.toString.call(val) is '[object Object]'
78
+
79
+ isArray = (val) ->
80
+ Object.prototype.toString.call(val) is '[object Array]'
81
+
82
+ class Breezy.Grafter
83
+ constructor: ->
84
+ @current_path = []
85
+
86
+ graftByKeypath: (path, leaf, obj, opts={}) ->
87
+ if typeof path is "string"
88
+ path = path.split('.')
89
+ return @graftByKeypath(path, leaf, obj, opts)
90
+
91
+ head = path[0]
92
+ @current_path.push(head)
93
+ child = obj[head] if obj?
94
+ remaining = path.slice(1)
95
+
96
+ if path.length is 0
97
+ if opts.type == 'add' and isArray(obj)
98
+ copy = []
99
+ for child in obj
100
+ copy.push child
101
+
102
+ copy.push leaf
103
+ return copy
104
+ else
105
+ return leaf
106
+
107
+ if isObject(obj)
108
+ copy = {}
109
+ found = false
110
+ for key, value of obj
111
+ if key is head
112
+ node = @graftByKeypath(remaining, leaf, child, opts)
113
+ found = true unless child is node
114
+ copy[key] = node
115
+ else
116
+ copy[key] = value
117
+
118
+ return if found
119
+ @current_path.pop()
120
+ copy
121
+ else
122
+ Breezy.Utils.warn "Could not find key #{head} in keypath #{@current_path.join('.')}"
123
+ obj
124
+
125
+ else if isArray(obj)
126
+ [attr, id] = head.split('=')
127
+ found = false
128
+ if id == undefined
129
+ index = parseInt(attr)
130
+ child = obj[index]
131
+ node = @graftByKeypath(remaining, leaf, child, opts)
132
+ found = true unless child is node
133
+
134
+ copy = obj.slice(0, index)
135
+ copy.push(node)
136
+ copy = copy.concat(obj.slice(index + 1, obj.length))
137
+ else
138
+ id = parseInt(id) || 0
139
+ copy = []
140
+ for child in obj
141
+ if child[attr] == id
142
+ node = @graftByKeypath(remaining, leaf, child, opts)
143
+ found = true unless child is node
144
+ copy.push node
145
+ else
146
+ copy.push child
147
+
148
+ return if found
149
+ @current_path.pop()
150
+ copy
151
+ else
152
+ Breezy.Utils.warn "Could not find key #{head} in keypath #{@current_path.join('.')}"
153
+ obj
154
+ else
155
+ obj
156
+
157
+
158
+
159
+ @Breezy.Utils =
160
+ warn: ->
161
+ console.warn.apply(@, arguments)
162
+ graftByKeypath: ->
163
+ grafter = new Breezy.Grafter
164
+ grafter.graftByKeypath.apply(grafter, arguments)
165
+ documentListenerForLinks: documentListenerForLinks
166
+ reverseMerge: reverseMerge
167
+ merge: merge
168
+ clone: clone
169
+ withDefaults: withDefaults
170
+ browserSupportsBreezy: browserSupportsBreezy
171
+ intersection: intersection
172
+ triggerEvent: triggerEvent
173
+
174
+