# Yahoo! UI Library

### AutoComplete Widget

Yahoo! UI Library > autocomplete > DataSource.js (source view)

    /****************************************************************************/
    /****************************************************************************/
    /****************************************************************************/

    /**
     * The DataSource classes manages sending a request and returning response from a live
     * database. Supported data include local JavaScript arrays and objects and databases
     * accessible via XHR connections. Supported response formats include JavaScript arrays,
     * JSON, XML, and flat-file textual data.
     *
     * @class DataSource
     * @constructor
     */
    YAHOO.widget.DataSource = function() {
        /* abstract class */
    };

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public constants
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Error message for null data responses.
     *
     * @property ERROR_DATANULL
     * @type String
     * @static
     * @final
     */
    YAHOO.widget.DataSource.ERROR_DATANULL = "Response data was null";

    /**
     * Error message for data responses with parsing errors.
     *
     * @property ERROR_DATAPARSE
     * @type String
     * @static
     * @final
     */
    YAHOO.widget.DataSource.ERROR_DATAPARSE = "Response data could not be parsed";

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public member variables
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Max size of the local cache.  Set to 0 to turn off caching.  Caching is
     * useful to reduce the number of server connections.  Recommended only for data
     * sources that return comprehensive results for queries or when stale data is
     * not an issue.
     *
     * @property maxCacheEntries
     * @type Number
     * @default 15
     */
    YAHOO.widget.DataSource.prototype.maxCacheEntries = 15;

    /**
     * Use this to equate cache matching with the type of matching done by your live
     * data source. If caching is on and queryMatchContains is true, the cache
     * returns results that "contain" the query string. By default,
     * queryMatchContains is set to false, meaning the cache only returns results
     * that "start with" the query string.
     *
     * @property queryMatchContains
     * @type Boolean
     * @default false
     */
    YAHOO.widget.DataSource.prototype.queryMatchContains = false;

    /**
     * Enables query subset matching. If caching is on and queryMatchSubset is
     * true, substrings of queries will return matching cached results. For
     * instance, if the first query is for "abc" susequent queries that start with
     * "abc", like "abcd", will be queried against the cache, and not the live data
     * source. Recommended only for DataSources that return comprehensive results
     * for queries with very few characters.
     *
     * @property queryMatchSubset
     * @type Boolean
     * @default false
     *
     */
    YAHOO.widget.DataSource.prototype.queryMatchSubset = false;

    /**
     * Enables query case-sensitivity matching. If caching is on and
     * queryMatchCase is true, queries will only return results for case-sensitive
     * matches.
     *
     * @property queryMatchCase
     * @type Boolean
     * @default false
     */
    YAHOO.widget.DataSource.prototype.queryMatchCase = false;

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    /////////////////////////////////////////////////////////////////////////////

     /**
     * Public accessor to the unique name of the DataSource instance.
     *
     * @method toString
     * @return {String} Unique name of the DataSource instance
     */
    YAHOO.widget.DataSource.prototype.toString = function() {
        return "DataSource " + this._sName;
    };

    /**
     * Retrieves query results, first checking the local cache, then making the
     * query request to the live data source as defined by the function doQuery.
     *
     * @method getResults
     * @param oCallbackFn {HTMLFunction} Callback function defined by oParent object to which to return results.
     * @param sQuery {String} Query string.
     * @param oParent {Object} The object instance that has requested data.
     */
    YAHOO.widget.DataSource.prototype.getResults = function(oCallbackFn, sQuery, oParent) {

        // First look in cache
        var aResults = this._doQueryCache(oCallbackFn,sQuery,oParent);

        // Not in cache, so get results from server
        if(aResults.length === 0) {
            this.queryEvent.fire(this, oParent, sQuery);
            this.doQuery(oCallbackFn, sQuery, oParent);
        }
    };

    /**
     * Abstract method implemented by subclasses to make a query to the live data
     * source. Must call the callback function with the response returned from the
     * query. Populates cache (if enabled).
     *
     * @method doQuery
     * @param oCallbackFn {HTMLFunction} Callback function implemented by oParent to which to return results.
     * @param sQuery {String} Query string.
     * @param oParent {Object} The object instance that has requested data.
     */
    YAHOO.widget.DataSource.prototype.doQuery = function(oCallbackFn, sQuery, oParent) {
        /* override this */
    };

    /**
     * Flushes cache.
     *
     * @method flushCache
     */
    YAHOO.widget.DataSource.prototype.flushCache = function() {
        if(this._aCache) {
            this._aCache = [];
        }
        if(this._aCacheHelper) {
            this._aCacheHelper = [];
        }
        this.cacheFlushEvent.fire(this);
    };

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public events
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Fired when a query is made to the live data source.
     *
     * @event queryEvent
     * @param oSelf {Object} The DataSource instance.
     * @param oParent {Object} The requesting object.
     * @param sQuery {String} The query string.
     */
    YAHOO.widget.DataSource.prototype.queryEvent = null;

    /**
     * Fired when a query is made to the local cache.
     *
     * @event cacheQueryEvent
     * @param oSelf {Object} The DataSource instance.
     * @param oParent {Object} The requesting object.
     * @param sQuery {String} The query string.
     */
    YAHOO.widget.DataSource.prototype.cacheQueryEvent = null;

    /**
     * Fired when data is retrieved from the live data source.
     *
     * @event getResultsEvent
     * @param oSelf {Object} The DataSource instance.
     * @param oParent {Object} The requesting object.
     * @param sQuery {String} The query string.
     * @param aResults {Object[]} Array of result objects.
     */
    YAHOO.widget.DataSource.prototype.getResultsEvent = null;

    /**
     * Fired when data is retrieved from the local cache.
     *
     * @event getCachedResultsEvent
     * @param oSelf {Object} The DataSource instance.
     * @param oParent {Object} The requesting object.
     * @param sQuery {String} The query string.
     * @param aResults {Object[]} Array of result objects.
     */
    YAHOO.widget.DataSource.prototype.getCachedResultsEvent = null;

    /**
     * Fired when an error is encountered with the live data source.
     *
     * @event dataErrorEvent
     * @param oSelf {Object} The DataSource instance.
     * @param oParent {Object} The requesting object.
     * @param sQuery {String} The query string.
     * @param sMsg {String} Error message string
     */
    YAHOO.widget.DataSource.prototype.dataErrorEvent = null;

    /**
     * Fired when the local cache is flushed.
     *
     * @event cacheFlushEvent
     * @param oSelf {Object} The DataSource instance
     */
    YAHOO.widget.DataSource.prototype.cacheFlushEvent = null;

    /////////////////////////////////////////////////////////////////////////////
    //
    // Private member variables
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Internal class variable to index multiple DataSource instances.
     *
     * @property _nIndex
     * @type Number
     * @private
     * @static
     */
    YAHOO.widget.DataSource._nIndex = 0;

    /**
     * Name of DataSource instance.
     *
     * @property _sName
     * @type String
     * @private
     */
    YAHOO.widget.DataSource.prototype._sName = null;

    /**
     * Local cache of data result objects indexed chronologically.
     *
     * @property _aCache
     * @type Object[]
     * @private
     */
    YAHOO.widget.DataSource.prototype._aCache = null;

    /////////////////////////////////////////////////////////////////////////////
    //
    // Private methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Initializes DataSource instance.
     *
     * @method _init
     * @private
     */
    YAHOO.widget.DataSource.prototype._init = function() {
        // Validate and initialize public configs
        var maxCacheEntries = this.maxCacheEntries;
        if(isNaN(maxCacheEntries) || (maxCacheEntries < 0)) {
            maxCacheEntries = 0;
        }
        // Initialize local cache
        if(maxCacheEntries > 0 && !this._aCache) {
            this._aCache = [];
        }

        this._sName = "instance" + YAHOO.widget.DataSource._nIndex;
        YAHOO.widget.DataSource._nIndex++;

        this.queryEvent = new YAHOO.util.CustomEvent("query", this);
        this.cacheQueryEvent = new YAHOO.util.CustomEvent("cacheQuery", this);
        this.getResultsEvent = new YAHOO.util.CustomEvent("getResults", this);
        this.getCachedResultsEvent = new YAHOO.util.CustomEvent("getCachedResults", this);
        this.dataErrorEvent = new YAHOO.util.CustomEvent("dataError", this);
        this.cacheFlushEvent = new YAHOO.util.CustomEvent("cacheFlush", this);
    };

    /**
     * Adds a result object to the local cache, evicting the oldest element if the
     * cache is full. Newer items will have higher indexes, the oldest item will have
     * index of 0.
     *
     * @method _addCacheElem
     * @param oResult {Object} Data result object, including array of results.
     * @private
     */
    YAHOO.widget.DataSource.prototype._addCacheElem = function(oResult) {
        var aCache = this._aCache;
        // Don't add if anything important is missing.
        if(!aCache || !oResult || !oResult.query || !oResult.results) {
            return;
        }

        // If the cache is full, make room by removing from index=0
        if(aCache.length >= this.maxCacheEntries) {
            aCache.shift();
        }

        // Add to cache, at the end of the array
        aCache.push(oResult);
    };

    /**
     * Queries the local cache for results. If query has been cached, the callback
     * function is called with the results, and the cached is refreshed so that it
     * is now the newest element.
     *
     * @method _doQueryCache
     * @param oCallbackFn {HTMLFunction} Callback function defined by oParent object to which to return results.
     * @param sQuery {String} Query string.
     * @param oParent {Object} The object instance that has requested data.
     * @return aResults {Object[]} Array of results from local cache if found, otherwise null.
     * @private
     */
    YAHOO.widget.DataSource.prototype._doQueryCache = function(oCallbackFn, sQuery, oParent) {
        var aResults = [];
        var bMatchFound = false;
        var aCache = this._aCache;
        var nCacheLength = (aCache) ? aCache.length : 0;
        var bMatchContains = this.queryMatchContains;

        // If cache is enabled...
        if((this.maxCacheEntries > 0) && aCache && (nCacheLength > 0)) {
            this.cacheQueryEvent.fire(this, oParent, sQuery);
            // If case is unimportant, normalize query now instead of in loops
            if(!this.queryMatchCase) {
                var sOrigQuery = sQuery;
                sQuery = sQuery.toLowerCase();
            }

            // Loop through each cached element's query property...
            for(var i = nCacheLength-1; i >= 0; i--) {
                var resultObj = aCache[i];
                var aAllResultItems = resultObj.results;
                // If case is unimportant, normalize match key for comparison
                var matchKey = (!this.queryMatchCase) ?
                    encodeURIComponent(resultObj.query).toLowerCase():
                    encodeURIComponent(resultObj.query);

                // If a cached match key exactly matches the query...
                if(matchKey == sQuery) {
                        // Stash all result objects into aResult[] and stop looping through the cache.
                        bMatchFound = true;
                        aResults = aAllResultItems;

                        // The matching cache element was not the most recent,
                        // so now we need to refresh the cache.
                        if(i != nCacheLength-1) {
                            // Remove element from its original location
                            aCache.splice(i,1);
                            // Add element as newest
                            this._addCacheElem(resultObj);
                        }
                        break;
                }
                // Else if this query is not an exact match and subset matching is enabled...
                else if(this.queryMatchSubset) {
                    // Loop through substrings of each cached element's query property...
                    for(var j = sQuery.length-1; j >= 0 ; j--) {
                        var subQuery = sQuery.substr(0,j);

                        // If a substring of a cached sQuery exactly matches the query...
                        if(matchKey == subQuery) {
                            bMatchFound = true;

                            // Go through each cached result object to match against the query...
                            for(var k = aAllResultItems.length-1; k >= 0; k--) {
                                var aRecord = aAllResultItems[k];
                                var sKeyIndex = (this.queryMatchCase) ?
                                    encodeURIComponent(aRecord[0]).indexOf(sQuery):
                                    encodeURIComponent(aRecord[0]).toLowerCase().indexOf(sQuery);

                                // A STARTSWITH match is when the query is found at the beginning of the key string...
                                if((!bMatchContains && (sKeyIndex === 0)) ||
                                // A CONTAINS match is when the query is found anywhere within the key string...
                                (bMatchContains && (sKeyIndex > -1))) {
                                    // Stash a match into aResults[].
                                    aResults.unshift(aRecord);
                                }
                            }

                            // Add the subset match result set object as the newest element to cache,
                            // and stop looping through the cache.
                            resultObj = {};
                            resultObj.query = sQuery;
                            resultObj.results = aResults;
                            this._addCacheElem(resultObj);
                            break;
                        }
                    }
                    if(bMatchFound) {
                        break;
                    }
                }
            }

            // If there was a match, send along the results.
            if(bMatchFound) {
                this.getCachedResultsEvent.fire(this, oParent, sOrigQuery, aResults);
                oCallbackFn(sOrigQuery, aResults, oParent);
            }
        }
        return aResults;
    };

    /****************************************************************************/
    /****************************************************************************/
    /****************************************************************************/

    /**
     * Implementation of YAHOO.widget.DataSource using XML HTTP requests that return
     * query results.
     *
     * @class DS_XHR
     * @extends YAHOO.widget.DataSource
     * @requires connection
     * @constructor
     * @param sScriptURI {String} Absolute or relative URI to script that returns query
     * results as JSON, XML, or delimited flat-file data.
     * @param aSchema {String[]} Data schema definition of results.
     * @param oConfigs {Object} (optional) Object literal of config params.
     */
    YAHOO.widget.DS_XHR = function(sScriptURI, aSchema, oConfigs) {
        // Set any config params passed in to override defaults
        if(typeof oConfigs == "object") {
            for(var sConfig in oConfigs) {
                this[sConfig] = oConfigs[sConfig];
            }
        }

        // Initialization sequence
        if(!aSchema || (aSchema.constructor != Array)) {
            YAHOO.log("Could not instantiate XHR DataSource due to invalid arguments", "error", this.toString());
            return;
        }
        else {
            this.schema = aSchema;
        }
        this.scriptURI = sScriptURI;
        this._init();
        YAHOO.log("XHR DataSource initialized","info",this.toString());
    };

    YAHOO.widget.DS_XHR.prototype = new YAHOO.widget.DataSource();

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public constants
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * JSON data type.
     *
     * @property TYPE_JSON
     * @type Number
     * @static
     * @final
     */
    YAHOO.widget.DS_XHR.TYPE_JSON = 0;

    /**
     * XML data type.
     *
     * @property TYPE_XML
     * @type Number
     * @static
     * @final
     */
    YAHOO.widget.DS_XHR.TYPE_XML = 1;

    /**
     * Flat-file data type.
     *
     * @property TYPE_FLAT
     * @type Number
     * @static
     * @final
     */
    YAHOO.widget.DS_XHR.TYPE_FLAT = 2;

    /**
     * Error message for XHR failure.
     *
     * @property ERROR_DATAXHR
     * @type String
     * @static
     * @final
     */
    YAHOO.widget.DS_XHR.ERROR_DATAXHR = "XHR response failed";

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public member variables
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Alias to YUI Connection Manager. Allows implementers to specify their own
     * subclasses of the YUI Connection Manager utility.
     *
     * @property connMgr
     * @type Object
     * @default YAHOO.util.Connect
     */
    YAHOO.widget.DS_XHR.prototype.connMgr = YAHOO.util.Connect;

    /**
     * Number of milliseconds the XHR connection will wait for a server response. A
     * a value of zero indicates the XHR connection will wait forever. Any value
     * greater than zero will use the Connection utility's Auto-Abort feature.
     *
     * @property connTimeout
     * @type Number
     * @default 0
     */
    YAHOO.widget.DS_XHR.prototype.connTimeout = 0;

    /**
     * Absolute or relative URI to script that returns query results. For instance,
     * queries will be sent to &#60;scriptURI&#62;?&#60;scriptQueryParam&#62;=userinput
     *
     * @property scriptURI
     * @type String
     */
    YAHOO.widget.DS_XHR.prototype.scriptURI = null;

    /**
     * Query string parameter name sent to scriptURI. For instance, queries will be
     * sent to &#60;scriptURI&#62;?&#60;scriptQueryParam&#62;=userinput
     *
     * @property scriptQueryParam
     * @type String
     * @default "query"
     */
    YAHOO.widget.DS_XHR.prototype.scriptQueryParam = "query";

    /**
     * String of key/value pairs to append to requests made to scriptURI. Define
     * this string when you want to send additional query parameters to your script.
     * When defined, queries will be sent to
     * &#60;scriptURI&#62;?&#60;scriptQueryParam&#62;=userinput&#38;&#60;scriptQueryAppend&#62;
     *
     * @property scriptQueryAppend
     * @type String
     * @default ""
     */
    YAHOO.widget.DS_XHR.prototype.scriptQueryAppend = "";

    /**
     * XHR response data type. Other types that may be defined are YAHOO.widget.DS_XHR.TYPE_XML
     * and YAHOO.widget.DS_XHR.TYPE_FLAT.
     *
     * @property responseType
     * @type String
     * @default YAHOO.widget.DS_XHR.TYPE_JSON
     */
    YAHOO.widget.DS_XHR.prototype.responseType = YAHOO.widget.DS_XHR.TYPE_JSON;

    /**
     * String after which to strip results. If the results from the XHR are sent
     * back as HTML, the gzip HTML comment appears at the end of the data and should
     * be ignored.
     *
     * @property responseStripAfter
     * @type String
     * @default "\n&#60;!-"
     */
    YAHOO.widget.DS_XHR.prototype.responseStripAfter = "\n<!-";

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Queries the live data source defined by scriptURI for results. Results are
     * passed back to a callback function.
     *
     * @method doQuery
     * @param oCallbackFn {HTMLFunction} Callback function defined by oParent object to which to return results.
     * @param sQuery {String} Query string.
     * @param oParent {Object} The object instance that has requested data.
     */
    YAHOO.widget.DS_XHR.prototype.doQuery = function(oCallbackFn, sQuery, oParent) {
        var isXML = (this.responseType == YAHOO.widget.DS_XHR.TYPE_XML);
        var sUri = this.scriptURI+"?"+this.scriptQueryParam+"="+sQuery;
        if(this.scriptQueryAppend.length > 0) {
            sUri += "&" + this.scriptQueryAppend;
        }
        YAHOO.log("DataSource is querying URL " + sUri, "info", this.toString());
        var oResponse = null;

        var oSelf = this;
        /*
         * Sets up ajax request callback
         *
         * @param {object} oReq          HTTPXMLRequest object
         * @private
         */
        var responseSuccess = function(oResp) {
            // Response ID does not match last made request ID.
            if(!oSelf._oConn || (oResp.tId != oSelf._oConn.tId)) {
                oSelf.dataErrorEvent.fire(oSelf, oParent, sQuery, YAHOO.widget.DataSource.ERROR_DATANULL);
                YAHOO.log(YAHOO.widget.DataSource.ERROR_DATANULL, "error", this.toString());
                return;
            }
    //DEBUG
    /*YAHOO.log(oResp.responseXML.getElementsByTagName("Result"),'warn');
    for(var foo in oResp) {
        YAHOO.log(foo + ": "+oResp[foo],'warn');
    }
    YAHOO.log('responseXML.xml: '+oResp.responseXML.xml,'warn');*/
            if(!isXML) {
                oResp = oResp.responseText;
            }
            else {
                oResp = oResp.responseXML;
            }
            if(oResp === null) {
                oSelf.dataErrorEvent.fire(oSelf, oParent, sQuery, YAHOO.widget.DataSource.ERROR_DATANULL);
                YAHOO.log(YAHOO.widget.DataSource.ERROR_DATANULL, "error", oSelf.toString());
                return;
            }

            var aResults = oSelf.parseResponse(sQuery, oResp, oParent);
            var resultObj = {};
            resultObj.query = decodeURIComponent(sQuery);
            resultObj.results = aResults;
            if(aResults === null) {
                oSelf.dataErrorEvent.fire(oSelf, oParent, sQuery, YAHOO.widget.DataSource.ERROR_DATAPARSE);
                YAHOO.log(YAHOO.widget.DataSource.ERROR_DATAPARSE, "error", oSelf.toString());
                aResults = [];
            }
            else {
                oSelf.getResultsEvent.fire(oSelf, oParent, sQuery, aResults);
                oSelf._addCacheElem(resultObj);
            }
            oCallbackFn(sQuery, aResults, oParent);
        };

        var responseFailure = function(oResp) {
            oSelf.dataErrorEvent.fire(oSelf, oParent, sQuery, YAHOO.widget.DS_XHR.ERROR_DATAXHR);
            YAHOO.log(YAHOO.widget.DS_XHR.ERROR_DATAXHR + ": " + oResp.statusText, "error", oSelf.toString());
            return;
        };

        var oCallback = {
            success:responseSuccess,
            failure:responseFailure
        };

        if(!isNaN(this.connTimeout) && this.connTimeout > 0) {
            oCallback.timeout = this.connTimeout;
        }

        if(this._oConn) {
            this.connMgr.abort(this._oConn);
        }

        oSelf._oConn = this.connMgr.asyncRequest("GET", sUri, oCallback, null);
    };

    /**
     * Parses raw response data into an array of result objects. The result data key
     * is always stashed in the [0] element of each result object.
     *
     * @method parseResponse
     * @param sQuery {String} Query string.
     * @param oResponse {Object} The raw response data to parse.
     * @param oParent {Object} The object instance that has requested data.
     * @returns {Object[]} Array of result objects.
     */
    YAHOO.widget.DS_XHR.prototype.parseResponse = function(sQuery, oResponse, oParent) {
        var aSchema = this.schema;
        var aResults = [];
        var bError = false;

        // Strip out comment at the end of results
        var nEnd = ((this.responseStripAfter !== "") && (oResponse.indexOf)) ?
            oResponse.indexOf(this.responseStripAfter) : -1;
        if(nEnd != -1) {
            oResponse = oResponse.substring(0,nEnd);
        }

        switch (this.responseType) {
            case YAHOO.widget.DS_XHR.TYPE_JSON:
                var jsonList;
                // Divert KHTML clients from JSON lib
                if(window.JSON && (navigator.userAgent.toLowerCase().indexOf('khtml')== -1)) {
                    // Use the JSON utility if available
                    var jsonObjParsed = JSON.parse(oResponse);
                    if(!jsonObjParsed) {
                        bError = true;
                        break;
                    }
                    else {
                        try {
                            // eval is necessary here since aSchema[0] is of unknown depth
                            jsonList = eval("jsonObjParsed." + aSchema[0]);
                        }
                        catch(e) {
                            bError = true;
                            break;
                       }
                    }
                }
                else {
                    // Parse the JSON response as a string
                    try {
                        // Trim leading spaces
                        while (oResponse.substring(0,1) == " ") {
                            oResponse = oResponse.substring(1, oResponse.length);
                        }

                        // Invalid JSON response
                        if(oResponse.indexOf("{") < 0) {
                            bError = true;
                            break;
                        }

                        // Empty (but not invalid) JSON response
                        if(oResponse.indexOf("{}") === 0) {
                            break;
                        }

                        // Turn the string into an object literal...
                        // ...eval is necessary here
                        var jsonObjRaw = eval("(" + oResponse + ")");
                        if(!jsonObjRaw) {
                            bError = true;
                            break;
                        }

                        // Grab the object member that contains an array of all reponses...
                        // ...eval is necessary here since aSchema[0] is of unknown depth
                        jsonList = eval("(jsonObjRaw." + aSchema[0]+")");
                    }
                    catch(e) {
                        bError = true;
                        break;
                   }
                }

                if(!jsonList) {
                    bError = true;
                    break;
                }

                if(jsonList.constructor != Array) {
                    jsonList = [jsonList];
                }

                // Loop through the array of all responses...
                for(var i = jsonList.length-1; i >= 0 ; i--) {
                    var aResultItem = [];
                    var jsonResult = jsonList[i];
                    // ...and loop through each data field value of each response
                    for(var j = aSchema.length-1; j >= 1 ; j--) {
                        // ...and capture data into an array mapped according to the schema...
                        var dataFieldValue = jsonResult[aSchema[j]];
                        if(!dataFieldValue) {
                            dataFieldValue = "";
                        }
                        //YAHOO.log("data: " + i + " value:" +j+" = "+dataFieldValue,"debug",this.toString());
                        aResultItem.unshift(dataFieldValue);
                    }
                    // If schema isn't well defined, pass along the entire result object
                    if(aResultItem.length == 1) {
                        aResultItem.push(jsonResult);
                    }
                    // Capture the array of data field values in an array of results
                    aResults.unshift(aResultItem);
                }
                break;
            case YAHOO.widget.DS_XHR.TYPE_XML:
                // Get the collection of results
                var xmlList = oResponse.getElementsByTagName(aSchema[0]);
                if(!xmlList) {
                    bError = true;
                    break;
                }
                // Loop through each result
                for(var k = xmlList.length-1; k >= 0 ; k--) {
                    var result = xmlList.item(k);
                    //YAHOO.log("Result"+k+" is "+result.attributes.item(0).firstChild.nodeValue,"debug",this.toString());
                    var aFieldSet = [];
                    // Loop through each data field in each result using the schema
                    for(var m = aSchema.length-1; m >= 1 ; m--) {
                        //YAHOO.log(aSchema[m]+" is "+result.attributes.getNamedItem(aSchema[m]).firstChild.nodeValue);
                        var sValue = null;
                        // Values may be held in an attribute...
                        var xmlAttr = result.attributes.getNamedItem(aSchema[m]);
                        if(xmlAttr) {
                            sValue = xmlAttr.value;
                            //YAHOO.log("Attr value is "+sValue,"debug",this.toString());
                        }
                        // ...or in a node
                        else{
                            var xmlNode = result.getElementsByTagName(aSchema[m]);
                            if(xmlNode && xmlNode.item(0) && xmlNode.item(0).firstChild) {
                                sValue = xmlNode.item(0).firstChild.nodeValue;
                                //YAHOO.log("Node value is "+sValue,"debug",this.toString());
                            }
                            else {
                                sValue = "";
                                //YAHOO.log("Value not found","debug",this.toString());
                            }
                        }
                        // Capture the schema-mapped data field values into an array
                        aFieldSet.unshift(sValue);
                    }
                    // Capture each array of values into an array of results
                    aResults.unshift(aFieldSet);
                }
                break;
            case YAHOO.widget.DS_XHR.TYPE_FLAT:
                if(oResponse.length > 0) {
                    // Delete the last line delimiter at the end of the data if it exists
                    var newLength = oResponse.length-aSchema[0].length;
                    if(oResponse.substr(newLength) == aSchema[0]) {
                        oResponse = oResponse.substr(0, newLength);
                    }
                    var aRecords = oResponse.split(aSchema[0]);
                    for(var n = aRecords.length-1; n >= 0; n--) {
                        aResults[n] = aRecords[n].split(aSchema[1]);
                    }
                }
                break;
            default:
                break;
        }
        sQuery = null;
        oResponse = null;
        oParent = null;
        if(bError) {
            return null;
        }
        else {
            return aResults;
        }
    };

    /////////////////////////////////////////////////////////////////////////////
    //
    // Private member variables
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * XHR connection object.
     *
     * @property _oConn
     * @type Object
     * @private
     */
    YAHOO.widget.DS_XHR.prototype._oConn = null;

    /****************************************************************************/
    /****************************************************************************/
    /****************************************************************************/

    /**
     * Implementation of YAHOO.widget.DataSource using a native Javascript function as
     * its live data source.
     *
     * @class DS_JSFunction
     * @constructor
     * @extends YAHOO.widget.DataSource
     * @param oFunction {HTMLFunction} In-memory Javascript function that returns query results as an array of objects.
     * @param oConfigs {Object} (optional) Object literal of config params.
     */
    YAHOO.widget.DS_JSFunction = function(oFunction, oConfigs) {
        // Set any config params passed in to override defaults
        if(typeof oConfigs == "object") {
            for(var sConfig in oConfigs) {
                this[sConfig] = oConfigs[sConfig];
            }
        }

        // Initialization sequence
        if(!oFunction  || (oFunction.constructor != Function)) {
            YAHOO.log("Could not instantiate JSFunction DataSource due to invalid arguments", "error", this.toString());
            return;
        }
        else {
            this.dataFunction = oFunction;
            this._init();
            YAHOO.log("JS Function DataSource initialized","info",this.toString());
        }
    };

    YAHOO.widget.DS_JSFunction.prototype = new YAHOO.widget.DataSource();

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public member variables
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * In-memory Javascript function that returns query results.
     *
     * @property dataFunction
     * @type HTMLFunction
     */
    YAHOO.widget.DS_JSFunction.prototype.dataFunction = null;

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Queries the live data source defined by function for results. Results are
     * passed back to a callback function.
     *
     * @method doQuery
     * @param oCallbackFn {HTMLFunction} Callback function defined by oParent object to which to return results.
     * @param sQuery {String} Query string.
     * @param oParent {Object} The object instance that has requested data.
     */
    YAHOO.widget.DS_JSFunction.prototype.doQuery = function(oCallbackFn, sQuery, oParent) {
        var oFunction = this.dataFunction;
        var aResults = [];

        aResults = oFunction(sQuery);
        if(aResults === null) {
            this.dataErrorEvent.fire(this, oParent, sQuery, YAHOO.widget.DataSource.ERROR_DATANULL);
            YAHOO.log(YAHOO.widget.DataSource.ERROR_DATANULL, "error", this.toString());
            return;
        }

        var resultObj = {};
        resultObj.query = decodeURIComponent(sQuery);
        resultObj.results = aResults;
        this._addCacheElem(resultObj);

        this.getResultsEvent.fire(this, oParent, sQuery, aResults);
        oCallbackFn(sQuery, aResults, oParent);
        return;
    };

    /****************************************************************************/
    /****************************************************************************/
    /****************************************************************************/

    /**
     * Implementation of YAHOO.widget.DataSource using a native Javascript array as
     * its live data source.
     *
     * @class DS_JSArray
     * @constructor
     * @extends YAHOO.widget.DataSource
     * @param aData {String[]} In-memory Javascript array of simple string data.
     * @param oConfigs {Object} (optional) Object literal of config params.
     */
    YAHOO.widget.DS_JSArray = function(aData, oConfigs) {
        // Set any config params passed in to override defaults
        if(typeof oConfigs == "object") {
            for(var sConfig in oConfigs) {
                this[sConfig] = oConfigs[sConfig];
            }
        }

        // Initialization sequence
        if(!aData || (aData.constructor != Array)) {
            YAHOO.log("Could not instantiate JSArray DataSource due to invalid arguments", "error", this.toString());
            return;
        }
        else {
            this.data = aData;
            this._init();
            YAHOO.log("JS Array DataSource initialized","info",this.toString());
        }
    };

    YAHOO.widget.DS_JSArray.prototype = new YAHOO.widget.DataSource();

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public member variables
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * In-memory Javascript array of strings.
     *
     * @property data
     * @type Array
     */
    YAHOO.widget.DS_JSArray.prototype.data = null;

    /////////////////////////////////////////////////////////////////////////////
    //
    // Public methods
    //
    /////////////////////////////////////////////////////////////////////////////

    /**
     * Queries the live data source defined by data for results. Results are passed
     * back to a callback function.
     *
     * @method doQuery
     * @param oCallbackFn {HTMLFunction} Callback function defined by oParent object to which to return results.
     * @param sQuery {String} Query string.
     * @param oParent {Object} The object instance that has requested data.
     */
    YAHOO.widget.DS_JSArray.prototype.doQuery = function(oCallbackFn, sQuery, oParent) {
        var aData = this.data; // the array
        var aResults = []; // container for results
        var bMatchFound = false;
        var bMatchContains = this.queryMatchContains;
        if(sQuery) {
            if(!this.queryMatchCase) {
                sQuery = sQuery.toLowerCase();
            }

            // Loop through each element of the array...
            // which can be a string or an array of strings
            for(var i = aData.length-1; i >= 0; i--) {
                var aDataset = [];

                if(aData[i]) {
                    if(aData[i].constructor == String) {
                        aDataset[0] = aData[i];
                    }
                    else if(aData[i].constructor == Array) {
                        aDataset = aData[i];
                    }
                }

                if(aDataset[0] && (aDataset[0].constructor == String)) {
                    var sKeyIndex = (this.queryMatchCase) ?
                    encodeURIComponent(aDataset[0]).indexOf(sQuery):
                    encodeURIComponent(aDataset[0]).toLowerCase().indexOf(sQuery);

                    // A STARTSWITH match is when the query is found at the beginning of the key string...
                    if((!bMatchContains && (sKeyIndex === 0)) ||
                    // A CONTAINS match is when the query is found anywhere within the key string...
                    (bMatchContains && (sKeyIndex > -1))) {
                        // Stash a match into aResults[].
                        aResults.unshift(aDataset);
                    }
                }
            }
        }

        this.getResultsEvent.fire(this, oParent, sQuery, aResults);
        oCallbackFn(sQuery, aResults, oParent);
    };

Copyright © 2006 Yahoo! Inc. All rights reserved.