    /**
     * @class YAHOO.ext.grid.LoadableDataModel
     * This class extends DefaultDataModel and adds the core functionality to load data remotely. Generally you will want to use one of it's subclasses.<br><br>
     * @extends YAHOO.ext.grid.DefaultDataModel
     * @constructor
     * @param {String} dataType YAHOO.ext.grid.LoadableDataModel.XML, YAHOO.ext.grid.LoadableDataModel.TEXT or YAHOO.ext.grid.JSON
    */
    YAHOO.ext.grid.LoadableDataModel = function(dataType){
        YAHOO.ext.grid.LoadableDataModel.superclass.constructor.call(this, []);

        /** Fires when a successful load is completed - fireDirect sig: (this)
         * @type YAHOO.util.CustomEvent
         * @deprecated Use addListener instead of accessing directly
         * @private
         */
        this.onLoad = new YAHOO.util.CustomEvent('load');
        /** Fires when a load fails - fireDirect sig: (this, errorMsg, responseObj)
         * @type YAHOO.util.CustomEvent
         * @deprecated Use addListener instead of accessing directly
         * @private
         */
        this.onLoadException = new YAHOO.util.CustomEvent('loadException');
        /**
           * @event load
           * Fires when new data has successfully been loaded
           * @param {DataModel} this
           */
        this.events['load'] = this.onLoad;
        /**
           * @event beforeload
           * Fires before a load takes place
           * @param {DataModel} this
           */
        this.events['beforeload'] = new YAHOO.util.CustomEvent('beforeload');
        /**
           * @event loadexception
           * Fires when there's an error loading data
           * @param {DataModel} this
           * @param {Exception} e The exception object or null
           * @param {Object} response The Connect response object
           */
        this.events['loadexception'] = this.onLoadException;

        /**@private*/
        this.dataType = dataType;
        /**@private*/
        this.preprocessors = [];
        /**@private*/
        this.postprocessors = [];

        // paging info
        /** The active page @type Number*/
        this.loadedPage = 1;
        /** True to use remote sorting, initPaging automatically sets this to true @type Boolean */
        this.remoteSort = false;
        /** The number of records per page @type Number*/
        this.pageSize = 0;
        /** The script/page to call to provide paged/sorted data @type String*/
        this.pageUrl = null;
        /** An object of key/value pairs to be passed as parameters
         * when loading pages/sorting @type Object*/
        this.baseParams = {};
        /** Maps named params to url parameters - Override to specify your own param names */
        this.paramMap = {'page':'page', 'pageSize':'pageSize', 'sortColumn':'sortColumn', 'sortDir':'sortDir'};

    };
    YAHOO.extendX(YAHOO.ext.grid.LoadableDataModel, YAHOO.ext.grid.DefaultDataModel, {

        /** @ignore */
        setLoadedPage: function(pageNum, userCallback){
            this.loadedPage = pageNum;
            if(typeof userCallback == 'function'){
                userCallback();
            }
        },

        /** Returns true if this model uses paging @return Boolean */
        isPaged: function(){
            return this.pageSize > 0;
        },

        /** Returns the total number of records available, override if needed @return {Number} */
        getTotalRowCount: function(){
            return this.totalCount || this.getRowCount();
        },

        /** Returns the number of records per page @return Number */
        getPageSize: function(){
            return this.pageSize;
        },

        /** Returns the total number of pages available @return Number */
        getTotalPages: function(){
            if(this.getPageSize() == 0 || this.getTotalRowCount() == 0){
                return 1;
            }
            return Math.ceil(this.getTotalRowCount()/this.getPageSize());
        },

        /** Initializes paging for this model.
         * @param {String} url
         * @param {Number} pageSize
         * @param {Object} baseParams (optional) Object containing key/value pairs to add to all requests
         */
        initPaging: function(url, pageSize, baseParams){
            this.pageUrl = url;
            this.pageSize = pageSize;
            this.remoteSort = true;
            if(baseParams) this.baseParams = baseParams;
        },

        /** @ignore */
        createParams: function(pageNum, sortColumn, sortDir){
            var params = {}, map = this.paramMap;
            for(var key in this.baseParams){
                if(typeof this.baseParams[key] != 'function'){
                    params[key] = this.baseParams[key];
                }
            }
            params[map['page']] = pageNum;
            params[map['pageSize']] = this.getPageSize();
            params[map['sortColumn']] = (typeof sortColumn == 'undefined' ? '' : sortColumn);
            params[map['sortDir']] = sortDir || '';
            return params;
        },

        /**
         * Loads a page of data.
         * @param {Number} pageNum Which page to load. The first page is 1.
         * @param {Function} callback (optional) Optional callback when loading is complete
         * @param {Boolean} keepExisting (optional) true to keep existing data and append the new data
         */
        loadPage: function(pageNum, callback, keepExisting){
            var sort = this.getSortState();
            var params = this.createParams(pageNum, sort.column, sort.direction);
            this.load(this.pageUrl, params, this.setLoadedPage.createDelegate(this, [pageNum, callback]),
                       keepExisting ? (pageNum-1) * this.pageSize : null);
        },

        /** @ignore */
        applySort: function(suppressEvent){
        	if(!this.remoteSort){
                YAHOO.ext.grid.LoadableDataModel.superclass.applySort.apply(this, arguments);
            }else if(!suppressEvent){
                var sort = this.getSortState();
                if(sort.column){
                   this.fireRowsSorted(sort.column, sort.direction, true);
                }
            }
        },

        /** @ignore */
        resetPaging: function(){
        	this.loadedPage = 1;
        },

        /* Overridden sort method to use remote sorting if turned on */
        sort: function(sortInfo, columnIndex, direction, suppressEvent){
            if(!this.remoteSort){
                YAHOO.ext.grid.LoadableDataModel.superclass.sort.apply(this, arguments);
            }else{
                this.sortInfo = sortInfo;
                this.sortColumn = columnIndex;
                this.sortDir = direction;
                var params = this.createParams(this.loadedPage, columnIndex, direction);
                this.load(this.pageUrl, params, this.fireRowsSorted.createDelegate(this, [columnIndex, direction, true]));
            }
        },

        /**
         * Initiates the loading of the data from the specified URL - Failed load attempts will
         * fire the {@link #loadexception} event.
         * @param {Object/String} url The url from which the data can be loaded
         * @param {<i>String/Object</i>} params (optional) The parameters to pass as either a url encoded string "param1=1&amp;param2=2" or as an object {param1: 1, param2: 2}
         * @param {<i>Function</i>} callback (optional) Callback when load is complete - called with signature (this, rowCountLoaded)
         * @param {<i>Number</i>} insertIndex (optional) if present, loaded data is inserted at the specified index instead of overwriting existing data
         */
        load: function(url, params, callback, insertIndex){
        	this.fireEvent('beforeload', this);
        	if(params && typeof params != 'string'){ // must be object
                var buf = [];
                for(var key in params){
                    if(typeof params[key] != 'function'){
                        buf.push(encodeURIComponent(key), '=', encodeURIComponent(params[key]), '&');
                    }
                }
                delete buf[buf.length-1];
                params = buf.join('');
            }
            var cb = {
                success: this.processResponse,
                failure: this.processException,
                scope: this,
        		argument: {callback: callback, insertIndex: insertIndex}
            };
            var method = params ? 'POST' : 'GET';
            YAHOO.util.Connect.asyncRequest(method, url, cb, params);
        },

        /**@private*/
        processResponse: function(response){
            var cb = response.argument.callback;
            var keepExisting = (typeof response.argument.insertIndex == 'number');
            var insertIndex = response.argument.insertIndex;
            switch(this.dataType){
            	case YAHOO.ext.grid.LoadableDataModel.XML:
            		this.loadData(response.responseXML, cb, keepExisting, insertIndex);
            	break;
            	case YAHOO.ext.grid.LoadableDataModel.JSON:
            		var rtext = response.responseText;
            		try { // this code is a modified version of Yahoo! UI DataSource JSON parsing
        		        // Trim leading spaces
        		        while(rtext.substring(0,1) == " ") {
        		            rtext = rtext.substring(1, rtext.length);
        		        }
        		        // Invalid JSON response
        		        if(rtext.indexOf("{") < 0) {
        		            throw "Invalid JSON response";
        		        }

        		        // Empty (but not invalid) JSON response
        		        if(rtext.indexOf("{}") === 0) {
        		            this.loadData({}, response.argument.callback);
        		            return;
        		        }

        		        // Turn the string into an object literal...
        		        // ...eval is necessary here
        		        var jsonObjRaw = eval("(" + rtext + ")");
        		        if(!jsonObjRaw) {
        		            throw "Error evaling JSON response";
        		        }
        				this.loadData(jsonObjRaw, cb, keepExisting, insertIndex);
        		    } catch(e) {
        		        this.fireLoadException(e, response);
        				if(typeof callback == 'function'){
        			    	callback(this, false);
        			    }
        		   	}
            	break;
            	case YAHOO.ext.grid.LoadableDataModel.TEXT:
            		this.loadData(response.responseText, cb, keepExisting, insertIndex);
            	break;
            };
        },

        /**@private*/
        processException: function(response){
            this.fireLoadException(null, response);
            if(typeof response.argument.callback == 'function'){
                response.argument.callback(this, false);
            }
        },

        fireLoadException: function(e, responseObj){
            this.onLoadException.fireDirect(this, e, responseObj);
        },

        fireLoadEvent: function(){
            this.fireEvent('load', this.loadedPage, this.getTotalPages());
        },

        /**
         * Adds a preprocessor function to parse data before it is added to the Model - ie. Date.parse to parse dates.
         * @param {Number} columnIndex
         * @param {Function} fn
         */
        addPreprocessor: function(columnIndex, fn){
            this.preprocessors[columnIndex] = fn;
        },

        /**
         * Gets the preprocessor function for the specified column.
         * @param {Number} columnIndex
         * @return {Function}
         */
        getPreprocessor: function(columnIndex){
            return this.preprocessors[columnIndex];
        },

        /**
         * Removes a preprocessor function.
         * @param {Number} columnIndex
         */
        removePreprocessor: function(columnIndex){
            this.preprocessors[columnIndex] = null;
        },

        /**
         * Adds a postprocessor function to format data before updating the underlying data source (ie. convert date to string before updating XML document).
         * @param {Number} columnIndex
         * @param {Function} fn
         */
        addPostprocessor: function(columnIndex, fn){
            this.postprocessors[columnIndex] = fn;
        },

        /**
         * Gets the postprocessor function for the specified column.
         * @param {Number} columnIndex
         * @return {Function}
         */
        getPostprocessor: function(columnIndex){
            return this.postprocessors[columnIndex];
        },

        /**
         * Removes a postprocessor function.
         * @param {Number} columnIndex
         */
        removePostprocessor: function(columnIndex){
            this.postprocessors[columnIndex] = null;
        },
        /**
         * Empty interface method - Called to process the data returned by the XHR - Classes which extend LoadableDataModel should implement this method.
         * See {@link YAHOO.ext.XMLDataModel} for an example implementation.
         */
        loadData: function(data, callback, keepExisting, insertIndex){

        }
    });

    YAHOO.ext.grid.LoadableDataModel.XML = 'xml';
    YAHOO.ext.grid.LoadableDataModel.JSON = 'json';
    YAHOO.ext.grid.LoadableDataModel.TEXT = 'text';

Copyright © 2006 Jack Slocum. All rights reserved.