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,70 @@
1
+ # -----------------------------------------------------------------------------
2
+ # LIST REORDER
3
+ # -----------------------------------------------------------------------------
4
+ # Dependencies:
5
+ # - slip
6
+ # -----------------------------------------------------------------------------
7
+
8
+ @_listBindReorder = (listEl) ->
9
+ items = listEl.items
10
+ list = listEl.$items.get(0)
11
+ arrayStore = listEl.config.arrayStore
12
+
13
+ config = arrayStore.reorderable
14
+
15
+ # NOTE: this is optimistic scenario when assumes that all positions are different
16
+ _getObjectNewPosition = (el) ->
17
+ $el =$ el
18
+
19
+ nextObjectId = $el.next().attr('data-id')
20
+ prevObjectId = $el.prev().attr('data-id')
21
+ nextObjectPosition = 0
22
+ prevObjectPosition = 0
23
+
24
+ if prevObjectId
25
+ prevObjectPosition = items[prevObjectId].position()
26
+
27
+ if nextObjectId
28
+ nextObjectPosition = items[nextObjectId].position()
29
+
30
+ if arrayStore.sortReverse
31
+ newPosition = nextObjectPosition + Math.abs(nextObjectPosition - prevObjectPosition) / 2.0
32
+ else
33
+ newPosition = prevObjectPosition + Math.abs(nextObjectPosition - prevObjectPosition) / 2.0
34
+
35
+ return newPosition
36
+
37
+ new Slip(list)
38
+
39
+ list.addEventListener 'slip:beforeswipe', (e) -> e.preventDefault()
40
+
41
+ list.addEventListener 'slip:beforewait', ((e) ->
42
+ if $(e.target).hasClass("icon-reorder") then e.preventDefault()
43
+ ), false
44
+
45
+ list.addEventListener 'slip:beforereorder', ((e) ->
46
+ if not $(e.target).hasClass("icon-reorder") then e.preventDefault()
47
+ ), false
48
+
49
+ list.addEventListener 'slip:reorder', ((e) =>
50
+ # NOTE: when `e.detail.insertBefore` is null, item put to the end of the list.
51
+ e.target.parentNode.insertBefore(e.target, e.detail.insertBefore)
52
+
53
+ objectPositionValue = _getObjectNewPosition(e.target)
54
+ objectId = $(e.target).attr('data-id')
55
+ value = {}
56
+ value["[#{arrayStore.sortBy}"] = objectPositionValue
57
+
58
+ arrayStore.update objectId, value,
59
+ # NOTE: error handling
60
+ onSuccess: (object) => ;
61
+ onError: (errors) => ;
62
+
63
+ return false
64
+ ), false
65
+
66
+ $(list).addClass 'reorderable'
67
+
68
+
69
+
70
+
@@ -0,0 +1,23 @@
1
+ # -----------------------------------------------------------------------------
2
+ # LIST SCROLL
3
+ # -----------------------------------------------------------------------------
4
+ @_listBindScroll = (listEl) ->
5
+ $container = listEl.$el
6
+ $list = listEl.$items
7
+ arrayStore = listEl.config.arrayStore
8
+
9
+ $list.scroll (e) =>
10
+ if not arrayStore.dataFetchLock
11
+ $listChildren = $list.children()
12
+ listChildrenCount = $listChildren.length
13
+ listFirstChildHeight = $listChildren.first().outerHeight()
14
+ listHeight = listChildrenCount * listFirstChildHeight
15
+ viewHeight = $container.height()
16
+
17
+ if listHeight < (viewHeight + e.target.scrollTop + 100)
18
+ listEl._loading -> arrayStore.fetchNextPage()
19
+
20
+
21
+
22
+
23
+
@@ -0,0 +1,28 @@
1
+ # -----------------------------------------------------------------------------
2
+ # LIST SEARCH
3
+ # -----------------------------------------------------------------------------
4
+ @_listBindSearch = (listEl) ->
5
+ $input = listEl.$search
6
+ arrayStore = listEl.config.arrayStore
7
+
8
+ $input.show()
9
+
10
+ $input.on 'keydown', 'input', (e) =>
11
+ if e.keyCode == 13
12
+ query = $(e.target).val()
13
+ listEl._loading -> arrayStore.search(query)
14
+
15
+ $input.on 'click', '.icon', (e) =>
16
+ e.preventDefault()
17
+ listEl.$el.addClass 'list-search'
18
+ $input.find('input').focus()
19
+
20
+ $input.on 'click', '.cancel', (e) =>
21
+ e.preventDefault()
22
+ listEl.$el.removeClass 'list-search'
23
+ $input.find('input').val('')
24
+ listEl._loading -> arrayStore.reset()
25
+
26
+
27
+
28
+
@@ -0,0 +1,98 @@
1
+ # -----------------------------------------------------------------------------
2
+ # MODULE
3
+ # -----------------------------------------------------------------------------
4
+ class @Module
5
+ constructor: (@chr, @name, @config) ->
6
+ @nestedLists = {}
7
+
8
+ @$el = $("<section class='module #{ @name }' style='display: none;'>")
9
+ @chr.$el.append @$el
10
+
11
+ menuTitle = @config.menuTitle ? @config.title
12
+ menuTitle ?= @name.titleize()
13
+ @chr.addMenuItem(@name, menuTitle)
14
+
15
+ @activeList = @rootList = new List(this, @name, @config)
16
+
17
+
18
+ _updateActiveListItems: ->
19
+ # NOTE: update list data if it's not visible, e.g. for update action we do not
20
+ # update whole list, this function should be called before active list got shown.
21
+ if not @activeList.isVisible()
22
+ @activeList.updateItems()
23
+
24
+ addNestedList: (listName, config, parentList) ->
25
+ @nestedLists[listName] = new List(this, listName, config, parentList)
26
+
27
+ selectActiveListItem: (href) ->
28
+ @unselectActiveListItem()
29
+ @activeList.selectItem(href)
30
+
31
+ unselectActiveListItem: ->
32
+ @activeList?.unselectItems()
33
+
34
+ hideActiveList: (animate=false)->
35
+ if animate then @activeList.$el.fadeOut() else @activeList.$el.hide()
36
+ @activeList = @activeList.parentList
37
+ @unselectActiveListItem()
38
+
39
+ showView: (object, config, title, animate=false) ->
40
+ newView = new View(this, config, @activeList.path, object, title)
41
+ @chr.$el.append(newView.$el)
42
+
43
+ @selectActiveListItem(location.hash)
44
+
45
+ newView.show animate, =>
46
+ @destroyView()
47
+ @view = newView
48
+
49
+ destroyView: ->
50
+ @view?.destroy()
51
+
52
+ show: ->
53
+ @chr.selectMenuItem(@name)
54
+ @unselectActiveListItem()
55
+
56
+ @_updateActiveListItems()
57
+ @$el.show()
58
+ @activeList.show(false)
59
+
60
+ hide: (animate=false) ->
61
+ @unselectActiveListItem()
62
+ @hideNestedLists()
63
+
64
+ if animate
65
+ # TODO: move animation to the view class
66
+ if @view then @view.$el.fadeOut $.fx.speeds._default, => @destroyView()
67
+ @$el.fadeOut()
68
+ else
69
+ @destroyView()
70
+ @$el.hide()
71
+
72
+ showNestedList: (listName, animate=false) ->
73
+ @selectActiveListItem(location.hash)
74
+ @activeList = @nestedLists[listName]
75
+
76
+ @_updateActiveListItems()
77
+
78
+ @activeList.show(animate)
79
+ if animate and @view then @view.$el.fadeOut $.fx.speeds._default, => @destroyView()
80
+
81
+ hideNestedLists: ->
82
+ @activeList = @rootList
83
+ list.hide() for key, list of @nestedLists
84
+
85
+ showViewWhenObjectsAreReady: (objectId, config) ->
86
+ object = config.arrayStore.get(objectId)
87
+ if object then return @showView(object, config)
88
+
89
+ $(config.arrayStore).one 'objects_added', (e, data) =>
90
+ object = config.arrayStore.get(objectId)
91
+ if object then return @showView(object, config)
92
+
93
+ console.log "object #{objectId} is not in the list"
94
+
95
+
96
+
97
+
98
+
@@ -0,0 +1,50 @@
1
+ # -----------------------------------------------------------------------------
2
+ # UTILS
3
+
4
+ # _last(array)
5
+ @_last = (array) -> array[array.length - 1]
6
+
7
+ # _first(array)
8
+ @_first = (array) -> array[0]
9
+
10
+ # _firstNonEmptyValue(hash)
11
+ @_firstNonEmptyValue = (o) -> ((return v if k[0] != '_' and v and v != '') for k, v of o) ; return null
12
+
13
+ # _stripHtml(string)
14
+ @_stripHtml = (string) -> String(string).replace(/<\/?[^>]+(>|$)/g, "")
15
+
16
+ # _escapeHtml(string)
17
+ @_entityMap = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': '&quot;', "'": '&#39;', "/": '&#x2F;' }
18
+ @_escapeHtml = (string) -> String(string).replace /[&<>"'\/]/g, (s) -> _entityMap[s]
19
+
20
+ # String.titleize
21
+ if typeof String.prototype.startsWith != 'function'
22
+ String.prototype.titleize = () -> return this.replace(/_/g, ' ').replace(/\b./g, ((m) -> m.toUpperCase()))
23
+
24
+ # String.reverse
25
+ if typeof String.prototype.reverse != 'function'
26
+ String.prototype.reverse = (str) -> return this.split("").reverse().join("")
27
+
28
+ # String.startsWith
29
+ if typeof String.prototype.startsWith != 'function'
30
+ String.prototype.startsWith = (str) -> return this.slice(0, str.length) == str
31
+
32
+ # String.endsWith
33
+ if typeof String.prototype.endsWith != 'function'
34
+ String.prototype.endsWith = (str) -> return this.slice(this.length - str.length, this.length) == str
35
+
36
+ # -----------------------------------------------------------------------------
37
+ # HELPERS
38
+
39
+ # Helps to figure out how many list items fits screen height
40
+ @_itemsPerScreen = -> itemHeight = 60 ; return Math.ceil($(window).height() / itemHeight)
41
+ @_itemsPerPageRequest = _itemsPerScreen() * 2
42
+
43
+ # Check if running on mobile
44
+ @_isMobile = -> $(window).width() < 760
45
+
46
+ # -----------------------------------------------------------------------------
47
+
48
+
49
+
50
+
@@ -0,0 +1,121 @@
1
+ # -----------------------------------------------------------------------------
2
+ # VIEW
3
+ # -----------------------------------------------------------------------------
4
+ class @View
5
+ _renderForm: ->
6
+ @form?.destroy()
7
+ @form = new Form(@object, @config)
8
+
9
+ unless @config.disableDelete or @config.objectStore or @_isNew()
10
+ @$deleteBtn =$ "<a href='#' class='delete'>Delete</a>"
11
+ @$deleteBtn.on 'click', (e) => @onDelete(e)
12
+ @form.$el.append @$deleteBtn
13
+
14
+ @$el.append @form.$el
15
+
16
+ _render: ->
17
+ title = @title
18
+ title ?= @object[@config.itemTitleField] if @config.itemTitleField
19
+ title ?= _firstNonEmptyValue(@object)
20
+ if title == "" then title = "No Title"
21
+
22
+ # NOTE: remove html tags from title to do not break layout
23
+ titleText = $("<div>#{ title }</div>").text()
24
+ @$title.html(titleText)
25
+
26
+ @_renderForm()
27
+
28
+ _initializeFormPlugins: ->
29
+ # NOTE: we might need a callback here to workaround plugins blink issue, by setting
30
+ # form opacity to 0 and then fading to 1 after plugins are ready.
31
+ @form.initializePlugins()
32
+ @config.onViewShow?(@)
33
+
34
+ _updateObject: (value) ->
35
+ @$el.addClass('view-saving')
36
+ @store.update @object._id, value,
37
+ onSuccess: (object) =>
38
+ # TODO: add a note here for this line, it's not obvious why it's here,
39
+ # looks like some logic related to title update
40
+ if @config.arrayStore then @title = null
41
+
42
+ formScrollPosition = @form.$el.scrollTop()
43
+ @_render()
44
+ @_initializeFormPlugins()
45
+ @form.$el.scrollTop(formScrollPosition)
46
+
47
+ setTimeout ( => @$el.removeClass('view-saving') ), 250
48
+ onError: (errors) =>
49
+ @validationErrors(errors)
50
+ setTimeout ( => @$el.removeClass('view-saving') ), 250
51
+
52
+ _createObject: (value) ->
53
+ @$el.addClass('view-saving')
54
+ @store.push value,
55
+ onSuccess: (object) =>
56
+ # NOTE: jump to the newely created item, added to the top of the list by default
57
+ location.hash = "#/#{ @closePath }/view/#{ object._id }"
58
+ onError: (errors) =>
59
+ @validationErrors(errors)
60
+ setTimeout ( => @$el.removeClass('view-saving') ), 250
61
+
62
+ _isNew: -> not @object
63
+
64
+ constructor: (@module, @config, @closePath, @object, @title) ->
65
+ @store = @config.arrayStore ? @config.objectStore
66
+
67
+ @$el =$ "<section class='view #{ @module.name }'>"
68
+ @$el.hide()
69
+
70
+ @$header =$ "<header></header>"
71
+ @$title =$ "<div class='title'></div>"
72
+ @$header.append @$title
73
+
74
+ @$closeBtn =$ "<a href='#/#{ @closePath }' class='close silent'>Close</a>"
75
+ @$closeBtn.on 'click', (e) => @onClose(e)
76
+ @$header.append @$closeBtn
77
+
78
+ unless @config.disableSave
79
+ @$saveBtn =$ "<a href='#' class='save'>Save</a>"
80
+ @$saveBtn.on 'click', (e) => @onSave(e)
81
+ @$header.append @$saveBtn
82
+
83
+ @$el.append @$header
84
+
85
+ @_render()
86
+
87
+ show: (animate, callback) ->
88
+ if animate
89
+ @$el.fadeIn($.fx.speeds._default, => @_initializeFormPlugins() ; callback?())
90
+ else
91
+ @$el.show 0, => @_initializeFormPlugins() ; callback?()
92
+
93
+ destroy: ->
94
+ @form?.destroy()
95
+ @$el.remove()
96
+
97
+ onClose: (e) ->
98
+ @module.unselectActiveListItem()
99
+ @$el.fadeOut $.fx.speeds._default, => @destroy()
100
+
101
+ onSave: (e) ->
102
+ e.preventDefault()
103
+ serializedObj = @form.serialize()
104
+ if @object then @_updateObject(serializedObj) else @_createObject(serializedObj)
105
+
106
+ onDelete: (e) ->
107
+ e.preventDefault()
108
+ if confirm("Are you sure?")
109
+ @store.remove(@object._id)
110
+ @$el.fadeOut $.fx.speeds._default, =>
111
+ window._skipHashchange = true
112
+ location.hash = "#/#{ @closePath }"
113
+ @destroy()
114
+
115
+ validationErrors: (errors) ->
116
+ @form.showValidationErrors(errors)
117
+
118
+
119
+
120
+
121
+
@@ -0,0 +1,205 @@
1
+ # -----------------------------------------------------------------------------
2
+ # FORM
3
+ # Generates form based on provided configuration schema.
4
+ # If schema is not provided generates default form based on object keys.
5
+ # -----------------------------------------------------------------------------
6
+
7
+ @_chrFormInputs ?= {}
8
+
9
+ class @Form
10
+ constructor: (@object, @config) ->
11
+ @groups = []
12
+ @inputs = {}
13
+ @$el = $(@config.rootEl || '<form>')
14
+ @schema = @_getSchema()
15
+ @isRemoved = false
16
+
17
+ @_buildSchema(@schema, @$el)
18
+ @_addNestedFormRemoveButton()
19
+
20
+ #
21
+ # SCHEMA
22
+ #
23
+
24
+ _getSchema: ->
25
+ schema = @config.formSchema
26
+ if @object
27
+ schema ?= @_generateDefaultSchema()
28
+ return schema
29
+
30
+ _generateDefaultSchema: ->
31
+ schema = {}
32
+ for key, value of @object
33
+ schema[key] = @_generateDefaultInputConfig(key, value)
34
+ return schema
35
+
36
+ _generateDefaultInputConfig: (fieldName, value) ->
37
+ config = {}
38
+
39
+ if fieldName[0] == '_'
40
+ config.type = 'hidden'
41
+
42
+ else if value in [ true, false ]
43
+ config.type = 'checkbox'
44
+
45
+ else if value
46
+
47
+ if value.hasOwnProperty('url')
48
+ config.type = 'file'
49
+
50
+ else if value.length > 60
51
+ config.type = 'text'
52
+
53
+ return config
54
+
55
+ #
56
+ # INPUTS
57
+ #
58
+
59
+ _buildSchema: (schema, $el) ->
60
+ for fieldName, config of schema
61
+ if config.type == 'group'
62
+ group = @_generateInputsGroup(fieldName, config)
63
+ $el.append group.$el
64
+ else
65
+ input = @_generateInput(fieldName, config)
66
+ $el.append input.$el
67
+
68
+ _generateInputsGroup: (klassName, groupConfig) ->
69
+ $group =$ """<div class='group #{ klassName }' />"""
70
+ if groupConfig.inputs
71
+ @_buildSchema(groupConfig.inputs, $group)
72
+ group = { $el: $group, klassName: klassName, onInitialize: groupConfig.onInitialize }
73
+ @groups.push group
74
+ return group
75
+
76
+ _generateInput: (fieldName, inputConfig) ->
77
+ if @object
78
+ value = @object[fieldName]
79
+ else
80
+ value = inputConfig.default
81
+
82
+ value ?= ''
83
+
84
+ inputName = inputConfig.name || fieldName
85
+ input = @_renderInput(inputName, inputConfig, value)
86
+ @inputs[fieldName] = input
87
+ return input
88
+
89
+ _renderInput: (name, config, value) ->
90
+ inputConfig = $.extend {}, config
91
+
92
+ inputConfig.label ?= @_titleizeLabel(name)
93
+ inputConfig.type ?= 'string'
94
+ inputConfig.klass ?= 'stacked'
95
+ inputConfig.klassName = name
96
+
97
+ inputClass = _chrFormInputs[inputConfig.type]
98
+ inputClass ?= _chrFormInputs['string']
99
+
100
+ inputName = if @config.namePrefix then "#{ @config.namePrefix }[#{ name }]" else "[#{ name }]"
101
+
102
+ # add prefix for nested form inputs
103
+ if inputConfig.type == 'form'
104
+ inputConfig.namePrefix = inputName.replace("[#{ name }]", "[#{name}_attributes]")
105
+ else
106
+ inputConfig.namePrefix = @config.namePrefix
107
+
108
+ return new inputClass(inputName, value, inputConfig, @object)
109
+
110
+ _titleizeLabel: (value) ->
111
+ value.titleize().replace('Id', 'ID')
112
+
113
+ #
114
+ # NESTED
115
+ #
116
+
117
+ _addNestedFormRemoveButton: ->
118
+ if @config.removeButton
119
+ # add special hidden input to the form
120
+ fieldName = '_destroy'
121
+ input = @_renderInput(fieldName, { type: 'hidden' }, false)
122
+ @inputs[fieldName] = input
123
+ @$el.append input.$el
124
+ # add button
125
+ @$removeButton =$ """<a href='#' class='nested-form-delete'>Delete</a>"""
126
+ @$el.append @$removeButton
127
+ # handle click event
128
+ @$removeButton.on 'click', (e) =>
129
+ e.preventDefault()
130
+ if confirm('Are you sure?')
131
+ input.updateValue('true')
132
+ @$el.hide()
133
+ @isRemoved = true
134
+ @config.onRemove?(this)
135
+
136
+ _forms: ->
137
+ forms = [ @ ]
138
+ addNestedForms = (form) ->
139
+ for name, input of form.inputs
140
+ if input.config.type == 'form'
141
+ forms = forms.concat(input.forms)
142
+ addNestedForms(form) for form in input.forms
143
+ addNestedForms(@)
144
+
145
+ return forms
146
+
147
+ #
148
+ # PUBLIC
149
+ #
150
+
151
+ destroy: ->
152
+ @$el.remove()
153
+
154
+ serialize: (obj={}) ->
155
+ # NOTE: this serializes everything except file inputs
156
+ obj[input.name] = input.value for input in @$el.serializeArray()
157
+
158
+ for form in @_forms()
159
+ # NOTE: serialize file inputs for all forms (root and nested)
160
+ for name, input of form.inputs
161
+ if input.config.type == 'file' or input.config.type == 'image'
162
+ file = input.$input.get()[0].files[0]
163
+ obj["__FILE__#{ input.name }"] = file
164
+ # NOTE: when no file uploaded and no file selected, send
165
+ # remove flag so carrierwave does not catch _old_ value
166
+ if not file and not input.filename then obj[input.removeName()] = 'true'
167
+
168
+ # NOTE: remove fields with ignoreOnSubmission
169
+ for name, input of form.inputs
170
+ if input.config.ignoreOnSubmission
171
+ delete obj[name]
172
+
173
+ return obj
174
+
175
+ hash: (hash={}) ->
176
+ for name, input of @inputs
177
+ input.hash(hash)
178
+ return hash
179
+
180
+ initializePlugins: ->
181
+ for group in @groups
182
+ group.onInitialize?(@, group)
183
+
184
+ for name, input of @inputs
185
+ input.initialize()
186
+
187
+ showValidationErrors: (errors) ->
188
+ @hideValidationErrors()
189
+ for inputName, messages of errors
190
+ input = @inputs[inputName]
191
+ firstMessage = messages[0]
192
+ input.showErrorMessage(firstMessage)
193
+
194
+ hideValidationErrors: ->
195
+ for inputName, input of @inputs
196
+ input.hideErrorMessage()
197
+
198
+ updateValues: (object) ->
199
+ for name, value of object
200
+ if @inputs[name]
201
+ @inputs[name].updateValue(value, object)
202
+
203
+
204
+
205
+