/**
* The Form Builder Component
*
* @module aui-form-builder
*/
var CSS_EDIT_LAYOUT_BUTTON = A.getClassName('form', 'builder', 'edit', 'layout', 'button'),
CSS_EMPTY_COL_ADD_BUTTON =
A.getClassName('form', 'builder', 'field', 'list', 'add', 'button'),
CSS_FIELD = A.getClassName('form', 'builder', 'field'),
CSS_HEADER = A.getClassName('form', 'builder', 'header'),
CSS_HEADER_TITLE = A.getClassName('form', 'builder', 'header', 'title'),
CSS_LAYOUT = A.getClassName('form', 'builder', 'layout'),
CSS_PAGE_HEADER = A.getClassName('form', 'builder', 'pages', 'header'),
CSS_PAGES = A.getClassName('form', 'builder', 'pages'),
CSS_TABS = A.getClassName('form', 'builder', 'tabs');
/**
* A base class for `A.FormBuilder`.
*
* @class A.FormBuilder
* @extends A.Widget
* @uses A.FormBuilderFieldTypes, A.FormBuilderLayoutBuilder
* @param {Object} config Object literal specifying widget configuration
* properties.
* @constructor
*/
A.FormBuilder = A.Base.create('form-builder', A.Widget, [
A.FormBuilderFieldTypes,
A.FormBuilderLayoutBuilder
], {
TPL_EDIT_LAYOUT_BUTTON: '<div class="' + CSS_EDIT_LAYOUT_BUTTON + '">' +
'<a>{editLayout}</a></div>',
TPL_HEADER: '<div class="' + CSS_HEADER + '">' +
'<div class="' + CSS_HEADER_TITLE + '">{formTitle}</div>' +
'</div>',
TPL_LAYOUT: '<div class="' + CSS_LAYOUT + '" ></div>',
TPL_PAGE_HEADER: '<div class="' + CSS_PAGE_HEADER + '" ></div>',
TPL_PAGES: '<div class="' + CSS_PAGES + '" ></div>',
TPL_TABVIEW: '<div class="' + CSS_TABS + '"></div>',
_fieldsChangeHandles: [],
/**
* Construction logic executed during the `A.FormBuilder`
* instantiation. Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function() {
this._fieldToolbar = new A.FormBuilderFieldToolbar(this.get('fieldToolbarConfig'));
this._eventHandles = [
this.after('layoutsChange', A.bind(this._afterLayoutsChange, this)),
this.after('layout:valueChange', this._afterLayoutChange),
this.after('layout:rowsChange', this._afterLayoutRowsChange),
this.after('layout-row:colsChange', this._afterLayoutColsChange)
];
A.Array.invoke(this.get('layouts'), 'addTarget', this);
this._addFieldsChangeListener(this.get('layouts'));
this._checkLayoutsLastRow();
},
/**
* Renders the `A.FormBuilder` UI. Lifecycle.
*
* @method renderUI
* @protected
*/
renderUI: function() {
this.getActiveLayout().addTarget(this);
this._renderContentBox();
this._renderEmptyColumns();
},
/**
* Bind the events for the `A.FormBuilder` UI. Lifecycle.
*
* @method bindUI
* @protected
*/
bindUI: function() {
var boundingBox = this.get('boundingBox'),
fieldToolbar = this._fieldToolbar,
pages = this.get('pages');
this._eventHandles.push(
boundingBox.delegate('click', this._onClickAddField, '.' + CSS_EMPTY_COL_ADD_BUTTON, this),
fieldToolbar.after('destroy', A.bind(this._afterDestroyFieldToolbar, this)),
pages.on('add', A.bind(this._addPage, this)),
pages.on('remove', A.bind(this._removeLayout, this)),
pages.after('activePageNumberChange', A.bind(this._afterActivePageNumberChange, this)),
pages.after('updatePageContent', A.bind(this._afterUpdatePageContentChange, this))
);
this._fieldToolbarHandles = [
this.get('contentBox').on('focus', A.bind(this._onFocus, this))
];
},
/**
* Syncs the UI. Lifecycle.
*
* @method syncUI
* @protected
*/
syncUI: function() {
this._updateUniqueFieldType();
},
/**
* Destructor lifecycle implementation for the `A.FormBuilder` class.
* Lifecycle.
*
* @method destructor
* @protected
*/
destructor: function() {
if (this._fieldSettingsModal) {
this._fieldSettingsModal.destroy();
}
if (this.get('pages')) {
this.get('pages').destroy();
}
(new A.EventHandle(this._eventHandles)).detach();
},
/**
* Adds a nested field to the given field.
*
* @method addNestedField
* @param {A.FormBuilderFieldBase} field
*/
addNestedField: function(field) {
this._newFieldContainer = field;
this.showFieldsPanel();
},
/**
* Opens the settings panel for editing the given field.
*
* @method editField
* @param {A.FormBuilderFieldBase} field
*/
editField: function(field) {
var fieldType = this.findTypeOfField(field);
this.showFieldSettingsPanel(field, fieldType.get('label'));
},
/**
* Returns the active `LayoutPage`.
*
* @method getActiveLayout
* @return {A.LayoutPage}
*/
getActiveLayout: function() {
return this.get('layouts')[this._getActiveLayoutIndex()];
},
/**
* Returns the row of the given field.
*
* @method getFieldRow
* @param {A.FormField} field
* @return {Node} The row where is the field parameter
*/
getFieldRow: function(field) {
return field.get('content').ancestor('.layout-row');
},
/**
* Removes the given field from the form builder.
*
* @method removeField
* @param {A.FormBuilderFieldBase} field
*/
removeField: function(field) {
var col,
parentField,
row,
nestedFieldsNode = field.get('content').ancestor('.form-builder-field-nested');
this._handleRemoveEvent(field);
if (nestedFieldsNode) {
parentField = nestedFieldsNode.ancestor('.form-builder-field').getData('field-instance');
parentField.removeNestedField(field);
this.getActiveLayout().normalizeColsHeight(new A.NodeList(this.getFieldRow(parentField)));
}
else {
col = field.get('content').ancestor('.col').getData('layout-col');
row = this.getFieldRow(field);
col.get('value').removeField(field);
this.getActiveLayout().normalizeColsHeight(new A.NodeList(row));
}
this._updateUniqueFieldType();
},
/**
* Shows the settings panel for the given field.
*
* @method showFieldSettingsPanel
* @param {A.FormField} field
* @param {String} typeName The name of the field type.
*/
showFieldSettingsPanel: function(field, typeName) {
if (!this._fieldSettingsModal) {
this._fieldSettingsModal = new A.FormBuilderSettingsModal();
this._fieldSettingsModal.after('hide', A.bind(this._afterFieldSettingsModalHide, this));
this._fieldSettingsModal.after('save', A.bind(this._afterFieldSettingsModalSave, this));
}
this._fieldSettingsModal.show(field, typeName);
},
/**
* Attach the `fieldsChange` event listener from all layouts on the
* `layouts` attribute.
*
* @method _addFieldsChangeListener
* @protected
*/
_addFieldsChangeListener: function(layouts) {
var i;
for(i = 0; i < layouts.length; i++) {
this._fieldsChangeHandles.push(
layouts[i].after(
'form-builder-field-list:fieldsChange',
A.bind(this._afterFieldsChange, this)
)
);
}
},
/**
* Adds a field into field's nested list and normalizes the columns height.
*
* @method _addNestedField
* @param {A.FormField} field The Field with nested list that will receive the field
* @param {A.FormField} nested Field to add as nested
* @param {Number} index The position where the nested field should be added
* @protected
*/
_addNestedField: function(field, nested, index) {
field.addNestedField(index, nested);
},
/**
* Adds a new page on form builder.
*
* @method _addPage
* @protected
*/
_addPage: function() {
var layouts = this.get('layouts');
var newLayout = new A.Layout({
rows: [
new A.LayoutRow()
]
});
layouts.push(newLayout);
this.set('layouts', layouts);
},
/**
* Fired after the `activePageNumber` change.
*
* @method _afterActivePageNumberChange
* @protected
*/
_afterActivePageNumberChange: function(event) {
var layouts = this.get('layouts'),
activeLayout = layouts[event.newVal - 1];
this._updatePageContent(activeLayout);
},
/**
* Fired after the Field Toolbar be destroyed.
*
* @method _afterDestroyFieldToolbar
* @protected
*/
_afterDestroyFieldToolbar: function() {
(new A.EventHandle(this._fieldToolbarHandles)).detach();
},
/**
* Fired when the field settings modal is hidden.
*
* @method _afterFieldSettingsModalHide
* @protected
*/
_afterFieldSettingsModalHide: function() {
this._newFieldContainer = null;
},
/**
* Fired when the field settings modal is saved.
*
* @method _afterFieldSettingsModalSave
* @param {EventFacade} event
* @protected
*/
_afterFieldSettingsModalSave: function(event) {
var field = event.field;
if (this._newFieldContainer) {
if (A.instanceOf(this._newFieldContainer.get('value'), A.FormBuilderFieldList)) {
this._newFieldContainer.get('value').addField(field);
this._newFieldContainer.set('removable', false);
}
else {
this._addNestedField(
this._newFieldContainer,
field,
this._newFieldContainer.get('nestedFields').length
);
}
this._newFieldContainer = null;
}
else {
this._handleEditEvent(field);
}
this.getActiveLayout().normalizeColsHeight(new A.NodeList(field.get('content').ancestor('.layout-row')));
this._handleCreateEvent(field);
this.disableUniqueFieldType(field);
},
/**
* Fires after layouts changes.
*
* @method _afterLayoutsChange
* @param {EventFacade} event
* @protected
*/
_afterLayoutsChange: function(event) {
var pages;
A.Array.invoke(event.prevVal, 'removeTarget', this);
A.Array.invoke(event.newVal, 'addTarget', this);
this._removeFieldsChangeListener(event.prevVal);
this._addFieldsChangeListener(event.newVal);
this._updatePageContent(this.get('layouts')[0]);
this._updateUniqueFieldType();
if (this.get('rendered')) {
pages = this.get('pages');
pages.set('activePageNumber', 1);
pages.set('pagesQuantity', this.get('layouts').length);
}
this._checkLayoutsLastRow();
},
/**
* Fired after the `layout-row:colsChange` event is triggered.
*
* @method _afterLayoutColsChange
* @protected
*/
_afterLayoutColsChange: function() {
this._updateUniqueFieldType();
},
/**
* Fired after the `layout:rowsChange` event is triggered.
*
* @method _afterLayoutRowsChange
* @param {EventFacade} event
* @protected
*/
_afterLayoutRowsChange: function(event) {
var rows = event.newVal;
for (var i = 0; i < rows.length; i++) {
rows[i].set('removable', true);
}
this._renderEmptyColumns();
this._updateUniqueFieldType();
this._checkLastRow(event.target);
},
/**
* Fired after the `activePageNumber` change.
*
* @method _afterUpdatePageContentChange
* @param {EventFacade} event
* @protected
*/
_afterUpdatePageContentChange: function(event) {
var layouts = this.get('layouts'),
activeLayout = layouts[event.newVal - 1];
this._updatePageContent(activeLayout);
},
/**
* Fired after the `form-builder-field-list:fieldsChange` event is triggered.
*
* @method _afterFieldsChange
* @param {EventFacade} event
* @protected
*/
_afterFieldsChange: function(event) {
this._checkLastRow(event.currentTarget);
},
/**
* Executes the '_checkLastRow' funciton for each layouts on 'layouts' attribute.
*
* @method _checkLayoutsLastRow
* @protected
*/
_checkLayoutsLastRow: function() {
this.get('layouts').forEach(this._checkLastRow, this);
},
/**
/**
* Fire event of create a field.
*
* @method _getActiveLayoutIndex
* @protected
*/
_getActiveLayoutIndex: function() {
return this.get('rendered') ? this.get('pages').get('activePageNumber') - 1 : 0;
},
/**
* Form Builder Pages instance initializer. Receives a custom
* object of configurations or using default configurations instead.
*
* @method _getPageManagerInstance
* @param {Object} config
* @return {A.FormBuilderPages}
* @protected
*/
_getPageManagerInstance: function(config) {
var contentBox = this.get('contentBox');
if (!this._pageManager) {
this._pageManager = new A.FormBuilderPageManager(A.merge({
pageHeader: contentBox.one('.' + CSS_PAGE_HEADER),
pagesQuantity: this.get('layouts').length,
paginationContainer: contentBox.one('.' + CSS_PAGES),
tabviewContainer: contentBox.one('.' + CSS_TABS)
}, config));
}
return this._pageManager;
},
/**
* Fire event of create a field.
*
* @method _handleCreateEvent
* @param {A.FormBuilderFieldBase} field
* @protected
*/
_handleCreateEvent: function(field) {
this.fire('create', {
field: field
});
},
/**
* Fire event of edit a field.
*
* @method _handleEditEvent
* @param {A.FormBuilderFieldBase} field
* @protected
*/
_handleEditEvent: function(field) {
this.fire('edit', {
field: field
});
},
/**
* Fire event of remove a field.
*
* @method _handleRemoveEvent
* @param {A.FormBuilderFieldBase} field
* @protected
*/
_handleRemoveEvent: function(field) {
this.fire('remove', {
field: field
});
},
/**
* Turns the given column into an empty form builder column.
*
* @method _makeEmptyFieldList
* @param {A.LayoutCol} col
* @protected
*/
_makeEmptyFieldList: function(col) {
var instance = this;
col.set('value', new A.FormBuilderFieldList({
strings: instance.get('strings')
}));
},
/**
* Fired when the button for adding a new field is clicked.
*
* @method _onClickAddField
* @param {EventFacade} event
* @protected
*/
_onClickAddField: function(event) {
this._openNewFieldPanel(event.currentTarget);
},
/**
* Fired when some node is focused inside content box.
*
* @method _onFocus
* @param {EventFacade} event
* @protected
*/
_onFocus: function(event) {
var fieldContainer,
target = event.target;
if (target.hasClass(CSS_FIELD)) {
fieldContainer = target;
}
else {
fieldContainer = target.ancestor('.' + CSS_FIELD);
}
if (fieldContainer && !this._fieldToolbar.get('disabled')) {
if (!fieldContainer.contains(this._fieldToolbar._toolbar)) {
this._fieldToolbar.close();
this._fieldToolbar.addForField(fieldContainer.getData('field-instance'));
}
}
else {
this._fieldToolbar.remove();
}
},
/**
* Opens a panel to select a new field type.
*
* @method _openNewFieldPanel
* @param {Node} target
* @protected
*/
_openNewFieldPanel: function(target) {
this._newFieldContainer = target.ancestor('.col').getData('layout-col');
this.showFieldsPanel();
},
/**
* Detach the `fieldsChange` event listener from all layouts on the
* `layouts` attribute.
*
* @method _removeFieldsChangeListener
* @protected
*/
_removeFieldsChangeListener: function() {
(new A.EventHandle(this._fieldsChangeHandles)).detach();
},
/**
* Remove a layout from the form builder. The paramenter `event` has the
* layout index to be removed.
*
* @method _removeLayout
* @params {EventFacade} event
* @protected
*/
_removeLayout: function(event) {
var layouts = this.get('layouts');
layouts[event.removedIndex].destroy();
layouts.splice(event.removedIndex, 1);
},
/**
* Render the form builder UI parts
*
* @method _renderContentBox
* @protected
*/
_renderContentBox: function() {
var contentBox = this.get('contentBox'),
headerTemplate = A.Lang.sub(this.TPL_HEADER, {
formTitle: this.get('strings').formTitle
});
contentBox.append(headerTemplate);
contentBox.append(this.TPL_PAGE_HEADER);
contentBox.append(this.TPL_TABVIEW);
contentBox.append(this.TPL_LAYOUT);
contentBox.append(this.TPL_PAGES);
},
/**
* Renders some content inside the empty columns of the current layout.
*
* @method _renderEmptyColumns
* @protected
*/
_renderEmptyColumns: function() {
var instance = this,
rows = this.get('layouts')[this._getActiveLayoutIndex()].get('rows');
A.Array.each(rows, function(row) {
A.Array.each(row.get('cols'), function(col) {
var colValue = col.get('value');
if (!colValue) {
instance._makeEmptyFieldList(col);
}
if (colValue && colValue._updateRemovableLayoutColProperty) {
colValue._updateRemovableLayoutColProperty();
}
});
});
},
/**
* Sets the `fieldToolbar` attribute.
*
* @method _setFieldToolbarConfig
* @params {Object} val
* @return {Object}
*/
_setFieldToolbarConfig: function(val) {
return A.merge({
formBuilder: this
}, val);
},
/**
* Sets the `layouts` attribute.
*
* @method _setLayouts
* @param {A.Array} val
* @protected
*/
_setLayouts: function(val) {
var layouts = [];
A.Array.each(val, function(layout) {
if (!A.instanceOf(layout, A.Layout)) {
layout = new A.Layout(layout);
}
if (layout.get('rows').length === 0) {
layout.set('rows', [new A.LayoutRow()]);
}
layout.get('rows')[layout.get('rows').length - 1].set('removable', false);
layouts.push(layout);
});
return layouts;
},
/**
* Fired after the `activePageNumber` change.
*
* @method _updatePageContent
* @protected
*/
_updatePageContent: function(activeLayout) {
activeLayout.addTarget(this);
if (this.get('rendered')) {
this._layoutBuilder.set('layout', activeLayout);
this._renderEmptyColumns();
}
}
}, {
/**
* Static property used to define the default attribute
* configuration for the `A.FormBuilder`.
*
* @property ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* A configuration object for the creation of the `A.FormBuilderFieldToolbar`
* instance to be used for the form builder field toolbars.
*
* @attribute fieldToolbarConfig
* @type Object
*/
fieldToolbarConfig: {
setter: '_setFieldToolbarConfig',
validator: A.Lang.isObject,
value: {}
},
/**
* The layouts where the forms fields will be rendered.
*
* @attribute layouts
* @default [A.Layout]
* @type Array
*/
layouts: {
setter: '_setLayouts',
validator: A.Lang.isArray,
valueFn: function() {
return [new A.Layout()];
}
},
/**
* A Form Builder Pages instance.
*
* @attribute pages
* @type {A.FormBuilderPages}
*/
pages: {
getter: '_getPageManagerInstance',
validator: A.Lang.isObject
},
/**
* Collection of strings used to label elements of the UI.
*
* @attribute strings
* @type {Object}
*/
strings: {
value: {
addColumn: 'Add Column',
addField: 'Add Field',
cancelRemoveRow: 'Cancel',
confirmRemoveRow: 'Yes, delete',
formTitle: 'Build your form',
modalHeader: 'Remove confirmation',
pasteHere: 'Paste Here',
removeRowModal: 'You will also delete fields with this row. ' +
'Are you sure you want delete it?'
},
writeOnce: true
}
},
/**
* Static property provides a string to identify the CSS prefix.
*
* @property CSS_PREFIX
* @type String
* @static
*/
CSS_PREFIX: A.getClassName('form-builder')
});