chr 0.1.0

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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/CONTRIBUTING.md +24 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE.md +21 -0
  6. data/README.md +18 -0
  7. data/Rakefile +3 -0
  8. data/app/assets/javascripts/chr.coffee +41 -0
  9. data/app/assets/javascripts/chr/core/_chr.coffee +89 -0
  10. data/app/assets/javascripts/chr/core/_item.coffee +85 -0
  11. data/app/assets/javascripts/chr/core/_list.coffee +154 -0
  12. data/app/assets/javascripts/chr/core/_listReorder.coffee +70 -0
  13. data/app/assets/javascripts/chr/core/_listScroll.coffee +23 -0
  14. data/app/assets/javascripts/chr/core/_listSearch.coffee +28 -0
  15. data/app/assets/javascripts/chr/core/_module.coffee +98 -0
  16. data/app/assets/javascripts/chr/core/_utils.coffee +50 -0
  17. data/app/assets/javascripts/chr/core/_view.coffee +121 -0
  18. data/app/assets/javascripts/chr/form/_form.coffee +205 -0
  19. data/app/assets/javascripts/chr/form/_inputCheckbox.coffee +70 -0
  20. data/app/assets/javascripts/chr/form/_inputColor.coffee +35 -0
  21. data/app/assets/javascripts/chr/form/_inputFile.coffee +82 -0
  22. data/app/assets/javascripts/chr/form/_inputHidden.coffee +41 -0
  23. data/app/assets/javascripts/chr/form/_inputList.coffee +142 -0
  24. data/app/assets/javascripts/chr/form/_inputSelect.coffee +59 -0
  25. data/app/assets/javascripts/chr/form/_inputString.coffee +87 -0
  26. data/app/assets/javascripts/chr/form/_inputText.coffee +23 -0
  27. data/app/assets/javascripts/chr/form/_nestedForm.coffee +164 -0
  28. data/app/assets/javascripts/chr/store/_store.coffee +104 -0
  29. data/app/assets/javascripts/chr/store/_storeRails.coffee +167 -0
  30. data/app/assets/javascripts/chr/vendor/jquery.scrollparent.js +14 -0
  31. data/app/assets/javascripts/chr/vendor/jquery.textarea_autosize.js +55 -0
  32. data/app/assets/javascripts/chr/vendor/jquery.typeahead.js +1782 -0
  33. data/app/assets/javascripts/chr/vendor/slip.js +804 -0
  34. data/app/assets/stylesheets/_chr.scss +7 -0
  35. data/app/assets/stylesheets/core/_icons.scss +124 -0
  36. data/app/assets/stylesheets/core/_list.scss +44 -0
  37. data/app/assets/stylesheets/core/_main.scss +89 -0
  38. data/app/assets/stylesheets/core/_responsive.scss +41 -0
  39. data/app/assets/stylesheets/form/_form.scss +50 -0
  40. data/app/assets/stylesheets/form/_input_checkbox.scss +87 -0
  41. data/app/assets/stylesheets/form/_input_color.scss +10 -0
  42. data/app/assets/stylesheets/form/_input_file.scss +28 -0
  43. data/app/assets/stylesheets/form/_input_list.scss +36 -0
  44. data/app/assets/stylesheets/form/_input_string.scss +8 -0
  45. data/app/assets/stylesheets/form/_input_text.scss +48 -0
  46. data/app/assets/stylesheets/form/_nested_form.scss +26 -0
  47. data/chr.gemspec +34 -0
  48. data/lib/chr.rb +15 -0
  49. data/lib/chr/engine.rb +5 -0
  50. data/lib/chr/version.rb +3 -0
  51. metadata +152 -0
@@ -0,0 +1,23 @@
1
+ # -----------------------------------------------------------------------------
2
+ # INPUT TEXT
3
+ # Dependencies:
4
+ # - jquery.textaread_autosize.js (https://github.com/javierjulio/textarea-autosize)
5
+ # - bugfix: https://github.com/javierjulio/textarea-autosize/issues/8#issuecomment-67300688
6
+ # -----------------------------------------------------------------------------
7
+ class @InputText extends InputString
8
+ _addInput: ->
9
+ @$input =$ "<textarea class='autosize' name='#{ @name }' id='#{ @name }' rows=1>#{ @_valueSafe() }</textarea>"
10
+ @$el.append @$input
11
+
12
+ initialize: ->
13
+ # TODO: refactor a bit plugin code so there is no blink while jumping from object to object
14
+ @$input.textareaAutoSize()
15
+
16
+ @config.onInitialize?(this)
17
+
18
+
19
+ _chrFormInputs['text'] = InputText
20
+
21
+
22
+
23
+
@@ -0,0 +1,164 @@
1
+ # -----------------------------------------------------------------------------
2
+ # NESTED FORM
3
+ # -----------------------------------------------------------------------------
4
+ class @NestedForm
5
+ constructor: (@name, @nestedObjects, @config, @object) ->
6
+ @forms = []
7
+
8
+ @config.namePrefix ||= name
9
+ @config.removeButton = true
10
+ @config.formSchema.id = { type: 'hidden' }
11
+ @reorderContainerClass = "nested-forms-#{@config.klassName}"
12
+
13
+ if @config.sortBy
14
+ @config.formSchema[@config.sortBy] = { type: 'hidden' }
15
+ if @nestedObjects
16
+ # NOTE: this is not required but make things a bit easier on the backend
17
+ # as object don't have to be in a specific order.
18
+ @nestedObjects.sort (a, b) => parseFloat(a[@config.sortBy]) - parseFloat(b[@config.sortBy])
19
+ # NOTE: normalizes nested objects positions
20
+ (o[@config.sortBy] = parseInt(i) + 1) for i, o of @nestedObjects
21
+
22
+ @$el =$ "<div class='input-stacked nested-forms #{ @config.klassName }'>"
23
+
24
+ @_addLabel()
25
+ @_addForms()
26
+ @_addFormsReorder()
27
+ @_addNewButton()
28
+
29
+ return this
30
+
31
+ _addLabel: ->
32
+ if @config.klass in [ 'inline', 'stacked' ]
33
+ @$label =$ "<span class='label'>#{ @config.label }</span>"
34
+ @$el.append @$label
35
+
36
+ @$errorMessage =$ "<span class='error-message'></span>"
37
+ @$label.append @$errorMessage
38
+
39
+ _addForms: ->
40
+ @$forms =$ "<ul>"
41
+ @$el.append @$forms
42
+
43
+ # if not default value which means no objects
44
+ if @nestedObjects != ''
45
+ for i, object of @nestedObjects
46
+ namePrefix = "#{ @config.namePrefix }[#{ i }]"
47
+ @forms.push @_renderForm(object, namePrefix, @config)
48
+
49
+ _renderForm: (object, namePrefix, config) ->
50
+ formConfig = $.extend {}, config,
51
+ namePrefix: namePrefix
52
+ rootEl: "<li>"
53
+
54
+ form = new Form(object, formConfig)
55
+ @$forms.append form.$el
56
+
57
+ return form
58
+
59
+ _addFormsReorder: ->
60
+ if @config.sortBy
61
+ list = @$forms.addClass(@reorderContainerClass).get(0)
62
+
63
+ new Slip(list)
64
+
65
+ list.addEventListener 'slip:beforeswipe', (e) -> e.preventDefault()
66
+
67
+ list.addEventListener 'slip:beforewait', ((e) ->
68
+ if $(e.target).hasClass("icon-reorder") then e.preventDefault()
69
+ ), false
70
+
71
+ list.addEventListener 'slip:beforereorder', ((e) ->
72
+ if not $(e.target).hasClass("icon-reorder") then e.preventDefault()
73
+ ), false
74
+
75
+ list.addEventListener 'slip:reorder', ((e) =>
76
+ # NOTE: this event called for all parent lists, add a check for context:
77
+ # process this event only if target form is in the @forms list.
78
+ targetForm = @_findFormByTarget(e.target)
79
+ if targetForm
80
+ # NOTE: when `e.detail.insertBefore` is null, item put to the end of the list.
81
+ e.target.parentNode.insertBefore(e.target, e.detail.insertBefore)
82
+
83
+ $targetForm =$ e.target
84
+ prevForm = @_findFormByTarget($targetForm.prev().get(0))
85
+ nextForm = @_findFormByTarget($targetForm.next().get(0))
86
+
87
+ prevFormPosition = if prevForm then prevForm.inputs[@config.sortBy].value else 0
88
+ nextFormPosition = if nextForm then nextForm.inputs[@config.sortBy].value else 0
89
+ newTargetFormPosition = prevFormPosition + Math.abs(nextFormPosition - prevFormPosition) / 2.0
90
+
91
+ targetForm.inputs[@config.sortBy].updateValue(newTargetFormPosition)
92
+
93
+ return false
94
+ ), false
95
+
96
+ @_addFormReorderButton(form) for form in @forms
97
+
98
+ _addFormReorderButton: (form) ->
99
+ form.$el.append("""<div class='icon-reorder' data-container-class='#{@reorderContainerClass}'></div>""").addClass('reorderable')
100
+
101
+ _findFormByTarget: (el) ->
102
+ if el
103
+ for form in @forms
104
+ if form.$el.get(0) == el then return form
105
+ return null
106
+
107
+ _addNewButton: ->
108
+ label = @config.newButtonLabel || "Add"
109
+ @$newButton =$ """<a href='#' class='nested-form-new'>#{ label }</a>"""
110
+ @$el.append @$newButton
111
+ @$newButton.on 'click', (e) => e.preventDefault() ; @addNewForm()
112
+
113
+ #
114
+ # PUBLIC
115
+ #
116
+
117
+ addNewForm: (object=null) ->
118
+ namePrefix = "#{ @config.namePrefix }[#{ Date.now() }]"
119
+ newFormConfig = $.extend({}, @config)
120
+
121
+ delete newFormConfig.formSchema.id
122
+
123
+ form = @_renderForm(object, namePrefix, newFormConfig)
124
+ form.initializePlugins()
125
+
126
+ if @config.sortBy
127
+ @_addFormReorderButton(form)
128
+ prevForm = _last(@forms)
129
+ position = if prevForm then prevForm.inputs[@config.sortBy].value + 1 else 1
130
+ form.inputs[@config.sortBy].updateValue(position)
131
+
132
+ @forms.push(form)
133
+
134
+ @config.onNew?(form)
135
+
136
+ return form
137
+
138
+ initialize: ->
139
+ for nestedForm in @forms
140
+ nestedForm.initializePlugins()
141
+ @config.onInitialize?(this)
142
+
143
+ showErrorMessage: (message) ->
144
+ @$el.addClass 'error'
145
+ @$errorMessage.html(message)
146
+
147
+ hideErrorMessage: ->
148
+ @$el.removeClass 'error'
149
+ @$errorMessage.html('')
150
+
151
+ updateValue: (@value) ->
152
+ # TODO: update
153
+
154
+ hash: (hash={})->
155
+ hash[@config.klassName] = []
156
+ for form in @forms
157
+ hash[@config.klassName].push form.hash()
158
+ return hash
159
+
160
+ _chrFormInputs['form'] = NestedForm
161
+
162
+
163
+
164
+
@@ -0,0 +1,104 @@
1
+ # -----------------------------------------------------------------------------
2
+ # OBJECT STORE
3
+ # -----------------------------------------------------------------------------
4
+ class @ObjectStore
5
+ constructor: (@config={}) ->
6
+ @_initializeDatabase()
7
+
8
+ _fetchData: ->
9
+ @_data = @config.data
10
+
11
+ _initializeDatabase: ->
12
+ @_fetchData()
13
+
14
+ get: ->
15
+ @_data
16
+
17
+ _updateDataObject: (value, callback) ->
18
+ callback?($.extend(@_data, value))
19
+
20
+ update: (id, value, callback) ->
21
+ @_updateDataObject(value, callback)
22
+
23
+
24
+ # -----------------------------------------------------------------------------
25
+ # ARRAY STORE
26
+ # -----------------------------------------------------------------------------
27
+ class @ArrayStore
28
+ _sortData: ->
29
+ # http://stackoverflow.com/questions/9796764/how-do-i-sort-an-array-with-coffeescript
30
+ if @sortBy
31
+ fieldName = @sortBy
32
+ direction = if @sortReverse then 1 else -1
33
+
34
+ sortBy = (key, a, b, dir) ->
35
+ if a[key] > b[key] then return -1*dir
36
+ if a[key] < b[key] then return +1*dir
37
+ return 0
38
+
39
+ @_data = @_data.sort (a, b) -> sortBy(fieldName, a, b, direction)
40
+
41
+ _mapData: ->
42
+ ( @_map[o._id] = o for o in @_data )
43
+
44
+ _addDataObject: (object, callback)->
45
+ @_map[object._id] = object
46
+ @_data.push(object)
47
+ @_sortData()
48
+ position = @_getDataObjectPosition(object._id)
49
+ $(this).trigger('object_added', { object: object, position: position, callback: callback })
50
+
51
+ _resetData: ->
52
+ for id, o of @_map
53
+ $(this).trigger('object_removed', { object_id: id })
54
+ @_map = {}
55
+ @_data = []
56
+
57
+ _removeDataObject: (id) ->
58
+ position = @_getDataObjectPosition(id)
59
+ if position >= 0
60
+ delete @_data[position]
61
+ delete @_map[id]
62
+ $(this).trigger('object_removed', { object_id: id })
63
+
64
+ _updateDataObject: (id, value, callback) ->
65
+ object = $.extend(@get(id), value)
66
+ @_sortData()
67
+ position = @_getDataObjectPosition(id)
68
+ $(this).trigger('object_changed', { object: object, position: position, callback: callback })
69
+
70
+ _getDataObjectPosition: (id) ->
71
+ ids = []
72
+ for o in @_data
73
+ if o then ids.push(o._id)
74
+ $.inArray id, ids
75
+
76
+ constructor: (@config={}) ->
77
+ @_map = {}
78
+ @_data = []
79
+ @_initializeDatabase()
80
+
81
+ _initializeDatabase: ->
82
+
83
+ off: (eventType) ->
84
+ if eventType then $(this).off(eventType) else $(this).off()
85
+
86
+ on: (eventType, callback) -> # event types: object_added, object_changed, object_removed
87
+ $(this).on eventType, (e, data) -> callback(e, data)
88
+ # NOTE: this called once when list subscribes to store updates
89
+ if eventType == 'object_added' then @_fetchData()
90
+
91
+ _fetchData: ->
92
+ if @config.data
93
+ @_addDataObject(o) for o in @config.data
94
+
95
+ get: (id) -> @_map[id]
96
+
97
+ update: (id, value, callback) ->
98
+ @_updateDataObject(id, value, callback)
99
+
100
+ push: (value, callback) ->
101
+ @_addDataObject($.extend({ _id: Date.now() }, value), callback)
102
+
103
+ remove: (id) ->
104
+ @_removeDataObject(id)
@@ -0,0 +1,167 @@
1
+ # -----------------------------------------------------------------------------
2
+ # RAILS OBJECT STORE IMPLEMENTATION
3
+ # -----------------------------------------------------------------------------
4
+ class @RailsObjectStore extends ObjectStore
5
+ # _initializeDatabase: ->
6
+ # @database = new Firebase("https://#{ @config.dbName }.firebaseio.com/")
7
+ # @dataRef = @database.child(@config.path)
8
+ # @_fetchData()
9
+
10
+ # _fetchData: ->
11
+ # @dataRef.once 'value', (dataSnapshot) =>
12
+ # @_data = dataSnapshot.val() ? {}
13
+
14
+ # update: (id, value, callback) ->
15
+ # @dataRef.set value, => @_updateDataObject(value, callback)
16
+
17
+
18
+ # -----------------------------------------------------------------------------
19
+ # RAILS ARRAY/COLLECTION STORE IMPLEMENTATION
20
+ # -----------------------------------------------------------------------------
21
+ class @RailsArrayStore extends ArrayStore
22
+ _initializeDatabase: ->
23
+ @resetData = false
24
+ @searchable = @config.searchable ? false
25
+ @pagination = @config.pagination ? true
26
+ @reorderable = @config.reorderable ? false
27
+ @dataFetchLock = false
28
+ @pagesCounter = 0
29
+ @searchQuery = ''
30
+
31
+ # if reorderable we need to set proper config for sorting
32
+ if @reorderable
33
+ @sortBy = @reorderable.positionFieldName
34
+ @sortReverse = @reorderable.sortReverse || false
35
+
36
+ # bootstraped data provided
37
+ if @config.data
38
+ @pagesCounter = 1
39
+
40
+ _addDataObjectToTheTop: (object, callback) ->
41
+ @_map[object._id] = object
42
+ @_data.unshift(object)
43
+ position = 0
44
+ $(this).trigger('object_added', { object: object, position: position, callback: callback })
45
+
46
+ _wrapRailsObject: (object) ->
47
+ # NOTE: generate form data object
48
+ data = new FormData()
49
+ for attr_name, attr_value of object
50
+ # NOTE: special case for LIST inputs, values separated with comma
51
+ if attr_name.indexOf('[__LIST__') > -1
52
+ attr_name = attr_name.replace('__LIST__', '')
53
+ values = attr_value.split(',')
54
+ for value in values
55
+ data.append("#{ @config.resource }#{ attr_name }[]", value)
56
+ else
57
+ # NOTE: special case for FILE inputs
58
+ if attr_name.startsWith('__FILE__')
59
+ attr_name = attr_name.replace('__FILE__', '')
60
+
61
+ data.append("#{ @config.resource }#{ attr_name }", attr_value)
62
+ return data
63
+
64
+ _urlWithParams: (url) ->
65
+ if @config.urlParams
66
+ extraParamString = $.param(@config.urlParams)
67
+ if url.indexOf('?') > 0
68
+ url = "#{ url }&#{ extraParamString }"
69
+ else
70
+ url = "#{ url }?#{ extraParamString }"
71
+ return url
72
+
73
+ _delete: (id, success) ->
74
+ @dataFetchLock = true
75
+ $.ajax
76
+ type: 'DELETE'
77
+ url: @_urlWithParams("#{ @config.path }/#{ id }.json")
78
+ success: (data, textStatus, jqXHR) =>
79
+ @dataFetchLock = false
80
+ success?(data)
81
+
82
+ _post: (object, success, error) ->
83
+ @dataFetchLock = true
84
+ $.ajax
85
+ type: 'POST'
86
+ url: @_urlWithParams("#{ @config.path }.json")
87
+ data: @_wrapRailsObject(object)
88
+ processData: false
89
+ contentType: false
90
+ success: (data, textStatus, jqXHR) =>
91
+ @dataFetchLock = false
92
+ success?(data)
93
+ error: (jqXHR, textStatus, errorThrown ) =>
94
+ @dataFetchLock = false
95
+ error?(jqXHR.responseJSON)
96
+
97
+ _put: (id, object, success, error) ->
98
+ @dataFetchLock = true
99
+ $.ajax
100
+ type: 'PUT'
101
+ url: @_urlWithParams("#{ @config.path }/#{ id }.json")
102
+ data: @_wrapRailsObject(object)
103
+ processData: false
104
+ contentType: false
105
+ success: (data, textStatus, jqXHR) =>
106
+ @dataFetchLock = false
107
+ success?(data)
108
+ error: (jqXHR, textStatus, errorThrown ) =>
109
+ @dataFetchLock = false
110
+ error?(jqXHR.responseJSON)
111
+
112
+ _get: (params, success) ->
113
+ @dataFetchLock = true
114
+ $.get @_urlWithParams("#{ @config.path }.json"), params, (data) =>
115
+ if data.length > 0
116
+ @pagesCounter = @pagesCounter + 1
117
+ for o in data
118
+ @_addDataObject(o)
119
+
120
+ @dataFetchLock = false
121
+ success?()
122
+ $(this).trigger('objects_added')
123
+
124
+ #
125
+ # PUBLIC
126
+ #
127
+
128
+ fetchNextPage: (callback) ->
129
+ params = {}
130
+ if @pagination
131
+ params.page = @pagesCounter + 1
132
+ params.perPage = _itemsPerPageRequest
133
+ if @searchQuery.length > 0
134
+ params.search = @searchQuery
135
+ @_get(params)
136
+
137
+ search: (@searchQuery, callback) ->
138
+ @pagesCounter = 0
139
+ @_resetData()
140
+ @fetchNextPage(callback)
141
+
142
+ reset: (callback) ->
143
+ @searchQuery = ''
144
+ @pagesCounter = 0
145
+ @_resetData()
146
+ @fetchNextPage(callback)
147
+
148
+ update: (id, object, callbacks) ->
149
+ @_put id, object, ((data) =>
150
+ @_updateDataObject(id, data, callbacks.onSuccess)
151
+ ), callbacks.onError
152
+
153
+ push: (object, callbacks) ->
154
+ @_post object, ((data) =>
155
+ if @config.sortBy
156
+ @_addDataObject(data, callbacks.onSuccess)
157
+ else
158
+ @_addDataObjectToTheTop(data, callbacks.onSuccess)
159
+ ), callbacks.onError
160
+
161
+ remove: (id) ->
162
+ @_delete id, (data) =>
163
+ @_removeDataObject(id)
164
+
165
+
166
+
167
+