/*______________ | ______ | 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 } } }); } });