diff options
Diffstat (limited to 'includes/js/dojox/data/QueryReadStore.js')
-rw-r--r-- | includes/js/dojox/data/QueryReadStore.js | 513 |
1 files changed, 513 insertions, 0 deletions
diff --git a/includes/js/dojox/data/QueryReadStore.js b/includes/js/dojox/data/QueryReadStore.js new file mode 100644 index 0000000..95af166 --- /dev/null +++ b/includes/js/dojox/data/QueryReadStore.js @@ -0,0 +1,513 @@ +if(!dojo._hasResource["dojox.data.QueryReadStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojox.data.QueryReadStore"] = true; +dojo.provide("dojox.data.QueryReadStore"); + +dojo.require("dojo.string"); + +dojo.declare("dojox.data.QueryReadStore", + null, + { + // summary: + // This class provides a store that is mainly intended to be used + // for loading data dynamically from the server, used i.e. for + // retreiving chunks of data from huge data stores on the server (by server-side filtering!). + // Upon calling the fetch() method of this store the data are requested from + // the server if they are not yet loaded for paging (or cached). + // + // For example used for a combobox which works on lots of data. It + // can be used to retreive the data partially upon entering the + // letters "ac" it returns only items like "action", "acting", etc. + // + // note: + // The field name "id" in a query is reserved for looking up data + // by id. This is necessary as before the first fetch, the store + // has no way of knowing which field the server will declare as + // identifier. + // + // examples: + // | // The parameter "query" contains the data that are sent to the server. + // | var store = new dojox.data.QueryReadStore({url:'/search.php'}); + // | store.fetch({query:{name:'a'}, queryOptions:{ignoreCase:false}}); + // + // | // Since "serverQuery" is given, it overrules and those data are + // | // sent to the server. + // | var store = new dojox.data.QueryReadStore({url:'/search.php'}); + // | store.fetch({serverQuery:{name:'a'}, queryOptions:{ignoreCase:false}}); + // + // | <div dojoType="dojox.data.QueryReadStore" + // | jsId="store2" + // | url="../tests/stores/QueryReadStore.php" + // | requestMethod="post"></div> + // | <div dojoType="dojox.grid.data.DojoData" + // | jsId="model2" + // | store="store2" + // | sortFields="[{attribute: 'name', descending: true}]" + // | rowsPerPage="30"></div> + // | <div dojoType="dojox.Grid" id="grid2" + // | model="model2" + // | structure="gridLayout" + // | style="height:300px; width:800px;"></div> + + // + // todo: + // - there is a bug in the paging, when i set start:2, count:5 after an initial fetch() and doClientPaging:true + // it returns 6 elemetns, though count=5, try it in QueryReadStore.html + // - add optional caching + // - when the first query searched for "a" and the next for a subset of + // the first, i.e. "ab" then we actually dont need a server request, if + // we have client paging, we just need to filter the items we already have + // that might also be tooo much logic + + url:"", + requestMethod:"get", + //useCache:false, + + // We use the name in the errors, once the name is fixed hardcode it, may be. + _className:"dojox.data.QueryReadStore", + + // This will contain the items we have loaded from the server. + // The contents of this array is optimized to satisfy all read-api requirements + // and for using lesser storage, so the keys and their content need some explaination: + // this._items[0].i - the item itself + // this._items[0].r - a reference to the store, so we can identify the item + // securly. We set this reference right after receiving the item from the + // server. + _items:[], + + // Store the last query that triggered xhr request to the server. + // So we can compare if the request changed and if we shall reload + // (this also depends on other factors, such as is caching used, etc). + _lastServerQuery:null, + + + // Store a hash of the last server request. Actually I introduced this + // for testing, so I can check if no unnecessary requests were issued for + // client-side-paging. + lastRequestHash:null, + + // summary: + // By default every request for paging is sent to the server. + doClientPaging:false, + + // summary: + // By default all the sorting is done serverside before the data is returned + // which is the proper place to be doing it for really large datasets. + doClientSorting:false, + + // Items by identify for Identify API + _itemsByIdentity:null, + + // Identifier used + _identifier:null, + + _features: {'dojo.data.api.Read':true, 'dojo.data.api.Identity':true}, + + _labelAttr: "label", + + constructor: function(/* Object */ params){ + dojo.mixin(this,params); + }, + + getValue: function(/* item */ item, /* attribute-name-string */ attribute, /* value? */ defaultValue){ + // According to the Read API comments in getValue() and exception is + // thrown when an item is not an item or the attribute not a string! + this._assertIsItem(item); + if (!dojo.isString(attribute)) { + throw new Error(this._className+".getValue(): Invalid attribute, string expected!"); + } + if(!this.hasAttribute(item, attribute)){ + // read api says: return defaultValue "only if *item* does not have a value for *attribute*." + // Is this the case here? The attribute doesn't exist, but a defaultValue, sounds reasonable. + if(defaultValue){ + return defaultValue; + } + console.log(this._className+".getValue(): Item does not have the attribute '"+attribute+"'."); + } + return item.i[attribute]; + }, + + getValues: function(/* item */ item, /* attribute-name-string */ attribute){ + this._assertIsItem(item); + var ret = []; + if(this.hasAttribute(item, attribute)){ + ret.push(item.i[attribute]); + } + return ret; + }, + + getAttributes: function(/* item */ item){ + this._assertIsItem(item); + var ret = []; + for(var i in item.i){ + ret.push(i); + } + return ret; + }, + + hasAttribute: function(/* item */ item, /* attribute-name-string */ attribute) { + // summary: + // See dojo.data.api.Read.hasAttribute() + return this.isItem(item) && typeof item.i[attribute]!="undefined"; + }, + + containsValue: function(/* item */ item, /* attribute-name-string */ attribute, /* anything */ value){ + var values = this.getValues(item, attribute); + var len = values.length; + for(var i=0; i<len; i++){ + if(values[i]==value){ + return true; + } + } + return false; + }, + + isItem: function(/* anything */ something){ + // Some basic tests, that are quick and easy to do here. + // >>> var store = new dojox.data.QueryReadStore({}); + // >>> store.isItem(""); + // false + // + // >>> var store = new dojox.data.QueryReadStore({}); + // >>> store.isItem({}); + // false + // + // >>> var store = new dojox.data.QueryReadStore({}); + // >>> store.isItem(0); + // false + // + // >>> var store = new dojox.data.QueryReadStore({}); + // >>> store.isItem({name:"me", label:"me too"}); + // false + // + if(something){ + return typeof something.r!="undefined" && something.r==this; + } + return false; + }, + + isItemLoaded: function(/* anything */ something) { + // Currently we dont have any state that tells if an item is loaded or not + // if the item exists its also loaded. + // This might change when we start working with refs inside items ... + return this.isItem(something); + }, + + loadItem: function(/* object */ args){ + if(this.isItemLoaded(args.item)){ + return; + } + // Actually we have nothing to do here, or at least I dont know what to do here ... + }, + + fetch:function(/* Object? */ request){ + // summary: + // See dojo.data.util.simpleFetch.fetch() this is just a copy and I adjusted + // only the paging, since it happens on the server if doClientPaging is + // false, thx to http://trac.dojotoolkit.org/ticket/4761 reporting this. + // Would be nice to be able to use simpleFetch() to reduce copied code, + // but i dont know how yet. Ideas please! + request = request || {}; + if(!request.store){ + request.store = this; + } + var self = this; + + var _errorHandler = function(errorData, requestObject){ + if(requestObject.onError){ + var scope = requestObject.scope || dojo.global; + requestObject.onError.call(scope, errorData, requestObject); + } + }; + + var _fetchHandler = function(items, requestObject, numRows){ + var oldAbortFunction = requestObject.abort || null; + var aborted = false; + + var startIndex = requestObject.start?requestObject.start:0; + if (self.doClientPaging==false) { + // For client paging we dont need no slicing of the result. + startIndex = 0; + } + var endIndex = requestObject.count?(startIndex + requestObject.count):items.length; + + requestObject.abort = function(){ + aborted = true; + if(oldAbortFunction){ + oldAbortFunction.call(requestObject); + } + }; + + var scope = requestObject.scope || dojo.global; + if(!requestObject.store){ + requestObject.store = self; + } + if(requestObject.onBegin){ + requestObject.onBegin.call(scope, numRows, requestObject); + } + if(requestObject.sort && this.doClientSorting){ + items.sort(dojo.data.util.sorter.createSortFunction(requestObject.sort, self)); + } + if(requestObject.onItem){ + for(var i = startIndex; (i < items.length) && (i < endIndex); ++i){ + var item = items[i]; + if(!aborted){ + requestObject.onItem.call(scope, item, requestObject); + } + } + } + if(requestObject.onComplete && !aborted){ + var subset = null; + if (!requestObject.onItem) { + subset = items.slice(startIndex, endIndex); + } + requestObject.onComplete.call(scope, subset, requestObject); + } + }; + this._fetchItems(request, _fetchHandler, _errorHandler); + return request; // Object + }, + + getFeatures: function(){ + return this._features; + }, + + close: function(/*dojo.data.api.Request || keywordArgs || null */ request){ + // I have no idea if this is really needed ... + }, + + getLabel: function(/* item */ item){ + // summary: + // See dojo.data.api.Read.getLabel() + if(this._labelAttr && this.isItem(item)){ + return this.getValue(item, this._labelAttr); //String + } + return undefined; //undefined + }, + + getLabelAttributes: function(/* item */ item){ + // summary: + // See dojo.data.api.Read.getLabelAttributes() + if(this._labelAttr){ + return [this._labelAttr]; //array + } + return null; //null + }, + + _fetchItems: function(request, fetchHandler, errorHandler){ + // summary: + // The request contains the data as defined in the Read-API. + // Additionally there is following keyword "serverQuery". + // + // The *serverQuery* parameter, optional. + // This parameter contains the data that will be sent to the server. + // If this parameter is not given the parameter "query"'s + // data are sent to the server. This is done for some reasons: + // - to specify explicitly which data are sent to the server, they + // might also be a mix of what is contained in "query", "queryOptions" + // and the paging parameters "start" and "count" or may be even + // completely different things. + // - don't modify the request.query data, so the interface using this + // store can rely on unmodified data, as the combobox dijit currently + // does it, it compares if the query has changed + // - request.query is required by the Read-API + // + // I.e. the following examples might be sent via GET: + // fetch({query:{name:"abc"}, queryOptions:{ignoreCase:true}}) + // the URL will become: /url.php?name=abc + // + // fetch({serverQuery:{q:"abc", c:true}, query:{name:"abc"}, queryOptions:{ignoreCase:true}}) + // the URL will become: /url.php?q=abc&c=true + // // The serverQuery-parameter has overruled the query-parameter + // // but the query parameter stays untouched, but is not sent to the server! + // // The serverQuery contains more data than the query, so they might differ! + // + + var serverQuery = request.serverQuery || request.query || {}; + //Need to add start and count + if(!this.doClientPaging){ + serverQuery.start = request.start || 0; + // Count might not be sent if not given. + if (request.count) { + serverQuery.count = request.count; + } + } + if(!this.doClientSorting){ + if(request.sort){ + var sort = request.sort[0]; + if(sort && sort.attribute){ + var sortStr = sort.attribute; + if(sort.descending){ + sortStr = "-" + sortStr; + } + serverQuery.sort = sortStr; + } + } + } + // Compare the last query and the current query by simply json-encoding them, + // so we dont have to do any deep object compare ... is there some dojo.areObjectsEqual()??? + if(this.doClientPaging && this._lastServerQuery!==null && + dojo.toJson(serverQuery)==dojo.toJson(this._lastServerQuery) + ){ + fetchHandler(this._items, request); + }else{ + var xhrFunc = this.requestMethod.toLowerCase()=="post" ? dojo.xhrPost : dojo.xhrGet; + var xhrHandler = xhrFunc({url:this.url, handleAs:"json-comment-optional", content:serverQuery}); + xhrHandler.addCallback(dojo.hitch(this, function(data){ + data = this._filterResponse(data); + if (data.label){ + this._labelAttr = data.label; + } + var numRows = data.numRows || -1; + + this._items = []; + // Store a ref to "this" in each item, so we can simply check if an item + // really origins form here (idea is from ItemFileReadStore, I just don't know + // how efficient the real storage use, garbage collection effort, etc. is). + dojo.forEach(data.items,function(e){ + this._items.push({i:e, r:this}); + },this); + + var identifier = data.identifier; + this._itemsByIdentity = {}; + if(identifier){ + this._identifier = identifier; + for(i = 0; i < this._items.length; ++i){ + var item = this._items[i].i; + var identity = item[identifier]; + if(!this._itemsByIdentity[identity]){ + this._itemsByIdentity[identity] = item; + }else{ + throw new Error(this._className+": The json data as specified by: [" + this.url + "] is malformed. Items within the list have identifier: [" + identifier + "]. Value collided: [" + identity + "]"); + } + } + }else{ + this._identifier = Number; + for(i = 0; i < this._items.length; ++i){ + this._items[i].n = i; + } + } + + // TODO actually we should do the same as dojo.data.ItemFileReadStore._getItemsFromLoadedData() to sanitize + // (does it really sanititze them) and store the data optimal. should we? for security reasons??? + numRows = (numRows === -1) ? this._items.length : numRows; + fetchHandler(this._items, request, numRows); + })); + xhrHandler.addErrback(function(error){ + errorHandler(error, request); + }); + // Generate the hash using the time in milliseconds and a randon number. + // Since Math.randon() returns something like: 0.23453463, we just remove the "0." + // probably just for esthetic reasons :-). + this.lastRequestHash = new Date().getTime()+"-"+String(Math.random()).substring(2); + this._lastServerQuery = dojo.mixin({}, serverQuery); + } + }, + + _filterResponse: function(data){ + // summary: + // If the data from servers needs to be processed before it can be processed by this + // store, then this function should be re-implemented in subclass. This default + // implementation just return the data unchanged. + // data: + // The data received from server + return data; + }, + + _assertIsItem: function(/* item */ item){ + // summary: + // It throws an error if item is not valid, so you can call it in every method that needs to + // throw an error when item is invalid. + // item: + // The item to test for being contained by the store. + if(!this.isItem(item)){ + throw new Error(this._className+": Invalid item argument."); + } + }, + + _assertIsAttribute: function(/* attribute-name-string */ attribute){ + // summary: + // This function tests whether the item passed in is indeed a valid 'attribute' like type for the store. + // attribute: + // The attribute to test for being contained by the store. + if(typeof attribute !== "string"){ + throw new Error(this._className+": Invalid attribute argument ('"+attribute+"')."); + } + }, + + fetchItemByIdentity: function(/* Object */ keywordArgs){ + // summary: + // See dojo.data.api.Identity.fetchItemByIdentity() + + // See if we have already loaded the item with that id + // In case there hasn't been a fetch yet, _itemsByIdentity is null + // and thus a fetch will be triggered below. + if(this._itemsByIdentity){ + var item = this._itemsByIdentity[keywordArgs.identity]; + if(!(item === undefined)){ + if(keywordArgs.onItem){ + var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; + keywordArgs.onItem.call(scope, {i:item, r:this}); + } + return; + } + } + + // Otherwise we need to go remote + // Set up error handler + var _errorHandler = function(errorData, requestObject){ + var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; + if(keywordArgs.onError){ + keywordArgs.onError.call(scope, error); + } + }; + + // Set up fetch handler + var _fetchHandler = function(items, requestObject){ + var scope = keywordArgs.scope?keywordArgs.scope:dojo.global; + try{ + // There is supposed to be only one result + var item = null; + if(items && items.length == 1){ + item = items[0]; + } + + // If no item was found, item is still null and we'll + // fire the onItem event with the null here + if(keywordArgs.onItem){ + keywordArgs.onItem.call(scope, item); + } + }catch(error){ + if(keywordArgs.onError){ + keywordArgs.onError.call(scope, error); + } + } + }; + + // Construct query + var request = {serverQuery:{id:keywordArgs.identity}}; + + // Dispatch query + this._fetchItems(request, _fetchHandler, _errorHandler); + }, + + getIdentity: function(/* item */ item){ + // summary: + // See dojo.data.api.Identity.getIdentity() + var identifier = null; + if(this._identifier === Number){ + identifier = item.n; // Number + }else{ + identifier = item.i[this._identifier]; + } + return identifier; + }, + + getIdentityAttributes: function(/* item */ item){ + // summary: + // See dojo.data.api.Identity.getIdentityAttributes() + return [this._identifier]; + } + } +); + +} |