-
-
Save johan--/613a0ba530c53de41e2b41228b330c44 to your computer and use it in GitHub Desktop.
formbuilder: nested fields for radios
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git index.html index.html | |
index a65e625..30503cf 100644 | |
--- index.html | |
+++ index.html | |
@@ -64,10 +64,20 @@ | |
"field_options": { | |
"options": [{ | |
"label": "Yes", | |
- "checked": false | |
+ "checked": false, | |
+ "nested_fields": true, | |
+ "fields": [{ | |
+ "label": "Start year", | |
+ "field_type": "number", | |
+ "required": true, | |
+ "field_options": {}, | |
+ "cid": "c15" | |
+ }] | |
}, { | |
"label": "No", | |
- "checked": false | |
+ "checked": false, | |
+ "nested_fields": false, | |
+ "fields": null | |
}], | |
"include_other_option": true | |
}, | |
diff --git src/scripts/fields/radio.coffee src/scripts/fields/radio.coffee | |
index 14dbfaf..fb88fe9 100644 | |
--- src/scripts/fields/radio.coffee | |
+++ src/scripts/fields/radio.coffee | |
@@ -9,6 +9,9 @@ Formbuilder.registerField 'radio', | |
<input type='radio' <%= rf.get(Formbuilder.options.mappings.OPTIONS)[i].checked && 'checked' %> onclick="javascript: return false;" /> | |
<%= rf.get(Formbuilder.options.mappings.OPTIONS)[i].label %> | |
</label> | |
+ <% if (rf.get(Formbuilder.options.mappings.OPTIONS)[i].nested_fields) { %> | |
+ <div class='fb-nested-<%= rf.cid %>-<%= i %>'></div> | |
+ <% } %> | |
</div> | |
<% } %> | |
@@ -36,10 +39,14 @@ Formbuilder.registerField 'radio', | |
# @todo | |
attrs.field_options.options = [ | |
label: "", | |
- checked: false | |
+ checked: false, | |
+ nested_fields: false, | |
+ fields: null | |
, | |
label: "", | |
- checked: false | |
+ checked: false, | |
+ nested_fields: false, | |
+ fields: null | |
] | |
attrs | |
\ No newline at end of file | |
diff --git src/scripts/main.coffee src/scripts/main.coffee | |
index 54aeb6b..b5df333 100644 | |
--- src/scripts/main.coffee | |
+++ src/scripts/main.coffee | |
@@ -1,11 +1,14 @@ | |
class FormbuilderModel extends Backbone.DeepModel | |
sync: -> # noop | |
- indexInDOM: -> | |
- $wrapper = $(".fb-field-wrapper").filter ( (_, el) => $(el).data('cid') == @cid ) | |
- $(".fb-field-wrapper").index $wrapper | |
+ indexInDOM: (view) -> | |
+ $wrapper = view.$el.find(".fb-field-wrapper").filter ( (_, el) => $(el).data('cid') == @cid ) | |
+ view.$el.find(".fb-field-wrapper").index $wrapper | |
is_input: -> | |
Formbuilder.inputFields[@get(Formbuilder.options.mappings.FIELD_TYPE)]? | |
+ toJSON: (options) -> | |
+ json = _.clone(@attributes) | |
+ | |
class FormbuilderCollection extends Backbone.Collection | |
initialize: -> | |
@@ -14,7 +17,7 @@ class FormbuilderCollection extends Backbone.Collection | |
model: FormbuilderModel | |
comparator: (model) -> | |
- model.indexInDOM() | |
+ model.indexInDOM(@view) | |
copyCidToModel: (model) -> | |
model.attributes.cid = model.cid | |
@@ -29,7 +32,7 @@ class ViewFieldView extends Backbone.View | |
'click .js-clear': 'clear' | |
initialize: (options) -> | |
- {@parentView} = options | |
+ {@builderView} = options | |
@listenTo @model, "change", @render | |
@listenTo @model, "destroy", @remove | |
@@ -38,17 +41,33 @@ class ViewFieldView extends Backbone.View | |
.data('cid', @model.cid) | |
.html(Formbuilder.templates["view/base#{if !@model.is_input() then '_non_input' else ''}"]({rf: @model})) | |
+ that = @ | |
+ model = @model | |
+ builderView = @builderView | |
+ if @model.attributes.field_options.options | |
+ _.each(@model.attributes.field_options.options, (e, i) -> | |
+ if e.nested_fields | |
+ if that.$responseFieldsView == undefined | |
+ that.$responseFieldsView = [] | |
+ if that.$responseFieldsView[i] == undefined | |
+ that.$responseFieldsView[i] = new ResponseFieldsView({builderView: builderView, bootstrapData: e.fields}) | |
+ e.fields = that.$responseFieldsView[i].collection | |
+ that.$responseFieldsView[i].setElement(that.$el.find(".fb-nested-#{model.cid}-#{i}")) | |
+ that.$responseFieldsView[i].render() | |
+ ) | |
+ | |
return @ | |
focusEditView: -> | |
- @parentView.createAndShowEditView(@model) | |
+ @builderView.createAndShowEditView(@model) | |
+ return false; | |
clear: (e) -> | |
e.preventDefault() | |
e.stopPropagation() | |
cb = => | |
- @parentView.handleFormUpdate() | |
+ @builderView.handleFormUpdate() | |
@model.destroy() | |
x = Formbuilder.options.CLEAR_FIELD_CONFIRM | |
@@ -65,7 +84,7 @@ class ViewFieldView extends Backbone.View | |
attrs = _.clone(@model.attributes) | |
delete attrs['id'] | |
attrs['label'] += ' Copy' | |
- @parentView.createField attrs, { position: @model.indexInDOM() + 1 } | |
+ @builderView.createField attrs, { position: @model.indexInDOM() + 1 } | |
class EditFieldView extends Backbone.View | |
@@ -127,6 +146,94 @@ class EditFieldView extends Backbone.View | |
forceRender: -> | |
@model.trigger('change') | |
+class ResponseFieldsView extends Backbone.View | |
+ initialize: (options) -> | |
+ {@builderView, @bootstrapData} = options | |
+ @collection = new FormbuilderCollection | |
+ @collection.view = @ | |
+ @collection.bind 'add', @addOne, @ | |
+ @collection.bind 'reset', @reset, @ | |
+ @collection.bind 'change', @builderView.handleFormUpdate, @builderView | |
+ @collection.bind 'destroy add reset', @hideShowNoResponseFields, @ | |
+ @collection.bind 'destroy', @builderView.ensureEditViewScrolled, @builderView | |
+ | |
+ render: -> | |
+ @$el.html Formbuilder.templates['partials/response_fields']() | |
+ @hideShowNoResponseFields() | |
+ @$responseFields = @$el.find('.fb-response-fields') | |
+ if @collection.isEmpty() then @collection.reset @bootstrapData else @addAll() | |
+ | |
+ reset: -> | |
+ @addAll() | |
+ | |
+ addAll: -> | |
+ @collection.each @addOne, @ | |
+ @setSortable() | |
+ | |
+ addOne: (responseField, _, options) -> | |
+ view = new ViewFieldView | |
+ model: responseField | |
+ builderView: @builderView | |
+ | |
+ ##### | |
+ # Calculates where to place this new field. | |
+ # | |
+ # Are we replacing a temporarily drag placeholder? | |
+ if options.$replaceEl? | |
+ options.$replaceEl.replaceWith view.render().el | |
+ | |
+ # Are we adding to the bottom? | |
+ else if !options.position? || options.position == -1 | |
+ @$responseFields.append view.render().el | |
+ | |
+ # Are we adding to the top? | |
+ else if options.position == 0 | |
+ @$responseFields.prepend view.render().el | |
+ | |
+ # Are we adding below an existing field? | |
+ else if ($replacePosition = @$el.find(".fb-field-wrapper").eq(options.position))[0] | |
+ $replacePosition.before view.render().el | |
+ | |
+ # Catch-all: add to bottom | |
+ else | |
+ @$responseFields.append view.render().el | |
+ @collection.sort() | |
+ | |
+ setSortable: -> | |
+ @$responseFields.sortable('destroy') if @$responseFields.hasClass('ui-sortable') | |
+ @$responseFields.sortable | |
+ forcePlaceholderSize: true | |
+ placeholder: 'sortable-placeholder' | |
+ stop: (e, ui) => | |
+ if ui.item.data('field-type') | |
+ rf = @collection.create Formbuilder.helpers.defaultFieldAttrs(ui.item.data('field-type')), {$replaceEl: ui.item} | |
+ @builderView.createAndShowEditView rf | |
+ | |
+ @builderView.handleFormUpdate() | |
+ return true | |
+ update: (e, ui) => | |
+ # ensureEditViewScrolled, unless we're updating from the draggable | |
+ @builderView.ensureEditViewScrolled() unless ui.item.data('field-type') | |
+ | |
+ @setDraggable() | |
+ | |
+ setDraggable: -> | |
+ $addFieldButtons = @builderView.$el.find("[data-field-type]") | |
+ | |
+ $addFieldButtons.draggable | |
+ connectToSortable: @builderView.$el.find('.fb-response-fields') | |
+ helper: => | |
+ $helper = $("<div class='response-field-draggable-helper' />") | |
+ $helper.css | |
+ width: @$responseFields.width() # hacky, won't get set without inline style | |
+ height: '80px' | |
+ | |
+ $helper | |
+ | |
+ hideShowNoResponseFields: -> | |
+ @$el.find(".fb-no-response-fields")[if @collection.length > 0 then 'hide' else 'show']() | |
+ | |
class BuilderView extends Backbone.View | |
SUBVIEWS: [] | |
@@ -145,16 +252,9 @@ class BuilderView extends Backbone.View | |
if selector? | |
@setElement $(selector) | |
- # Create the collection, and bind the appropriate events | |
- @collection = new FormbuilderCollection | |
- @collection.bind 'add', @addOne, @ | |
- @collection.bind 'reset', @reset, @ | |
- @collection.bind 'change', @handleFormUpdate, @ | |
- @collection.bind 'destroy add reset', @hideShowNoResponseFields, @ | |
- @collection.bind 'destroy', @ensureEditViewScrolled, @ | |
- | |
@render() | |
- @collection.reset(@bootstrapData) | |
+ @$responseFieldsView = new ResponseFieldsView({builderView: @, bootstrapData: @bootstrapData, el: '.fb-right'}) | |
+ @$responseFieldsView.render() | |
@bindSaveEvent() | |
bindSaveEvent: -> | |
@@ -170,19 +270,14 @@ class BuilderView extends Backbone.View | |
$(window).bind 'beforeunload', => | |
if @formSaved then undefined else Formbuilder.options.dict.UNSAVED_CHANGES | |
- reset: -> | |
- @$responseFields.html('') | |
- @addAll() | |
- | |
render: -> | |
@$el.html Formbuilder.templates['page']() | |
# Save jQuery objects for easy use | |
@$fbLeft = @$el.find('.fb-left') | |
- @$responseFields = @$el.find('.fb-response-fields') | |
+ @$fbRight = @$el.find('.fb-right') | |
@bindWindowScrollEvent() | |
- @hideShowNoResponseFields() | |
# Render any subviews (this is an easy way of extending the Formbuilder) | |
new subview({parentView: @}).render() for subview in @SUBVIEWS | |
@@ -193,7 +288,7 @@ class BuilderView extends Backbone.View | |
$(window).on 'scroll', => | |
return if @$fbLeft.data('locked') == true | |
newMargin = Math.max(0, $(window).scrollTop() - @$el.offset().top) | |
- maxMargin = @$responseFields.height() | |
+ maxMargin = @$fbRight.height() | |
@$fbLeft.css | |
'margin-top': Math.min(maxMargin, newMargin) | |
@@ -209,72 +304,6 @@ class BuilderView extends Backbone.View | |
if target == '#editField' && !@editView && (first_model = @collection.models[0]) | |
@createAndShowEditView(first_model) | |
- addOne: (responseField, _, options) -> | |
- view = new ViewFieldView | |
- model: responseField | |
- parentView: @ | |
- | |
- ##### | |
- # Calculates where to place this new field. | |
- # | |
- # Are we replacing a temporarily drag placeholder? | |
- if options.$replaceEl? | |
- options.$replaceEl.replaceWith(view.render().el) | |
- | |
- # Are we adding to the bottom? | |
- else if !options.position? || options.position == -1 | |
- @$responseFields.append view.render().el | |
- | |
- # Are we adding to the top? | |
- else if options.position == 0 | |
- @$responseFields.prepend view.render().el | |
- | |
- # Are we adding below an existing field? | |
- else if ($replacePosition = @$responseFields.find(".fb-field-wrapper").eq(options.position))[0] | |
- $replacePosition.before view.render().el | |
- | |
- # Catch-all: add to bottom | |
- else | |
- @$responseFields.append view.render().el | |
- | |
- setSortable: -> | |
- @$responseFields.sortable('destroy') if @$responseFields.hasClass('ui-sortable') | |
- @$responseFields.sortable | |
- forcePlaceholderSize: true | |
- placeholder: 'sortable-placeholder' | |
- stop: (e, ui) => | |
- if ui.item.data('field-type') | |
- rf = @collection.create Formbuilder.helpers.defaultFieldAttrs(ui.item.data('field-type')), {$replaceEl: ui.item} | |
- @createAndShowEditView(rf) | |
- | |
- @handleFormUpdate() | |
- return true | |
- update: (e, ui) => | |
- # ensureEditViewScrolled, unless we're updating from the draggable | |
- @ensureEditViewScrolled() unless ui.item.data('field-type') | |
- | |
- @setDraggable() | |
- | |
- setDraggable: -> | |
- $addFieldButtons = @$el.find("[data-field-type]") | |
- | |
- $addFieldButtons.draggable | |
- connectToSortable: @$responseFields | |
- helper: => | |
- $helper = $("<div class='response-field-draggable-helper' />") | |
- $helper.css | |
- width: @$responseFields.width() # hacky, won't get set without inline style | |
- height: '80px' | |
- | |
- $helper | |
- | |
- addAll: -> | |
- @collection.each @addOne, @ | |
- @setSortable() | |
- | |
- hideShowNoResponseFields: -> | |
- @$el.find(".fb-no-response-fields")[if @collection.length > 0 then 'hide' else 'show']() | |
- | |
addField: (e) -> | |
field_type = $(e.currentTarget).data('field-type') | |
@createField Formbuilder.helpers.defaultFieldAttrs(field_type) | |
@@ -287,6 +316,8 @@ class BuilderView extends Backbone.View | |
createAndShowEditView: (model) -> | |
$responseFieldEl = @$el.find(".fb-field-wrapper").filter( -> $(@).data('cid') == model.cid ) | |
$responseFieldEl.addClass('editing').siblings('.fb-field-wrapper').removeClass('editing') | |
+ $responseFieldEl.parents('.fb-field-wrapper').removeClass('editing') | |
+ $responseFieldEl.find('.fb-field-wrapper').removeClass('editing') | |
if @editView | |
if @editView.model.cid is model.cid | |
@@ -313,7 +344,7 @@ class BuilderView extends Backbone.View | |
scrollLeftWrapper: ($responseFieldEl) -> | |
@unlockLeftWrapper() | |
return unless $responseFieldEl[0] | |
- $.scrollWindowTo ((@$el.offset().top + $responseFieldEl.offset().top) - @$responseFields.offset().top), 200, => | |
+ $.scrollWindowTo ((@$el.offset().top + $responseFieldEl.offset().top) - @$fbRight.offset().top), 200, => | |
@lockLeftWrapper() | |
lockLeftWrapper: -> | |
@@ -331,8 +362,7 @@ class BuilderView extends Backbone.View | |
return if @formSaved | |
@formSaved = true | |
@saveFormButton.attr('disabled', true).text(Formbuilder.options.dict.ALL_CHANGES_SAVED) | |
- @collection.sort() | |
- payload = JSON.stringify fields: @collection.toJSON() | |
+ payload = JSON.stringify fields: @$responseFieldsView.collection | |
if Formbuilder.options.HTTP_ENDPOINT then @doAjaxSave(payload) | |
@formBuilder.trigger 'save', payload | |
diff --git src/styles/formbuilder.styl src/styles/formbuilder.styl | |
index 62925db..0fea3bc 100644 | |
--- src/styles/formbuilder.styl | |
+++ src/styles/formbuilder.styl | |
@@ -136,6 +136,11 @@ $editing-border = darken($editing-bg, 7%) | |
} | |
} | |
+div[class^="fb-nested"] > .fb-response-fields { | |
+ padding-bottom: 0px; | |
+ min-height: 80px; | |
+} | |
+ | |
.fb-response-fields { | |
padding-bottom: 150px; | |
} | |
diff --git src/templates/edit/options.html src/templates/edit/options.html | |
index 6a7ee08..929d8e3 100644 | |
--- src/templates/edit/options.html | |
+++ src/templates/edit/options.html | |
@@ -12,6 +12,10 @@ | |
<input type="text" data-rv-input="option:label" class='option-label-input' /> | |
<a class="js-add-option <%= Formbuilder.options.BUTTON_CLASS %>" title="Add Option"><i class='fa fa-plus-circle'></i></a> | |
<a class="js-remove-option <%= Formbuilder.options.BUTTON_CLASS %>" title="Remove Option"><i class='fa fa-minus-circle'></i></a> | |
+ <label> | |
+ <input type='checkbox' data-rv-checked='option:nested_fields' /> | |
+ Nested fields? | |
+ </label> | |
</div> | |
<% if (typeof includeOther !== 'undefined'){ %> | |
diff --git src/templates/partials/right_side.html src/templates/partials/right_side.html | |
index 7164c13..5b42836 100644 | |
--- src/templates/partials/right_side.html | |
+++ src/templates/partials/right_side.html | |
@@ -1,4 +1,2 @@ | |
<div class='fb-right'> | |
- <div class='fb-no-response-fields'>No response fields</div> | |
- <div class='fb-response-fields'></div> | |
</div> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment