Table Sorter Code

Site Navigation 

The table sorting script was designed to be as fast and as self contained as possible. That is partly achieved by applying a technique of nesting objects so that most functions are inner functions or inner functions of inner functions. Unfortunately the resulting code takes some understanding and this script is too big to explain in sufficient detail. Hopefully I have provided enough information to enable its use and my article on private static members in JavaScript should provide some explanation of the structure of the code.

<script type="text/javascript"
 src="scripts/Finalizer.js">
</script>

This script is dependent on my Finalizer script.

<script type="text/javascript">

var SortColumn = function(){
  var defaultDirection = 'down';  //'up'|'down' 
  function getSortFnc(type, index){
    var regEx;
    switch(type){
      case 'byDate':
        regEx = /\-/g;
        return function (n1,n2){
          var d1 = n1.cells[index];
          var d2 = n2.cells[index];
          if(!d1.sortKey)d1.sortKey =
           Date.parse(getInnerText(d1).replace(regEx, '/'));
          if(!d2.sortKey)d2.sortKey =
           Date.parse(getInnerText(d2).replace(regEx, '/'));
          var dif = d1.sortKey - d2.sortKey;
          if(dif < 0){
            return -1;
          }else if(dif > 0){
            return 1;
          }else{
            return 0;
          }
        };

Do not trust this sort-by-Date implementation because the interpretation of the string argument to Date.parse is implementation dependent and may vary due to local settings.

      case 'byNoCase':
        return function(n1,n2){
          var d1 = n1.cells[index];
          var d2 = n2.cells[index];
          if(!d1.sortKey)d1.sortKey =
                          getInnerText(d1).toUpperCase();
          if(!d2.sortKey)d2.sortKey =
                          getInnerText(d2).toUpperCase();
          if(d1.sortKey < d2.sortKey){

Functions used with Array.prototype.sort will be called for each and every comparison used when sorting an array so they should do as little work as possible if the sorting operation is going to be quick. These functions need to retrieve the text within the cell, process it and then do the comparison with it. To avoid doing the retrieval and processing on each function call the result is being assigned to an expando property of the cell called sortKey. This means that the sortKey only need to be created when the cell is first compared with another, from then on it is available as a property of the cell. Avoiding the need to re-call getInnerText and repeat any following processing (Date creation, case conversion and string to number conversion.

            return -1;
          }else if(d1.sortKey > d2.sortKey){
            return 1;
          }else{
            return 0;
          }
        };
      case 'byNumber':
        regEx = /[^0-9\.]/g;
        return function (n1,n2){
          var d1 = n1.cells[index];
          var d2 = n2.cells[index];
          if(!d1.sortKey)d1.sortKey =
                  (+getInnerText(d1).replace(regEx, ""));
          if(!d2.sortKey)d2.sortKey =
                  (+getInnerText(d2).replace(regEx, ""));
          var dif = d1.sortKey - d2.sortKey;
          if(dif < 0){
            return -1;
          }else if(dif > 0){
            return 1;
          }else{
            return 0;
          }
        };
      case 'byCase':
      default:
        return function(n1,n2){
          var d1 = n1.cells[index];
          var d2 = n2.cells[index];
          if(!d1.sortKey)d1.sortKey = getInnerText(d1);
          if(!d2.sortKey)d2.sortKey = getInnerText(d2);
          if(d1.sortKey < d2.sortKey){
            return -1;
          }else if(d1.sortKey > d2.sortKey){
            return 1;
          }else{
            return 0;
          }
        };
    }
  }

The preceding function is arranged to allow the easy modification and extension of the sorting abilities of the script.

  function getParent(el, pTagName) {
    if(el != null){
      if((el.nodeType == 1)&&
        (el.tagName.toUpperCase() ==
                            pTagName.toUpperCase())){
        return el;
      }else if(el.parentNode){
        return getParent(el.parentNode, pTagName);
      }
    }
    return null;
  }
  function getDecendentByTagName(el, dTagName) {
    var ndList;
    if(el.getElementsByTagName){
      ndList = el.getElementsByTagName(dTagName)
    }else if(el.all){
      ndList = el.all.tags(dTagName);
    }
    if(!ndList){
      ndList = [];
    }else if(typeof ndList.length == 'undefined'){
      ndList = [ndList];
    }
    return ndList;
  }
  function getInnerTextQ(el){ //IE and Opera 7
    return el.innerText;
  }
  function getInnerTextW(el){//other DOM 2 browsers
    var str = "";
    for (var i=0; i<el.childNodes.length; i++) {
      switch (el.childNodes.item(i).nodeType){
        case 1: //ELEMENT_NODE
          str +=
              arguments.callee(el.childNodes.item(i));
          break;
        case 3: //TEXT_NODE
          str += el.childNodes.item(i).nodeValue;
          break;
        default:
          break;
      }
    }
    return str;
  }
  var getInnerText;

  function SortTable(rowAr, contRow){
    function getTableRows(){
      var a = [];
      for(var c = rowAr.length;c--;){
        a[c] = rowAr[c];
      }
      return a;
    }
    function SortColumn(index, conCell){
      function sortIt(){
        direction = (direction == 'up')?'down':'up';
        for(var c = contRow.length;c--;){
          contRow[c].displayOff();
        }
        if(!orderedRows)orderedRows =
              getTableRows().sort(getSortFnc(type, index));
        tableEl.removeChild(parentEl);
        if(direction == 'up'){
          for(var c = 0;c < orderedRows.length;c++){
            parentEl.appendChild(orderedRows[c]);
          }
        }else{
          for(var c = orderedRows.length;c--;){
            parentEl.appendChild(orderedRows[c]);
          }
        }
        tableEl.appendChild(parentEl);
        arrowParent.replaceChild(
                    indicator['arrow'+direction],dispNode);
        dispNode = indicator['arrow'+direction];
        return false;
      }
      var direction = defaultDirection;
      var arrowParent, dispNode, orderedRows;
      var type = '';
      var indicator = {
        arrow:(arrow.cloneNode(true)),
        arrowup:(arrowUp.cloneNode(true)),
        arrowdown:(arrowDown.cloneNode(true))
      };
      var ndList = getDecendentByTagName(conCell, 'IMG');
      if((ndList)&&(ndList.length > 0)){
        dispNode = ndList[0];
        arrowParent = dispNode.parentNode;
      }else{
        arrowParent = conCell;
        dispNode = arrowParent.appendChild(indicator.arrow);
      }
      ndList = getDecendentByTagName(conCell, 'A');
      if((ndList)&&(ndList.length > 0)){
        for(var c = ndList.length;c--;){
          if(typeof ndList[c].onclick == 'function'){
            var s = ''+ndList[c].onclick;
            var i = s.indexOf('SortColumn');
            if(i >= 0){
              s = s.substring((i+11),s.length);
              type = s.substring(0,s.indexOf('('));
              ndList[c].onclick = sortIt;
              break;
            }
          }
        }
      }
      var parentEl = rowAr[0].parentNode;
      this.displayOff = function(){
        if(indicator.arrow != dispNode){
          arrowParent.replaceChild(indicator.arrow,dispNode);
          dispNode = indicator.arrow;
        }
      }
      this.finalize = function(){
        var ndList = getDecendentByTagName(conCell, 'A');
        if((ndList)&&(ndList.length > 0)){
          for(var c = ndList.length;c--;){
            ndList[c].onclick = null;
          }
        }
        arrowParent = (ndList = (conCell =
        (this.arrow =
        (this.arrowup =
        (this.arrowdown =
        (dispNode = null))))));
      };
    }
    var temp;
    var arrow = document.createElement("SPAN");
    temp = document.createElement("IMG");
    temp.src = "images/sortBlank.gif";
    temp.alt = (temp.title = "");
    arrow.appendChild(temp);
    var arrowUp = document.createElement("SPAN");
    temp = document.createElement("IMG");
    temp.src = "images/sortUp.gif";
    temp.alt = (temp.title = "(Sorted Ascending)");
    arrowUp.appendChild(temp);
    var arrowDown = document.createElement("SPAN");
    temp = document.createElement("IMG");
    temp.src = "images/sortDown.gif";
    temp.alt = (temp.title = "(Sorted Descending)");
    arrowDown.appendChild(temp);
    temp = [];
    for(var c = 0;c < contRow.length;c++){
      temp[c] = new SortColumn(c, contRow[c]);
    }
    contRow = temp;
    temp = null;
    if(typeof finalizeMe != 'undefined'){
      finalizeMe(
        function(){
          for(var c = contRow.length;c--;){
            contRow[c].finalize();
            contRow[c] = null;
          }
          for(var c = rowAr.length;c--;){
            rowAr[c] = null;
          }
          arrowDown =
          (arrowUp =
          (arrow =
          (contRow =
          (rowAr = null))));
        }
      );
    }
  };

  function init(obj){
    var switchRow = getParent(obj, "TR");
    if((switchRow)&&(switchRow.cells)&&
               (switchRow.parentNode)&&
             (switchRow.replaceChild)&&
                (switchRow.cloneNode)&&
             (document.createElement)&&
              (switchRow.appendChild)){
      if(typeof switchRow.innerText != 'undefined'){
        getInnerText = getInnerTextQ;
      }else if(typeof switchRow.childNodes != 'undefined'){
        getInnerText = getInnerTextW;
      }
      if(getInnerText){
        var contTable = getParent(switchRow, "TABLE");
        if(contTable){
          var rowArr = [];
          var rowList;
          var ndList =
                   getDecendentByTagName(contTable, 'TBODY');
          for(var c = ndList.length;c--;){
            rowList = getDecendentByTagName(ndList[c], 'TR');
            for(var i = rowList.length;i--;){
              rowArr[rowArr.length] = rowList[i];
            }
          }
          var switchCells = switchRow.cells;
          new SortTable(rowArr, switchCells);
          rowList = (ndList = (rowArr = null));
          return;
        }
      }

    }
    arguments.callee.byCase =
    (arguments.callee.byDate =
    (arguments.callee.byNoCase =
    (arguments.callee.byNumber = function(){return true;})));
  }

Notice the amount of feature testing done in the init function. This follows a principle of initialising the script through a "gateway" that tests all of the required DOM features to see if the browser is going to be able to execute it and if not the init function disables the script by assigning a function to each item in it's public interface that will just return true; leaving the browser to fall back on the navigation specified in the link's href attributes.

  init.byNumber = function(obj){
    init(obj);
    if(arguments.callee == init.byNumber){
/*trigger the sorting for this cell by calling the
  dynamically assigned onclick method that is now
  attached to the A element.*/
      return obj.onclick();
    }else{
/*script was disabled so fall back to server side.*/
      return true;
    }
  }

Because the init.byNumber function will be changed to a harmless function if the script decides to disable itself, comparing arguments.callee and init.byNumber will tell this function whether its call to init has resulted in the script being disabled (so just return true to allow the href navigation) or not (so call the onclick function and return whatever it returns).

  init.byNoCase = function(obj){
    init(obj);
    if(arguments.callee == init.byNoCase){
      return obj.onclick();
    }else{
      return true;
    }
  }
  init.byCase = function(obj){
    init(obj);
    if(arguments.callee == init.byCase){
      return obj.onclick();
    }else{
      return true;
    }
  }
  init.byDate = function(obj){
    init(obj);
    if(arguments.callee == init.byDate){
      return obj.onclick();
    }else{
      return true;
    }
  }
  return init;
}();
</script>

If another sort function is added to the getSortFnc function then a corresponding init.byNewCriteria function would need to be added above, following the same pattern as the existing ones.

<table>
  <thead>
    <tr>
      <th><a href="serverFallback.html?type=byNoCase"
         onclick="return SortColumn.byNoCase(this)">
         Case Insensitive

The table sorting is triggered by the onclick event of an A element. The initial onclick function calls a function property of the SortColumn function which returns true or false depending on whether the script is going to execute or fall back to the server script specified in the href attribute. Notice that the href attribute is provided with a query string so that the server code (if it really existed) would know how to sort the table it returned.

The this argument passed to the sorting function is a reference to the A element to which the onclick function is attached. It is used by the code to locate the THEAD and TABLE that contains it.

Alternatively, the script can be initialised in the BODY onload event. This requires that the SortColumn function is called directly and provided with a reference to an element that is a descendant of the THEAD (the TR is the ideal candidate). So, if the TR had an ID attribute "trId" the BODY onload code would be something like:-

<body onload="if(document.getElementById)SortColumn(document.getElementById('trId');">

Doing this will reduce the initial sorting time because some of the set-up work will have already been done when the table head cells are clicked.

         <img src="images/sortBlank.gif"alt=""></a>
      </th>
      <th><a href="serverFallback.html?type=byCase"
        onclick="return SortColumn.byCase(this)">
        Case Sensitive
        <img src="images/sortBlank.gif"alt=""></a>
      </th>
      <th><a href="serverFallback.html?type=byNumber"
        onclick="return SortColumn.byNumber(this)">
        Number<img src="images/sortBlank.gif"alt=""></a>
      </th>
      <th><a href="serverFallback.html?type=byDate"
        onclick="return SortColumn.byDate(this)">
        Date<img src="images/sortBlank.gif"alt=""></a>
      </th>
    </tr>
  </thead>

The script differentiates between rows that should be sorted and rows that should not by assuming that rows in any TBODY elements are to be sorted and rows within THEAD (and TFOOT) should not. As a result, the cells (logically TH, else the "TH" strings in the script will need to be changed) in the THEAD should contain the controlling links). If multiple TBODY elements are used the DOM manipulation script will migrate all rows into the first after the first row sort.

  <tbody>
    <!-- the table rows go here -->
    ...
  </tbody>
</table>

© Richard Cornford 2004