breezy 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+