DataGrid Class

if(!javaxt) var javaxt={};
if(!javaxt.dhtml) javaxt.dhtml={};

//******************************************************************************
//**  DataGrid
//******************************************************************************
/**
 *   Custom grid control based on javaxt.dhtml.Table. Supports remote loading,
 *   sorting, and infinite scroll.
 *
 ******************************************************************************/

javaxt.dhtml.DataGrid = function(parent, config) {
    this.className = "javaxt.dhtml.DataGrid";

    var me = this;
    var table;
    var mask;

    var currPage = 1;
    var eof = false;
    var pageRequests = {};
    var loading = false;

    var checkboxHeader;
    var columns;
    var rowHeight; //<-- Assumes all the rows are the same height!


  //Legacy config parameters
    var filterName, filter;



  //Create default style
    var defaultStyle = {};
    if (javaxt.dhtml.style){
        if (javaxt.dhtml.style.default){
            var tableStyle = javaxt.dhtml.style.default.table;
            if (tableStyle){
                defaultStyle = tableStyle;
                var checkboxStyle = javaxt.dhtml.style.default.checkbox;
                defaultStyle.checkbox = checkboxStyle;
            }
        }
    }



  //Set default config
    var defaultConfig = {


      /** Style config. See default styles in javaxt.dhtml.Table for a list of
       *  options. In addition to the table styles, you can also specify a
       *  checkbox style
       */
        style: defaultStyle,

      /** The URL endpoint that this component uses to fetch records
       */
        url: "",

      /** JSON object with key/value pairs that are appended to the URL
       */
        params: null,

      /** Data to send in the body of the request. This parameter is optional.
       *  If payload is not null, the component executes a POST request.
       */
        payload: null,

      /** If true, and if the payload is empty, will sent params as a URL
       *  encoded string in a POST request. Otherwise the params will be
       *  appended to the query string in the request URL.
       */
        post: false,

      /** Used to specify the page size (i.e. the maximum number records to
       *  fetch from the server at a time)
       */
        limit: 50,

      /** If true, the server will be asked to return a total record count.
       *  This is a legacy feature and is NOT required for pagination.
       */
        count: false,

      /** If true, the grid will automatically fetch records from the server
       *  on start-up
       */
        autoload: false,

      /** If true, the grid will sort values in the grid locally using whatever
       *  data is available. If fetching data from a remote server, recommend
       *  setting localSort to false (default)
       */
        localSort: false,

      /** Optional list of field names used to specify which database fields
       *  should be returned from the server. If not provided, uses the "field"
       *  attributes in the column definition. If both are provided, the list
       *  of fields are merged into a unique list.
       */
        fields: [],

      /** Default method used to get responses from the server. Typically, you
       *  do not need to override this method.
       */
        getResponse: function(url, payload, callback){


          //Transform GET request into POST request if possbile. This will
          //tidy up the URLs and reduce log size
            if (!payload && config.post==true){
                var idx = url.indexOf("?");
                if (idx>-1){
                    payload = url.substring(idx+1);
                    url = url.substring(0, idx);
                }
            }


          //Get response
            javaxt.dhtml.utils.get(url, {
                payload: payload,
                success: function(text, xml, url, request){
                    callback.apply(me, [request]);
                },
                failure: function(request){
                    callback.apply(me, [request]);
                }
            });
        },

      /** Default method used to parse responses from the server. Should return
       *  an array of rows with an array of values in each row. Example:
       *  [["Bob","12/30","$5.25"],["Jim","10/28","$7.33"]]
       */
        parseResponse: function(request){
            return [];
        },

      /** Default method used to update a row with values. This method can be
       *  overridden to render values in a variety of different ways (e.g.
       *  different fonts, links, icons, etc). The row.set() method accepts
       *  strings, numbers, and DOM elements.
       */
        update: function(row, record){
            for (var i=0; i<record.length; i++){
                row.set(i, record[i]);
            }
        },

      /** DOM element with custom show() and hide() methods. The mask is
       *  typically rendered over the grid control to prevent users from
       *  interacting with the grid during load events. It is rendered before
       *  load and is hidden after data is rendered. If a mask is not defined,
       *  a simple mask will be created automatically using the "mask" style
       *  config.
       */
        mask: null
    };



  //**************************************************************************
  //** Constructor
  //**************************************************************************
    var init = function(){

      //Merge config with default config
        config = merge(config, defaultConfig);



      //Extract required config variables
        filterName = config.filterName;
        if (config.sort==="local") config.localSort = true;
        if (config.fields){
            if (!isArray(config.fields)){
                config.fields = config.fields.split(",");
            }
        }
        else{
            config.fields = [];
        }


      //Parse column config
        var multiselect = config.multiselect;
        columns = [];
        for (var i=0; i<config.columns.length; i++){
            var column = config.columns[i];


          //Set "fields" class variable
            if (column.field){
                var addField = true;
                for (var j=0; j<config.fields.length; j++){
                    if (config.fields[j]==column.field){
                        addField = false;
                        break;
                    }
                }
                if (addField) config.fields.push(column.field);
            }
            else{
              //Don't allow remote sorting on columns without a field name
                if (config.localSort!==true){
                    column.sortable = false;
                }
            }


          //Clone the column config
            var clone = {};
            for (var p in column) {
                if (column.hasOwnProperty(p)) {
                    clone[p] = column[p];
                }
            }


          //Update "header" setting in the column config
            var header = column.header;
            if (header==='x' && !checkboxHeader){
                clone.header = createCheckbox();
                var checkbox = clone.header.checkbox;
                multiselect = true;
                checkboxHeader = {
                    idx: i,
                    isChecked: checkbox.isChecked,
                    check: checkbox.select,
                    uncheck: checkbox.deselect
                };
            }
            else{
                clone.header = createHeader(header, column.sortable);
            }

            columns.push(clone);
        }




      //Update column config using the "sort" filter
        setFilter(config.filter);



      //Create table
        table = new javaxt.dhtml.Table(parent, {
            multiselect: multiselect,
            columns: columns,
            style: config.style
        });
        me.el = table.el;


      //Get mask
        mask = config.mask;
        if (!mask) mask = table.getMask();


      //Add load function to the table
        table.load = function(records, append){
            if (!append) me.clear();
            var rows = table.addRows(records.length);


            var select = false;
            if (checkboxHeader){
                select = checkboxHeader.isChecked();
            }

            var isDataStore = (records instanceof javaxt.dhtml.DataStore);

            for (var i=0; i<rows.length; i++){

              //Assign the record to the row
                rows[i].record = isDataStore ? records.get(i) : records[i];


              //Override the update method
                rows[i].update = function(){
                    config.update(this, this.record);
                };


              //Override the native "set" method on the row
                rows[i]._set = rows[i].set;
                rows[i].set = function(key, val){
                    if (key==='x'){

                      //Create checkbox
                        for (var j=0; j<config.columns.length; j++){
                            if (config.columns[j].header==='x'){
                                var field = config.columns[j].field;

                                var col = this.childNodes[j];
                                var obj = col.getContent();
                                if (obj){
                                    //Column has a checkbox already?
                                }
                                else{
                                    val = field  ? this.record[field] : this.record;
                                    var checkboxDiv = createCheckbox(val, this);
                                    var checkbox = checkboxDiv.checkbox;
                                    if (select) checkbox.select();
                                    this._set(j, checkboxDiv);
                                }
                                return;
                            }
                        }
                    }
                    else{

                      //Wrap value in an overflow div as requested
                        for (var j=0; j<config.columns.length; j++){
                            if (config.columns[j].header===key && config.columns[j].wrap===true){
                                val = wrap(val);
                                break;
                            }
                        }

                        this._set(key, val);
                    }
                };



              //Override the native "onclick" method on the row
                rows[i]._onclick = rows[i].onclick;
                rows[i].onclick = function(e){


                  //Special case for columns with checkboxes
                    if (checkboxHeader){

                      //Check if the client clicked inside a checkbox
                        var insideCheckbox = false;
                        var checkboxCol = this.childNodes[checkboxHeader.idx];
                        var checkboxDiv = checkboxCol.getContent();
                        var rect = _getRect(checkboxDiv);
                        var clientX = e.clientX;
                        var clientY = e.clientY;
                        if (clientX>=rect.left && clientX<=rect.right){
                            if (clientY>=rect.top && clientY<=rect.bottom){
                                insideCheckbox = true;
                            }
                        }

                        if (insideCheckbox){
                            if (this.selected){
                                table.deselect(this);
                                //checkboxHeader.uncheck();
                            }
                            else{
                                table.select(this);
                            }
                            me.onSelectionChange();
                            return;
                        }
                        else{

                            if (!e.ctrlKey && !e.shiftKey){
                                var numSelectedRows = 0;
                                table.forEachRow(function (row) {
                                    if (row.selected){
                                        numSelectedRows++;
                                        if (numSelectedRows>1){

                                          //Update the click event to simulate a ctrl+click
                                            e = new MouseEvent("click", {
                                                isTrusted: e.isTrusted,
                                                view: e.view,
                                                bubbles: e.bubbles,
                                                cancelable: e.cancelable,
                                                clientX: e.clientX,
                                                clientY: e.clientY,
                                                screenX: e.screenX,
                                                screenY: e.screenY,
                                                altKey: e.altKey,
                                                ctrlKey: true
                                            });


                                          //Exit forEachRow
                                            return true;
                                        }
                                    }
                                });
                            }
                        }
                    }


                    this._onclick(e);
                };


              //Update the row
                rows[i].update();
            }

            if (!append) table.update();
        };


      //Watch for selection change events
        table.onSelectionChange = function(rows){


          //Update checkboxes
            if (checkboxHeader){
                for (var i=0; i<rows.length; i++){
                    var row = rows[i];

                    var checkboxCol = row.childNodes[checkboxHeader.idx];
                    var checkboxDiv = checkboxCol.getContent();
                    var checkbox = checkboxDiv.checkbox;


                    if (row.selected){
                        checkbox.select();
                    }
                    else{
                        checkbox.deselect();
                    }
                }
            }


            me.onSelectionChange();
        };


      //Watch for scroll events
        table.onScroll = function(y, maxY, h){

          //Calculate start and end rows
            var startRow = Math.ceil(y/rowHeight);
            if (startRow==0) startRow = 1;
            var endRow = Math.ceil((y+h)/rowHeight);


            var prevPage = currPage;
            setPage(Math.ceil(endRow/config.limit));

            //console.log(startRow + "/" + endRow + " (Page: " + currPage + ")");


            var d = Math.abs(maxY-y);
            if (d<rowHeight){ //if (y===maxY){
                if (!eof){
                    if (currPage===prevPage) setPage(currPage+1);
                    load(currPage);
                }
            }

            me.onScroll();
        };


      //Watch for header click events
        table.onHeaderClick = sort;


      //Watch for row click events
        table.onRowClick = function(row, e){
            me.onRowClick(row, e);
        };


      //Watch for key events
        table.onKeyEvent = function(keyCode, modifiers){
            me.onKeyEvent(keyCode, modifiers);
        };



      //Load records
        if (config.autoload===true) load();

    };


  //**************************************************************************
  //** setFilter
  //**************************************************************************
  /** Used to update the filter with new params
   *  @deprecated The filter object is a legacy feature and will be removed
   */
    this.setFilter = function(newFilter){
        console.warn(
        "The filter object in the javaxt.dhtml.DataGrid class is a legacy " +
        "feature and will be removed in the future.");
        setFilter(newFilter);
    };

    var setFilter = function(newFilter){
        if (!newFilter) newFilter = {};
        if (!filter) filter = newFilter;


      //Remove duplicates from newFilter
        removeDuplicateParams(newFilter);


        for (var key in newFilter) {
            if (newFilter.hasOwnProperty(key)) {
                filter[key] = newFilter[key];
            }
        }

        var deletions = [];
        for (var key in filter) {
            if (filter.hasOwnProperty(key)) {
                if (newFilter[key]===null || newFilter[key]==='undefined'){
                    deletions.push(key);
                }
            }
        }

        for (var i=0; i<deletions.length; i++){
            var key = deletions[i];
            delete filter[key];
        }


        for (var j=0; j<columns.length; j++){
            columns[j].sort = null;
            var colHeader = columns[j].header;
            if (colHeader.setSortIndicator) colHeader.setSortIndicator(null);
        }


        if (filter.orderby){
            var arr = filter.orderby.split(",");
            for (var i=0; i<arr.length; i++){
                var field = arr[i].trim();
                if (field.length>0){

                    var fieldName, sortDirection;
                    field = field.toUpperCase();
                    if (field.endsWith(" ASC") || field.endsWith(" DESC")){
                        var x = field.lastIndexOf(" ");
                        fieldName = field.substring(0, x).trim();
                        sortDirection = field.substring(x).trim();
                    }
                    else{
                        fieldName = field;
                        sortDirection = "ASC";
                    }



                    for (var j=0; j<columns.length; j++){
                        if (columns[j].field){
                            if (columns[j].field.toUpperCase() === fieldName){
                                columns[j].sort = sortDirection;
                                var colHeader = columns[j].header;
                                if (colHeader.setSortIndicator){
                                    colHeader.setSortIndicator(sortDirection);
                                }
                                break;
                            }
                        }
                    }

                }
            }

        }
    };


  //**************************************************************************
  //** getFilter
  //**************************************************************************
  /** Returns the current filter
   *  @deprecated The filter object is a legacy feature and will be removed
   */
    this.getFilter = function(){
        console.warn(
        "The filter object in the javaxt.dhtml.DataGrid class is a legacy " +
        "feature and will be removed in the future.");
        return filter;
    };


  //**************************************************************************
  //** getParams
  //**************************************************************************
    this.getParams = function(){
        return config.params;
    };


  //**************************************************************************
  //** setPage
  //**************************************************************************
  /** Used to set the currPage variable and call the onPageChange method.
   */
    var setPage = function(page){
        if (isNaN(page) || page<1) page = 1;
        if (page!=currPage){
            var prevPage = currPage;
            currPage = page;
            me.onPageChange(currPage, prevPage);
        }
    };


  //**************************************************************************
  //** getCurrPage
  //**************************************************************************
  /** Returns the current page number
   */
    this.getCurrPage = function(){
        return currPage;
    };


  //**************************************************************************
  //** getScrollInfo
  //**************************************************************************
    this.getScrollInfo = function(){
        return table.getScrollInfo();
    };


  //**************************************************************************
  //** Events
  //**************************************************************************
    this.onScroll = function(){};
    this.onPageChange = function(currPage, prevPage){};
    this.onSelectionChange = function(){};
    this.beforeLoad = function(page){};
    this.onLoad = function(){};
    this.onError = function(request){};
    this.onRowClick = function(row, e){};
    this.onKeyEvent = function(keyCode, modifiers){};
    this.onCheckbox = function(value, checked, checkbox){};
    this.onSort = function(idx, sortDirection){};


  //**************************************************************************
  //** forEachRow
  //**************************************************************************
  /** Used to traverse all the rows in the table and extract contents of each
   *  cell. Example:
   <pre>
    grid.forEachRow(function(row, content){
        console.log(row, row.record, content);
    });
   </pre>
   *
   *  Optional: return true in the callback function if you wish to stop
   *  processing rows.
   */
    this.forEachRow = function(callback){
        table.forEachRow(callback);
    };


  //**************************************************************************
  //** select
  //**************************************************************************
  /** Used to select a given row in the grid
   */
    this.select = function(row){
        table.select(row);
    };


  //**************************************************************************
  //** selectAll
  //**************************************************************************
  /** Selects all the rows in the grid
   */
    this.selectAll = function(){
        table.selectAll();
    };


  //**************************************************************************
  //** deselectAll
  //**************************************************************************
  /** Deselects all the rows in the grid
   */
    this.deselectAll = function(){
        table.deselectAll();
    };


  //**************************************************************************
  //** clear
  //**************************************************************************
  /** Removes all the rows from the grid
   */
    this.clear = function(){
        table.clear();
        currPage = 1;
        pageRequests = {};
    };


  //**************************************************************************
  //** remove
  //**************************************************************************
  /** Removes a row from the grid
   */
    this.remove = function(row){
        table.removeRow(row);
    };


  //**************************************************************************
  //** show
  //**************************************************************************
  /** Used to unhide the grid if it is hidden
   */
    this.show = function(){
        table.show();
    };


  //**************************************************************************
  //** hide
  //**************************************************************************
  /** Used to hide the grid
   */
    this.hide = function(){
        table.hide();
    };
    

  //**************************************************************************
  //** focus
  //**************************************************************************
    this.focus = function(){
        table.focus();
    };


  //**************************************************************************
  //** refresh
  //**************************************************************************
  /** Used to clear the grid and reload the first page.
   *  @param callback Optional callback function called after the grid has
   *  been refreshed and loaded with data.
   */
    this.refresh = function(callback){
        me.clear();
        //eof = false;
        setPage(1);
        load(1, callback);
    };


  //**************************************************************************
  //** load
  //**************************************************************************
  /** Used to load records from the remote store. Optionally, you can pass an
   *  array of records, along with a page number, to append rows to the table.
   */
    this.load = function(){

        if (arguments.length>0){

            var records = arguments[0];
            var page = 1;
            if (arguments.length>1) page = parseInt(arguments[1]);
            if (isNaN(page) || page<1) page = 1;


            if (isArray(records) || records instanceof javaxt.dhtml.DataStore){
                me.beforeLoad(page);
                table.load(records, page>1);
                setPage(page);
                calculateRowHeight();


                if (arguments.length===1){
                    eof = true;
                }
                else{ //caller provided a page number
                    if (records.length<config.limit) eof = true;
                }

                me.onLoad();
            }
        }
        else{
            load();
        }
    };


  //**************************************************************************
  //** setLimit
  //**************************************************************************
  /** Used to update the number of records to load per page.
   */
    this.setLimit = function(limit){
        config.limit = limit;
    };


  //**************************************************************************
  //** scrollTo
  //**************************************************************************
  /** Scrolls to the top of a page or to a row in the table
   *  @param obj Page number (integer) or row in the grid (DOM element)
   */
    this.scrollTo = function(obj){

        if (isElement(obj)){
            var y = 0;
            table.forEachRow(function (row) {

                if (row==obj){
                    return true;
                }

                y += row.offsetHeight;

            });
            table.scrollTo(0, y);
            return;
        }

        if (!isNumber(obj)) return;
        var page = parseInt(obj);


      //Calculate scroll
        var y = ((page-1)*config.limit)*rowHeight;
        if (y<0) y = 0;


      //Check whether the page is already loaded in the grid
        var numPagesInGrid = Math.ceil(table.getRowCount()/config.limit);
        if (page>numPagesInGrid){

            if (page==numPagesInGrid+1){

              //Caller wants to jumpt to the next available page.
              //Load the page and scroll when ready.
                load(page, function(){
                    table.scrollTo(0, y);
                });
            }
            else{
              //Caller wants to skip several pages ahead...
                console.log("Fetch pages! " + numPagesInGrid + " page loaded. Caller wants page " + page + "...");
                return;
            }
        }
        else{
            table.scrollTo(0, y);
        }
    };



  //**************************************************************************
  //** calculateRowHeight
  //**************************************************************************
    var calculateRowHeight = function(){
        if (!rowHeight){
            table.forEachRow(function (row, content) {
                rowHeight = row.offsetHeight;
                return;
            });
        }
    };


  //**************************************************************************
  //** getSelectedRecords
  //**************************************************************************
  /** Returns an array of selected records from the grid.
   *  @param key Optional field name or index (e.g. 'id' or 0). If given, the
   *  output array will only contain values for the given key. This can be
   *  useful for limiting the amount of data returned in the array. For
   *  example, you might only need selected IDs instead of a full record.
   *  @param callback Callback function. If the table has a checkbox header,
   *  and the check box is checked, then the user expects to get back ALL
   *  "selected" records, even those that have yet to load. In this case, we
   *  will fetch records from the server and invoke the callback with
   *  additional data.
   */
    this.getSelectedRecords = function(key, callback){


      //Map key to a column id as needed
        var colID;
        if (key){
            if (typeof key === "string"){
                for (var i=0; i<config.fields.length; i++){
                    if (key === config.fields[i]){
                        colID = i;
                        break;
                    }
                }
                if (!colID){
                    for (var i=0; i<config.columns.length; i++){
                        var column = config.columns[i];
                        if (key === column.header){
                            colID = i;
                            break;
                        }
                    }
                }
            }
        }
        if (!colID) colID = 0;



      //Special case for tables with checkbox headers. Check whether the
      //checkbox in the header is selected
        var useCheckbox = false;
        var selectAll = false;
        if (checkboxHeader){
            if (checkboxHeader.idx === colID){
                useCheckbox = true;
                if (checkboxHeader.isChecked()){
                    selectAll = true;
                }
            }
        }


      //Get selected records from the table
        var arr = [];
        table.forEachRow(function (row, content) {
            if (key){
                if (useCheckbox){
                    var checkboxDiv = content[colID];
                    var checkbox = checkboxDiv.checkbox;
                    if (checkbox.isChecked()) arr.push(checkbox.getValue());
                }
                else{
                    if (row.selected) arr.push(row.record[key]);
                }
            }
            else{
                if (row.selected) arr.push(row.record);
            }
        });


      //Fetch additional records from the server as needed
        if (callback && selectAll && !eof){


          //Build URL
            var url = config.url;
            if (url.indexOf("?")==-1) url+= "?";


          //Generate list of fields
            var fieldNames = "";
            if (key){
                fieldNames = config.columns[colID].field;
            }
            else{
                for (var i=0; i<config.fields.length; i++){
                    if (i>0) fieldNames += ",";
                    fieldNames += config.fields[i];
                }
            }


            url += "fields=" + fieldNames + "&count=false&offset=" + (currPage*config.limit);


          //Add query params
            url += encodeParams(config.params);


          //Add filter
            if (filter){
                for (var key in filter) {
                    if (filter.hasOwnProperty(key)) {
                        if (key.toLowerCase()!=='orderby'){
                            var str = filter[key];
                            if (str){
                                str = str.trim();
                                if (str.length>0){
                                    url += "&" + key + "=" + str;
                                }
                            }
                        }
                    }
                }
            }


          //Execute service request and process response
            config.getResponse(url, config.payload, function(request){
                if (request.status===200){
                    var arr = [];
                    var records = config.parseResponse.apply(me, [request]);
                    for (var i=0; i<records.length; i++){
                        var val = records[i][fieldName];
                        if (val) arr.push(val);
                    }
                    if (callback) callback.apply(me, arr);
                }
                else{
                    me.onError(request);
                }
            });
        }

        return arr;
    };


  //**************************************************************************
  //** createHeader
  //**************************************************************************
    var createHeader = function(label, sortable){
        var div = document.createElement("div");
        if (sortable===false){
            div.style.cursor = "default";
        }

        var span = document.createElement("span");
        span.innerHTML = label;
        div.appendChild(span);

        var iconDiv = document.createElement("div");
        iconDiv.style.position = "relative";
        iconDiv.style.display = "inline-block";
        div.appendChild(iconDiv);

        var icon = document.createElement("div");
        iconDiv.appendChild(icon);
        div.sortIndicator = icon;

        div.setSortIndicator = function(sortDirection){
            var className = "";
            if (sortDirection){
                if (typeof sortDirection === "string"){
                    sortDirection = sortDirection.toUpperCase().trim();
                    if (sortDirection=="ASC"){
                        className = config.style.ascendingSortIcon;
                    }
                    else if (sortDirection=="DESC"){
                        className = config.style.descendingSortIcon;
                    }
                }
            }
            this.sortIndicator.className = className;
        };


        return div;
    };


  //**************************************************************************
  //** createCheckbox
  //**************************************************************************
    var createCheckbox = function(value, row){
        var div = document.createElement('div');
        div.style.display = "inline-block";
        div.style.position = "relative";
        //div.style.padding = "9px 0 0 1px";
        var checkbox = new javaxt.dhtml.Checkbox(div,{
            value: value,
            style: config.style.checkbox
        });


        if (typeof value !== 'undefined'){
            checkbox.onClick = function(checked){
                var value = this.getValue();
                me.onCheckbox(value, checked, this);
                if (checked){
                    //table.select(row);
                }

                //me.onSelectionChange();
            };
        }
        else{ //checkbox header
            checkbox.onClick = function(checked){

              /*
              //Method 1: Select/deselect using the table control. This will
              //highlight the rows and the onSelectionChange listener will
              //update the checkboxes.
                if (checked) {
                    table.selectAll();
                }
                else{
                    table.deselectAll();
                }
                */


              //Method 2: Select/deselect manually. No rows will highlight.
                table.forEachRow(function (row, content) {

                    var checkboxDiv = content[checkboxHeader.idx];
                    var checkbox = checkboxDiv.checkbox;
                    if (checked) {
                        checkbox.select();
                    }
                    else{
                        checkbox.deselect();
                    }

                });

                //me.onSelectionChange();
            };
        }


        div.checkbox = checkbox;
        return div;
    };


  //**************************************************************************
  //** load
  //**************************************************************************
    var load = function(page, callback){

        if (pageRequests[page+""]) return;
        if (loading) return;
        loading = true;

        mask.show();
        pageRequests[page+""] = page;


      //Parse page
        if (page){
            page = parseInt(page);
            if (isNaN(page) || page<1) page = 1;
        }
        else{
            page = 1;
        }
        if (page==1) eof = false;


      //Generate list of fields
        var fieldNames = "";
        for (var i=0; i<config.fields.length; i++){
            if (i>0) fieldNames += ",";
            fieldNames += config.fields[i];
        }


      //Fire "beforeLoad" event
        me.beforeLoad(page);



      //Create params for the querystring
        var params = {
            page: page,
            limit: config.limit,
            fields: fieldNames
        };


      //Add config.params to the querystring as needed
        if (config.params){
            for (var key in config.params) {
                if (config.params.hasOwnProperty(key)) {
                    if (!hasParam(key, params)){
                        if (key.toLowerCase()!=='orderby'){
                            params[key] = config.params[key];
                        }
                    }
                }
            }
        }


      //Add count to the querystring
        if (config.count==true && page==1) params.count = true;
        else params.count = false;


      //Add filter to the querystring
        var orderby = "";
        if (filter){
            for (var key in filter) {
                if (filter.hasOwnProperty(key)) {
                    if (key.toLowerCase()==='orderby'){
                        if (!hasParam(key, params)){
                            orderby = filter[key];
                            orderby = ((orderby!=null && orderby!="") ? "&orderby=" + encodeURIComponent(orderby) : "");
                        }
                    }
                    else{
                        var str = filter[key];
                        str = str+"";
                        str = str.trim();
                        if (str.length>0){
                            if (!hasParam(key, params)){
                                params[key] = str;
                                //url += "&" + key + "=" + encodeURIComponent(str);
                            }
                        }
                    }
                }
            }
        }


      //Get orderby from config.params only if it is not found in the filter
        if (orderby.length===0){
            if (config.params){
                for (var key in config.params) {
                    if (config.params.hasOwnProperty(key)) {
                        if (key.toLowerCase()==='orderby'){
                            orderby = config.params[key];
                            orderby = ((orderby!=null && orderby!="") ? "&orderby=" + encodeURIComponent(orderby) : "");
                        }
                    }
                }
            }
        }



      //Build URL
        var url = config.url;
        if (url.indexOf("?")==-1) url+= "?";
        url += encodeParams(params);
        url += orderby;



      //Execute service request and process response
        config.getResponse(url, config.payload, function(request){
            if (request.status===200){


              //Parse response
                var records = config.parseResponse.apply(me, [request]);
                if (records.length===0){
                    //table.clear();
                    eof = true;
                }
                else{
                    if (records.length<config.limit) eof = true;

                    table.load(records, page>1);
                    setPage(page);
                    calculateRowHeight();
                }

                if (callback) callback.apply(me, []);
                loading = false;
                mask.hide();
                me.onLoad();
            }
            else{
                loading = false;
                mask.hide();
                me.onError(request);
            }
        });
    };


  //**************************************************************************
  //** hasParam
  //**************************************************************************
  /** Performs case-insensitve search for a parameter. Returns true if a
   *  parameter exists for a given key
   */
    var hasParam = function(key, params){
        for (var k in params) {
            if (params.hasOwnProperty(k)) {
                if (k.toLowerCase()===key.toLowerCase()){
                    return true;
                }
            }
        }
        return false;
    };


  //**************************************************************************
  //** encodeParams
  //**************************************************************************
    var encodeParams = function(params){
        var url = "";
        if (params){
            for (var key in params) {
                if (params.hasOwnProperty(key)) {
                    var val = params[key];
                    if (isArray(val)){
                        for (var i=0; i<val.length; i++){
                            var v = val[i];
                            if (typeof v === "string") v = v.trim();
                            url += "&" + key + "=" + encodeURIComponent(v);
                        }
                    }
                    else{
                        if (typeof val === "string") val = val.trim();
                        url += "&" + key + "=" + encodeURIComponent(val);
                    }
                }
            }
        }
        return url;
    };


  //**************************************************************************
  //** removeDuplicateParams
  //**************************************************************************
  /** Performs a case-insensitve search for unique keys and removes duplicates
   *  (e.g OrderBy == orderBy == orderby). Alters the given parameters instead
   *  or returning a new parameter map..
   */
    var removeDuplicateParams = function(params){

        var uniqueProperties = {};
        for (var key in params) {
            if (params.hasOwnProperty(key)) {
                if (!hasParam(key, uniqueProperties)){
                    uniqueProperties[key] = params[key];
                }
            }
        }

        var deletions = [];
        for (var key in params) {
            if (params.hasOwnProperty(key)) {
                if (!uniqueProperties.hasOwnProperty(key)){
                    deletions.push(key);
                }
            }
        }

        for (var i=0; i<deletions.length; i++){
            var key = deletions[i];
            delete params[key];
        }
    };


  //**************************************************************************
  //** setSortIndicator
  //**************************************************************************
    this.setSortIndicator = function(idx, sortDirection){
        for (var i=0; i<columns.length; i++){
            var colHeader = columns[i].header;
            if (colHeader.setSortIndicator){
                if (i===idx){
                    colHeader.setSortIndicator(sortDirection);
                    columns[i].sort = sortDirection;
                }
                else{
                    colHeader.setSortIndicator(null);
                    columns[i].sort = null;
                }
            }
        }
    };


  //**************************************************************************
  //** sort
  //**************************************************************************
  /** Called whenever a client clicks on a header.
   */
    var sort = function(idx, colConfig, cell, event){
        if (colConfig.sortable!==true) return;

      //Get sort direction
        var sort = colConfig.sort;
        if (sort=="DESC") sort = "";
        else sort = "DESC";
        colConfig.sort = sort;


      //Update sort indicator
        var sortDirection = sort.length==0 ? "ASC" : "DESC";
        me.setSortIndicator(idx, sortDirection);


      //Sort records
        if (config.localSort){

            if (typeof config.localSort === "function"){
                //TODO: allow users to implement thier own sorting function
            }


            var arr = [];
            var rows = [];

          //Collect column content to sort
            table.forEachRow(function (row) {

                var content = row.get(idx);
                var data = row.record[idx];
                if (content){
                    if (content.nodeType===1) content = content.innerText;
                    content = content.trim();
                }
                else{
                    content = "";
                }
                arr.push(content);
                row.sortKey = content;
                rows.push(row);
            });

            if (rows.length === 0) return;


          //Analyze and update the sort keys
            var dates = 0;
            var numbers = 0;
            var nulls = [];
            for (var i=0; i<arr.length; i++){
                var val = arr[i];

                var f = parseFloat(val.replace(/[^0-9\.]+/g,""));
                if (!isNaN(f)){
                    numbers++;
                }
                else{

                    if (typeof val === "string"){
                        if (val=="" || val=="-" || val=="n/a"){
                            nulls.push(i);
                        }
                        else{
                            var d = new Date(val).getTime();
                            if (!isNaN(d)){
                                dates++;
                            }
                        }
                    }
                }
            }


            var numericSort = false;

          //Sort by numbers if possible
            if ((numbers+nulls.length)===rows.length){ //numeric sort
                numericSort = true;
                for (var i=0; i<arr.length; i++){
                    var isNull = false;
                    for (var j=0; j<nulls.length; nulls++){
                        if (nulls[j]===i){
                            isNull = true;
                            break;
                        }
                    }

                    var val = arr[i];
                    var f = isNull ? 0 : parseFloat(val.replace(/[^0-9\.]+/g,""));
                    arr[i] = f;
                    rows[i].sortKey = f;
                }
            }

          //Sort by dates if possible
            if ((dates+nulls.length)===rows.length){
                numericSort = true;
                for (var i=0; i<arr.length; i++){
                    var isNull = false;
                    for (var j=0; j<nulls.length; nulls++){
                        if (nulls[j]===i){
                            isNull = true;
                            break;
                        }
                    }

                    var val = arr[i];
                    var f = isNull ? 0 : new Date(val).getTime();
                    arr[i] = f;
                    rows[i].sortKey = f;
                }
            }


          //Sort the values
            if (numericSort){
                if (sort == "DESC"){
                    arr.sort(function(a, b){return b - a;});
                }
                else{
                    arr.sort(function(a, b){return a - b;});
                }
            }
            else{
                arr.sort();
                if (sort == "DESC"){
                    arr.reverse();
                }
            }


          //Remove unsorted rows
            var parent = rows[0].parentNode;
            for (var i=0; i<rows.length; i++){
                parent.removeChild(rows[i]);
            }


          //Insert sorted rows
            for (var i=0; i<arr.length; i++){
                var key = arr[i];
                for (var j=0; j<rows.length; j++){
                    var row = rows[j];
                    if (row.sortKey == key){
                        row.sortKey = null;
                        parent.appendChild(row);
                        rows.splice(j, 1);
                        break;
                    }
                }
            }


          //Insert unsorted rows
            for (var i=0; i<rows.length; i++){
                parent.appendChild(rows[i]);
            }


          //Fire onSort event
            me.onSort(idx, sortDirection);

        }
        else{
            if (colConfig.field!=null){
                me.clear();
                if (!filter) filter = {};
                filter.orderby = (colConfig.field + " " + sort).trim();
                me.onSort(idx, sortDirection);
                load();
            }
        }

    };

  //**************************************************************************
  //** wrap
  //**************************************************************************
  /** Used to wrap an html element or plain text string in a div. If the width
   *  if the given object exceeds the available width in a column, the content
   *  will be partially hidden from view. When a client hovers over the cell,
   *  the full content will become visible.
   */
    var wrap = function(obj){
        if (obj==null) return;
        var div = document.createElement("div");
        div.style.height = "100%";
        var maxWidth = 300;

        var content;
        if (typeof obj === "string"){
            content = document.createElement("span");
            content.innerHTML = obj;
        }
        else{
            content = document.createElement("div");
            content.style.display = "inline-block";
            content.appendChild(obj);
        }
        div.onmousemove = function(){
            var content = this.firstChild;
            var parent = this.parentNode;
            if (content.offsetWidth>parent.offsetWidth){

              //Update the height of the content
                if (content.offsetWidth>maxWidth){
                    content.style.width = maxWidth+ "px";
                    content.style.minHeight = "115px";
                    content.style.maxHeight = maxWidth+ "px";
                    content.style.whiteSpace = "normal";
                    content.style.lineHeight = "18px";
                    content.style.display = "block";
                    content.style.overflowY = "auto";
                    content.style.overflowX = "hidden";
                }


                parent.className = "grid-overflow-popup";
                parent.style.width = "";
                parent.style.height = "";
                parent.onmouseout = function(e){

                    var el = e.toElement || e.relatedTarget;
                    if (el.parentNode == this || el == this) {
                        return;
                    }
                    while (el.parentNode){
                        if (el.parentNode == this.firstChild) return;
                        el = el.parentNode;
                    }

                    content.style = "";
                    content.style.display = "inline-block";
                    this.className = "";
                    this.style.width = "100%";
                    this.style.height = "100%";
                };
            }

        };

        div.appendChild(content);
        return div;
    };


  //**************************************************************************
  //** Utils
  //**************************************************************************
    var merge = javaxt.dhtml.utils.merge;
    var _getRect = javaxt.dhtml.utils.getRect;
    var isArray = javaxt.dhtml.utils.isArray;
    var isNumber = javaxt.dhtml.utils.isNumber;
    var isElement = javaxt.dhtml.utils.isElement;

    init();
};