SOURCE CODE: Uize.Widget.TableSort
VIEW REFERENCE

/*______________
|       ______  |   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://www.uize.com
| /____/ /__/_| | COPYRIGHT : (c)2005-2012 UIZE
|          /___ |   LICENSE : Available under MIT License or GNU General Public License
|_______________|             http://www.uize.com/license.html
*/

/* Module Meta Data
  type: Class
  importance: 3
  codeCompleteness: 80
  testCompleteness: 0
  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.Node',
  builder:function (_superclass) {
    /*** Variables for Scruncher Optimization ***/
      var
        _undefined,
        _null = null,
        _true = true,
        _false = false,
        _Uize_Node = Uize.Node
      ;

    /*** 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 _Uize_Node.getById (_node).getElementsByTagName ('tbody') [0];
      }

      function _getRowCells (_row) {
        var _cells = _getChildNodesByTagName (_row,'TD');
        if (!_cells.length) _cells = _getChildNodesByTagName (_row,'TH');
        return _cells;
      }

    /*** Class Constructor ***/
      var
        _class = _superclass.subclass (),
        _classPrototype = _class.prototype
      ;

    /*** Private Instance Methods ***/
      _classPrototype._isColumnNextSortOrderAscending = function (_columnNo) {
        var _this = this;
        return (
          _columnNo == _this._headingNoSorted
            ? !_this._ascendingOrder
            :
              (
                (_this._dominantSortOrderByColumn && _this._dominantSortOrderByColumn [_columnNo]) ||
                _this._dominantSortOrder
              ) == 'ascending'
        );
      };

      _classPrototype._updateColumnUi = function (_columnNo) {
        var _this = this;
        if (_this.isWired) {
          var _heading = _this._headings [_columnNo];
          _heading.className =
            (
              _columnNo == _this._headingNoSorted
                ? _this._headingLitClass
                : (
                  _columnNo == _this._headingNoOver
                    ? _this._headingOverClass
                    : _this._headingsOldClasses [_columnNo]
                )
            ) || ''
          ;
          _heading.title = _this._isColumnNextSortOrderAscending (_columnNo)
            ? _this._languageSortAscending
            : _this._languageSortDescending
          ;
        }
      };

      _classPrototype._updateRowUi = function (_row) {
        var _this = this;
        if (_this.isWired && _row)
          _row.className = (_row == _this._rowOver ? _this._rowOverClass : _row.Uize_Widget_TableSort_oldClassName) || ''
        ;
      };

      _classPrototype._headingMouseover = function (_columnNo) {
        var _this = this;
        _this._headingMouseout ();
        _this._headingNoOver = _columnNo;
        _this._updateColumnUi (_columnNo);
      };

      _classPrototype._headingMouseout = function () {
        var _this = this;
        if (_this._headingNoOver != _null) {
          var _lastHeadingNoOver = _this._headingNoOver;
          _this._headingNoOver = _null;
          _this._updateColumnUi (_lastHeadingNoOver);
        }
      };

      _classPrototype._rowMouseover = function (_row) {
        var _this = this;
        _this._rowMouseout ();
        _this._rowOver = _row;
        _this._updateRowUi (_row);
      };

      _classPrototype._rowMouseout = function () {
        var _this = this;
        if (_this._rowOver) {
          var _lastRowOver = _this._rowOver;
          _this._rowOver = _null;
          _this._updateRowUi (_lastRowOver);
        }
      };

    /*** Public Instance Methods ***/
      _classPrototype.sort = function (_columnNo) {
        var
          _this = this,
          _table = _this.getNode ()
        ;
        if (_table) {
          var
            _tableBody = _getTableBody (_table),
            _rows = _getChildNodesByTagName (_tableBody,'TR'),
            _rowsLength = _rows.length,
            _columnValues = [],
            _columnSortMap = [],
            _columnIsNumber = _true,
            _columnIsDate = _true
          ;
          _this._ascendingOrder = _this._isColumnNextSortOrderAscending (_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 != _this._headingsRowNo) {
                var _cells = _getRowCells (_rows [_rowNo]);
                if (_cells.length == _this._headings.length) {
                  var _cellText = _Uize_Node.getText (_cells [_columnNo]);
                  if (_cellText) {
                    _columnIsDate = _columnIsDate && !isNaN (+new Date (_cellText));
                    _columnIsNumber = _columnIsNumber && /\d/.test (_cellText);
                    _columnValues [_rowNo] = _cellText;
                  }
                }
              }
            }

          /*** sort the sort map ***/
            function _compareGeneral (_valueA,_valueB) {
              return _valueA == _valueB ? 0 : (_valueA < _valueB ? -1 : 1);
            }
            var
              _compareNumbers = _compareGeneral, // for now, at least
              _columnIsDateOrNumber = _columnIsDate || _columnIsNumber,
              _comparisonFunction = _columnIsDateOrNumber ? _compareNumbers : _compareGeneral,
              _incorrectComparisonFunctionResult = _this._ascendingOrder ? 1 : -1
            ;
            function _skipRow (_sortMapIndex) {
              return _columnValues [_columnSortMap [_sortMapIndex]] === _undefined;
            }
            /*** 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))
                    _columnValues [_rowNo] = _columnIsDate
                      ? +new Date (_columnValues [_rowNo])
                      : +_columnValues [_rowNo].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 != _this._headingNoSorted) {
              if (_this._headingNoSorted != _null) {
                var _lastHeadingNoSorted = _this._headingNoSorted;
                _this._headingNoSorted = _null;
                _this._updateColumnUi (_lastHeadingNoSorted);
              }
              _this._headingNoSorted = _columnNo;
              _this._updateColumnUi (_columnNo);
            }
        }
      };

      _classPrototype.updateUi = function () {
        var _this = this;
        if (_this.isWired) {
          for (var _columnNo = -1; ++_columnNo < _this._headings.length;)
            _this._updateColumnUi (_columnNo)
          ;
          _this._updateRowUi (_this._rowOver);
        }
      };

      _classPrototype.wireUi = function () {
        var _this = this;
        if (!_this.isWired) {
          /*** Initialize Instance Properties ***/
            _this._headings = [];
            _this._headingsOldClasses = [];
            _this._headingNoOver = _this._headingNoSorted = _this._rowOver = _null;
            _this._ascendingOrder = _true;

          var _table = _this.getNode ();
          if (_table) {
            var
              _tableBody = _getTableBody (_this.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)
              ;
              function _tryFindHeadings (_rows) {
                for (var _rowNo = -1, _rowsLength = _rows.length; ++_rowNo < _rowsLength;) {
                  var _rowCells = _getRowCells (_rows [_rowNo]);
                  if (_rowCells.length == _maxColumns) {
                    _this._headings = _rowCells;
                    _this._headingsRowNo = _rowNo;
                    break;
                  }
                }
              }
              var _tableHeads = _table.getElementsByTagName ('thead');
              if (_tableHeads.length > 0) {
                var _tableHeadRows = _getChildNodesByTagName (_tableHeads [0],'TR');
                if (!_tableHeadRows.length) _tableHeadRows = [_tableHeads [0]];
                _tryFindHeadings (_tableHeadRows);
              }
              _this._headingsRowNo = -1;
              _this._headings.length || _tryFindHeadings (_tableBodyRows);

            /*** wire up headings ***/
              Uize.forEach (
                _this._headings,
                function (_heading,_headingNo) {
                  _this._headingsOldClasses [_headingNo] = _heading.className;
                  _this.wireNode (
                    _heading,
                    {
                      mouseover:function () {_this._headingMouseover (_headingNo)},
                      mouseout:function () {_this._headingMouseout ()},
                      click:function () {_this.sort (_headingNo)}
                    }
                  );
                }
              );

            /*** wire up rows with highlight behavior and title attributes for columns ***/
              var _headingsText = Uize.map (
                _this._headings,
                function (_heading) {return _Uize_Node.getText (_heading)}
              );
              function _wireRow (_row) {
                _row.Uize_Widget_TableSort_oldClassName = _row.className;
                _this.wireNode (
                  _row,
                  {
                    mouseover:function () {_this._rowMouseover (_row)},
                    mouseout:function () {_this._rowMouseout ()}
                  }
                );
              }
              for (
                var _rowNo = -1, _tableBodyRowsLength = _tableBodyRows.length;
                ++_rowNo < _tableBodyRowsLength;
              ) {
                /* NOTE: conditionalized to skip over the headings row (if in table body) and any rows with too few cells */
                if (_rowNo != _this._headingsRowNo) {
                  var
                    _row = _tableBodyRows [_rowNo],
                    _cells = _getRowCells (_row)
                  ;
                  _cells.length == _this._headings.length && _wireRow (_row);
                  for (var _cellNo = -1; ++_cellNo < _cells.length;) {
                    if (
                      _this._cellTooltipsByColumn && _cellNo in _this._cellTooltipsByColumn
                        ? _this._cellTooltipsByColumn [_cellNo]
                        : _this._cellTooltips
                    )
                      _cells [_cellNo].title = _headingsText [_cellNo]
                    ;
                  }
                }
              }
          }

          _superclass.prototype.wireUi.call (_this);
        }
      };

    /*** Register Properties ***/
      var _updateUi = 'updateUi';
      _class.registerProperties ({
        _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
        }
      });

    return _class;
  }
});