/**
* The Form Builder Component
*
* @module aui-form-builder
* @submodule aui-form-builder-base
*/
var L = A.Lang,
isObject = L.isObject,
aArray = A.Array,
getAvailableFieldById = A.PropertyBuilderAvailableField.getAvailableFieldById,
isAvailableField = function(v) {
return (v instanceof A.PropertyBuilderAvailableField);
},
isFormBuilderField = function(v) {
return (v instanceof A.FormBuilderField);
},
getCN = A.getClassName,
AVAILABLE_FIELDS_ID_PREFIX = 'availableFields' + '_' + 'field' + '_',
FIELDS_ID_PREFIX = 'fields' + '_' + 'field' + '_',
CSS_DD_DRAGGING = getCN('dd', 'dragging'),
CSS_PROPERTY_BUILDER_CONTENT_CONTAINER = getCN('property', 'builder', 'content', 'container'),
CSS_PROPERTY_BUILDER_FIELD_DRAGGABLE = getCN('property', 'builder', 'field', 'draggable'),
CSS_FIELD_HOVER = getCN('form', 'builder', 'field', 'hover'),
CSS_FORM_BUILDER_DROP_ZONE = getCN('form', 'builder', 'drop', 'zone'),
CSS_FORM_BUILDER_FIELD = getCN('form', 'builder', 'field'),
CSS_FORM_BUILDER_PLACEHOLDER = getCN('form', 'builder', 'placeholder'),
CSS_FORM_BUILDER_TABS = getCN('form', 'builder', 'tabs'),
MOBILE_TOUCH_ENABLED = A.UA.touchEnabled && A.UA.mobile,
TPL_PLACEHOLDER = '<div class="' + CSS_FORM_BUILDER_PLACEHOLDER + '"></div>';
/**
* A base class for `A.FormBuilder`.
*
* @class A.FormBuilder
* @extends A.PropertyBuilder
* @param {Object} config Object literal specifying widget configuration
* properties.
* @constructor
* @example
```
<div id="myFormBuilder"></div>
```
* @example
```
YUI().use(
'aui-form-builder',
function(Y) {
new Y.FormBuilder(
{
availableFields: [
{
iconClass: 'form-builder-field-icon-text',
id: 'uniqueTextField',
label: 'Text',
readOnlyAttributes: ['name'],
type: 'text',
unique: true,
width: 75
},
{
hiddenAttributes: ['tip'],
iconClass: 'form-builder-field-icon-textarea',
label: 'Textarea',
type: 'textarea'
},
{
iconClass: 'form-builder-field-icon-checkbox',
label: 'Checkbox',
type: 'checkbox'
},
{
iconClass: 'form-builder-field-icon-button',
label: 'Button',
type: 'button'
},
{
iconClass: 'form-builder-field-icon-select',
label: 'Select',
type: 'select'
},
{
iconClass: 'form-builder-field-icon-radio',
label: 'Radio Buttons',
type: 'radio'
},
{
iconClass: 'form-builder-field-icon-fileupload',
label: 'File Upload',
type: 'fileupload'
},
{
iconClass: 'form-builder-field-icon-fieldset',
label: 'Fieldset',
type: 'fieldset'
}
],
boundingBox: '#myFormBuilder',
fields: [
{
label: 'City',
options: [
{
label: 'Ney York',
value: 'new york'
},
{
label: 'Chicago',
value: 'chicago'
}
],
predefinedValue: 'chicago',
type: 'select'
},
{
label: 'Colors',
options: [
{
label: 'Red',
value: 'red'
},
{
label: 'Green',
value: 'green'
},
{
label: 'Blue',
value: 'blue'
}
],
type: 'radio'
}
]
}
).render();
}
);
```
*/
var FormBuilder = A.Component.create({
/**
* Static property provides a string to identify the class.
*
* @property NAME
* @type String
* @static
*/
NAME: 'form-builder',
/**
* Static property used to define the default attribute
* configuration for the `A.FormBuilder`.
*
* @property ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* Checks if removing required fields is permitted or not.
*
* @attribute allowRemoveRequiredFields
* @default false
* @type Boolean
*/
allowRemoveRequiredFields: {
validator: A.Lang.isBoolean,
value: false
},
/**
* Enables a field to be editable.
*
* @attribute enableEditing
* @default true
* @type Boolean
*/
enableEditing: {
value: true
},
/**
* Collection of sortable fields.
*
* @attribute fieldsSortableListConfig
* @default null
* @type Object
*/
fieldsSortableListConfig: {
setter: '_setFieldsSortableListConfig',
validator: isObject,
value: null
},
/**
* Collection of strings used to label elements of the UI.
*
* @attribute strings
* @type Object
*/
strings: {
value: {
addNode: 'Add field',
close: 'Close',
propertyName: 'Property Name',
save: 'Save',
settings: 'Settings',
value: 'Value'
}
},
/**
* Stores an instance of `A.TabView`.
*
* @attribute tabView
* @default null
* @type Object
* @writeOnce
*/
tabView: {
value: {
cssClass: CSS_FORM_BUILDER_TABS
}
}
},
/**
* Static property used to define which component it extends.
*
* @property EXTENDS
* @type String
* @static
*/
EXTENDS: A.PropertyBuilder,
/**
* Static property used to define the UI attributes.
*
* @property UI_ATTRS
* @type Array
* @static
*/
UI_ATTRS: ['allowRemoveRequiredFields'],
/**
* Static property used to define the fields tab.
*
* @property FIELDS_TAB
* @default 0
* @type Number
* @static
*/
FIELDS_TAB: 0,
/**
* Static property used to define the settings tab.
*
* @property SETTINGS_TAB
* @default 1
* @type Number
* @static
*/
SETTINGS_TAB: 1,
prototype: {
CONTENT_CONTAINER_TEMPLATE: '<div class="col-xs-12 col-sm-6 col-md-8 ' + CSS_PROPERTY_BUILDER_CONTENT_CONTAINER + '"></div>',
selectedFieldsLinkedSet: null,
uniqueFieldsMap: null,
/**
* Construction logic executed during `A.FormBuilder` instantiation.
* Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function() {
var instance = this;
instance.uniqueFieldsMap = new A.Map();
instance.uniqueFieldsMap.after({
put: A.bind(instance._afterUniqueFieldsMapPut, instance),
remove: A.bind(instance._afterUniqueFieldsMapRemove, instance)
});
instance.selectedFieldsLinkedSet = new A.LinkedSet();
instance.selectedFieldsLinkedSet.after({
add: A.bind(instance._afterSelectedFieldsSetAdd, instance),
remove: A.bind(instance._afterSelectedFieldsSetRemove, instance)
});
instance.on({
'cancel': instance._onCancel,
'drag:end': instance._onDragEnd,
'drag:start': instance._onDragStart,
'drag:mouseDown': instance._onDragMouseDown,
'save': instance._onSave
});
instance.after('*:focusedChange', instance._afterFieldFocusedChange);
instance.dropContainer.delegate('click', A.bind(instance._onClickField, instance), '.' + CSS_FORM_BUILDER_FIELD);
if (!MOBILE_TOUCH_ENABLED) {
instance.dropContainer.delegate('mouseover', A.bind(instance._onMouseOverField, instance), '.' + CSS_FORM_BUILDER_FIELD);
instance.dropContainer.delegate('mouseout', A.bind(instance._onMouseOutField, instance), '.' + CSS_FORM_BUILDER_FIELD);
}
instance.get('contentBox').addClass('row');
},
/**
* Sync the `A.FormBuilder` UI. Lifecycle.
*
* @method syncUI
* @protected
*/
syncUI: function() {
var instance = this;
instance._setupAvailableFieldsSortableList();
instance._setupFieldsSortableList();
},
/**
* Selects the field tab and disables the setting tabs.
*
* @method closeEditProperties
*/
closeEditProperties: function() {
var instance = this;
instance.tabView.selectChild(A.FormBuilder.FIELDS_TAB);
instance.tabView.disableTab(A.FormBuilder.SETTINGS_TAB);
},
/**
* Creates a field and returns its configuration.
*
* @method createField
* @param config
* @return {Object}
*/
createField: function(config) {
var instance = this,
attrs = {
builder: instance,
parent: instance
};
if (isFormBuilderField(config)) {
config.setAttrs(attrs);
}
else {
A.each(config, function(value, key) {
if (value === undefined) {
delete config[key];
}
});
config = new(instance.getFieldClass(config.type || 'field'))(A.mix(attrs, config));
}
config.addTarget(instance);
return config;
},
/**
* Gets the current field index and then clones the field. Inserts the
* new one after the current field index, inside of the current field
* parent.
*
* @method duplicateField
* @param field
*/
duplicateField: function(field) {
var instance = this,
index = instance._getFieldNodeIndex(field.get('boundingBox')),
newField = instance._cloneField(field, true),
boundingBox = newField.get('boundingBox');
boundingBox.setStyle('opacity', 0);
instance.insertField(newField, ++index, field.get('parent'));
boundingBox.show(true);
},
/**
* Checks if the current field is a `A.FormBuilderField` instance and
* selects it.
*
* @method editField
* @param field
*/
editField: function(field) {
var instance = this;
if (isFormBuilderField(field)) {
instance.editingField = field;
instance.unselectFields();
instance.selectFields(field);
}
},
/**
* Gets the field class based on the `A.FormBuilder` type. If the type
* doesn't exist, logs an error message.
*
* @method getFieldClass
* @param type
* @return {Object | null}
*/
getFieldClass: function(type) {
var clazz = A.FormBuilderField.types[type];
if (clazz) {
return clazz;
}
else {
A.log('The field type: [' + type + '] couldn\'t be found.');
return null;
}
},
/**
* Gets a list of properties from the field.
*
* @method getFieldProperties
* @param field
* @return {Array}
*/
getFieldProperties: function(field, excludeHidden) {
return field.getProperties(excludeHidden);
},
/**
* Removes field from previous parent and inserts into the new parent.
*
* @method insertField
* @param field
* @param index
* @param parent
*/
insertField: function(field, index, parent) {
var instance = this;
parent = parent || instance;
// remove from previous parent
field.get('parent').removeField(field);
parent.addField(field, index);
},
/**
* Enables the settings tab.
*
* @method openEditProperties
* @param field
*/
openEditProperties: function(field) {
var instance = this;
instance.tabView.enableTab(A.FormBuilder.SETTINGS_TAB);
instance.tabView.selectChild(A.FormBuilder.SETTINGS_TAB);
instance.propertyList.set('data', instance.getFieldProperties(field, true));
},
/**
* Renders a field in the container.
*
* @method plotField
* @param field
* @param container
*/
plotField: function(field, container) {
var instance = this,
boundingBox = field.get('boundingBox');
if (!field.get('rendered')) {
field.render(container);
}
else {
container.append(boundingBox);
}
instance._syncUniqueField(field);
instance.fieldsSortableList.add(boundingBox);
},
/**
* Renders a list of fields in the container.
*
* @method plotFields
* @param fields
* @param container
*/
plotFields: function(fields, container) {
var instance = this;
container = container || instance.dropContainer;
fields = fields || instance.get('fields');
container.setContent('');
A.each(fields, function(field) {
instance.plotField(field, container);
});
},
/**
* Adds fields to a `A.LinkedSet` instance.
*
* @method selectFields
* @param fields
*/
selectFields: function(fields) {
var instance = this,
selectedFieldsLinkedSet = instance.selectedFieldsLinkedSet;
aArray.each(aArray(fields), function(field) {
selectedFieldsLinkedSet.add(field);
});
},
/**
* Triggers a focus event in the current field and a blur event in the
* last focused field.
*
* @method simulateFocusField
* @param field
*/
simulateFocusField: function(field) {
var instance = this,
lastFocusedField = instance.lastFocusedField;
if (lastFocusedField && (field !== lastFocusedField)) {
lastFocusedField.blur();
}
instance.lastFocusedField = field.focus();
},
/**
* Removes fields from the `A.LinkedSet` instance.
*
* @method unselectFields
* @param fields
*/
unselectFields: function(fields) {
var instance = this,
selectedFieldsLinkedSet = instance.selectedFieldsLinkedSet;
if (!fields) {
fields = selectedFieldsLinkedSet.values();
}
aArray.each(aArray(fields), function(field) {
selectedFieldsLinkedSet.remove(field);
});
},
/**
* Triggers after field focused change.
*
* @method _afterFieldFocusedChange
* @param event
* @protected
*/
_afterFieldFocusedChange: function(event) {
var instance = this,
field = event.target;
if (event.newVal && isFormBuilderField(field) && !MOBILE_TOUCH_ENABLED) {
instance.editField(field);
}
},
/**
* Triggers after adding selected fields to a `A.LinkedSet` instance.
*
* @method _afterSelectedFieldsSetAdd
* @param event
* @protected
*/
_afterSelectedFieldsSetAdd: function(event) {
var instance = this;
event.value.set('selected', true);
instance.openEditProperties(event.value);
},
/**
* Triggers after removing selected fields from the `A.LinkedSet`
* instance.
*
* @method _afterSelectedFieldsSetRemove
* @param event
* @protected
*/
_afterSelectedFieldsSetRemove: function(event) {
var instance = this;
event.value.set('selected', false);
instance.closeEditProperties();
},
/**
* Triggers after adding unique fields to a `A.Map` instance.
*
* @method _afterUniqueFieldsMapPut
* @param event
* @protected
*/
_afterUniqueFieldsMapPut: function(event) {
var availableField = getAvailableFieldById(event.key),
node;
if (isAvailableField(availableField)) {
node = availableField.get('node');
availableField.set('draggable', false);
node.unselectable();
}
},
/**
* Triggers after removing unique fields from the `A.Map` instance.
*
* @method _afterUniqueFieldsMapRemove
* @param event
* @protected
*/
_afterUniqueFieldsMapRemove: function(event) {
var availableField = getAvailableFieldById(event.key),
node;
if (isAvailableField(availableField)) {
node = availableField.get('node');
availableField.set('draggable', true);
node.selectable();
}
},
/**
* Clones a field.
*
* @method _cloneField
* @param field
* @param deep
* @protected
* @return {Object}
*/
_cloneField: function(field, deep) {
var instance = this,
config = field.getAttributesForCloning();
if (deep) {
config.fields = [];
A.each(field.get('fields'), function(child, index) {
if (!child.get('unique')) {
config.fields[index] = instance._cloneField(child, deep);
}
});
}
return instance.createField(config);
},
/**
* Executes when the field is dropped.
*
* @method _dropField
* @param dragNode
* @protected
*/
_dropField: function(dragNode) {
var instance = this,
availableField = dragNode.getData('availableField'),
field = A.Widget.getByNode(dragNode);
if (isAvailableField(availableField)) {
var config = {
hiddenAttributes: availableField.get('hiddenAttributes'),
label: availableField.get('label'),
localizationMap: availableField.get('localizationMap'),
options: availableField.get('options'),
predefinedValue: availableField.get('predefinedValue'),
readOnlyAttributes: availableField.get('readOnlyAttributes'),
required: availableField.get('required'),
showLabel: availableField.get('showLabel'),
tip: availableField.get('tip'),
type: availableField.get('type'),
unique: availableField.get('unique'),
width: availableField.get('width')
};
if (config.unique) {
config.id = instance._getFieldId(availableField);
config.name = availableField.get('name');
}
field = instance.createField(config);
}
if (isFormBuilderField(field)) {
var parentNode = dragNode.get('parentNode'),
dropField = A.Widget.getByNode(parentNode),
index = instance._getFieldNodeIndex(dragNode);
if (!isFormBuilderField(dropField)) {
dropField = instance;
}
instance.insertField(field, index, dropField);
}
},
/**
* Gets the field id.
*
* @method _getFieldId
* @param field
* @protected
* @return {String}
*/
_getFieldId: function(field) {
var id = field.get('id'),
prefix;
if (isAvailableField(field)) {
prefix = AVAILABLE_FIELDS_ID_PREFIX;
}
else {
prefix = FIELDS_ID_PREFIX;
}
return id.replace(prefix, '');
},
/**
* Gets the index from the field node.
*
* @method _getFieldNodeIndex
* @param fieldNode
* @protected
*/
_getFieldNodeIndex: function(fieldNode) {
return fieldNode.get('parentNode').all(
// prevent the placeholder interference on the index
// calculation
'> *:not(' + '.' + CSS_FORM_BUILDER_PLACEHOLDER + ')'
).indexOf(fieldNode);
},
/**
* Triggers on cancel. Unselect fields, stops the event propagation and
* prevents the default event behavior.
*
* @method _onCancel
* @param event
* @protected
*/
_onCancel: function(event) {
var instance = this;
instance.unselectFields();
event.halt();
},
/**
* Triggers when the drag ends.
*
* @method _onDragEnd
* @param event
* @protected
*/
_onDragEnd: function(event) {
var instance = this,
drag = event.target,
dragNode = drag.get('node');
instance._dropField(dragNode);
// skip already instanciated fields
if (!isFormBuilderField(A.Widget.getByNode(dragNode))) {
dragNode.remove();
drag.set('node', instance._originalDragNode);
}
},
/**
* Triggers when a field is clicked.
*
* @method _onClickField
* @param event
* @protected
*/
_onClickField: function(event) {
var instance = this,
field = A.Widget.getByNode(event.target);
instance.simulateFocusField(field);
event.stopPropagation();
},
/**
* Triggers when the drag mouse down.
*
* @method _onDragMouseDown
* @param event
* @protected
*/
_onDragMouseDown: function(event) {
var dragNode = event.target.get('node'),
availableField = A.PropertyBuilderAvailableField.getAvailableFieldByNode(dragNode);
if (isAvailableField(availableField) && !availableField.get('draggable')) {
event.halt();
}
},
/**
* Triggers when the drag starts.
*
* @method _onDragStart
* @param event
* @protected
*/
_onDragStart: function(event) {
var instance = this,
drag = event.target,
dragNode = drag.get('node');
// skip already instanciated fields
if (isFormBuilderField(A.Widget.getByNode(dragNode))) {
return;
}
// in the dragEnd we`re going to restore the drag node
// to the original node
instance._originalDragNode = dragNode;
var clonedDragNode = dragNode.clone();
dragNode.placeBefore(clonedDragNode);
drag.set('node', clonedDragNode);
var availableFieldData = dragNode.getData('availableField');
clonedDragNode.setData('availableField', availableFieldData);
clonedDragNode.attr('id', '');
clonedDragNode.hide();
dragNode.removeClass(CSS_DD_DRAGGING);
dragNode.show();
instance.fieldsSortableList.add(clonedDragNode);
},
/**
* Triggers when the mouse is out a field.
*
* @method _onMouseOutField
* @param event
* @protected
*/
_onMouseOutField: function(event) {
var field = A.Widget.getByNode(event.currentTarget);
field.controlsToolbar.hide();
field.get('boundingBox').removeClass(CSS_FIELD_HOVER);
event.stopPropagation();
},
/**
* Triggers when the mouse is over a field.
*
* @method _onMouseOverField
* @param event
* @protected
*/
_onMouseOverField: function(event) {
var field = A.Widget.getByNode(event.currentTarget);
field.controlsToolbar.show();
field.get('boundingBox').addClass(CSS_FIELD_HOVER);
event.stopPropagation();
},
/**
* Triggers on saving a field. First checks if the field is being
* edited, if it is then sets the data and syncs on the UI.
*
* @method _onSave
* @param event
* @protected
*/
_onSave: function() {
var instance = this,
editingField = instance.editingField;
if (editingField) {
var modelList = instance.propertyList.get('data');
modelList.each(function(model) {
editingField.set(model.get('attributeName'), model.get('value'));
});
instance._syncUniqueField(editingField);
}
},
/**
* Overrides PropertyBuilderSettings's `_renderTabs` method, which renders
* the builder's tabs.
*
* @method _renderTabs
* @protected
*/
_renderTabs: function() {
FormBuilder.superclass._renderTabs.apply(this, arguments);
this.tabView.get('boundingBox').addClass('col-xs-12 col-sm-6 col-md-4');
},
/**
* Set list of available fields by checking if a field is a
* `A.PropertyBuilderAvailableField` instance. If not creates a new
* instance of `A.FormBuilderAvailableField`.
*
* @method _setAvailableFields
* @param val
* @protected
* @return {Array}
*/
_setAvailableFields: function(val) {
var fields = [];
aArray.each(val, function(field) {
fields.push(
isAvailableField(field) ? field : new A.FormBuilderAvailableField(field)
);
});
return fields;
},
/**
* Set the `fieldsSortableListConfig` attribute.
*
* @method _setFieldsSortableListConfig
* @param val
* @protected
*/
_setFieldsSortableListConfig: function(val) {
var instance = this,
dropContainer = instance.dropContainer;
return A.merge({
bubbleTargets: instance,
dd: {
groups: ['availableFields'],
plugins: [
{
cfg: {
horizontal: false,
scrollDelay: 150
},
fn: A.Plugin.DDWinScroll
}
]
},
dropCondition: function(event) {
var dropNode = event.drop.get('node'),
field = A.Widget.getByNode(dropNode);
if (isFormBuilderField(field)) {
return true;
}
return false;
},
placeholder: A.Node.create(TPL_PLACEHOLDER),
dropOn: '.' + CSS_FORM_BUILDER_DROP_ZONE,
sortCondition: function(event) {
var dropNode = event.drop.get('node');
return (dropNode !== instance.dropContainer &&
dropContainer.contains(dropNode));
}
},
val || {}
);
},
/**
* Setup a `A.SortableList` of available fields.
*
* @method _setupAvailableFieldsSortableList
* @protected
*/
_setupAvailableFieldsSortableList: function() {
var instance = this;
if (!instance.availableFieldsSortableList) {
var availableFieldsNodes = instance.fieldsContainer.all(
'.' + CSS_PROPERTY_BUILDER_FIELD_DRAGGABLE
);
instance.availableFieldsSortableList = new A.SortableList(
A.merge(
instance.get('fieldsSortableListConfig'), {
nodes: availableFieldsNodes
}
)
);
}
},
/**
* Setup a `A.SortableList` of fields.
*
* @method _setupFieldsSortableList
* @protected
*/
_setupFieldsSortableList: function() {
var instance = this;
if (!instance.fieldsSortableList) {
instance.fieldsSortableList = new A.SortableList(
instance.get('fieldsSortableListConfig')
);
}
},
/**
* Sync unique fields.
*
* @method _syncUniqueField
* @param field
* @protected
*/
_syncUniqueField: function(field) {
var instance = this,
fieldId = instance._getFieldId(field),
availableField = getAvailableFieldById(fieldId);
if (isAvailableField(availableField)) {
if (availableField.get('unique') || field.get('unique')) {
instance.uniqueFieldsMap.put(fieldId, field);
}
}
},
/**
* Set the `allowRemoveRequiredFields` attribute on the UI.
*
* @method _uiSetAllowRemoveRequiredFields
* @param val
* @protected
*/
_uiSetAllowRemoveRequiredFields: function() {
var instance = this;
instance.get('fields').each(function(field) {
field._uiSetRequired(field.get('required'));
});
}
}
});
A.FormBuilder = FormBuilder;