/**
* The Form Builder Layout Builder Component
*
* @module aui-form-builder
* @submodule aui-form-builder-layout-builder
*/
var CSS_CHOOSE_COL_MOVE = A.getClassName('form', 'builder', 'choose', 'col', 'move'),
CSS_CHOOSE_COL_MOVE_TARGET = A.getClassName('form', 'builder', 'choose', 'col', 'move', 'target'),
CSS_FIELD = A.getClassName('form', 'builder', 'field'),
CSS_FIELD_MOVE_BUTTON = A.getClassName('form', 'builder', 'field', 'move', 'button'),
CSS_FIELD_MOVE_TARGET = A.getClassName('form', 'builder', 'field', 'move', 'target'),
CSS_FIELD_MOVE_TARGET_INVALID = A.getClassName('form', 'builder', 'field', 'move', 'target', 'invalid'),
CSS_FIELD_MOVING = A.getClassName('form', 'builder', 'field', 'moving'),
CSS_LAYOUT = A.getClassName('form', 'builder', 'layout'),
CSS_LAYOUT_BUILDER_MOVE_CANCEL = A.getClassName('layout', 'builder', 'move', 'cancel'),
CSS_MOVE_COL_TARGET = A.getClassName('layout', 'builder', 'move', 'col', 'target'),
CSS_MOVE_ROW_TARGET = A.getClassName('layout', 'builder', 'move', 'row', 'target'),
CSS_MOVE_TARGET = A.getClassName('layout', 'builder', 'move', 'target'),
CSS_REMOVE_ROW_MODAL = A.getClassName('form', 'builder', 'remove', 'row', 'modal');
/**
* `A.FormBuilder` extension, which handles the `A.LayoutBuilder` inside it.
*
* @class A.FormBuilderLayoutBuilder
* @param {Object} config Object literal specifying layout builder configuration
* properties.
* @constructor
*/
A.FormBuilderLayoutBuilder = function() {};
A.FormBuilderLayoutBuilder.prototype = {
/**
* Construction logic executed during the `A.FormBuilderLayoutBuilder`
* instantiation. Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function() {
this._initRemoveConfirmationModal();
this.after({
render: this._afterLayoutBuilderRender,
'layout-row:colsChange': this._afterLayoutBuilderColsChange
});
},
/**
* Destructor implementation for the `A.FormBuilderLayoutBuilder` class.
* Lifecycle.
*
* @method destructor
* @protected
*/
destructor: function() {
if (this._layoutBuilder) {
this._layoutBuilder.destroy();
}
this._removeConfirmationModal.destroy();
},
/**
* Overrides default `addColMoveButton` attribute.
*
* @method _addColMoveButton
* @param {Node} colNode
* @param {Node} rowNode
* @protected
*/
_addColMoveButton: function(colNode, rowNode) {
var targetNodes = colNode.all('.' + CSS_FIELD_MOVE_BUTTON);
targetNodes.setData('node-col', colNode);
targetNodes.setData('node-row', rowNode);
this._renderEmptyColumns();
},
/**
* Overrides default `addColMoveTarget` attribute.
*
* @method _addColMoveTarget
* @param {A.LayoutCol} col
* @protected
*/
_addColMoveTarget: function(col) {
var colNode = col.get('node'),
targetNodes;
colNode.addClass(CSS_CHOOSE_COL_MOVE_TARGET);
targetNodes = colNode.all('.' + CSS_FIELD_MOVE_TARGET);
targetNodes.setData('col', col);
},
/**
* Executed after the `layout:rowsChange` is fired.
*
* @method _afterLayoutBuilderColsChange
* @protected
*/
_afterLayoutBuilderColsChange: function() {
var activeLayout = this.getActiveLayout();
this._checkLastRow(activeLayout);
},
/**
* Executed after the `layout-builder:moveEnd` is fired.
*
* @method _afterLayoutBuilderMoveEnd
* @protected
*/
_afterLayoutBuilderMoveEnd: function() {
this._detachCancelMoveRowEvents();
this._enableAddFields();
this._fieldToolbar.set('disabled', false);
this._pageManager.enablePaginations();
},
/**
* Executed after the `layout-builder:moveStart` is fired.
*
* @method _afterLayoutBuilderMoveStart
* @param {EventFacade} event
* @protected
*/
_afterLayoutBuilderMoveStart: function(event) {
if (event.moveElement instanceof A.LayoutRow) {
this._bindMoveRowEvents();
}
this._disableAddFields();
this._fieldToolbar.set('disabled', true);
this._pageManager.disablePaginations();
},
/**
* Fired after this widget is rendered.
*
* @method _afterLayoutBuilderRender
* @protected
*/
_afterLayoutBuilderRender: function() {
var originalChooseColMoveTargetFn;
this._layoutBuilder = new A.LayoutBuilder({
addColMoveButton: A.bind(this._addColMoveButton, this),
addColMoveTarget: A.bind(this._addColMoveTarget, this),
clickColMoveTarget: A.bind(this._clickColMoveTarget, this),
clickRemoveRow: A.bind(this._clickRemoveRow, this),
container: this.get('contentBox').one('.' + CSS_LAYOUT),
layout: this.getActiveLayout(),
removeColMoveButtons: A.bind(this._removeColMoveButtons, this),
removeColMoveTargets: A.bind(this._removeColMoveTargets, this),
strings: this.get('strings')
});
this._layoutBuilder.after('layout-builder:moveStart', A.bind(this._afterLayoutBuilderMoveStart, this));
this._layoutBuilder.after('layout-builder:moveEnd', A.bind(this._afterLayoutBuilderMoveEnd, this));
originalChooseColMoveTargetFn = this._layoutBuilder.get('chooseColMoveTarget');
this._layoutBuilder.set('chooseColMoveTarget', A.bind(this._chooseColMoveTarget, this,
originalChooseColMoveTargetFn));
this._eventHandles.push(
this._fieldToolbar.on('onToolbarHasAddedToField', A.bind(this._onFormBuilderToolbarHasAddedToField, this))
);
this._removeLayoutCutColButtons();
},
/**
* Bind events related to the cancel move row funcionality.
*
* @method _bindMoveRowEvents
* @protected
*/
_bindMoveRowEvents: function() {
this._cancelMoveRowsHandles = [
A.one(A.config.doc).on('click', A.bind(this._onClickOutsideMoveRowTarget, this))
];
},
/**
* Checks if the last row has more than one col or if it has at least one field,
* if true a new row is created and set on the last position.
*
* @method _checkLastRow
* @param {A.Layout} layout
* @protected
*/
_checkLastRow: function(layout) {
var cols,
lastRow,
nextToLast,
rows;
lastRow = this._getLastRow(layout);
cols = lastRow.get('cols');
if (cols.length > 1 || !this._isColumnEmpty(cols[0])) {
this._createLastRow(layout);
}
else {
rows = layout.get('rows');
nextToLast = rows[rows.length - 2];
if (nextToLast) {
cols = nextToLast.get('cols');
if (cols.length === 1 && this._isColumnEmpty(cols[0])) {
layout.removeRow(nextToLast);
}
}
}
this._getLastRow(layout).set('removable', false);
},
/**
* Overrides default `chooseColMoveTarget` attribute.
*
* @method _chooseColMoveTarget
* @param {Function} originalFn
* @param {Node} cutButton
* @param {A.LayoutCol} col
* @protected
*/
_chooseColMoveTarget: function(originalFn, cutButton, col) {
var fieldNode = cutButton.ancestor('.' + CSS_FIELD),
layout = this.getActiveLayout(),
targetNode;
this._fieldBeingMoved = fieldNode.getData('field-instance');
this._fieldListBeingMoved = col.get('value');
this._fieldBeingMovedCol = col;
fieldNode.addClass(CSS_FIELD_MOVING);
fieldNode.all('.' + CSS_FIELD_MOVE_TARGET).addClass(CSS_FIELD_MOVE_TARGET_INVALID);
targetNode = fieldNode.previous('.' + CSS_FIELD_MOVE_TARGET);
if (targetNode) {
targetNode.addClass(CSS_FIELD_MOVE_TARGET_INVALID);
}
targetNode = fieldNode.next('.' + CSS_FIELD_MOVE_TARGET);
if (targetNode) {
targetNode.addClass(CSS_FIELD_MOVE_TARGET_INVALID);
}
originalFn(cutButton, col);
this._addColMoveTarget(col);
layout.normalizeColsHeight(layout.get('node').all('.row'));
this._selectFirstValidMoveTarget();
this._cancelMoveFieldHandles = [
A.one(A.config.doc).on('click', A.bind(this._onClickOutsideMoveColTarget, this)),
A.one(A.config.doc).on('key', A.bind(this._onEscKeyPressMoveTarget, this), 'down:27')
];
},
/**
* Overrides default `clickColMoveTarget` attribute.
*
* @method _clickColMoveTarget
* @param {Node} moveTarget
* @protected
*/
_clickColMoveTarget: function(moveTarget) {
var layout = this.getActiveLayout(),
parentFieldNode = this._fieldBeingMoved.get('content').ancestor('.' + CSS_FIELD),
row,
targetNestedParent = moveTarget.getData('nested-field-parent'),
toolbarMoveIconCancelMode = this._fieldToolbar.getItem('.' + CSS_LAYOUT_BUILDER_MOVE_CANCEL);
if (toolbarMoveIconCancelMode) {
toolbarMoveIconCancelMode.removeClass(CSS_LAYOUT_BUILDER_MOVE_CANCEL);
}
if (parentFieldNode) {
parentFieldNode.getData('field-instance').removeNestedField(this._fieldBeingMoved);
}
else {
row = this.getFieldRow(this._fieldBeingMoved);
this._fieldListBeingMoved.removeField(this._fieldBeingMoved);
}
this._enableAddFields();
if (targetNestedParent) {
this._addNestedField(
targetNestedParent,
this._fieldBeingMoved,
moveTarget.getData('nested-field-index')
);
}
else {
moveTarget.getData('col').get('value').addField(
this._fieldBeingMoved,
moveTarget.getData('field-list-index')
);
}
this._removeLayoutCutColButtons();
layout.normalizeColsHeight(new A.NodeList(this.getFieldRow(this._fieldBeingMoved)));
this._detachCancelMoveFieldEvents();
},
/**
* Overrides default `clickRemoveRow` attribute. Check if the parameter `row` has fields.
*
* @method _clickRemoveRow
* @param {A.LayoutRow} row
* @return {Boolean}
* @protected
*/
_clickRemoveRow: function(row) {
var cols = row.get('cols'),
currentList,
index;
for (index = 0; index < cols.length; index++) {
currentList = cols[index].get('value');
if (currentList && currentList.get('fields').length) {
this._removeConfirmationModal.show();
this._removingRow = row;
return false;
}
}
return true;
},
/**
* Creates a new row in the last position.
*
* @method _createLastRow
* @param {A.Layout} layout
* @protected
*/
_createLastRow: function(layout) {
var lastRow = new A.LayoutRow(),
rows = layout.get('rows');
layout.addRow(rows.length, lastRow);
},
/**
* Detaches events related to the cancel move row funcionality.
*
* @method _detachCancelMoveRowEvents
* @protected
*/
_detachCancelMoveRowEvents: function() {
new A.EventHandle(this._cancelMoveRowsHandles).detach();
},
/**
* Detaches events related to the cancel move field funcionality.
*
* @method _detachCancelMoveFieldEvents
* @protected
*/
_detachCancelMoveFieldEvents: function() {
new A.EventHandle(this._cancelMoveFieldHandles).detach();
},
/**
* Disable add fields functionality.
*
* @method _disableAddFields
* @protected
*/
_disableAddFields: function() {
this.getActiveLayout().get('rows').forEach(function(row) {
row.get('cols').forEach(function(col) {
col.get('value').set('enableAddFields', false);
});
});
},
/**
* Enable add fields functionality.
*
* @method _enableAddFields
* @protected
*/
_enableAddFields: function() {
this.getActiveLayout().get('rows').forEach(function(row) {
row.get('cols').forEach(function(col) {
col.get('value').set('enableAddFields', true);
});
});
},
/**
* Removes a row even with field.
*
* @method _forceRemoveRow
* @protected
*/
_forceRemoveRow: function() {
this.getActiveLayout().removeRow(this._removingRow);
this._removeConfirmationModal.hide();
},
/**
* Gets the last row.
*
* @method _getLastRow
* @param {A.Layout} layout
* @protected
* @return {A.LayoutRow}
*/
_getLastRow: function(layout) {
var rows = layout.get('rows');
return rows[rows.length - 1];
},
/**
* Create a confirmation modal to be used when a remove row button from a row with
* fields is clicked.
*
* @method _initRemoveConfirmationModal
* @protected
*/
_initRemoveConfirmationModal: function() {
var modal = new A.Modal({
bodyContent: this.get('strings').removeRowModal,
centered: true,
cssClass: CSS_REMOVE_ROW_MODAL,
headerContent: this.get('strings').modalHeader,
modal: true,
resizable: false,
visible: false,
zIndex: 4
}).render();
modal.addToolbar([
{
cssClass: 'btn-primary',
label: this.get('strings').confirmRemoveRow,
on: {
click: A.bind(this._forceRemoveRow, this)
},
render: true
},
{
label: this.get('strings').cancelRemoveRow,
on: {
click: function() {
modal.hide();
}
},
render: true
}
]);
this._removeConfirmationModal = modal;
},
/**
* Checks if the given column is empty.
*
* @method _isColumnEmpty
* @param {A.LayoutCol} col
* @return {Boolean}
* @protected
*/
_isColumnEmpty: function(col) {
return !col.get('value') || !col.get('value').get('fields').length;
},
/**
* Fires when click event is triggered.
*
* @method _onClickOutsideMoveColTarget
* @param {EventFacade} event
* @protected
*/
_onClickOutsideMoveColTarget: function(event) {
var targetNode = event.target,
toolbarMoveIconCancelMode = this._fieldToolbar.getItem('.' + CSS_LAYOUT_BUILDER_MOVE_CANCEL);
if (toolbarMoveIconCancelMode) {
toolbarMoveIconCancelMode.removeClass(CSS_LAYOUT_BUILDER_MOVE_CANCEL);
}
if (!(targetNode.hasClass(CSS_MOVE_TARGET) && targetNode.hasClass(CSS_MOVE_COL_TARGET))) {
this._layoutBuilder.cancelMove();
this._detachCancelMoveFieldEvents();
}
},
/**
* Fires when click event is triggered.
*
* @method _onClickOutsideMoveRowTarget
* @param {EventFacade} event
* @protected
*/
_onClickOutsideMoveRowTarget: function(event) {
var targetNode = event.target;
if (!(targetNode.hasClass(CSS_MOVE_ROW_TARGET))) {
this._layoutBuilder.cancelMove();
}
},
/**
* Fires when esc key press event is triggered.
*
* @method _onEscKeyPressMoveTarget
* @protected
*/
_onEscKeyPressMoveTarget: function() {
var toolbarMoveIconCancelMode = this._fieldToolbar.getItem('.' + CSS_LAYOUT_BUILDER_MOVE_CANCEL);
if (toolbarMoveIconCancelMode) {
toolbarMoveIconCancelMode.removeClass(CSS_LAYOUT_BUILDER_MOVE_CANCEL);
}
this._layoutBuilder.cancelMove();
this._detachCancelMoveFieldEvents();
},
/**
* Fired when mouse enters a toolbar's field.
*
* @method _onFormBuilderToolbarHasAddedToField
* @params {EventFacade} event
* @protected
*/
_onFormBuilderToolbarHasAddedToField: function(event) {
this._setMoveButtonData(event.colNode);
},
/**
* Overrides default `removeColMoveButtons` attribute.
*
* @method _removeColMoveButtons
* @protected
*/
_removeColMoveButtons: function() {
this.get('contentBox').all('.' + CSS_CHOOSE_COL_MOVE).removeClass(CSS_CHOOSE_COL_MOVE);
},
/**
* Overrides default `removeColMoveTargets` attribute.
*
* @method _removeColMoveTargets
* @protected
*/
_removeColMoveTargets: function() {
var contentBox = this.get('contentBox'),
layout = this.getActiveLayout();
contentBox.all('.' + CSS_CHOOSE_COL_MOVE_TARGET).removeClass(CSS_CHOOSE_COL_MOVE_TARGET);
contentBox.all('.' + CSS_FIELD_MOVING).removeClass(CSS_FIELD_MOVING);
contentBox.all('.' + CSS_FIELD_MOVE_TARGET_INVALID).removeClass(CSS_FIELD_MOVE_TARGET_INVALID);
layout.normalizeColsHeight(layout.get('node').all('.row'));
},
/**
* Remove original layout cut col buttons.
*
* @method _removeLayoutCutColButtons
* @protected
*/
_removeLayoutCutColButtons: function() {
this._layoutBuilder.get('removeColMoveButtons')();
},
/**
* Find and focus on first valid move target.
*
* @method _selectFirstValidMoveTarget
* @protected
*/
_selectFirstValidMoveTarget: function() {
var moveTarget = A.one('.' + CSS_FIELD_MOVE_TARGET + ':not(.' + CSS_FIELD_MOVE_TARGET_INVALID + ')');
moveTarget.focus();
},
/**
* Show or hide move item in toolbar.
*
* @method _setMoveButtonData
* @param {Node} colNode
* @protected
*/
_setMoveButtonData: function(colNode) {
var moveItem = this._fieldToolbar.getItem('.layout-builder-move-cut-button');
moveItem.setData('layout-row', colNode.ancestor('.row').getData('layout-row'));
moveItem.setData('node-col', colNode);
}
};