/**
* The Datatable Component
*
* @module aui-datatable
* @submodule aui-datatable-selection
*/
var Lang = A.Lang,
isArray = Lang.isArray,
isString = Lang.isString,
isObject = Lang.isObject,
clamp = function(value, min, max) {
return Math.min(Math.max(value, min), max);
};
/**
* A base class for DataTableSelection.
*
* @class A.DataTableSelection
* @param {Object} config Object literal specifying widget configuration
* properties.
* @constructor
*/
var DataTableSelection = function() {};
/**
* Static property used to define the default attribute
* configuration for the `A.DataTableSelection`.
*
* @property ATTRS
* @type Object
* @static
*/
DataTableSelection.ATTRS = {
/**
* Defines the active cell of the `A.DataTableSelection`.
*
* @attribute activeCell
* @type Node
*/
activeCell: {
getter: '_getActiveCell'
},
/**
* Defines and stores the active cell coordinates, `[row, cell]`, of the
* `A.DataTableSelection`.
*
* @attribute activeCoord
* @default [-1, -1]
* @type Array
*/
activeCoord: {
value: [-1, -1]
},
/**
* Defines the active row of the `A.DataTableSelection`.
*
* @attribute activeRow
* @type Node
*/
activeRow: {
getter: '_getActiveRow'
},
/**
* Defines the selected cells and rows of the `A.DataTableSelection`.
*
* @attribute selection
* @type Object
*/
selection: {
setter: '_setSelection'
},
/**
* Defines and stores the `tabIndex` of the `activeCell`.
*
* @attribute tabIndex
* @default 0
* @type Number
*/
tabIndex: {
value: 0
}
};
A.mix(DataTableSelection.prototype, {
_capturing: false,
_selectionEnd: null,
_selectionSeed: null,
_selectionStart: null,
/**
* Construction logic executed during `A.DataTableSelection` instantiation.
* Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function() {
var instance = this,
boundingBox = instance.get('boundingBox');
instance.CLASS_NAMES_SELECTION = {
cell: instance.getClassName('cell'),
selection: instance.getClassName('selection')
};
instance._bindSelectionUI();
boundingBox.addClass(instance.CLASS_NAMES_SELECTION.selection);
},
/**
* Destructor lifecycle implementation for the `A.DataTableSelection` class.
* Detaches `_selectionKeyHandler` event listener.
*
* @method destroy
* @protected
*/
destroy: function() {
var instance = this;
instance._selectionKeyHandler.detach();
},
/**
* Return the selected cells and within the coordinates `coords`.
*
* @method captureSelection
* @param {Object} coords Cell coordinates.
* @return {Object} Selected cells and rows.
*/
captureSelection: function(coords) {
var instance = this,
cells = [],
cols = [],
records = [],
rows = [],
i;
for (i = 0; i < coords.length; i++) {
var c = coords[i],
cell = instance.getCell(c);
rows.push(c[0]);
cells.push(cell);
cols.push(instance.getColumn(cell));
}
rows = A.Array.unique(rows);
cols = A.Array.unique(cols);
for (i = 0; i < rows.length; i++) {
rows[i] = instance.getRow(rows[i]);
records[i] = instance.getRecord(rows[i]);
}
return {
cells: cells,
cols: cols,
coords: coords,
records: records,
rows: rows
};
},
/**
* Gets the active column based off the `activeCell` attribute.
*
* @method getActiveColumn
* @return {Object} Active column.
*/
getActiveColumn: function() {
var instance = this;
return instance.getColumn(instance.get('activeCell'));
},
/**
* Gets the active record based odd the `activeRow` attribute.
*
* @method getActiveRecord
* @return {Object} Active record.
*/
getActiveRecord: function() {
var instance = this;
return instance.getRecord(instance.get('activeRow'));
},
/**
* Gets the cell coordinates of the passed `seed`.
*
* @method getCoord
* @param {Node} seed
* @return {Array} Cell coordinates.
*/
getCoord: function(seed) {
var instance = this,
cell = instance.getCell(seed),
tbody = instance.body.tbodyNode,
rowIndexOffset = tbody.get('firstChild.rowIndex');
return [cell.get('parentNode.rowIndex') - rowIndexOffset, cell.get('cellIndex')];
},
/**
* Focus the active cell on datatable sorting.
*
* @method _afterActiveCoordChange
* @param {EventFacade} event
* @protected
*/
_afterActiveCoordChange: function(event) {
var instance = this,
activeCell = instance.getCell(event.newVal);
if (activeCell) {
activeCell.setAttribute('tabindex', 0).focus();
}
},
/**
* Bind the selection UI.
*
* @method _bindSelectionUI
* @protected
*/
_bindSelectionUI: function() {
var instance = this,
classNames = instance.CLASS_NAMES_SELECTION;
instance._selectionKeyHandler = A.getDoc().on(
'key', A.bind(instance._onSelectionKey, instance), 'down:enter,37,38,39,40');
instance.after('activeCoordChange', instance._afterActiveCoordChange);
instance.delegate('mouseup', A.bind(instance._onSelectionMouseUp, instance), '.' + classNames.cell);
instance.delegate('mousedown', A.bind(instance._onSelectionMouseDown, instance), '.' + classNames.cell);
instance.delegate('mouseenter', A.bind(instance._onSelectionMouseEnter, instance), '.' + classNames.cell);
},
/**
* Return the active cell.
*
* @method _getActiveCell
* @protected
*/
_getActiveCell: function() {
var instance = this,
activeCoord = instance.get('activeCoord'),
activeRowIndex = activeCoord[0],
activeCellIndex = activeCoord[1];
if (activeRowIndex > -1 && activeCellIndex > -1) {
return instance.getCell([activeRowIndex, activeCellIndex]);
}
return null;
},
/**
* Return the active row.
*
* @method _getActiveRow
* @protected
*/
_getActiveRow: function() {
var instance = this,
activeCoord = instance.get('activeCoord'),
activeRowIndex = activeCoord[0];
if (activeRowIndex > -1) {
return instance.getRow(activeRowIndex);
}
return null;
},
/**
* Fires on `mousedown` event.
*
* @method _onSelectionMouseDown
* @param {EventFacade} event
* @protected
*/
_onSelectionMouseDown: function(event) {
var instance = this,
seed = event.currentTarget,
boundingBox = instance.get('boundingBox'),
coords = instance.getCoord(seed);
boundingBox.unselectable();
instance._capturing = true;
instance._selectionSeed = seed;
instance._selectionStart = instance._selectionEnd = instance.getCoord(seed);
instance.set('activeCoord', coords);
},
/**
* Fires on `mouseenter` event.
*
* @method _onSelectionMouseEnter
* @param {EventFacade} event
* @protected
*/
_onSelectionMouseEnter: function(event) {
var instance = this,
seed = event.currentTarget;
if (!instance._capturing) {
return;
}
instance._selectionSeed = seed;
instance._selectionEnd = instance.getCoord(seed);
instance.set('selection', {
start: instance._selectionStart,
end: instance._selectionEnd
});
},
/**
* Fires on `mouseup` event.
*
* @method _onSelectionMouseUp
* @param {EventFacade} event
* @protected
*/
_onSelectionMouseUp: function() {
var instance = this,
boundingBox = instance.get('boundingBox');
if (instance.get('focused')) {
instance._selectionEnd = instance.getCoord(instance._selectionSeed);
instance.set('selection', {
start: instance._selectionStart,
end: instance._selectionEnd
});
}
boundingBox.selectable();
instance._capturing = false;
},
/**
* Fires on `key` event with the listened selection keys.
*
* @method _onSelectionKey
* @param {EventFacade} event
* @protected
*/
_onSelectionKey: function(event) {
var instance = this,
body = instance.body,
tbody = body.tbodyNode,
keyCode = event.keyCode,
activeCell = instance.get('activeCell'),
activeCoord,
imax = tbody.get('children').size(),
jmax = body.get('columns').length,
i,
j;
if (activeCell && instance.get('focused')) {
activeCoord = instance.getCoord(activeCell);
i = activeCoord[0];
j = activeCoord[1];
if (keyCode === 37) {
j--;
}
else if (keyCode === 38) {
i--;
}
else if (keyCode === 39) {
j++;
}
else if (keyCode === 40) {
i++;
}
i = clamp(i, 0, imax - 1);
j = clamp(j, 0, jmax - 1);
instance.set('activeCoord', [i, j]);
instance.set('selection', [i, j]);
event.preventDefault();
}
},
/**
* Parse selection coordinates range.
*
* @method _parseRange
* @param {Array} val
* @return {Array} coords
* @protected
*/
_parseRange: function(val) {
var c1 = val[0],
c2 = val[1],
coords = [],
i,
j;
for (i = Math.min(c1[0], c2[0]); i <= Math.max(c1[0], c2[0]); i++) {
for (j = Math.min(c1[1], c2[1]); j <= Math.max(c1[1], c2[1]); j++) {
coords.push([i, j]);
}
}
return coords;
},
/**
* Set selection.
*
* @method _setSelection
* @param val
* @return {Object} Selected cells and rows.
* @protected
*/
_setSelection: function(val) {
var instance = this;
if (isArray(val)) {
if (!isArray(val[0])) {
val = [val];
}
}
else if (isObject(val)) {
val = instance._parseRange([val.start, val.end]);
}
else if (A.instanceOf(val, A.Node)) {
val = [instance.getCoord(val)];
}
return instance.captureSelection(val);
}
});
A.DataTable.Selection = DataTableSelection;
A.Base.mix(A.DataTable, [DataTableSelection]);
/**
* Calculate and return the column based on the passed `seed`.
*
* @method getColumn
* @param {Node} seed
* @return {Object} Column.
*/
A.DataTable.prototype.getColumn = (function(original) {
return function(seed) {
var cell;
if (A.instanceOf(seed, A.Node)) {
cell = this.getCell(seed);
seed = cell && (cell.get('className').match(
new RegExp(this.getClassName('col', '(\\w+)'))) || [])[1];
}
return original.call(this, seed);
};
}(A.DataTable.prototype.getColumn));
/**
* Return the row based on the passed `seed`.
*
* Add support to get a row by seed on DataTable getRow
* See http://yuilibrary.com/projects/yui3/ticket/2532605
*
* @method getRow
* @param {Node} seed
* @return {Object} Row.
*/
A.DataTable.prototype.getRow = (function(original) {
return function(seed) {
var instance = this,
tbody = instance.body.tbodyNode,
row;
if (A.instanceOf(seed, A.Node)) {
row = seed.ancestor(function(node) {
return node.get('parentNode').compareTo(tbody);
}, true);
return row;
}
else {
return original.call(this, seed);
}
};
}(A.DataTable.prototype.getRow));
/**
* DataTable columns configuration breaks on n-depth cloning complex objects
* See http://yuilibrary.com/projects/yui3/ticket/2532597
*
* @method _setColumns
* @protected
*/
A.DataTable.prototype._setColumns = function(val) {
var keys = {},
known = [],
knownCopies = [],
arrayIndex = A.Array.indexOf,
isArray = A.Lang.isArray;
function copyObj(o) {
var copy = {},
key, val, i;
known.push(o);
knownCopies.push(copy);
for (key in o) {
if (o.hasOwnProperty(key)) {
val = o[key];
if (isArray(val)) {
copy[key] = val.slice();
// Patch is right here, the second condition
}
else if (isObject(val, true) && val.constructor === copy.constructor) {
i = arrayIndex(val, known);
copy[key] = i === -1 ? copyObj(val) : knownCopies[i];
}
else {
copy[key] = o[key];
}
}
}
return copy;
}
function genId(name) {
// Sanitize the name for use in generated CSS classes.
// TODO: is there more to do for other uses of _id?
name = name.replace(/\s+/, '-');
if (keys[name]) {
name += (keys[name]++);
}
else {
keys[name] = 1;
}
return name;
}
function process(cols, parent) {
var columns = [],
i, len, col, yuid;
for (i = 0, len = cols.length; i < len; ++i) {
columns[i] = // chained assignment
col = isString(cols[i]) ? {
key: cols[i]
} : copyObj(cols[i]);
yuid = A.stamp(col);
// For backward compatibility
if (!col.id) {
// Implementers can shoot themselves in the foot by setting
// this config property to a non-unique value
col.id = yuid;
}
if (col.field) {
// Field is now known as "name" to avoid confusion with data
// fields or schema.resultFields
col.name = col.field;
}
if (parent) {
col._parent = parent;
}
else {
delete col._parent;
}
// Unique id based on the column's configured name or key,
// falling back to the yuid. Duplicates will have a counter
// added to the end.
col._id = genId(col.name || col.key || col.id);
if (isArray(col.children)) {
col.children = process(col.children, col);
}
}
return columns;
}
return val && process(val);
};