define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/dom-construct',
'dojo/dom-class',
'dojo/on',
'dojo/has',
'./List',
'./util/misc',
'dojo/_base/sniff'
], function (declare, lang, domConstruct, domClass, listen, has, List, miscUtil) {
function appendIfNode(parent, subNode) {
if (subNode && subNode.nodeType) {
parent.appendChild(subNode);
}
}
function replaceInvalidChars(str) {
// Replaces invalid characters for a CSS identifier with hyphen,
// as dgrid does for field names / column IDs when adding classes.
return miscUtil.escapeCssIdentifier(str, '-');
}
var Grid = declare(List, {
columns: null,
// hasNeutralSort: Boolean
// Determines behavior of toggling sort on the same column.
// If false, sort toggles between ascending and descending and cannot be
// reset to neutral without sorting another column.
// If true, sort toggles between ascending, descending, and neutral.
hasNeutralSort: false,
// cellNavigation: Boolean
// This indicates that focus is at the cell level. This may be set to false to cause
// focus to be at the row level, which is useful if you want only want row-level
// navigation.
cellNavigation: true,
tabableHeader: true,
showHeader: true,
column: function (target) {
// summary:
// Get the column object by node, or event, or a columnId
if (typeof target !== 'object') {
return this.columns[target];
}
else {
return this.cell(target).column;
}
},
listType: 'grid',
cell: function (target, columnId) {
// summary:
// Get the cell object by node, or event, id, plus a columnId
if (target.column && target.element) {
return target;
}
if (target.target && target.target.nodeType) {
// event
target = target.target;
}
var element;
if (target.nodeType) {
do {
if (this._rowIdToObject[target.id]) {
break;
}
var colId = target.columnId;
if (colId) {
columnId = colId;
element = target;
break;
}
target = target.parentNode;
} while (target && target !== this.domNode);
}
if (!element && typeof columnId !== 'undefined') {
var row = this.row(target),
rowElement = row && row.element;
if (rowElement) {
var elements = rowElement.getElementsByTagName('td');
for (var i = 0; i < elements.length; i++) {
if (elements[i].columnId === columnId) {
element = elements[i];
break;
}
}
}
}
if (target != null) {
return {
row: row || this.row(target),
column: columnId && this.column(columnId),
element: element
};
}
},
createRowCells: function (tag, createCell, subRows, item, options) {
// summary:
// Generates the grid for each row (used by renderHeader and and renderRow)
var row = domConstruct.create('table', {
className: 'dgrid-row-table',
role: 'presentation'
}),
// IE < 9 needs an explicit tbody; other browsers do not
tbody = (has('ie') < 9) ? domConstruct.create('tbody', null, row) : row,
tr,
si, sl, i, l, // iterators
subRow, column, id, extraClasses, className,
cell, colSpan, rowSpan; // used inside loops
// Allow specification of custom/specific subRows, falling back to
// those defined on the instance.
subRows = subRows || this.subRows;
for (si = 0, sl = subRows.length; si < sl; si++) {
subRow = subRows[si];
// for single-subrow cases in modern browsers, TR can be skipped
// http://jsperf.com/table-without-trs
tr = domConstruct.create('tr', null, tbody);
if (subRow.className) {
tr.className = subRow.className;
}
for (i = 0, l = subRow.length; i < l; i++) {
// iterate through the columns
column = subRow[i];
id = column.id;
extraClasses = column.field ?
' field-' + replaceInvalidChars(column.field) :
'';
className = typeof column.className === 'function' ?
column.className(item) : column.className;
if (className) {
extraClasses += ' ' + className;
}
cell = domConstruct.create(tag, {
className: 'dgrid-cell' +
(id ? ' dgrid-column-' + replaceInvalidChars(id) : '') + extraClasses,
role: tag === 'th' ? 'columnheader' : 'gridcell'
});
cell.columnId = id;
colSpan = column.colSpan;
if (colSpan) {
cell.colSpan = colSpan;
}
rowSpan = column.rowSpan;
if (rowSpan) {
cell.rowSpan = rowSpan;
}
createCell(cell, column, item, options);
// add the td to the tr at the end for better performance
tr.appendChild(cell);
}
}
return row;
},
_createBodyRowCell: function (cellElement, column, item, options) {
var cellData = item;
// Support get function or field property (similar to DataGrid)
if (column.get) {
cellData = column.get(item);
}
else if ('field' in column && column.field !== '_item') {
cellData = item[column.field];
}
if (column.renderCell) {
// A column can provide a renderCell method to do its own DOM manipulation,
// event handling, etc.
appendIfNode(cellElement, column.renderCell(item, cellData, cellElement, options));
}
else {
this._defaultRenderCell.call(column, item, cellData, cellElement, options);
}
},
_createHeaderRowCell: function (cellElement, column) {
var contentNode = column.headerNode = cellElement;
var field = column.field;
if (field) {
cellElement.field = field;
}
// allow for custom header content manipulation
if (column.renderHeaderCell) {
appendIfNode(contentNode, column.renderHeaderCell(contentNode));
}
else if ('label' in column || column.field) {
contentNode.appendChild(document.createTextNode(
'label' in column ? column.label : column.field));
}
if (column.sortable !== false && field && field !== '_item') {
cellElement.sortable = true;
cellElement.className += ' dgrid-sortable';
}
},
left: function (cell, steps) {
if (!cell.element) {
cell = this.cell(cell);
}
return this.cell(this._move(cell, -(steps || 1), 'dgrid-cell'));
},
right: function (cell, steps) {
if (!cell.element) {
cell = this.cell(cell);
}
return this.cell(this._move(cell, steps || 1, 'dgrid-cell'));
},
_defaultRenderCell: function (object, value, td) {
// summary:
// Default renderCell implementation.
// NOTE: Called in context of column definition object.
// object: Object
// The data item for the row currently being rendered
// value: Mixed
// The value of the field applicable to the current cell
// td: DOMNode
// The cell element representing the current item/field
// options: Object?
// Any additional options passed through from renderRow
if (this.formatter) {
// Support formatter, with or without formatterScope
var formatter = this.formatter,
formatterScope = this.grid.formatterScope;
td.innerHTML = typeof formatter === 'string' && formatterScope ?
formatterScope[formatter](value, object) : this.formatter(value, object);
}
else if (value != null) {
td.appendChild(document.createTextNode(value));
}
},
renderRow: function (item, options) {
var row = this.createRowCells('td', lang.hitch(this, '_createBodyRowCell'),
options && options.subRows, item, options);
// row gets a wrapper div for a couple reasons:
// 1. So that one can set a fixed height on rows (heights can't be set on
's AFAICT)
// 2. So that outline style can be set on a row when it is focused,
// and Safari's outline style is broken on
var div = domConstruct.create('div', { role: 'row' });
div.appendChild(row);
return div;
},
renderHeader: function () {
// summary:
// Setup the headers for the grid
var grid = this,
headerNode = this.headerNode;
headerNode.setAttribute('role', 'row');
// clear out existing header in case we're resetting
domConstruct.empty(headerNode);
var row = this.createRowCells('th', lang.hitch(this, '_createHeaderRowCell'),
this.subRows && this.subRows.headerRows);
this._rowIdToObject[row.id = this.id + '-header'] = this.columns;
headerNode.appendChild(row);
// If the columns are sortable, re-sort on clicks.
// Use a separate listener property to be managed by renderHeader in case
// of subsequent calls.
if (this._sortListener) {
this._sortListener.remove();
}
this._sortListener = listen(row, 'click,keydown', function (event) {
// respond to click, space keypress, or enter keypress
if (event.type === 'click' || event.keyCode === 32 ||
(!has('opera') && event.keyCode === 13)) {
var target = event.target;
var field;
var sort;
var newSort;
var eventObj;
do {
if (target.sortable) {
field = target.field || target.columnId;
sort = grid.sort[0];
if (!grid.hasNeutralSort || !sort || sort.property !== field || !sort.descending) {
// If the user toggled the same column as the active sort,
// reverse sort direction
newSort = [{
property: field,
descending: sort && sort.property === field &&
!sort.descending
}];
}
else {
// If the grid allows neutral sort and user toggled an already-descending column,
// clear sort entirely
newSort = [];
}
// Emit an event with the new sort
eventObj = {
bubbles: true,
cancelable: true,
grid: grid,
parentType: event.type,
sort: newSort
};
if (listen.emit(event.target, 'dgrid-sort', eventObj)) {
// Stash node subject to DOM manipulations,
// to be referenced then removed by sort()
grid._sortNode = target;
grid.set('sort', newSort);
}
break;
}
} while ((target = target.parentNode) && target !== headerNode);
}
});
},
resize: function () {
// extension of List.resize to allow accounting for
// column sizes larger than actual grid area
var headerTableNode = this.headerNode.firstChild,
contentNode = this.contentNode,
width;
this.inherited(arguments);
// Force contentNode width to match up with header width.
contentNode.style.width = ''; // reset first
if (contentNode && headerTableNode) {
if ((width = headerTableNode.offsetWidth) > contentNode.offsetWidth) {
// update size of content node if necessary (to match size of rows)
// (if headerTableNode can't be found, there isn't much we can do)
contentNode.style.width = width + 'px';
}
}
},
destroy: function () {
// Run _destroyColumns first to perform any column plugin tear-down logic.
this._destroyColumns();
if (this._sortListener) {
this._sortListener.remove();
}
this.inherited(arguments);
},
_setSort: function () {
// summary:
// Extension of List.js sort to update sort arrow in UI
// Normalize sort first via inherited logic, then update the sort arrow
this.inherited(arguments);
this.updateSortArrow(this.sort);
},
_findSortArrowParent: function (field) {
// summary:
// Method responsible for finding cell that sort arrow should be
// added under. Called by updateSortArrow; separated for extensibility.
var columns = this.columns;
for (var i in columns) {
var column = columns[i];
if (column.field === field) {
return column.headerNode;
}
}
},
updateSortArrow: function (sort, updateSort) {
// summary:
// Method responsible for updating the placement of the arrow in the
// appropriate header cell. Typically this should not be called (call
// set("sort", ...) when actually updating sort programmatically), but
// this method may be used by code which is customizing sort (e.g.
// by reacting to the dgrid-sort event, canceling it, then
// performing logic and calling this manually).
// sort: Array
// Standard sort parameter - array of object(s) containing property name
// and optional descending flag
// updateSort: Boolean?
// If true, will update this.sort based on the passed sort array
// (i.e. to keep it in sync when custom logic is otherwise preventing
// it from being updated); defaults to false
// Clean up UI from any previous sort
if (this._lastSortedArrow) {
// Remove the sort classes from the parent node
domClass.remove(this._lastSortedArrow.parentNode, 'dgrid-sort-up dgrid-sort-down');
// Destroy the lastSortedArrow node
domConstruct.destroy(this._lastSortedArrow);
delete this._lastSortedArrow;
}
if (updateSort) {
this.sort = sort;
}
if (!sort[0]) {
return; // Nothing to do if no sort is specified
}
var prop = sort[0].property,
desc = sort[0].descending,
// if invoked from header click, target is stashed in _sortNode
target = this._sortNode || this._findSortArrowParent(prop),
arrowNode;
delete this._sortNode;
// Skip this logic if field being sorted isn't actually displayed
if (target) {
target = target.contents || target;
// Place sort arrow under clicked node, and add up/down sort class
arrowNode = this._lastSortedArrow = domConstruct.create('div', {
className: 'dgrid-sort-arrow ui-icon',
innerHTML: ' ',
role: 'presentation'
}, target, 'first');
domClass.add(target, 'dgrid-sort-' + (desc ? 'down' : 'up'));
// Call resize in case relocation of sort arrow caused any height changes
this.resize();
}
},
styleColumn: function (colId, css) {
// summary:
// Dynamically creates a stylesheet rule to alter a column's style.
return this.addCssRule('#' + miscUtil.escapeCssIdentifier(this.domNode.id) +
' .dgrid-column-' + replaceInvalidChars(colId), css);
},
/*=====
_configColumn: function (column, rowColumns, prefix) {
// summary:
// Method called when normalizing base configuration of a single
// column. Can be used as an extension point for behavior requiring
// access to columns when a new configuration is applied.
},=====*/
_configColumns: function (prefix, rowColumns) {
// configure the current column
var subRow = [],
isArray = rowColumns instanceof Array;
function configColumn(column, columnId) {
if (typeof column === 'string') {
rowColumns[columnId] = column = { label: column };
}
if (!isArray && !column.field) {
column.field = columnId;
}
columnId = column.id = column.id || (isNaN(columnId) ? columnId : (prefix + columnId));
// allow further base configuration in subclasses
if (this._configColumn) {
this._configColumn(column, rowColumns, prefix);
// Allow the subclasses to modify the column id.
columnId = column.id;
}
if (isArray) {
this.columns[columnId] = column;
}
// add grid reference to each column object for potential use by plugins
column.grid = this;
subRow.push(column); // make sure it can be iterated on
}
miscUtil.each(rowColumns, configColumn, this);
return isArray ? rowColumns : subRow;
},
_destroyColumns: function () {
// summary:
// Extension point for column-related cleanup. This is called
// immediately before configuring a new column structure,
// and when the grid is destroyed.
// First remove rows (since they'll be refreshed after we're done),
// so that anything temporarily extending removeRow can run.
// (cleanup will end up running again, but with nothing to iterate.)
this.cleanup();
},
configStructure: function () {
// configure the columns and subRows
var subRows = this.subRows,
columns = this._columns = this.columns;
// Reset this.columns unless it was already passed in as an object
this.columns = !columns || columns instanceof Array ? {} : columns;
if (subRows) {
// Process subrows, which will in turn populate the this.columns object
for (var i = 0; i < subRows.length; i++) {
subRows[i] = this._configColumns(i + '-', subRows[i]);
}
}
else {
this.subRows = [this._configColumns('', columns)];
}
},
_getColumns: function () {
// _columns preserves what was passed to set("columns"), but if subRows
// was set instead, columns contains the "object-ified" version, which
// was always accessible in the past, so maintain that accessibility going
// forward.
return this._columns || this.columns;
},
_setColumns: function (columns) {
this._destroyColumns();
// reset instance variables
this.subRows = null;
this.columns = columns;
// re-run logic
this._updateColumns();
},
_setSubRows: function (subrows) {
this._destroyColumns();
this.subRows = subrows;
this._updateColumns();
},
_updateColumns: function () {
// summary:
// Called when columns, subRows, or columnSets are reset
this.configStructure();
this.renderHeader();
this.refresh();
// re-render last collection if present
this._lastCollection && this.renderArray(this._lastCollection);
// After re-rendering the header, re-apply the sort arrow if needed.
if (this._started) {
if (this.sort.length) {
this._lastSortedArrow = null;
this.updateSortArrow(this.sort);
} else {
// Only call resize directly if we didn't call updateSortArrow,
// since that calls resize itself when it updates.
this.resize();
}
}
}
});
Grid.appendIfNode = appendIfNode;
return Grid;
});