/*______________
| ______ | U I Z E J A V A S C R I P T F R A M E W O R K
| / / | ---------------------------------------------------
| / O / | MODULE : Uize.Widget.TableSort Class
| / / / |
| / / / /| | ONLINE : http://uize.com
| /____/ /__/_| | COPYRIGHT : (c)2005-2016 UIZE
| /___ | LICENSE : Available under MIT License or GNU General Public License
|_______________| http://uize.com/license.html
*/
/* Module Meta Data
type: Class
importance: 3
codeCompleteness: 80
docCompleteness: 2
*/
/*?
Introduction
The =Uize.Widget.TableSort= class adds sorting functionality to tables, and provides row highlighting as well as column name tooltips for table cells.
*DEVELOPERS:* `Chris van Rensburg`
*/
Uize.module ({
name:'Uize.Widget.TableSort',
required:[
'Uize.Dom.Basics',
'Uize.Dom.Text'
],
builder:function (_superclass) {
'use strict';
var
/*** Variables for Scruncher Optimization ***/
_null = null,
_true = true,
_updateUi = 'updateUi',
_getById = Uize.Dom.Basics.getById,
_getText = Uize.Dom.Text.getText
;
/*** Utility Functions ***/
function _getChildNodesByTagName (_node,_tagName) {
var
_result = [],
_childNodes = _node.childNodes,
_childNodesLength = _childNodes.length
;
for (var _childNo = -1; ++_childNo < _childNodesLength;) {
var _childNode = _childNodes [_childNo];
_childNode.tagName == _tagName && _result.push (_childNode);
}
return _result;
}
function _getTableBody (_node) {
return _getById (_node).getElementsByTagName ('tbody') [0];
}
function _getRowCells (_row) {
var _cells = _getChildNodesByTagName (_row,'TD');
if (!_cells.length) _cells = _getChildNodesByTagName (_row,'TH');
return _cells;
}
/*** Private Instance Methods ***/
function _isColumnNextSortOrderAscending (m,_columnNo) {
return (
_columnNo == m._headingNoSorted
? !m._ascendingOrder
:
(
(m._dominantSortOrderByColumn && m._dominantSortOrderByColumn [_columnNo]) ||
m._dominantSortOrder
) == 'ascending'
);
}
function _updateColumnUi (m,_columnNo) {
if (m.isWired) {
var _heading = m._headings [_columnNo];
_heading.className =
(
_columnNo == m._headingNoSorted
? m._headingLitClass
: (
_columnNo == m._headingNoOver
? m._headingOverClass
: m._headingsOldClasses [_columnNo]
)
) || ''
;
_heading.title = _isColumnNextSortOrderAscending (m,_columnNo)
? m._languageSortAscending
: m._languageSortDescending
;
}
}
function _updateRowUi (m,_row) {
if (m.isWired && _row)
_row.className = (_row == m._rowOver ? m._rowOverClass : _row.Uize_Widget_TableSort_oldClassName) || ''
;
}
function _headingMouseover (m,_columnNo) {
_headingMouseout (m);
m._headingNoOver = _columnNo;
_updateColumnUi (m,_columnNo);
}
function _headingMouseout (m) {
if (m._headingNoOver != _null) {
var _lastHeadingNoOver = m._headingNoOver;
m._headingNoOver = _null;
_updateColumnUi (m,_lastHeadingNoOver);
}
}
function _rowMouseover (m,_row) {
_rowMouseout (m);
m._rowOver = _row;
_updateRowUi (m,_row);
}
function _rowMouseout (m) {
if (m._rowOver) {
var _lastRowOver = m._rowOver;
m._rowOver = _null;
_updateRowUi (m,_lastRowOver);
}
}
return _superclass.subclass ({
instanceMethods:{
sort:function (_columnNo) {
var
m = this,
_table = m.getNode ()
;
if (_table) {
var
_tableBody = _getTableBody (_table),
_rows = _getChildNodesByTagName (_tableBody,'TR'),
_rowsLength = _rows.length,
_columnValues = [],
_columnSortMap = [],
_columnHasNumerals = _true,
_columnIsPureNumber = _true,
_columnIsDate = _true
;
m._ascendingOrder = _isColumnNextSortOrderAscending (m,_columnNo);
/*** initialize sort map, harvest sort column's values, and inspect to determine type ***/
for (var _rowNo = -1; ++_rowNo < _rowsLength;) {
_columnSortMap [_rowNo] = _rowNo;
/* NOTE: conditionalized to skip over the headings row (if in table body) and any rows with too few cells */
if (_rowNo != m._headingsRowNo) {
var _cells = _getRowCells (_rows [_rowNo]);
if (_cells.length == m._headings.length) {
var _cellText = _getText (_cells [_columnNo]);
if (_cellText) {
var _cellTestIsPureNumber = !Uize.isNaN (+_cellText);
_columnIsPureNumber = _columnIsPureNumber && _cellTestIsPureNumber;
_columnIsDate =
_columnIsDate && !_cellTestIsPureNumber && !Uize.isNaN (+new Date (_cellText))
;
_columnHasNumerals =
_columnHasNumerals && (_cellTestIsPureNumber || /\d/.test (_cellText))
;
_columnValues [_rowNo] = _cellText;
}
}
}
}
_columnIsDate = _columnIsDate && !_columnIsPureNumber;
/*** sort the sort map ***/
var
_compareGeneral = function (_valueA,_valueB) {
return _valueA == _valueB ? 0 : (_valueA < _valueB ? -1 : 1);
},
_compareNumbers = _compareGeneral, // for now, at least
_columnIsDateOrNumber = _columnIsDate || _columnHasNumerals,
_comparisonFunction = _columnIsDateOrNumber ? _compareNumbers : _compareGeneral,
_incorrectComparisonFunctionResult = m._ascendingOrder ? 1 : -1,
_skipRow = function (_sortMapIndex) {
return _columnValues [_columnSortMap [_sortMapIndex]] === undefined;
},
_columnValue
;
/*** for number and date columns, convert text values to numbers for more efficient sort ***/
if (_columnIsDateOrNumber) {
for (var _rowNo = -1; ++_rowNo < _rowsLength;) {
if (!_skipRow (_rowNo)) {
_columnValue = _columnValues [_rowNo];
_columnValues [_rowNo] = +(
_columnIsPureNumber
? _columnValue
: _columnIsDate
? new Date (_columnValue)
: _columnValue.replace (/[^\d\.]/g,'')
);
}
}
}
/* NOTES:
- conditionalized to leave headings row (if in table body) and "spacer" rows in same position
- any row for which no sort column value has been determined (see above) is left in its original order
- using a hand-rolled bubble sort here, since it's the only way to guarantee fixed rows keep their order (the Array.sort method doesn't guarantee order)
*/
var _rowsLengthMinus1 = _rowsLength - 1;
for (var _sortMapIndexA = -1; ++_sortMapIndexA < _rowsLengthMinus1;) {
if (!_skipRow (_sortMapIndexA)) {
for (var _sortMapIndexB = _sortMapIndexA; ++_sortMapIndexB < _rowsLength;) {
if (!_skipRow (_sortMapIndexB)) {
if (
_incorrectComparisonFunctionResult == _comparisonFunction (
_columnValues [_columnSortMap [_sortMapIndexA]],
_columnValues [_columnSortMap [_sortMapIndexB]]
)
) {
var _temp = _columnSortMap [_sortMapIndexA];
_columnSortMap [_sortMapIndexA] = _columnSortMap [_sortMapIndexB];
_columnSortMap [_sortMapIndexB] = _temp;
}
}
}
}
}
/*** apply the sort map ***/
for (var _rowNo = -1; ++_rowNo < _rowsLength;)
_tableBody.appendChild (_rows [_columnSortMap [_rowNo]])
;
/*** update the heading UI to reflect new sort status ***/
if (_columnNo != m._headingNoSorted) {
if (m._headingNoSorted != _null) {
var _lastHeadingNoSorted = m._headingNoSorted;
m._headingNoSorted = _null;
_updateColumnUi (m,_lastHeadingNoSorted);
}
m._headingNoSorted = _columnNo;
_updateColumnUi (m,_columnNo);
}
}
},
updateUi:function () {
var m = this;
if (m.isWired) {
for (var _columnNo = -1; ++_columnNo < m._headings.length;)
_updateColumnUi (m,_columnNo)
;
_updateRowUi (m,m._rowOver);
}
},
wireUi:function () {
var m = this;
if (!m.isWired) {
/*** Initialize Instance Properties ***/
m._headings = [];
m._headingsOldClasses = [];
m._headingNoOver = m._headingNoSorted = m._rowOver = _null;
m._ascendingOrder = _true;
var _table = m.getNode ();
if (_table) {
var
_tableBody = _getTableBody (m.getNode ()),
_tableBodyRows = _getChildNodesByTagName (_tableBody,'TR')
;
/*** find column headings row (could be in table head or table body) ***/
/* NOTES:
- headings are the first row found (either in the table head or the table body) with the maximum number of columns of all the table's rows
*/
var
_maxColumns = 0,
_tableBodyRowsLength = _tableBodyRows.length
;
for (var _rowNo = -1; ++_rowNo < _tableBodyRowsLength;)
_maxColumns = Math.max (_maxColumns,_getRowCells (_tableBodyRows [_rowNo]).length)
;
var
_tryFindHeadings = function (_rows) {
for (var _rowNo = -1, _rowsLength = _rows.length; ++_rowNo < _rowsLength;) {
var _rowCells = _getRowCells (_rows [_rowNo]);
if (_rowCells.length == _maxColumns) {
m._headings = _rowCells;
m._headingsRowNo = _rowNo;
break;
}
}
},
_tableHeads = _table.getElementsByTagName ('thead')
;
if (_tableHeads.length > 0) {
var _tableHeadRows = _getChildNodesByTagName (_tableHeads [0],'TR');
if (!_tableHeadRows.length) _tableHeadRows = [_tableHeads [0]];
_tryFindHeadings (_tableHeadRows);
}
m._headingsRowNo = -1;
m._headings.length || _tryFindHeadings (_tableBodyRows);
/*** wire up headings ***/
Uize.forEach (
m._headings,
function (_heading,_headingNo) {
m._headingsOldClasses [_headingNo] = _heading.className;
m.wireNode (
_heading,
{
mouseover:function () {_headingMouseover (m,_headingNo)},
mouseout:function () {_headingMouseout (m)},
click:function () {m.sort (_headingNo)}
}
);
}
);
/*** wire up rows with highlight behavior and title attributes for columns ***/
for (
var
_rowNo = -1,
_tableBodyRowsLength = _tableBodyRows.length,
_headingsText = Uize.map (
m._headings,
function (_heading) {return _getText (_heading)}
),
_wireRow = function (_row) {
_row.Uize_Widget_TableSort_oldClassName = _row.className;
m.wireNode (
_row,
{
mouseover:function () {_rowMouseover (m,_row)},
mouseout:function () {_rowMouseout (m)}
}
);
}
;
++_rowNo < _tableBodyRowsLength;
) {
/* NOTE: conditionalized to skip over the headings row (if in table body) and any rows with too few cells */
if (_rowNo != m._headingsRowNo) {
var
_row = _tableBodyRows [_rowNo],
_cells = _getRowCells (_row)
;
_cells.length == m._headings.length && _wireRow (_row);
for (var _cellNo = -1; ++_cellNo < _cells.length;) {
if (
m._cellTooltipsByColumn && _cellNo in m._cellTooltipsByColumn
? m._cellTooltipsByColumn [_cellNo]
: m._cellTooltips
)
_cells [_cellNo].title = _headingsText [_cellNo]
;
}
}
}
}
_superclass.doMy (m,'wireUi');
}
}
},
stateProperties:{
_cellTooltips:{
name:'cellTooltips',
value:_true
},
_cellTooltipsByColumn:'cellTooltipsByColumn',
_dominantSortOrder:{
name:'dominantSortOrder',
value:'ascending'
},
_dominantSortOrderByColumn:'dominantSortOrderByColumn',
_headingOverClass:{
name:'headingOverClass',
onChange:_updateUi
},
_headingLitClass:{
name:'headingLitClass',
onChange:_updateUi
},
_languageSortAscending:{
name:'languageSortAscending',
onChange:_updateUi,
value:'Click to sort in ascending order'
},
_languageSortDescending:{
name:'languageSortDescending',
onChange:_updateUi,
value:'Click to sort in descending order'
},
_rowOverClass:{
name:'rowOverClass',
onChange:_updateUi
}
}
});
}
});