/**
* @fileoverview
* Implementation of a data-grid widget based on HTML table.
* @author Partridge Jiang
*/
/*
* requires /lan/classes.js
* requires /utils/kekule.utils.js
* requires /utils/kekule.domUtils.js
* requires /xbrowsers/kekule.x.js
* requires /widgets/kekule.widget.base.js
* requires /widgets/kekule.widget.styleResources.js
* requires /widgets/kekule.widget.containers.js
* requires /widgets/commonCtrls/kekule.widget.buttons.js
* requires /widgets/commonCtrls/kekule.widget.formControls.js
* requires /widgets/grids/kekule.widget.dataSets.js
* requires /localizations/
*/
(function(){
"use strict";
var DU = Kekule.DomUtils;
var EU = Kekule.HtmlElementUtils;
var CNS = Kekule.Widget.HtmlClassNames;
/** @ignore */
Kekule.Widget.HtmlClassNames = Object.extend(Kekule.Widget.HtmlClassNames, {
DATAGRID: 'K-DataGrid',
DATATABLE: 'K-DataTable',
DATATABLE_ROW_ODD: 'K-Odd',
DATATABLE_ROW_EVEN: 'K-Even',
DATATABLE_CELL_WRAPPER: 'K-DataTable-CellWrapper',
DATATABLE_HEADCELL_INTERACTABLE: 'K-DataTable-HeadCellInteractable',
DATATABLE_OPER_COL: 'K-DataTable-OperCol',
DATATABLE_OPER_CELL: 'K-DataTable-OperCell',
DATATABLE_CHECK_COL: 'K-DataTable-CheckCol',
DATATABLE_CHECK_CELL: 'K-DataTable-CheckCell',
DATATABLE_DATA_COL: 'K-DataTable-DataCol',
DATATABLE_DATA_CELL: 'K-DataTable-DataCell',
DATATABLE_SORTMARK: 'K-DataTable-SortMark',
DATATABLE_SORTASC: 'K-Sort-Asc',
DATATABLE_SORTDESC: 'K-Sort-Desc',
DATATABLE_EDIT: 'K-DataTable-Edit',
DATATABLE_DELETE: 'K-DataTable-Delete',
DATATABLE_INSERT: 'K-DataTable-Insert',
/*
DATATABLE_OPER_COL_MODE_ALL: 'K-DataTable-OperColMode-All',
DATATABLE_OPER_COL_MODE_HOVER: 'K-DataTable-OperColMode-Hover',
DATATABLE_OPER_COL_MODE_ACTIVE: 'K-DataTable-OperColMode-Active',
*/
PAGENAVIGATOR: 'K-PageNavigator',
PAGENAVIGATOR_FIRST: 'K-PageNavigator-First',
PAGENAVIGATOR_LAST: 'K-PageNavigator-Last',
PAGENAVIGATOR_PREV: 'K-PageNavigator-Prev',
PAGENAVIGATOR_NEXT: 'K-PageNavigator-Next',
PAGENAVIGATOR_PAGEINDEXER: 'K-PageNavigator-PageIndexer',
PAGENAVIGATOR_PAGEINPUT: 'K-PageNavigator-PageInput',
PAGENAVIGATOR_PAGESELECTOR: 'K-PageNavigator-PageSelector'
});
/**
* Enumeration of predifined data table column names.
* @enum
*/
Kekule.Widget.DataTableColNames = {
OPER: 'OPER',
CHECK: 'CHECK',
ALL: '*'
};
var TCN = Kekule.Widget.DataTableColNames;
/**
* An widget to to display array of data in grid table (based on HTML table).
* @class
* @augments Kekule.Widget.BaseWidget
*
* @property {Array} columns Column definitions. Each item is a hash that defines the aspects of column, e.g.:
* [
* {'name': 'fieldName', 'text': 'Caption of column', 'hint': 'hint of column head',
* 'disableInteract': trueOrFalse,
* 'className': 'HtmlClassOfEachColumnCell', 'style': 'CSSInlineStyleOfEachColumnCell',
* 'colClassName': 'HtmlClassOfColElem', 'colStyle': 'CSSInlineStyleOfColElem'}
* ]
* name can be set to some special values to create special columns:
* 'OPER': operation column,
* 'CHECK': check box column,
* '*': Show all fields in data
* e.g.: [{'name': 'CHECK'}, {'name': '*'}, {'name': 'OPER}],
* or use string directly: ['CHECK', '*', 'OPER'].
*
* @property {Array} data Data displayed in grid, each item is a hash. e.g.:
* [
* {'id': 1, 'name': 'Smith', 'email': 'smith@ab.com'},
* {'id': 2, 'name': 'Bob', 'email': null}
* ]
* @property {Array} sortFields Fields to sort data. If field name is prefixed with '!', means sort in desc order.
* e.g. ['id', '!name']
* @property {Func} sortFunc Custom function to sort data. Func(dataItem1, dataItem2).
* Note: if data is provided by data pager, this property will be unusable.
* @property {String} operColShowMode Value from {@link Kekule.Widget.DataTable.OperColShowMode},
* determinate when to show operation column in data table.
* @property {Array} operWidgets Array of predefined button names or widget definition hashes shown in operation column.
* @property {Bool} showTableHead Whether table head row is displayed.
* @property {Bool} enableHeadInteraction If this property is true, click on head cell with sort data in table automatically.
* @property {Bool} enableActiveRow If this property is true, click on data row/cell will mark it as active
* and mouse hover will mark the row as "hover".
*/
Kekule.Widget.DataTable = Class.create(Kekule.Widget.BaseWidget,
/** @lends Kekule.Widget.DataTable# */
{
/** @private */
CLASS_NAME: 'Kekule.Widget.DataTable',
/** @private */
BINDABLE_TAG_NAMES: ['div', 'span'], //['table'],
/** @private */
PREFIX_SORT_DESC: '!',
/** @private */
COLDEF_FIELD: '__$colDef__',
/** @private */
ROWDATA_FIELD: '__$rowData__',
/** @private */
ROW_OPER_TOOLBAR_FIELD: '__$rowOperToolbar__',
/** @private */
ROW_CHECKBOX_FIELD: '__$rowCheckBox__',
/** @private */
COLNAME_OPER: TCN.OPER,
/** @private */
COLNAME_CHECK: TCN.CHECK,
/** @private */
COLNAME_ALL: TCN.ALL,
/** @constructs */
initialize: function($super, parentOrElementOrDocument)
{
//this._tableElem = null;
this._displayData = null;
this.setPropStoreFieldValue('showTableHead', true);
$super(parentOrElementOrDocument);
},
/** @private */
initProperties: function()
{
this.defineProp('data', {'dataType': DataType.ARRAY});
this.defineProp('columns', {'dataType': DataType.ARRAY});
this.defineProp('operColShowMode', {'dataType': DataType.STRING});
this.defineProp('operWidgets', {'dataType': DataType.ARRAY});
this.defineProp('sortFields', {
'dataType': DataType.ARRAY,
'setter': function(value)
{
var a = value ? Kekule.ArrayUtils.toArray(value) : null;
this.setPropStoreFieldValue('sortFields', a);
if (this.getDataPager())
{
this.getDataPager().setSortFields(a);
}
}
});
this.defineProp('sortFunc', {'dataType': DataType.FUNCTION});
this.defineProp('showTableHead', {'dataType': DataType.BOOL});
this.defineProp('enableHeadInteraction', {'dataType': DataType.BOOL});
this.defineProp('enableActiveRow', {'dataType': DataType.BOOL,
'setter': function(value)
{
this.setPropStoreFieldValue('enableActiveRow', value);
if (!value)
{
this.setActiveCell(null);
}
}
});
// private properties
this.defineProp('activeCell', {'dataType': DataType.OBJECT,
'setter': function(value)
{
var old = this.getActiveCell();
if (old !== value)
{
if (old)
EU.removeClass(old, CNS.STATE_ACTIVE);
this.setPropStoreFieldValue('activeCell', value);
if (value)
{
EU.addClass(value, CNS.STATE_ACTIVE);
var row = this.getParentRow(value);
if (row)
this.setActiveRow(row);
}
else
this.setActiveRow(null);
}
}
});
this.defineProp('activeRow', {'dataType': DataType.OBJECT,
'setter': function(value)
{
var old = this.getActiveRow();
if (old !== value)
{
this.activeRowChanged(old, value);
this.setPropStoreFieldValue('activeRow', value);
}
}
});
this.defineProp('hoverCell', {'dataType': DataType.OBJECT,
'setter': function(value)
{
var old = this.getHoverCell();
if (old !== value)
{
if (old)
EU.removeClass(old, CNS.STATE_HOVER);
this.setPropStoreFieldValue('hoverCell', value);
if (value)
{
EU.addClass(value, CNS.STATE_HOVER);
var row = this.getParentRow(value);
if (row)
this.setHoverRow(row);
}
else
this.setHoverRow(null);
}
}
});
this.defineProp('hoverRow', {'dataType': DataType.OBJECT,
'setter': function(value)
{
var old = this.getHoverRow();
if (old !== value)
{
this.hoverRowChanged(old, value);
this.setPropStoreFieldValue('hoverRow', value);
}
}
});
this.defineProp('dataPager', {'dataType': 'Kekule.Widget.DataPager',
'setter': function(value)
{
var old = this.getDataPager();
if (old !== value)
{
this.setPropStoreFieldValue('dataPager', value);
this.dataPagerChanged(value, old);
}
}
});
},
/** @ignore */
initPropValues: function($super)
{
$super();
this.reactOperEditBind = this.reactOperEdit.bind(this);
this.reactOperDeleteBind = this.reactOperDelete.bind(this);
this.reactOperInsertBind = this.reactOperInsert.bind(this);
},
/** @ignore */
doObjectChange: function($super, modifiedPropNames)
{
$super(modifiedPropNames);
var relatedProps = [
'data', 'columns', 'sortFields', 'sortFunc', 'showTableHead', 'enableHeadInteraction',
'operColShowMode', 'operWidgets'
];
if (Kekule.ArrayUtils.intersect(modifiedPropNames, relatedProps).length) // need recreate
{
this.recreateChildContent();
}
},
/** @ignore */
doGetWidgetClassName: function($super)
{
return $super() + ' ' + CNS.DATATABLE;
},
/** @ignore */
doCreateRootElement: function(doc)
{
//var result = doc.createElement('table');
var result = doc.createElement('div');
//this._tableElem = result;
return result;
},
/** @ignore */
doCreateSubElements: function(doc, rootElem)
{
//return this.recreateChildContent(doc, rootElem);
return this.doCreateDataTable(doc, rootElem);
},
/** @private */
doCreateDataTable: function(doc, parentElem)
{
var result = doc.createElement('table');
//this._tableElem = result;
this.recreateChildContent(doc, result);
parentElem.appendChild(result);
return result;
},
/** @private */
getTableElement: function()
{
return this.getElement() && this.getElement().getElementsByTagName('table')[0];
},
/** @private */
getShowOperCol: function()
{
var mode = this.getOperColShowMode() || Kekule.Widget.DataTable.OperColShowMode.NONE;
return mode !== Kekule.Widget.DataTable.OperColShowMode.NONE;
},
/**
* Recreate child elements in table.
* @private
*/
recreateChildContent: function(doc, parentElem)
{
this.setActiveCell(null);
this.setHoverCell(null);
var data = this.prepareData();
// remove old table and create new one
var tableElem = parentElem || this.getTableElement();
var result = [];
if (tableElem)
{
DU.clearChildContent(tableElem);
if (!doc)
var doc = tableElem.ownerDocument;
result.push(this.doCreateDataTableColGroup(doc, tableElem, data.columns));
if (this.getShowTableHead())
result.push(this.doCreateDataTableHead(doc, tableElem, data.columns));
result.push(this.doCreateDataTableBody(doc, tableElem, data.columns, data.data));
}
return result;
},
/** @private */
doCreateDataTableColGroup: function(doc, parentElem, columns)
{
var cols = columns || [];
var result = doc.createElement('colgroup');
for (var i = 0, l = cols.length; i < l; ++i)
{
var colDef = cols[i] || {};
//if (colDef)
{
var colElem = doc.createElement('col');
if (colDef.colClassName)
colElem.className = colDef.colClassName;
if (colDef.colStyle)
colElem.style.cssText = colDef.colStyle;
result.appendChild(colElem);
}
}
parentElem.appendChild(result);
return result;
},
/** @private */
doCreateDataTableCellWrapper: function(doc, parentElem)
{
var result = doc.createElement('span');
result.className = CNS.DATATABLE_CELL_WRAPPER;
parentElem.appendChild(result);
return result;
},
/** @private */
doCreateDataTableHead: function(doc, parentElem, columns)
{
var cols = columns || [];
var headInteractable = this.getEnableHeadInteraction();
var result = doc.createElement('thead');
var rowElem = doc.createElement('tr');
for (var i = 0, l = cols.length; i < l; ++i)
{
var colDef = cols[i] || {};
this.doCreateDataTableHeadCell(doc, rowElem, colDef, headInteractable);
}
result.appendChild(rowElem);
parentElem.appendChild(result);
return result;
},
/** @private */
doCreateDataTableHeadCell: function(doc, parentElem, colDef, headInteractable)
{
var elem = doc.createElement('th');
if (colDef.className)
elem.className = colDef.className;
if (colDef.style)
elem.style.cssText = colDef.style;
var wrapperElem = this.doCreateDataTableCellWrapper(doc, elem);
var interactable = Kekule.ObjUtils.notUnset(colDef.disableInteract)? !colDef.disableInteract: headInteractable;
if (interactable)
{
EU.addClass(/*wrapperElem*/elem, CNS.DATATABLE_HEADCELL_INTERACTABLE);
}
DU.setElementText(wrapperElem, colDef.text || colDef.name);
var sortMark = this.doCreateDataTableHeadCellSortMark(doc, elem);
if (colDef.sorting === 1) // sort asc
EU.addClass(sortMark, CNS.DATATABLE_SORTASC);
else if (colDef.sorting === -1) // sort desc
EU.addClass(sortMark, CNS.DATATABLE_SORTDESC);
else // no sort
{
}
wrapperElem.title = colDef.hint || '';
elem[this.COLDEF_FIELD] = colDef;
parentElem.appendChild(elem);
return elem;
},
/** @private */
doCreateDataTableHeadCellSortMark: function(doc, parentElem)
{
var result = doc.createElement('span');
result.className = CNS.DATATABLE_SORTMARK;
parentElem.appendChild(result);
return result;
},
/** @private */
doCreateDataTableBody: function(doc, parentElem, columns, data)
{
var cols = columns || [];
var data = data || [];
var result = doc.createElement('tbody');
// prepare widgets in operation column
if (this.getShowOperCol())
{
var childWidgetDefinitions = this.getOperWidgetDefinitions();
}
var isOdd = true;
for (var i = 0, l = data.length; i < l; ++i)
{
var rowElem = doc.createElement('tr');
rowElem.className = isOdd? CNS.DATATABLE_ROW_ODD: CNS.DATATABLE_ROW_EVEN;
isOdd = !isOdd;
var rowData = data[i] || {};
rowElem[this.ROWDATA_FIELD] = rowData;
for (var j = 0, k = cols.length; j < k; ++j)
{
var colDef = cols[j];
var key = colDef.name;
var value = rowData[key];
var cellElem = doc.createElement('td');
if (colDef.className)
cellElem.className = colDef.className;
if (colDef.style)
cellElem.style.cssText = colDef.style;
var wrapperElem = this.doCreateDataTableCellWrapper(doc, cellElem);
if (colDef.isOperCol)
{
var ops = {
'rowData': rowData,
'rowIndex': i,
'colIndex': j,
'childWidgetDefinitions': childWidgetDefinitions
};
var w = this.doCreateOperCellContent(wrapperElem, ops);
rowElem[this.ROW_OPER_TOOLBAR_FIELD] = w;
}
else if (colDef.isCheckCol)
{
var ops = {
'rowData': rowData,
'rowIndex': i,
'colIndex': j
}
var c = this.doCreateCheckCellContent(wrapperElem, ops);
rowElem[this.ROW_CHECKBOX_FIELD] = c;
}
else // normal cell
{
var ops = {
'rowData': rowData,
'cellKey': key,
'cellValue': value,
'rowIndex': i,
'colIndex': j
};
this.doCreateDataCellContent(wrapperElem, ops);
}
cellElem.appendChild(wrapperElem);
rowElem.appendChild(cellElem);
}
result.appendChild(rowElem);
}
parentElem.appendChild(result);
return result;
},
/**
* Create content of body cell in table.
* Descendants may override this method.
* @param {HTMLElement} parentElem
* @param {Hash} options A hash that containing essential infos about cell, including:
* {
* rowData: Hash,
* cellKey: String,
* cellValue: Variant,
* rowIndex: Integer,
* colIndex: Integer
* }
* @private
*/
doCreateDataCellContent: function(parentElem, options)
{
// TODO: need a better method to convert to string
var svalue = '';
if (Kekule.ObjUtils.notUnset(options.cellValue))
svalue = '' + options.cellValue;
DU.setElementText(parentElem, svalue);
},
/**
* Create content of cell in check column in table.
* Descendants may override this method and must return a created checkable widget.
* @param {HTMLElement} parentElem
* @param {Hash} options A hash that containing essential infos about cell, including:
* {
* rowData: Hash,
* rowIndex: Integer,
* colIndex: Integer,
* childWidgetDefinitions: Hash
* }
* @returns {Kekule.Widget.BaseWidget}
* @private
*/
doCreateCheckCellContent: function(parentElem, options)
{
var result = new Kekule.Widget.CheckBox(this);
result.appendToElem(parentElem);
return result;
},
/**
* Create content of cell in operation column in table.
* Descendants may override this method and must return a created operation widget.
* @param {HTMLElement} parentElem
* @param {Hash} options A hash that containing essential infos about cell, including:
* {
* rowData: Hash,
* rowIndex: Integer,
* colIndex: Integer,
* childWidgetDefinitions: Hash
* }
* @returns {Kekule.Widget.BaseWidget}
* @private
*/
doCreateOperCellContent: function(parentElem, options)
{
var toolbar = this.doCreateOperToolbar(parentElem, options.childWidgetDefinitions);
var SM = Kekule.Widget.DataTable.OperColShowMode;
var mode = this.getOperColShowMode();
if (mode !== SM.ALL)
{
toolbar.setVisible(false);
}
return toolbar;
},
doCreateOperToolbar: function(parentElem, childDefinitions)
{
var result = new Kekule.Widget.ButtonGroup(this);
result.setChildDefs(childDefinitions);
result.appendToElem(parentElem);
return result;
},
/** @private */
getDefaultOperButtons: function()
{
var CS = Kekule.Widget.DataTable.Components;
return [CS.EDIT, CS.DELETE];
},
/** @private */
getOperWidgetDefinitions: function()
{
var result = [];
var comps = this.getOperWidgets() || this.getDefaultOperButtons();
for (var i = 0, l = comps.length; i < l; ++i)
{
var comp = comps[i];
if (DataType.isObjectValue(comp))
result.push(comp);
else // predefined names
{
var def = this.getDefaultComponentDefinitionHash(comp);
if (def)
result.push(def);
}
}
return result;
},
/** @private */
getDefaultComponentDefinitionHash: function(compName)
{
var CS = Kekule.Widget.DataTable.Components;
var result;
if (compName === CS.EDIT)
{
result = {
'widget': 'Kekule.Widget.Button', 'htmlClass': CNS.DATATABLE_EDIT,
'text': Kekule.$L('WidgetTexts.CAPTION_DATATABLE_EDIT'), 'hint': Kekule.$L('WidgetTexts.HINT_DATATABLE_EDIT'),
'#execute': this.reactOperEditBind
};
}
else if (compName === CS.DELETE)
{
result = {
'widget': 'Kekule.Widget.Button', 'htmlClass': CNS.DATATABLE_DELETE,
'text': Kekule.$L('WidgetTexts.CAPTION_DATATABLE_DELETE'), 'hint': Kekule.$L('WidgetTexts.HINT_DATATABLE_DELETE'),
'#execute': this.reactOperDeleteBind
};
}
else if (compName === CS.INSERT)
{
result = {
'widget': 'Kekule.Widget.Button', 'htmlClass': CNS.DATATABLE_INSERT,
'text': Kekule.$L('WidgetTexts.CAPTION_DATATABLE_INSERT'), 'hint': Kekule.$L('WidgetTexts.HINT_DATATABLE_INSERT'),
'#execute': this.reactOperInsertBind
};
}
if (result)
result.internalName = compName;
return result;
},
/**
* Whether data is provided by external dataset or data pager.
* @private
*/
hasExternalDataProvider: function()
{
return !!this.getDataPager();
},
/**
* Returns whether the data need to be sorted.
* @private
*/
needSort: function()
{
return this.getSortFields() || this.getSortFunc();
},
/**
* Sort data in table.
* @private
*/
sortData: function()
{
var data = this.getData() || [];
if (this.needSort())
{
/*
var sortFields = this.getSortFields();
var sortFieldInfos = [];
for (var i = 0, l = sortFields.length; i < l; ++i)
{
var info = {};
var field = sortFields[i] || '';
if (field.startsWith(this.PREFIX_SORT_DESC)) // sort desc
{
info.field = field.substr(1);
info.desc = true;
}
else
{
info.field = field;
info.desc = false;
}
sortFieldInfos.push(info);
}
*/
if (!this.hasExternalDataProvider()) // if using datapager, sort by external dataset
{
var dupData = Kekule.ArrayUtils.clone(data);
/*
var sortFunc = this.getSortFunc() || function(hash1, hash2)
{
var compareValue = 0;
for (var i = 0, l = sortFieldInfos.length; i < l; ++i)
{
var field = sortFieldInfos[i].field;
var v1 = hash1[field] || '';
var v2 = hash2[field] || '';
compareValue = (v1 > v2) ? 1 :
(v1 < v2) ? -1 : 0;
if (sortFieldInfos[i].desc)
compareValue = -compareValue;
if (compareValue !== 0)
break;
}
return compareValue;
};
dupData.sort(sortFunc);
*/
Kekule.ArrayUtils.sortHashArray(dupData, this.getSortFields());
return dupData;
}
}
return data;
},
/**
* Prepare final data to display.
* @private
*/
prepareData: function()
{
var result = {};
var data = this.sortData();
result.data = data;
var createDefaultCols = function(data)
{
var fields = [];
for (var i = 0, l = data.length; i < l; ++i)
{
var currFields = Kekule.ObjUtils.getOwnedFieldNames(data[i]);
Kekule.ArrayUtils.pushUnique(fields, currFields);
}
var columns = [];
for (var i = 0, l = fields.length; i < l; ++i)
{
columns.push({'name': fields[i], 'text': fields[i]});
}
return columns;
};
if (this.getColumns()) // column definition set, use it to decide shown columns
{
result.columns = Kekule.ArrayUtils.clone(this.getColumns());
}
else // else get column heads by data fields
{
/*
var fields = [];
for (var i = 0, l = data.length; i < l; ++i)
{
var currFields = Kekule.ObjUtils.getOwnedFieldNames(data[i]);
Kekule.ArrayUtils.pushUnique(fields, currFields);
}
result.columns = [];
for (var i = 0, l = fields.length; i < l; ++i)
{
result.columns.push({'name': fields[i], 'text': fields[i]});
}
*/
result.columns = createDefaultCols(data);
}
// add default col/cell class names
//for (var i = 0, l = result.columns.length; i < l; ++i)
var i = 0;
while (i < result.columns.length)
{
var col = result.columns[i];
if (typeof(col) === 'string')
col = {'name': col};
if (col.name === this.COLNAME_OPER)
{
result.columns[i] = {'name': '', 'text': '', 'disableInteract': true, 'isOperCol': true,
'colClassName': CNS.DATATABLE_OPER_COL, 'className': CNS.DATATABLE_OPER_CELL};
}
else if (col.name === this.COLNAME_CHECK)
{
result.columns[i] = {'name': '', 'text': '', 'disableInteract': true, 'isCheckCol': true,
'colClassName': CNS.DATATABLE_CHECK_COL, 'className': CNS.DATATABLE_CHECK_CELL};
}
else if (col.name === this.COLNAME_ALL)
{
var newCols = createDefaultCols(data);
var arg = newCols;
arg.unshift(1);
arg.unshift(i);
result.columns.splice.apply(result.columns, arg);
}
else
{
col.colClassName = CNS.DATATABLE_DATA_COL + ' ' + (col.colClassName || '');
col.className = CNS.DATATABLE_DATA_CELL + ' ' + (col.className || '');
}
++i;
}
// mark column sort states
if (this.needSort() && !this.getSortFunc()) // sort by fields
{
var sortFields = this.getSortFields();
for (var i = 0, l = result.columns.length; i < l; ++i)
{
var colDef = result.columns[i];
var colName = colDef.name;
if (sortFields.indexOf(colName) >= 0)
result.columns[i].sorting = 1; // mark sort asc
else if (sortFields.indexOf(this.PREFIX_SORT_DESC + colName) >= 0)
result.columns[i].sorting = -1; // mark sort desc
else // no sort
result.columns[i].sorting = 0;
}
}
/*
// if necessary, add operation column and check column
if (this.getShowOperCol())
{
result.columns.push({'name': '', 'text': '', 'enableInteract': false, 'isOperCol': true,
'colClassName': CNS.DATATABLE_OPER_COL, 'className': CNS.DATATABLE_OPER_CELL});
}
*/
return result;
},
/** @private */
activeRowChanged: function(oldRow, newRow)
{
var SM = Kekule.Widget.DataTable.OperColShowMode;
var operShowMode = this.getOperColShowMode();
if (oldRow)
{
EU.removeClass(oldRow, CNS.STATE_ACTIVE);
if (operShowMode === SM.ACTIVE || operShowMode === SM.HOVER)
this.hideRowOperToolbar(oldRow);
}
if (newRow)
{
EU.addClass(newRow, CNS.STATE_ACTIVE);
if (operShowMode === SM.ACTIVE || operShowMode === SM.HOVER)
this.showRowOperToolbar(newRow);
}
},
/** @private */
hoverRowChanged: function(oldRow, newRow)
{
var SM = Kekule.Widget.DataTable.OperColShowMode;
var operShowMode = this.getOperColShowMode();
if (oldRow)
{
EU.removeClass(oldRow, CNS.STATE_HOVER);
if (operShowMode === SM.HOVER && oldRow !== this.getActiveRow())
this.hideRowOperToolbar(oldRow);
}
if (newRow)
{
EU.addClass(newRow, CNS.STATE_HOVER);
if (operShowMode === SM.HOVER)
this.showRowOperToolbar(newRow);
}
},
/**
* Returns nearest parent cell element.
* @param {HTMLElement} elem
* @return {HTMLElement}
*/
getParentCell: function(elem)
{
if (DU.isDescendantOf(elem, this.getElement()))
return DU.getNearestAncestorByTagName(elem, 'td', true);
else
return null;
},
/**
* Returns nearest parent head cell element.
* @param {HTMLElement} elem
* @return {HTMLElement}
*/
getParentHeadCell: function(elem)
{
if (DU.isDescendantOf(elem, this.getElement()))
return DU.getNearestAncestorByTagName(elem, 'th', true);
else
return null;
},
/**
* Returns nearest parent data row element.
* @param {HTMLElement} elem
* @return {HTMLElement}
*/
getParentRow: function(elem)
{
if (DU.isDescendantOf(elem, this.getElement()))
return DU.getNearestAncestorByTagName(elem, 'tr', true);
else
return null;
},
/**
* Returns count of columns defined by property columns.
* @returns {Int}
*/
getColCount: function()
{
return (this.getColumns() || []).length;
},
/**
* Returns count of rows defined by property data.
* @returns {Int}
*/
getRowCount: function()
{
return (this.getData() || []).length;
},
/**
* Returns all data row elements in table.
* @returns {Array}
*/
getDataRows: function()
{
var result = [];
var tbody = this.getElement().getElementsByTagName('tbody')[0];
return tbody && tbody.getElementsByTagName('tr');
},
/**
* Returns all checked data row elements in table.
* @returns {Array}
*/
getCheckedRows: function()
{
var result = [];
var rows = this.getDataRows();
for (var i = 0, l = rows.length; i < l; ++i)
{
if (this.isRowChecked(rows[i]))
result.push(rows[i]);
}
return result;
},
/**
* Returns data associated with a row element.
* @param {HTMLElement} rowElem
* @returns {Variant}
*/
getRowData: function(rowElem)
{
return rowElem? rowElem[this.ROWDATA_FIELD]: null;
},
/**
* Returns whether check box of a row is checked.
* @param {HTMLElement} rowElem
* @returns {Bool}
*/
isRowChecked: function(rowElem)
{
var c = this.getRowCheckBox(rowElem);
return c && c.getChecked && c.getChecked();
},
/**
* Returns check box inside row.
* @param {HTMLElement} rowElem
* @returns {Kekule.Widget.BaseWidget}
* @private
*/
getRowCheckBox: function(rowElem)
{
return rowElem && rowElem[this.ROW_CHECKBOX_FIELD];
},
/**
* Returns operation toolbar inside row.
* @param {HTMLElement} rowElem
* @returns {Kekule.Widget.BaseWidget}
* @private
*/
getRowOperToolbar: function(rowElem)
{
return rowElem? rowElem[this.ROW_OPER_TOOLBAR_FIELD]: null;
},
/** @private */
showRowOperToolbar: function(rowElem)
{
var w = this.getRowOperToolbar(rowElem);
if (w)
w.setVisible(true);
},
/** @private */
hideRowOperToolbar: function(rowElem)
{
var w = this.getRowOperToolbar(rowElem);
if (w)
w.setVisible(false);
},
/**
* Called when dataPager property is changed.
* @param newPager
* @param oldPager
* @private
*/
dataPagerChanged: function(newPager, oldPager)
{
if (oldPager)
{
oldPager.removeEventListener('pageRetrieve', this.reactPagerRetrieve, this);
oldPager.removeEventListener('dataFetched', this.reactPagerFetched, this);
oldPager.removeEventListener('dataError', this.reactPagerError, this);
}
if (newPager)
{
newPager.addEventListener('pageRetrieve', this.reactPagerRetrieve, this);
newPager.addEventListener('dataFetched', this.reactPagerFetched, this);
newPager.addEventListener('dataError', this.reactPagerError, this);
this.setData(newPager.getCurrPageData());
}
},
/** @private */
reactPagerRetrieve: function(e)
{
if (this.reportMessage)
{
this._loadingDataMsg = this.reportMessage(Kekule.$L('WidgetTexts.MSG_RETRIEVING_DATA'), Kekule.Widget.MsgType.INFO);
}
},
/** @private */
reactPagerFetched: function(e)
{
this.setData(e.data);
if (this._loadingDataMsg && this.removeMessage)
this.removeMessage(this._loadingDataMsg);
},
/** @private */
reactPagerError: function(e)
{
if (this._loadingDataMsg && this.removeMessage)
this.removeMessage(this._loadingDataMsg);
if (this.flashMessage)
this.flashMessage(e.error.message || e.error, Kekule.Widget.MsgType.ERROR);
},
// event handlers of operation col
/** @private */
reactOperEdit: function(e)
{
return this.reactOperExecute(TCS.EDIT, e);
},
/** @private */
reactOperDelete: function(e)
{
return this.reactOperExecute(TCS.DELETE, e);
},
/** @private */
reactOperInsert: function(e)
{
return this.reactOperExecute(TCS.INSERT, e);
},
/**
* Called when widget in operation column cell is executed.
* @param {String} compName Predefined button name.
* @param {Object} e Normal execute event params.
* @private
*/
reactOperExecute: function(compName, e)
{
var target = e.target;
var rowElem = this.getParentRow(target.getElement());
var rowData = this.getRowData(rowElem);
return this.doReactOperExecute(compName, rowData, rowElem, e);
},
/**
* Called when widget in operation column cell is executed.
* @param {String} compName Predefined button name.
* @param {Variant} rowData
* @param {HTMLElement} rowElem
* @param {Object} e Normal execute event params.
* Descendant may override this method.
*/
doReactOperExecute: function(compName, rowData, rowElem, e)
{
// do nothing here
//console.log('execute on', compName, rowData);
},
// event handlers
/** @ignore */
react_click: function($super, e)
{
$super(e);
var elem = e.getTarget();
// if click on head cell, sort this column
var headCell = this.getParentHeadCell(elem);
if (headCell)
this._autoSortOnHeadCell(headCell);
else
{
if (this.getEnableActiveRow())
{
// if click on data row, set row active
var dataCell = this.getParentCell(elem);
if (dataCell)
{
this.setActiveCell(dataCell);
}
}
}
},
/** @ignore */
react_pointerover: function($super, e)
{
$super(e);
var elem = e.getTarget();
if (this.getEnableActiveRow())
{
var dataCell = this.getParentCell(elem);
if (dataCell)
this.setHoverCell(dataCell);
else
this.setHoverCell(null);
}
},
/** @ignore */
react_pointerleave: function($super, e)
{
$super(e);
var elem = e.getTarget();
if (this.getEnableActiveRow())
{
this.setHoverCell(null);
}
},
/** @private */
_autoSortOnHeadCell: function(headCell)
{
if (headCell && this.getEnableHeadInteraction())
{
var colDef = headCell[this.COLDEF_FIELD];
if (colDef && !colDef.disableInteract)
{
var sortField = colDef.name;
if (colDef.sorting === 1)
sortField = this.PREFIX_SORT_DESC + sortField;
this.setSortFields([sortField]);
}
}
},
// public methods
/**
* Set data and sort fields at same time.
* @param {Array} data
* @param {Array} columns
* @param {Array} sortFields
* @param {Func} sortFunc
*/
load: function(data, columns, sortFields, sortFunc)
{
this.beginUpdate();
try
{
this.setData(data);
this.setColumns(columns);
this.setSortFields(sortFields ? Kekule.ArrayUtils.toArray(sortFields) : null);
this.setSortFunc(sortFunc);
}
finally
{
this.endUpdate();
}
return this;
},
/**
* Reload data in table.
*/
reload: function()
{
this.recreateChildContent();
return this;
}
});
/**
* Enumeration of mode to show operation column in data table widget.
* @enum
*/
Kekule.Widget.DataTable.OperColShowMode = {
NONE: 'none',
ACTIVE: 'active',
HOVER: 'hover',
ALL: 'all'
};
/**
* Possible child components inside data table.
* @enum
*/
Kekule.Widget.DataTable.Components = {
// Operation buttons
EDIT: 'edit',
DELETE: 'delete',
INSERT: 'insert'
};
var TCS = Kekule.Widget.DataTable.Components;
/**
* An widget to help to navigate between pages, including navigator buttons and page index inputor.
* @class
* @augments Kekule.Widget.ButtonGroup
*
* @property {Array} components Component names to show in navigator, may containing the following value:
* [
* 'first', 'prev', 'next', 'last', 'pageInput' (text box to input page index), 'pageSelect' (selector to show drop down list of pages)
* ]
* In the array widget definition hash can also be used.
* @property {Int} firstIndex First page index.
* @property {Int} lastIndex Last page index.
* @property {Int} currIndex Current page index.
* @property {Kekule.Widget.DataPager} dataPager Source pager associate with this widget.
*/
/**
* Invoked when current page index is changed.
* event param of it has field: {currIndex}
* @name Kekule.Widget.PageNavigator#pageChange
* @event
*/
Kekule.Widget.PageNavigator = Class.create(Kekule.Widget.ButtonGroup,
/** @lends Kekule.Widget.PageNavigator# */
{
/** @private */
CLASS_NAME: 'Kekule.Widget.PageNavigator',
/** @private */
initProperties: function()
{
this.defineProp('components', {'dataType': DataType.ARRAY});
this.defineProp('firstIndex', {'dataType': DataType.INT});
this.defineProp('lastIndex', {'dataType': DataType.INT});
this.defineProp('currIndex', {'dataType': DataType.INT});
this.defineProp('dataPager', {'dataType': 'Kekule.Widget.DataPager',
'setter': function(value)
{
var old = this.getDataPager();
if (old !== value)
{
this.setPropStoreFieldValue('dataPager', value);
this.dataPagerChanged(value, old);
}
}
});
},
/** @ignore */
initPropValues: function($super)
{
$super();
this.setShowText(true);
this.setShowGlyph(true);
this.setFirstIndex(1);
this.reactFirstBind = this.reactFirst.bind(this);
this.reactLastBind = this.reactLast.bind(this);
this.reactPrevBind = this.reactPrev.bind(this);
this.reactNextBind = this.reactNext.bind(this);
this.reactPageInputChangeBind = this.reactPageInputChange.bind(this);
},
/** @ignore */
doObjectChange: function($super, modifiedPropNames)
{
$super(modifiedPropNames);
if (Kekule.ArrayUtils.intersect(modifiedPropNames, ['firstIndex', 'lastIndex', 'currIndex']).length)
{
this.updateChildComponent();
}
if (modifiedPropNames.indexOf('currIndex') >= 0)
this.invokeEvent('pageChange', {'currIndex': this.getCurrIndex()});
},
/** @ignore */
doGetWidgetClassName: function($super)
{
return $super() + ' ' + CNS.PAGENAVIGATOR;
},
/** @ignore */
doCreateSubElements: function(doc, rootElem)
{
return this.recreateChildContent(doc, rootElem);
},
/** @private */
getDefaultComponents: function()
{
return [PNC.FIRST, PNC.PREV, /*PNC.PAGEINPUT,*/ PNC.PAGESELECTOR, PNC.NEXT, PNC.LAST];
},
/** @private */
recreateChildContent: function()
{
var comps = this.getComponents() || this.getDefaultComponents();
var widgetDefs = [];
for (var i = 0, l = comps.length; i < l; ++i)
{
var comp = comps[i];
if (DataType.isObjectValue(comp)) // hash
widgetDefs.push(comp);
else // string, name
{
var def = this.getDefaultComponentDefinitionHash(comp);
if (def)
widgetDefs.push(def);
}
}
this.setChildDefs(widgetDefs);
this.updateChildComponent();
},
/** @private */
getDefaultComponentDefinitionHash: function(compName)
{
var result;
if (compName === PNC.FIRST)
{
result = {
'widget': 'Kekule.Widget.Button', 'htmlClass': CNS.PAGENAVIGATOR_FIRST,
'text': Kekule.$L('WidgetTexts.CAPTION_FIRST_PAGE'), 'hint': Kekule.$L('WidgetTexts.HINT_FIRST_PAGE'),
'#execute': this.reactFirstBind
};
}
else if (compName === PNC.LAST)
{
result = {
'widget': 'Kekule.Widget.Button', 'htmlClass': CNS.PAGENAVIGATOR_LAST,
'text': Kekule.$L('WidgetTexts.CAPTION_LAST_PAGE'), 'hint': Kekule.$L('WidgetTexts.HINT_LAST_PAGE'),
'#execute': this.reactLastBind
};
}
else if (compName === PNC.PREV)
{
result = {
'widget': 'Kekule.Widget.Button', 'htmlClass': CNS.PAGENAVIGATOR_PREV,
'text': Kekule.$L('WidgetTexts.CAPTION_PREV_PAGE'), 'hint': Kekule.$L('WidgetTexts.HINT_PREV_PAGE'),
'#execute': this.reactPrevBind
};
}
else if (compName === PNC.NEXT)
{
result = {
'widget': 'Kekule.Widget.Button', 'htmlClass': CNS.PAGENAVIGATOR_NEXT,
'text': Kekule.$L('WidgetTexts.CAPTION_NEXT_PAGE'), 'hint': Kekule.$L('WidgetTexts.HINT_NEXT_PAGE'),
'#execute': this.reactNextBind
};
}
else if (compName === PNC.PAGEINPUT)
{
result = {
'widget': 'Kekule.Widget.TextBox', 'htmlClass': [CNS.PAGENAVIGATOR_PAGEINPUT, CNS.PAGENAVIGATOR_PAGEINDEXER],
'hint': Kekule.$L('WidgetTexts.HINT_CURR_PAGE'),
'#valueChange': this.reactPageInputChangeBind
};
}
else if (compName === PNC.PAGESELECTOR)
{
result = {
'widget': 'Kekule.Widget.SelectBox', 'htmlClass': [CNS.PAGENAVIGATOR_PAGESELECTOR, CNS.PAGENAVIGATOR_PAGEINDEXER],
'hint': Kekule.$L('WidgetTexts.HINT_CURR_PAGE'),
'#valueChange': this.reactPageInputChangeBind
};
}
else
result = null;
if (result)
result.internalName = compName;
return result;
},
/**
* Returns redefined child widget by component name.
* @param {String} compName
* @returns {Kekule.Widget.BaseWidget}
*/
getComponent: function(compName)
{
return this.getChildWidgetByInternalName(compName);
},
/** @private */
setChildComponentEnabled: function(compName, enabled)
{
var w = this.getComponent(compName);
if (w)
w.setEnabled(enabled);
},
/**
* Update state of child components due to page index change.
* @private
*/
updateChildComponent: function()
{
var firstIndex = this.getFirstIndex() || 0;
var lastIndex = this.getLastIndex() || 0;
var currIndex = this.getCurrIndex() || 0;
var isFirst = currIndex <= firstIndex;
var isLast = currIndex >= lastIndex;
this.setChildComponentEnabled(PNC.FIRST, !isFirst);
this.setChildComponentEnabled(PNC.PREV, !isFirst);
this.setChildComponentEnabled(PNC.NEXT, !isLast);
this.setChildComponentEnabled(PNC.LAST, !isLast);
this.setChildComponentEnabled(PNC.PAGEINPUT, lastIndex > firstIndex);
this.setChildComponentEnabled(PNC.PAGESELECTOR, lastIndex > firstIndex);
// change page index
var pageInput = this.getComponent(PNC.PAGEINPUT);
if (pageInput)
pageInput.setValue(currIndex);
var pageSelector = this.getComponent(PNC.PAGESELECTOR);
if (pageSelector)
{
var items = [];
for (var i = firstIndex; i <= lastIndex; ++i)
{
items.push({'value': i});
}
pageSelector.setItems(items);
pageSelector.setValue(currIndex);
}
},
/**
* Called when dataPager property is changed.
* @param newPager
* @param oldPager
* @private
*/
dataPagerChanged: function(newPager, oldPager)
{
if (oldPager)
{
oldPager.removeEventListener('dataFetched', this.reactPagerFetched, this);
newPager.removeEventListener('pageCountChange', this.reactPagerPageCountChange, this);
}
if (newPager)
{
newPager.addEventListener('dataFetched', this.reactPagerFetched, this);
newPager.addEventListener('pageCountChange', this.reactPagerPageCountChange, this);
this.updatePageDetails(newPager);
this.setCurrIndex(newPager.getCurrPageIndex() + this.getFirstIndex());
}
},
/** @private */
reactPagerFetched: function(e)
{
this.setCurrIndex(e.pageIndex + this.getFirstIndex());
},
/** @private */
reactPagerPageCountChange: function(e)
{
this.updatePageDetails(this.getDataPager());
},
/** @private */
updatePageDetails: function(pager)
{
var firstIndex = this.getFirstIndex();
this.setLastIndex(pager.getPageCount() + firstIndex - 1);
},
/** @private */
requestChangeCurrIndex: function(newIndex)
{
var pager = this.getDataPager();
if (pager)
{
pager.switchToPage(newIndex - this.getFirstIndex());
}
else
this.setCurrIndex(newIndex);
},
// Event handlers
/** @private */
reactFirst: function(e)
{
this.requestChangeCurrIndex(this.getFirstIndex() || 0);
},
/** @private */
reactLast: function(e)
{
this.requestChangeCurrIndex(this.getLastIndex() || 0);
},
/** @private */
reactPrev: function(e)
{
var firstIndex = this.getFirstIndex() || 0;
var currIndex = this.getCurrIndex() || 0;
this.requestChangeCurrIndex(Math.max(currIndex - 1, firstIndex));
},
/** @private */
reactNext: function(e)
{
var lastIndex = this.getLastIndex() || 0;
var currIndex = this.getCurrIndex() || 0;
this.requestChangeCurrIndex(Math.min(currIndex + 1, lastIndex));
},
/** @private */
reactPageInputChange: function(e)
{
var value = e.target.getValue();
if (typeof(value) === 'string')
value = parseInt(value);
var firstIndex = this.getFirstIndex() || 0;
var lastIndex = this.getLastIndex() || 0;
if (value >= firstIndex && value <= lastIndex)
this.requestChangeCurrIndex(value);
else
Kekule.error(Kekule.$L('ErrorMsg.PAGE_INDEX_OUTOF_RANGE'));
}
});
/**
* Component names used in {@link Kekule.Widget.PageNavigator}
* @enum
*/
Kekule.Widget.PageNavigator.Components = {
FIRST: 'first',
LAST: 'last',
PREV: 'prev',
NEXT: 'next',
PAGEINPUT: 'pageInput',
PAGESELECTOR: 'pageSelector'
};
var PNC = Kekule.Widget.PageNavigator.Components;
})();