/**
* The Layout Component
*
* @module aui-layout
*/
var CSS_LAYOUT_NODE = A.getClassName('layout', 'node'),
MAXIMUM_COLS_PER_ROW = 12,
RESPONSIVENESS_BREAKPOINT = 992,
SELECTOR_COL = '.col',
SELECTOR_LAYOUT_COL_CONTENT = '.layout-col-content',
SELECTOR_LAYOUT_ROW_CONTAINER_ROW = '.layout-row-container-row',
SELECTOR_ROW = '.row';
/**
* A base class for Layout.
*
* @class A.Layout
* @extends Base
* @param {Object} config Object literal specifying layout configuration
* properties.
* @constructor
*/
A.Layout = A.Base.create('layout', A.Base, [], {
/**
* Determines if progressive enhancement mode will be used.
*
* @property _useProgressiveEnhancement
* @type {Boolean}
* @protected
*/
_useProgressiveEnhancement: false,
/**
* Construction logic executed during Layout instantiation. Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function(config) {
if (!config || !config.rows) {
this._useProgressiveEnhancement = true;
}
this._eventHandles = [
this.after('rowsChange', A.bind(this._afterRowsChange, this)),
this.after('layout-row:colsChange', A.bind(this._afterLayoutColsChange, this)),
this.after('layout-col:valueChange', A.bind(this._afterLayoutValueChange, this)),
A.on('windowresize', A.bind(this._afterLayoutWindowResize, this))
];
A.Array.invoke(this.get('rows'), 'addTarget', this);
this._uiSetRows(this.get('rows'));
},
/**
* Destructor implementation for the `A.Layout` class. Lifecycle.
*
* @method destructor
* @protected
*/
destructor: function() {
(new A.EventHandle(this._eventHandles)).detach();
this.get('node').remove();
},
/**
* Adds a new row with specified number of cols to the current layout.
*
* @method addRowWithSpecifiedColNumber
* @param {Number} numberOfCols Number of cols to create the row with.
*/
addRowWithSpecifiedColNumber: function(numberOfCols) {
var cols = [],
i,
row,
rows = this.get('rows').concat();
numberOfCols = numberOfCols || 1;
for (i = 0; i < numberOfCols; i++) {
cols.push(new A.LayoutCol({ size: MAXIMUM_COLS_PER_ROW / numberOfCols }));
}
row = new A.LayoutRow({ cols: cols });
rows.splice(rows.length, 0, row);
this.set('rows', rows);
},
/**
* Adds a new row to the current layout.
*
* @method addRow
* @param {Number} index Position to insert the new row.
* @param {Node} row A brand new row.
*/
addRow: function(index, row) {
var rows = this.get('rows').concat();
if (A.Lang.isUndefined(index)) {
index = rows.length;
}
if (!row) {
row = new A.LayoutRow();
}
rows.splice(index, 0, row);
this.set('rows', rows);
},
/**
* Renders the layout rows and columns into the given container.
*
* @method draw
* @param {Node} container The container to draw the layout on.
*/
draw: function(container) {
var layoutNode = container.one('.' + CSS_LAYOUT_NODE),
node = this.get('node');
if (this._useProgressiveEnhancement && layoutNode) {
this._set('node', layoutNode);
this._setProgressiveEnhancementLayout(container);
}
else {
container.setHTML(node);
}
this._handleResponsive(A.config.win.innerWidth);
},
/**
* Moves a row to a different position.
*
* @method moveRow
* @param {Number} index The new position of the row.
* @param {Node} row Row to change the position.
*/
moveRow: function(index, row) {
this.removeRow(row);
this.addRow(index, row);
},
/**
* Normalize all cols' height for each row.
*
* @method normalizeColsHeight
* @param {NodeList} rows Rows to normalize cols' height.
*/
normalizeColsHeight: function(rows) {
var instance = this,
colClientHeight,
cols,
highestCol = 0;
rows.each(function(row) {
cols = row.all(SELECTOR_COL);
if (instance.get('isColumnMode')) {
if (row.getData('layout-row').get('equalHeight')) {
A.Array.invoke(cols, 'setStyle', 'height', 'auto');
cols.each(function(col) {
colClientHeight = col.get('clientHeight');
if (colClientHeight > highestCol) {
highestCol = colClientHeight;
}
});
A.Array.invoke(cols, 'setStyle', 'height', highestCol + 'px');
highestCol = 0;
}
}
else {
A.Array.invoke(cols, 'setStyle', 'height', 'auto');
}
});
},
/**
* Removes a row from this layout.
*
* @method removeRow
* @param {Number | A.LayoutRow} row Row index or row to be removed from this layout
*/
removeRow: function(row) {
if (A.Lang.isNumber(row)) {
this._removeRowByIndex(row);
}
else if (A.instanceOf(row, A.LayoutRow)) {
this._removeRowByReference(row);
}
},
/**
* Fires after `layout-row:colsChange` event.
*
* @method _afterLayoutColsChange
* @protected
*/
_afterLayoutColsChange: function(event) {
var row = event.target;
this.normalizeColsHeight(new A.NodeList(row.get('node').one(SELECTOR_ROW)));
},
/**
* Fires after cols' valueChange event.
*
* @method _afterLayoutValueChange
* @protected
*/
_afterLayoutValueChange: function(event) {
var col = event.target,
targets;
targets = A.Array.filter(col.getTargets(), function(target) {
if (target.name === 'layout-row') {
return target;
}
});
this.normalizeColsHeight(new A.NodeList(targets[0].get('node').one(SELECTOR_ROW)));
},
/**
* Fires after rows changes.
*
* @method _afterRowsChange
* @param {EventFacade} event
* @protected
*/
_afterRowsChange: function(event) {
A.Array.invoke(event.prevVal, 'removeTarget', this);
A.Array.invoke(event.newVal, 'addTarget', this);
this._uiSetRows(this.get('rows'));
},
/**
* Fires after window's resize.
*
* @method _afterLayoutWindowResize
* @param {EventFacade} event
* @protected
*/
_afterLayoutWindowResize: function(event) {
var viewportSize = event.target.get('innerWidth');
this._handleResponsive(viewportSize);
},
/**
* Create LayoutCol objects to use with progressive enhancement
*
* @method _createLayoutCols
* @param {Array} cols
* @protected
*/
_createLayoutCols: function(cols) {
var bootstrapClassRegex = /col-\w+-\d+/,
colClass,
colSizeRegex = /\d$/,
layoutCols = [];
cols.each(function(col) {
colClass = col.get('className').match(bootstrapClassRegex)[0];
layoutCols.push(new A.LayoutCol(
{
size: A.Number.parse(colClass.match(colSizeRegex)[0]),
value: { content: col.one(SELECTOR_LAYOUT_COL_CONTENT).getHTML() }
}
));
});
return layoutCols;
},
/**
* Calculates column mode.
*
* @method _handleResponsive
* @param {Number} viewportSize
* @protected
*/
_handleResponsive: function(viewportSize) {
var enableColumnMode = viewportSize >= RESPONSIVENESS_BREAKPOINT;
if (this.get('isColumnMode') !== enableColumnMode) {
this._set('isColumnMode', enableColumnMode);
this.normalizeColsHeight(this.get('node').all(SELECTOR_ROW));
}
},
/**
* Removes a row from this layout by it's index.
*
* @method _removeRowByIndex
* @param {Number} index Row index to be removed from this layout
* @protected
*/
_removeRowByIndex: function(index) {
var rows = this.get('rows').concat();
rows.splice(index, 1);
this.set('rows', rows);
},
/**
* Removes a row from this layout by it's reference.
*
* @method _removeRowByReference
* @param {A.LayoutRow} row Column to be removed from this layout
* @protected
*/
_removeRowByReference: function(row) {
var index,
rows = this.get('rows').concat();
index = A.Array.indexOf(rows, row);
if (index >= 0) {
this._removeRowByIndex(index);
}
},
/**
* Builds layout through progressive enhancement
*
* @method _setProgressiveEnhancementLayout
* @param {Node} container Node to append the layout
* @protected
*/
_setProgressiveEnhancementLayout: function(container) {
var instance = this,
layoutCols = [],
layoutRow,
layoutRows = [],
rows = container.all(SELECTOR_ROW);
rows.each(function(row) {
layoutCols = instance._createLayoutCols(row.all(SELECTOR_COL));
layoutRow = new A.LayoutRow(
{
cols: layoutCols,
node: row.ancestor(SELECTOR_LAYOUT_ROW_CONTAINER_ROW)
}
);
layoutCols = [];
layoutRows.push(layoutRow);
});
this.set('rows', layoutRows);
},
/**
* Sets the `row` attribute.
*
* @method _setRows
* @param {Array} val
* @protected
*/
_setRows: function(val) {
var i,
newVal = [],
row;
for (i = 0; i < val.length; i++) {
row = val[i];
if (!A.instanceOf(row, A.LayoutRow)) {
row = new A.LayoutRow(row);
}
newVal.push(row);
}
return newVal;
},
/**
* Updates the UI according to the value of the `rows` attribute.
*
* @method _uiSetRows
* @param {Array} rows
* @protected
*/
_uiSetRows: function(rows) {
var node = this.get('node');
node.empty();
A.each(rows, function(row) {
node.append(row.get('node'));
});
}
}, {
/**
* Static property used to define the default attribute
* configuration for the Layout.
*
* @property ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* Determines if columns should collapse.
*
* @property isColumnMode
* @type {Boolean}
* @protected
*/
isColumnMode: {
readOnly: true,
validator: A.Lang.isBoolean,
value: false
},
/**
* The node where this column will be rendered.
*
* @attribute node
* @type Node
*/
node: {
validator: A.Lang.isNode,
valueFn: function() {
return A.Node.create('<div class="' + CSS_LAYOUT_NODE + '"></div>');
},
writeOnce: 'initOnly'
},
/**
* Rows to be appended into container node
*
* @attribute rows
* @type {Array}
*/
rows: {
setter: '_setRows',
validator: A.Lang.isArray,
value: []
}
}
});