Dave Rolsky - Widget.SortableTable-0.12

Documentation | Source

NAME

Widget.SortableTable - A widget to make a table sortable

SYNOPSIS

DESCRIPTION

Instruments a table so that each of its headers is a clickable link that resorts the table based on that column.

METHODS

  • new Widget.SortableTable( { tableId: "table-id", ... } )
  • Returns a new Widget.SortableTable object for the specified element.

    This method expects an anonymous object as its only parameter. The only required property for this object is "tableId".

    The columns of the table are numbered starting at 0, from left to right

    The object passed to the constructor can have two additional properties, "initialSortColumn" and "columnSpecs". If "initialSortColumn" is specified, it tells the widget what column it should sort the table on when the widget is created. By default it sorts the table on column 0.

    The "columnSpecs" property is an array containing information about each column in the table.

    The spec for a single column is an anonymous object which can contain three properties, "skip", "sort", and "defaultDir". If "skip" is true, then this column will not be sortable. The "sort" property may be a custom sort function for that column, or a type name. See "TYPES AND SORTING" for more details on what type names are allowed. Finally, the "defaultDir" property may be either "asc" or "desc". This specifies which direction the column will be sorted on by default.

    If you want do not want to give a spec for a column you can use "null" to fill its place in the array.

    An example:

      var table = new Widget.SortableTable {
        "tableId":           "my-table",
        "initialSortColumn": 1,
        "columnSpecs": [
          null,                   no spec for column 0
          { skip: true },         do not sort column 1
          { sort: "text" },       sort column 2 as text
          { defaultDir: "desc" }, sort column 3 in descending order by default
          { sort: mySortFunc,     sort column 4 with a custom function ...
            defaultDir: "desc" }, ... sort in descending order by default
        ]
      };

    Since this class uses DOM.Ready to get the table, you can create a widget object whenever you want during the course of page rendering.

  • sortOnColumn(number)
  • Tells the widget to sort the table on the specified column. If the widget is not ready (the table is not in the DOM yet), then this method will simply return.

HOW THE WIDGET ALTERS THE TABLE

When the widget object is first instantiated, it makes a number of changes to the DOM for the table. It assumes that the first row contains column headers. For each cell, it takes the contents of the cell, whatever they may be, and wraps them in a new <a> tag. The href for this tag is "#". The tag has an "onClick" event set which calls sortOnColumn() with the appropriate column number. Its class name is "w-st-resort-column".

The widget will call sortOnColumn() as soon as it can in order to establish an initial sort order for the table.

When the table is sorted, the widget will make a number of changes to the class names for table elements. The changes are as follows:

  • Current sort-by column
  • All cells in this column will have the "w-st-current-sorted-column" added. In addition, the header cell, at the top of the column, will also have the class "w-st-asc-column-header" or "w-st-desc-column-header", depending on the current sort order.

  • Other column header cells
  • All other header cells will be given the "w-st-unsorted-column-header" class.

  • Sort-by column changes
  • When the column upon which the widget is sorting changes, it removes the "w-st-current-sorted-column" class from the previous sort-by column.

Any class names you assign in your HTML will be left untouched by the widget.

Current Sort Indicators

This widget does not add any DOM elements to show which column is the current sort-by column. Instead, you can take advantage of the CSS class names it uses to get the same effect.

For example, let's start with this HTML for a column header cell:

  <th>Name
      <img class="none" src="none.png" />
      <img class="asc"  src="asc.png" />
      <img class="desc" src="desc.png" />
  </th>

Then in your CSS you can define the following style rules:

 img.asc, img.desc {
     display: none;
 }

 th.w-st-current-sorted-column img.none {
     display: none;
 }

 th.w-st-asc-column-header   img.asc  {
     display: inline;
 }

 th.w-st-desc-column-header  img.desc  {
     display: inline;
 }

With these rules, the appropriate image will be displayed simply based on the class name changes that the widget makes.

AUTHOR

Dave Rolsky, <autarch@urth.org>.

COPYRIGHT

Copyright (c) 2006 Dave Rolsky. All rights reserved.

This module is free software; you can redistribute it and/or modify it under the same terms as the Perl programming language (your choice of GPL or the Perl Artistic license).

JSAN.use("DOM.Ready");
JSAN.use("DOM.Events");

if ( typeof Widget == "undefined" ) Widget = {};

Widget.SortableTable = function (params) {
    this._initialize(params);
}

Widget.SortableTable.VERSION = "0.12";

Widget.SortableTable.prototype._initialize = function (params) {
    if ( ! params ) {
        throw new Error("Cannot create a new Widget.SortableTable without parameters");
    }

    if ( ! params["tableId"] ) {
        throw new Error("Widget.SortableTable requires a tableId parameter");
    }

    this._initialSortColumn = params.initialSortColumn;
    if ( ! this._initialSortColumn ) {
        this._initialSortColumn = 0;
    }
    this._col_specs = [];
    if ( params.columnSpecs ) {
        for ( var i = 0; i < params.columnSpecs.length; i++ ) {
            if ( params.columnSpecs[i] ) {
                this._col_specs[i] = params.columnSpecs[i];
            }
        }
    }

    var self = this;
    DOM.Ready.onIdReady( params.tableId,
                         function (elt) { self._instrumentTable(elt) }
                       );
}

Widget.SortableTable.prototype._instrumentTable = function (table) {
    this._table = table;

    var head = table.rows[0];

    if ( ! head ) {
        return;
    }

    for ( var i = 0; i < head.cells.length; i++ ) {
        if ( this._col_specs[i] && this._col_specs[i]["skip"] ) {
            continue;
        }
        this._makeColumnSortable( head.cells[i], i );
        this._addCSSClass( head.cells[i], "w-st-unsorted-column-header" );
    }

    this.sortOnColumn( this._initialSortColumn );
}

Widget.SortableTable.prototype._makeColumnSortable = function (cell, idx) {
    var href = document.createElement("a");
    href.setAttribute( "href", "#" );
    href.setAttribute( "class", "w-st-resort-column" );

    this._moveChildTree( cell, href );
    cell.appendChild(href);

    var self = this;
    DOM.Events.addListener( href,
                            "click",
                            function () { self.sortOnColumn(idx); return false; }
                          );
}

Widget.SortableTable.prototype._moveChildTree = function (from, to) {
    if ( document.implementation.hasFeature( "Range", "2.0" ) ) {
        var range = document.createRange();
        range.selectNodeContents(from);

        to.appendChild( range.extractContents() );
    }
    else {
        /* XXX - this is gross but seems to work */
        to.innerHTML = from.innerHTML;
        from.innerHTML = "";
    }
}

Widget.SortableTable.prototype.sortOnColumn = function (idx) {
    if (! this._table ) {
        return;
    }

    var cell_data = [];
    var rows = [];
    /* start at 1 to ingore the header row when sorting */
    for ( var i = 1; i < this._table.rows.length; i++ ) {
        var text = this._getAllText( this._table.rows[i].cells[idx] );
        cell_data.push( [ text, i - 1 ] );
        rows.push( this._table.rows[i] );
    }

    var sort_info = this._sortFor( idx, cell_data[0][0] );
    cell_data.sort( Widget.SortableTable._sortFilter( sort_info["func"] ) );

    var dir = sort_info["dir"];
    /* XXX - not super-efficient since we might reverse it again very
     * shortly */
    if ( dir == "desc" ) {
        cell_data.reverse();
    }

    /* if this is all true we're resorting the current column in
     * reverse */
    if ( this._currentSort
         && this._currentSort["index"] == idx
         && this._currentSort["dir"]   == dir ) {
        cell_data.reverse();
        dir = dir == "asc" ? "desc" : "asc";
    }

    for ( var i = 0; i < cell_data.length; i++ ) {
        this._table.tBodies[0].appendChild( rows[ cell_data[i][1] ] );
    }

    this._updateCSSClasses( idx, dir );

    this._currentSort = { "index": idx,
                          "dir":   dir };
}

Widget.SortableTable.prototype._updateCSSClasses = function (idx, dir) {
    if ( ( ! this._currentSort )
         ||
         ( this._currentSort && this._currentSort["index"] != idx ) ) {

        for ( var i = 0; i < this._table.rows.length; i++ ) {
            this._addCSSClass( this._table.rows[i].cells[idx], "w-st-current-sorted-column" );
            if ( this._currentSort ) {
                old_idx = this._currentSort["index"];
                this._removeCSSClass( this._table.rows[i].cells[old_idx], "w-st-current-sorted-column" );
            }
        }
    }

    if ( this._currentSort ) {
        var old_header_cell = this._table.rows[0].cells[ this._currentSort["index"] ];
        this._removeCSSClass(
            old_header_cell,
            this._currentSort["dir"] == "asc" ? "w-st-asc-column-header" : "w-st-desc-column-header" );
        this._addCSSClass( old_header_cell, "w-st-unsorted-column-header" );
    }

    var header_cell = this._table.rows[0].cells[idx];
    if ( this._currentSort && this._currentSort["index"] == idx ) {
        var old_dir = this._currentSort["dir"];
        this._removeCSSClass( header_cell,
                              "w-st-" + old_dir + "-column-header" );
    }
    else {
        this._removeCSSClass( header_cell, "w-st-unsorted-column-header" );
    }
    this._addCSSClass( header_cell, "w-st-" + dir + "-column-header" );
}

Widget.SortableTable._sortFilter = function (real_func) {
    return function(a, b) {
        return real_func( a[0], b[0] );
    };
}

Widget.SortableTable.prototype._addCSSClass = function (elt, add_class) {
    var class_regex = new RegExp(add_class);
    if ( ! elt.className.match(class_regex) ) {
        elt.className = elt.className + (elt.className.length ? " " : "" ) + add_class;
    }
}

Widget.SortableTable.prototype._removeCSSClass = function (elt, remove_class) {
    var class_regex = new RegExp( "\\s*" + remove_class );
    elt.className = elt.className.replace( class_regex, "" );
}


Widget.SortableTable.prototype._sortFor = function (idx, content) {
    var func;
    var type;
    if ( this._col_specs[idx] && this._col_specs[idx]["sort"] ) {
        if ( typeof this._col_specs[idx]["sort"] == "function" ) {
            func = this._col_specs[idx]["sort"];
        }
        else {
            var sort_name = this._col_specs[idx]["sort"];
            type = sort_name;
            func = Widget.SortableTable._sortFunctionsByType[sort_name];
        }
    }

    if ( ! func ) {
        if ( content.match( /^\s*[\$\u20AC]\s*\d+(?:\.\d+)?\s*$/ ) ) {
            type = "currency";
            func = Widget.SortableTable._sortFunctionsByType["currency"];
        }
        else if ( content.match( /^\s*\d+(?:\.\d+)?\s*$/ ) ) {
            type = "number";
            func = Widget.SortableTable._sortFunctionsByType["number"];
        }
        else if ( content.match( /^\s*\d\d\d\d[^\d]+\d\d[^\d]+\d\d\s*$/ ) ) {
            type = "date";
            func = Widget.SortableTable._sortFunctionsByType["date"];
        }
        else {
            type = "text";
            func = Widget.SortableTable._sortFunctionsByType["text"];
        }
    }

    var dir;
    if ( this._col_specs[idx] && this._col_specs[idx]["defaultDir"] ) {
        dir = this._col_specs[idx]["defaultDir"];
    }
    else if (type)  {
        dir = Widget.SortableTable._defaultDirByType[type];
    }
    else {
        dir = "asc";
    }

    return { "func": func,
             "dir":  dir };
}

/* More or less copied from
 * http://www.kryogenix.org/code/browser/sorttable/sorttable.js
 */
Widget.SortableTable.prototype._getAllText = function (elt) {
    if ( typeof elt == "string") {
        return elt;
    }
    if ( typeof elt == "undefined") {
        return "";
    }

    var text = "";
	
    var children = elt.childNodes;
    for ( var i = 0; i < children.length; i++ ) {
        switch ( children[i].nodeType) {
        case 1: //ELEMENT_NODE
            text += this._getAllText( children[i] );
            break;
        case 3:	//TEXT_NODE
            text += children[i].nodeValue;
            break;
        }
    }

    return text;
}

var foo =  1;
Widget.SortableTable._sortCurrency = function (a, b) {
    var a_num = parseFloat( a.replace( /[^\d\.]/g, "" ) )
    var b_num = parseFloat( b.replace( /[^\d\.]/g, "" ) )

    return Widget.SortableTable._sortNumberOrNaN(a_num, b_num);
}

Widget.SortableTable._sortNumber = function (a, b) {
    var a_num = parseFloat(a);
    var b_num = parseFloat(b);

    return Widget.SortableTable._sortNumberOrNaN(a_num, b_num);
}

Widget.SortableTable._sortNumberOrNaN = function (a, b) {
    if ( isNaN(a) && isNaN(b) ) {
        return 0;
    }
    else if ( isNaN(a) ) {
        return -1;
    }
    else if ( isNaN(b) ) {
        return 1;
    }
    else if ( a < b ) {
        return -1;
    }
    else if ( a > b ) {
        return 1;
    }
    else {
        return 0;
    }
}

Widget.SortableTable._sortDate = function (a, b) {
    var a_match = a.match( /(\d\d\d\d)[^\d]+(\d\d)[^\d]+(\d\d)/ );
    var b_match = b.match( /(\d\d\d\d)[^\d]+(\d\d)[^\d]+(\d\d)/ );

    if ( ! a_match ) {
        a_match = [ "", -9999, 1, 1 ];
    }

    if ( ! b_match ) {
        b_match = [ "", -9999, 1, 1 ];
    }

    var a_date = new Date( a_match[1], a_match[2], a_match[3] );
    var b_date = new Date( b_match[1], b_match[2], b_match[3] );

    if ( a_date < b_date ) {
        return -1;
    }
    else if ( a_date > b_date ) {
        return 1;
    }
    else {
        return 0;
    }
}

Widget.SortableTable._sortText = function (a, b) {
    var a_text = a.toLowerCase();
    var b_text = b.toLowerCase();

    if ( a_text < b_text ) {
        return -1;
    }
    else if ( a_text > b_text ) {
        return 1;
    }
    else {
        return 0;
    }
}

Widget.SortableTable._sortFunctionsByType = {
    "currency": Widget.SortableTable._sortCurrency,
    "number":   Widget.SortableTable._sortNumber,
    "date":     Widget.SortableTable._sortDate,
    "text":     Widget.SortableTable._sortText
};

Widget.SortableTable._defaultDirByType = {
    "currency": "asc",
    "number":   "asc",
    "date":     "desc",
    "text":     "asc"
};


/*

*/