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.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/CONTRIBUTING.md +24 -0
- data/Gemfile +3 -0
- data/LICENSE.md +21 -0
- data/README.md +18 -0
- data/Rakefile +3 -0
- data/app/assets/javascripts/chr.coffee +41 -0
- data/app/assets/javascripts/chr/core/_chr.coffee +89 -0
- data/app/assets/javascripts/chr/core/_item.coffee +85 -0
- data/app/assets/javascripts/chr/core/_list.coffee +154 -0
- data/app/assets/javascripts/chr/core/_listReorder.coffee +70 -0
- data/app/assets/javascripts/chr/core/_listScroll.coffee +23 -0
- data/app/assets/javascripts/chr/core/_listSearch.coffee +28 -0
- data/app/assets/javascripts/chr/core/_module.coffee +98 -0
- data/app/assets/javascripts/chr/core/_utils.coffee +50 -0
- data/app/assets/javascripts/chr/core/_view.coffee +121 -0
- data/app/assets/javascripts/chr/form/_form.coffee +205 -0
- data/app/assets/javascripts/chr/form/_inputCheckbox.coffee +70 -0
- data/app/assets/javascripts/chr/form/_inputColor.coffee +35 -0
- data/app/assets/javascripts/chr/form/_inputFile.coffee +82 -0
- data/app/assets/javascripts/chr/form/_inputHidden.coffee +41 -0
- data/app/assets/javascripts/chr/form/_inputList.coffee +142 -0
- data/app/assets/javascripts/chr/form/_inputSelect.coffee +59 -0
- data/app/assets/javascripts/chr/form/_inputString.coffee +87 -0
- data/app/assets/javascripts/chr/form/_inputText.coffee +23 -0
- data/app/assets/javascripts/chr/form/_nestedForm.coffee +164 -0
- data/app/assets/javascripts/chr/store/_store.coffee +104 -0
- data/app/assets/javascripts/chr/store/_storeRails.coffee +167 -0
- data/app/assets/javascripts/chr/vendor/jquery.scrollparent.js +14 -0
- data/app/assets/javascripts/chr/vendor/jquery.textarea_autosize.js +55 -0
- data/app/assets/javascripts/chr/vendor/jquery.typeahead.js +1782 -0
- data/app/assets/javascripts/chr/vendor/slip.js +804 -0
- data/app/assets/stylesheets/_chr.scss +7 -0
- data/app/assets/stylesheets/core/_icons.scss +124 -0
- data/app/assets/stylesheets/core/_list.scss +44 -0
- data/app/assets/stylesheets/core/_main.scss +89 -0
- data/app/assets/stylesheets/core/_responsive.scss +41 -0
- data/app/assets/stylesheets/form/_form.scss +50 -0
- data/app/assets/stylesheets/form/_input_checkbox.scss +87 -0
- data/app/assets/stylesheets/form/_input_color.scss +10 -0
- data/app/assets/stylesheets/form/_input_file.scss +28 -0
- data/app/assets/stylesheets/form/_input_list.scss +36 -0
- data/app/assets/stylesheets/form/_input_string.scss +8 -0
- data/app/assets/stylesheets/form/_input_text.scss +48 -0
- data/app/assets/stylesheets/form/_nested_form.scss +26 -0
- data/chr.gemspec +34 -0
- data/lib/chr.rb +15 -0
- data/lib/chr/engine.rb +5 -0
- data/lib/chr/version.rb +3 -0
- 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 = { "&": "&", "<": "<", ">": ">", '"': '"', "'": ''', "/": '/' }
|
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
|
+
|