diff options
Diffstat (limited to 'includes/js/dojox/data/jsonPathStore.js')
-rw-r--r-- | includes/js/dojox/data/jsonPathStore.js | 1191 |
1 files changed, 1191 insertions, 0 deletions
diff --git a/includes/js/dojox/data/jsonPathStore.js b/includes/js/dojox/data/jsonPathStore.js new file mode 100644 index 0000000..01e4e23 --- /dev/null +++ b/includes/js/dojox/data/jsonPathStore.js @@ -0,0 +1,1191 @@ +if(!dojo._hasResource["dojox.data.jsonPathStore"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. +dojo._hasResource["dojox.data.jsonPathStore"] = true; +dojo.provide("dojox.data.jsonPathStore"); +dojo.require("dojox.jsonPath"); +dojo.require("dojo.date"); +dojo.require("dojo.date.locale"); +dojo.require("dojo.date.stamp"); + +dojox.data.ASYNC_MODE = 0; +dojox.data.SYNC_MODE = 1; + +dojo.declare("dojox.data.jsonPathStore", + null, + { + mode: dojox.data.ASYNC_MODE, + metaLabel: "_meta", + hideMetaAttributes: false, + autoIdPrefix: "_auto_", + autoIdentity: true, + idAttribute: "_id", + indexOnLoad: true, + labelAttribute: "", + url: "", + _replaceRegex: /\'\]/gi, + + constructor: function(options){ + //summary: + // jsonPathStore constructor, instantiate a new jsonPathStore + // + // Takes a single optional parameter in the form of a Javascript object + // containing one or more of the following properties. + // + // data: /*JSON String*/ || /* Javascript Object */, + // JSON String or Javascript object this store will control + // JSON is converted into an object, and an object passed to + // the store will be used directly. If no data and no url + // is provide, an empty object, {}, will be used as the initial + // store. + // + // url: /* string url */ + // Load data from this url in JSON format and use the Object + // created from the data as the data source. + // + // indexOnLoad: /* boolean */ + // Defaults to true, but this may change in the near future. + // Parse the data object and set individual objects up as + // appropriate. This will add meta data and assign + // id's to objects that dont' have them as defined by the + // idAttribute option. Disabling this option will keep this + // parsing from happening until a query is performed at which + // time only the top level of an item has meta info stored. + // This might work in some situations, but you will almost + // always want to indexOnLoad or use another option which + // will create an index. In the future we will support a + // generated index that maps by jsonPath allowing the + // server to take some of this load for larger data sets. + // + // idAttribute: /* string */ + // Defaults to '_id'. The name of the attribute that holds an objects id. + // This can be a preexisting id provided by the server. + // If an ID isn't already provided when an object + // is fetched or added to the store, the autoIdentity system + // will generate an id for it and add it to the index. There + // are utility routines for exporting data from the store + // that can clean any generated IDs before exporting and leave + // preexisting id's in tact. + // + // metaLabel: /* string */ + // Defaults to '_meta' overrides the attribute name that is used by the store + // for attaching meta information to an object while + // in the store's control. Defaults to '_meta'. + // + // hideMetaAttributes: /* boolean */ + // Defaults to False. When enabled, calls to getAttributes() will not + // include the meta attribute. + // + // autoIdPrefix: /*string*/ + // Defaults to "_auto_". This string is used as the prefix to any + // objects which have a generated id. A numeric index is appended + // to this string to complete the ID + // + // mode: dojox.data.ASYNC_MODE || dojox.data.SYNC_MODE + // Defaults to ASYNC_MODE. This option sets the default mode for this store. + // Sync calls return their data immediately from the calling function + // instead of calling the callback functions. Functions such as + // fetchItemByIdentity() and fetch() both accept a string parameter in addtion + // to the normal keywordArgs parameter. When passed this option, SYNC_MODE will + // automatically be used even when the default mode of the system is ASYNC_MODE. + // A normal request to fetch or fetchItemByIdentity (with kwArgs object) can also + // include a mode property to override this setting for that one request. + + //setup a byId alias to the api call + this.byId=this.fetchItemByIdentity; + + if (options){ + dojo.mixin(this,options); + } + + this._dirtyItems=[]; + this._autoId=0; + this._referenceId=0; + this._references={}; + this._fetchQueue=[]; + this.index={}; + + //regex to identify when we're travelling down metaObject (which we don't want to do) + var expr="("+this.metaLabel+"\'\])"; + this.metaRegex = new RegExp(expr); + + + //no data or url, start with an empty object for a store + if (!this.data && !this.url){ + this.setData({}); + } + + //we have data, but no url, set the store as the data + if (this.data && !this.url){ + this.setData(this.data); + + //remove the original refernce, we're now using _data from here on out + delete this.data; + } + + //given a url, load json data from as the store + if (this.url){ + dojo.xhrGet({ + url: options.url, + handleAs: "json", + load: dojo.hitch(this, "setData"), + sync: this.mode + }); + } + }, + + _loadData: function(data){ + // summary: + // load data into the store. Index it if appropriate. + if (this._data){ + delete this._data; + } + + if (dojo.isString(data)){ + this._data = dojo.fromJson(data); + }else{ + this._data = data; + } + + if (this.indexOnLoad){ + this.buildIndex(); + } + + this._updateMeta(this._data, {path: "$"}); + + this.onLoadData(this._data); + }, + + onLoadData: function(data){ + // summary + // Called after data has been loaded in the store. + // If any requests happened while the startup is happening + // then process them now. + + while (this._fetchQueue.length>0){ + var req = this._fetchQueue.shift(); + this.fetch(req); + } + + }, + + setData: function(data){ + // summary: + // set the stores' data to the supplied object and then + // load and/or setup that data with the required meta info + this._loadData(data); + }, + + buildIndex: function(path, item){ + //summary: + // parse the object structure, and turn any objects into + // jsonPathStore items. Basically this just does a recursive + // series of fetches which itself already examines any items + // as they are retrieved and setups up the required meta information. + // + // path: /* string */ + // jsonPath Query for the starting point of this index construction. + + if (!this.idAttribute){ + throw new Error("buildIndex requires idAttribute for the store"); + } + + item = item || this._data; + var origPath = path; + path = path||"$"; + path += "[*]"; + var data = this.fetch({query: path,mode: dojox.data.SYNC_MODE}); + for(var i=0; i<data.length;i++){ + if(dojo.isObject(data[i])){ + var newPath = data[i][this.metaLabel]["path"]; + if (origPath){ + //console.log("newPath: ", newPath); + //console.log("origPath: ", origPath); + //console.log("path: ", path); + //console.log("data[i]: ", data[i]); + var parts = origPath.split("\[\'"); + var attribute = parts[parts.length-1].replace(this._replaceRegex,''); + //console.log("attribute: ", attribute); + //console.log("ParentItem: ", item, attribute); + if (!dojo.isArray(data[i])){ + this._addReference(data[i], {parent: item, attribute:attribute}); + this.buildIndex(newPath, data[i]); + }else{ + this.buildIndex(newPath,item); + } + }else{ + var parts = newPath.split("\[\'"); + var attribute = parts[parts.length-1].replace(this._replaceRegex,''); + this._addReference(data[i], {parent: this._data, attribute:attribute}); + this.buildIndex(newPath, data[i]); + } + } + } + }, + + _correctReference: function(item){ + // summary: + // make sure we have an reference to the item in the store + // and not a clone. Takes an item, matches it to the corresponding + // item in the store and if it is the same, returns itself, otherwise + // it returns the item from the store. + + if (this.index[item[this.idAttribute]][this.metaLabel]===item[this.metaLabel]){ + return this.index[item[this.idAttribute]]; + } + return item; + }, + + getValue: function(item, property){ + // summary: + // Gets the value of an item's 'property' + // + // item: /* object */ + // property: /* string */ + // property to look up value for + item = this._correctReference(item); + return item[property]; + }, + + getValues: function(item, property){ + // summary: + // Gets the value of an item's 'property' and returns + // it. If this value is an array it is just returned, + // if not, the value is added to an array and that is returned. + // + // item: /* object */ + // property: /* string */ + // property to look up value for + + item = this._correctReference(item); + return dojo.isArray(item[property]) ? item[property] : [item[property]]; + }, + + getAttributes: function(item){ + // summary: + // Gets the available attributes of an item's 'property' and returns + // it as an array. If the store has 'hideMetaAttributes' set to true + // the attributed identified by 'metaLabel' will not be included. + // + // item: /* object */ + + item = this._correctReference(item); + var res = []; + for (var i in item){ + if (this.hideMetaAttributes && (i==this.metaLabel)){continue;} + res.push(i); + } + return res; + }, + + hasAttribute: function(item,attribute){ + // summary: + // Checks to see if item has attribute + // + // item: /* object */ + // attribute: /* string */ + + item = this._correctReference(item); + if (attribute in item){return true;} + return false; + }, + + containsValue: function(item, attribute, value){ + // summary: + // Checks to see if 'item' has 'value' at 'attribute' + // + // item: /* object */ + // attribute: /* string */ + // value: /* anything */ + item = this._correctReference(item); + + if (item[attribute] && item[attribute]==value){return true} + if (dojo.isObject(item[attribute]) || dojo.isObject(value)){ + if (this._shallowCompare(item[attribute],value)){return true} + } + return false; + }, + + _shallowCompare: function(a, b){ + //summary does a simple/shallow compare of properties on an object + //to the same named properties on the given item. Returns + //true if all props match. It will not descend into child objects + //but it will compare child date objects + + if ((dojo.isObject(a) && !dojo.isObject(b))|| (dojo.isObject(b) && !dojo.isObject(a))) { + return false; + } + + if ( a["getFullYear"] || b["getFullYear"] ){ + //confirm that both are dates + if ( (a["getFullYear"] && !b["getFullYear"]) || (b["getFullYear"] && !a["getFullYear"]) ){ + return false; + }else{ + if (!dojo.date.compare(a,b)){ + return true; + } + return false; + } + } + + for (var i in b){ + if (dojo.isObject(b[i])){ + if (!a[i] || !dojo.isObject(a[i])){return false} + + if (b[i]["getFullYear"]){ + if(!a[i]["getFullYear"]){return false} + if (dojo.date.compare(a,b)){return false} + }else{ + if (!this._shallowCompare(a[i],b[i])){return false} + } + }else{ + if (!b[i] || (a[i]!=b[i])){return false} + } + } + + //make sure there werent props on a that aren't on b, if there aren't, then + //the previous section will have already evaluated things. + + for (var i in a){ + if (!b[i]){return false} + } + + return true; + }, + + isItem: function(item){ + // summary: + // Checks to see if a passed 'item' + // is really a jsonPathStore item. Currently + // it only verifies structure. It does not verify + // that it belongs to this store at this time. + // + // item: /* object */ + // attribute: /* string */ + + if (!dojo.isObject(item) || !item[this.metaLabel]){return false} + if (this.requireId && this._hasId && !item[this._id]){return false} + return true; + }, + + isItemLoaded: function(item){ + // summary: + // returns isItem() :) + // + // item: /* object */ + + item = this._correctReference(item); + return this.isItem(item); + }, + + loadItem: function(item){ + // summary: + // returns true. Future implementatins might alter this + return true; + }, + + _updateMeta: function(item, props){ + // summary: + // verifies that 'item' has a meta object attached + // and if not it creates it by setting it to 'props' + // if the meta attribute already exists, mix 'props' + // into it. + + if (item && item[this.metaLabel]){ + dojo.mixin(item[this.metaLabel], props); + return; + } + + item[this.metaLabel]=props; + }, + + cleanMeta: function(data, options){ + // summary + // Recurses through 'data' and removes an + // meta information that has been attached. This + // function will also removes any id's that were autogenerated + // from objects. It will not touch id's that were not generated + + data = data || this._data; + + if (data[this.metaLabel]){ + if(data[this.metaLabel]["autoId"]){ + delete data[this.idAttribute]; + } + delete data[this.metaLabel]; + } + + if (dojo.isArray(data)){ + for(var i=0; i<data.length;i++){ + if(dojo.isObject(data[i]) || dojo.isArray(data[i]) ){ + this.cleanMeta(data[i]); + } + } + } else if (dojo.isObject(data)){ + for (var i in data){ + this.cleanMeta(data[i]); + } + } + }, + + fetch: function(args){ + //console.log("fetch() ", args); + // summary + // + // fetch takes either a string argument or a keywordArgs + // object containing the parameters for the search. + // If passed a string, fetch will interpret this string + // as the query to be performed and will do so in + // SYNC_MODE returning the results immediately. + // If an object is supplied as 'args', its options will be + // parsed and then contained query executed. + // + // query: /* string or object */ + // Defaults to "$..*". jsonPath query to be performed + // on data store. **note that since some widgets + // expect this to be an object, an object in the form + // of {query: '$[*'], queryOptions: "someOptions"} is + // acceptable + // + // mode: dojox.data.SYNC_MODE || dojox.data.ASYNC_MODE + // Override the stores default mode. + // + // queryOptions: /* object */ + // Options passed on to the underlying jsonPath query + // system. + // + // start: /* int */ + // Starting item in result set + // + // count: /* int */ + // Maximum number of items to return + // + // sort: /* function */ + // Not Implemented yet + // + // The following only apply to ASYNC requests (the default) + // + // onBegin: /* function */ + // called before any results are returned. Parameters + // will be the count and the original fetch request + // + // onItem: /*function*/ + // called for each returned item. Parameters will be + // the item and the fetch request + // + // onComplete: /* function */ + // called on completion of the request. Parameters will + // be the complete result set and the request + // + // onError: /* function */ + // colled in the event of an error + + // we're not started yet, add this request to a queue and wait till we do + if (!this._data){ + this._fetchQueue.push(args); + return args; + } + if(dojo.isString(args)){ + query = args; + args={query: query, mode: dojox.data.SYNC_MODE}; + + } + + var query; + if (!args || !args.query){ + if (!args){ + var args={}; + } + + if (!args.query){ + args.query="$..*"; + query=args.query; + } + + } + + if (dojo.isObject(args.query)){ + if (args.query.query){ + query = args.query.query; + }else{ + query = args.query = "$..*"; + } + if (args.query.queryOptions){ + args.queryOptions=args.query.queryOptions + } + }else{ + query=args.query; + } + + if (!args.mode) {args.mode = this.mode;} + if (!args.queryOptions) {args.queryOptions={};} + + args.queryOptions.resultType='BOTH'; + var results = dojox.jsonPath.query(this._data, query, args.queryOptions); + var tmp=[]; + var count=0; + for (var i=0; i<results.length; i++){ + if(args.start && i<args.start){continue;} + if (args.count && (count >= args.count)) { continue; } + + var item = results[i]["value"]; + var path = results[i]["path"]; + if (!dojo.isObject(item)){continue;} + if(this.metaRegex.exec(path)){continue;} + + //this automatically records the objects path + this._updateMeta(item,{path: results[i].path}); + + //if autoIdentity and no id, generate one and add it to the item + if(this.autoIdentity && !item[this.idAttribute]){ + var newId = this.autoIdPrefix + this._autoId++; + item[this.idAttribute]=newId; + item[this.metaLabel]["autoId"]=true; + } + + //add item to the item index if appropriate + if(item[this.idAttribute]){this.index[item[this.idAttribute]]=item} + count++; + tmp.push(item); + } + results = tmp; + var scope = args.scope || dojo.global; + + if ("sort" in args){ + console.log("TODO::add support for sorting in the fetch"); + } + + if (args.mode==dojox.data.SYNC_MODE){ + return results; + }; + + if (args.onBegin){ + args["onBegin"].call(scope, results.length, args); + } + + if (args.onItem){ + for (var i=0; i<results.length;i++){ + args["onItem"].call(scope, results[i], args); + } + } + + if (args.onComplete){ + args["onComplete"].call(scope, results, args); + } + + return args; + }, + + dump: function(options){ + // summary: + // + // exports the store data set. Takes an options + // object with a number of parameters + // + // data: /* object */ + // Defaults to the root of the store. + // The data to be exported. + // + // clone: /* boolean */ + // clone the data set before returning it + // or modifying it for export + // + // cleanMeta: /* boolean */ + // clean the meta data off of the data. Note + // that this will happen to the actual + // store data if !clone. If you want + // to continue using the store after + // this operation, it is probably better to export + // it as a clone if you want it cleaned. + // + // suppressExportMeta: /* boolean */ + // By default, when data is exported from the store + // some information, such as as a timestamp, is + // added to the root of exported data. This + // prevents that from happening. It is mainly used + // for making tests easier. + // + // type: "raw" || "json" + // Defaults to 'json'. 'json' will convert the data into + // json before returning it. 'raw' will just return a + // reference to the object + + var options = options || {}; + var d=options.data || this._data; + + if (!options.suppressExportMeta && options.clone){ + data = dojo.clone(d); + if (data[this.metaLabel]){ + data[this.metaLabel]["clone"]=true; + } + }else{ + var data=d; + } + + if (!options.suppressExportMeta && data[this.metaLabel]){ + data[this.metaLabel]["last_export"]=new Date().toString() + } + + if(options.cleanMeta){ + this.cleanMeta(data); + } + + //console.log("Exporting: ", options, dojo.toJson(data)); + switch(options.type){ + case "raw": + return data; + case "json": + default: + return dojo.toJson(data); + } + }, + + getFeatures: function(){ + // summary: + // return the store feature set + + return { + "dojo.data.api.Read": true, + "dojo.data.api.Identity": true, + "dojo.data.api.Write": true, + "dojo.data.api.Notification": true + } + }, + + getLabel: function(item){ + // summary + // returns the label for an item. The label + // is created by setting the store's labelAttribute + // property with either an attribute name or an array + // of attribute names. Developers can also + // provide the store with a createLabel function which + // will do the actaul work of creating the label. If not + // the default will just concatenate any of the identified + // attributes together. + item = this._correctReference(item); + var label=""; + + if (dojo.isFunction(this.createLabel)){ + return this.createLabel(item); + } + + if (this.labelAttribute){ + if (dojo.isArray(this.labelAttribute)) { + for(var i=0; i<this.labelAttribute.length; i++){ + if (i>0) { label+=" ";} + label += item[this.labelAttribute[i]]; + } + return label; + }else{ + return item[this.labelAttribute]; + } + } + return item.toString(); + }, + + getLabelAttributes: function(item){ + // summary: + // returns an array of attributes that are used to create the label of an item + item = this._correctReference(item); + return dojo.isArray(this.labelAttribute) ? this.labelAttribute : [this.labelAttribute]; + }, + + sort: function(a,b){ + console.log("TODO::implement default sort algo"); + }, + + //Identity API Support + + getIdentity: function(item){ + // summary + // returns the identity of an item or throws + // a not found error. + + if (this.isItem(item)){ + return item[this.idAttribute]; + } + throw new Error("Id not found for item"); + }, + + getIdentityAttributes: function(item){ + // summary: + // returns the attributes which are used to make up the + // identity of an item. Basically returns this.idAttribute + + return [this.idAttribute]; + }, + + fetchItemByIdentity: function(args){ + // summary: + // fetch an item by its identity. This store also provides + // a much more finger friendly alias, 'byId' which does the + // same thing as this function. If provided a string + // this call will be treated as a SYNC request and will + // return the identified item immediatly. Alternatively it + // takes a object as a set of keywordArgs: + // + // identity: /* string */ + // the id of the item you want to retrieve + // + // mode: dojox.data.SYNC_MODE || dojox.data.ASYNC_MODE + // overrides the default store fetch mode + // + // onItem: /* function */ + // Result call back. Passed the fetched item. + // + // onError: /* function */ + // error callback. + var id; + if (dojo.isString(args)){ + id = args; + args = {identity: id, mode: dojox.data.SYNC_MODE} + }else{ + if (args){ + id = args["identity"]; + } + if (!args.mode){args.mode = this.mode} + } + + if (this.index && (this.index[id] || this.index["identity"])){ + + if (args.mode==dojox.data.SYNC_MODE){ + return this.index[id]; + } + + if (args.onItem){ + args["onItem"].call(args.scope || dojo.global, this.index[id], args); + } + + return args; + }else{ + if (args.mode==dojox.data.SYNC_MODE){ + return false; + } + } + + + if(args.onError){ + args["onItem"].call(args.scope || dojo.global, new Error("Item Not Found: " + id), args); + } + + return args; + }, + + //Write API Support + newItem: function(data, options){ + // summary: + // adds a new item to the store at the specified point. + // Takes two parameters, data, and options. + // + // data: /* object */ + // The data to be added in as an item. This could be a + // new javascript object, or it could be an item that + // already exists in the store. If it already exists in the + // store, then this will be added as a reference. + // + // options: /* object */ + // + // item: /* item */ + // reference to an existing store item + // + // attribute: /* string */ + // attribute to add the item at. If this is + // not provided, the item's id will be used as the + // attribute name. If specified attribute is an + // array, the new item will be push()d on to the + // end of it. + // oldValue: /* old value of item[attribute] + // newValue: new value item[attribute] + + var meta={}; + + //default parent to the store root; + var pInfo ={item:this._data}; + + if (options){ + if (options.parent){ + options.item = options.parent; + } + + dojo.mixin(pInfo, options); + } + + if (this.idAttribute && !data[this.idAttribute]){ + if (this.requireId){throw new Error("requireId is enabled, new items must have an id defined to be added");} + if (this.autoIdentity){ + var newId = this.autoIdPrefix + this._autoId++; + data[this.idAttribute]=newId; + meta["autoId"]=true; + } + } + + if (!pInfo && !pInfo.attribute && !this.idAttribute && !data[this.idAttribute]){ + throw new Error("Adding a new item requires, at a minumum, either the pInfo information, including the pInfo.attribute, or an id on the item in the field identified by idAttribute"); + } + + //pInfo.parent = this._correctReference(pInfo.parent); + //if there is no parent info supplied, default to the store root + //and add to the pInfo.attribute or if that doestn' exist create an + //attribute with the same name as the new items ID + if(!pInfo.attribute){pInfo.attribute = data[this.idAttribute]} + + pInfo.oldValue = this._trimItem(pInfo.item[pInfo.attribute]); + if (dojo.isArray(pInfo.item[pInfo.attribute])){ + this._setDirty(pInfo.item); + pInfo.item[pInfo.attribute].push(data); + }else{ + this._setDirty(pInfo.item); + pInfo.item[pInfo.attribute]=data; + } + + pInfo.newValue = pInfo.item[pInfo.attribute]; + + //add this item to the index + if(data[this.idAttribute]){this.index[data[this.idAttribute]]=data} + + this._updateMeta(data, meta) + + //keep track of all references in the store so we can delete them as necessary + this._addReference(data, pInfo); + + //mark this new item as dirty + this._setDirty(data); + + //Notification API + this.onNew(data, pInfo); + + //returns the original item, now decorated with some meta info + return data; + }, + + _addReference: function(item, pInfo){ + // summary + // adds meta information to an item containing a reference id + // so that references can be deleted as necessary, when passed + // only a string, the string for parent info, it will only + // it will be treated as a string reference + + //console.log("_addReference: ", item, pInfo); + var rid = '_ref_' + this._referenceId++; + if (!item[this.metaLabel]["referenceIds"]){ + item[this.metaLabel]["referenceIds"]=[]; + } + + item[this.metaLabel]["referenceIds"].push(rid); + this._references[rid] = pInfo; + }, + + deleteItem: function(item){ + // summary + // deletes item and any references to that item from the store. + // If the desire is to delete only one reference, unsetAttribute or + // setValue is the way to go. + + item = this._correctReference(item); + console.log("Item: ", item); + if (this.isItem(item)){ + while(item[this.metaLabel]["referenceIds"].length>0){ + console.log("refs map: " , this._references); + console.log("item to delete: ", item); + var rid = item[this.metaLabel]["referenceIds"].pop(); + var pInfo = this._references[rid]; + + console.log("deleteItem(): ", pInfo, pInfo.parent); + parentItem = pInfo.parent; + var attribute = pInfo.attribute; + if(parentItem && parentItem[attribute] && !dojo.isArray(parentItem[attribute])){ + this._setDirty(parentItem); + this.unsetAttribute(parentItem, attribute); + delete parentItem[attribute]; + } + + if (dojo.isArray(parentItem[attribute])){ + console.log("Parent is array"); + var oldValue = this._trimItem(parentItem[attribute]); + var found=false; + for (var i=0; i<parentItem[attribute].length && !found;i++){ + if (parentItem[attribute][i][this.metaLabel]===item[this.metaLabel]){ + found=true; + } + } + + if (found){ + this._setDirty(parentItem); + var del = parentItem[attribute].splice(i-1,1); + delete del; + } + + var newValue = this._trimItem(parentItem[attribute]); + this.onSet(parentItem,attribute,oldValue,newValue); + } + delete this._references[rid]; + + } + this.onDelete(item); + delete item; + } + }, + + _setDirty: function(item){ + // summary: + // adds an item to the list of dirty items. This item + // contains a reference to the item itself as well as a + // cloned and trimmed version of old item for use with + // revert. + + //if an item is already in the list of dirty items, don't add it again + //or it will overwrite the premodification data set. + for (var i=0; i<this._dirtyItems.length; i++){ + if (item[this.idAttribute]==this._dirtyItems[i][this.idAttribute]){ + return; + } + } + + this._dirtyItems.push({item: item, old: this._trimItem(item)}); + this._updateMeta(item, {isDirty: true}); + }, + + setValue: function(item, attribute, value){ + // summary: + // sets 'attribute' on 'item' to 'value' + item = this._correctReference(item); + + this._setDirty(item); + var old = item[attribute] | undefined; + item[attribute]=value; + this.onSet(item,attribute,old,value); + + }, + + setValues: function(item, attribute, values){ + // summary: + // sets 'attribute' on 'item' to 'value' value + // must be an array. + + + item = this._correctReference(item); + if (!dojo.isArray(values)){throw new Error("setValues expects to be passed an Array object as its value");} + this._setDirty(item); + var old = item[attribute] || null; + item[attribute]=values + this.onSet(item,attribute,old,values); + }, + + unsetAttribute: function(item, attribute){ + // summary: + // unsets 'attribute' on 'item' + + item = this._correctReference(item); + this._setDirty(item); + var old = item[attribute]; + delete item[attribute]; + this.onSet(item,attribute,old,null); + }, + + save: function(kwArgs){ + // summary: + // Takes an optional set of keyword Args with + // some save options. Currently only format with options + // being "raw" or "json". This function goes through + // the dirty item lists, clones and trims the item down so that + // the items children are not part of the data (the children are replaced + // with reference objects). This data is compiled into a single array, the dirty objects + // are all marked as clean, and the new data is then passed on to the onSave handler. + + var data = []; + + if (!kwArgs){kwArgs={}} + while (this._dirtyItems.length > 0){ + var item = this._dirtyItems.pop()["item"]; + var t = this._trimItem(item); + var d; + switch(kwArgs.format){ + case "json": + d = dojo.toJson(t); + break; + case "raw": + default: + d = t; + } + data.push(d); + this._markClean(item); + } + + this.onSave(data); + }, + + _markClean: function(item){ + // summary + // remove this meta information marking an item as "dirty" + + if (item && item[this.metaLabel] && item[this.metaLabel]["isDirty"]){ + delete item[this.metaLabel]["isDirty"]; + } + }, + + revert: function(){ + // summary + // returns any modified data to its original state prior to a save(); + + while (this._dirtyItems.length>0){ + var d = this._dirtyItems.pop(); + this._mixin(d.item, d.old); + } + this.onRevert(); + }, + + _mixin: function(target, data){ + // summary: + // specialized mixin that hooks up objects in the store where references are identified. + + if (dojo.isObject(data)){ + if (dojo.isArray(data)){ + while(target.length>0){target.pop();} + for (var i=0; i<data.length;i++){ + if (dojo.isObject(data[i])){ + if (dojo.isArray(data[i])){ + var mix=[]; + }else{ + var mix={}; + if (data[i][this.metaLabel] && data[i][this.metaLabel]["type"] && data[i][this.metaLabel]["type"]=='reference'){ + target[i]=this.index[data[i][this.idAttribute]]; + continue; + } + } + + this._mixin(mix, data[i]); + target.push(mix); + }else{ + target.push(data[i]); + } + } + }else{ + for (var i in target){ + if (i in data){continue;} + delete target[i]; + } + + for (var i in data){ + if (dojo.isObject(data[i])){ + if (dojo.isArray(data[i])){ + var mix=[]; + }else{ + if (data[i][this.metaLabel] && data[i][this.metaLabel]["type"] && data[i][this.metaLabel]["type"]=='reference'){ + target[i]=this.index[data[i][this.idAttribute]]; + continue; + } + + var mix={}; + } + this._mixin(mix, data[i]); + target[i]=mix; + }else{ + target[i]=data[i]; + } + } + + } + } + }, + + isDirty: function(item){ + // summary + // returns true if the item is marked as dirty. + + item = this._correctReference(item); + return item && item[this.metaLabel] && item[this.metaLabel]["isDirty"]; + }, + + _createReference: function(item){ + // summary + // Create a small reference object that can be used to replace + // child objects during a trim + + var obj={}; + obj[this.metaLabel]={ + type:'reference' + }; + + obj[this.idAttribute]=item[this.idAttribute]; + return obj; + }, + + _trimItem: function(item){ + //summary: + // copy an item recursively stoppying at other items that have id's + // and replace them with a refrence object; + var copy; + if (dojo.isArray(item)){ + copy = []; + for (var i=0; i<item.length;i++){ + if (dojo.isArray(item[i])){ + copy.push(this._trimItem(item[i])) + }else if (dojo.isObject(item[i])){ + if (item[i]["getFullYear"]){ + copy.push(dojo.date.stamp.toISOString(item[i])); + }else if (item[i][this.idAttribute]){ + copy.push(this._createReference(item[i])); + }else{ + copy.push(this._trimItem(item[i])); + } + } else { + copy.push(item[i]); + } + } + return copy; + } + + if (dojo.isObject(item)){ + copy = {}; + + for (var attr in item){ + if (!item[attr]){ copy[attr]=undefined;continue;} + if (dojo.isArray(item[attr])){ + copy[attr] = this._trimItem(item[attr]); + }else if (dojo.isObject(item[attr])){ + if (item[attr]["getFullYear"]){ + copy[attr] = dojo.date.stamp.toISOString(item[attr]); + }else if(item[attr][this.idAttribute]){ + copy[attr]=this._createReference(item[attr]); + } else { + copy[attr]=this._trimItem(item[attr]); + } + } else { + copy[attr]=item[attr]; + } + } + return copy; + } + }, + + //Notifcation Support + + onSet: function(){ + }, + + onNew: function(){ + + }, + + onDelete: function(){ + + }, + + onSave: function(items){ + // summary: + // notification of the save event..not part of the notification api, + // but probably should be. + //console.log("onSave() ", items); + }, + + onRevert: function(){ + // summary: + // notification of the revert event..not part of the notification api, + // but probably should be. + + } + } +); + +//setup an alias to byId, is there a better way to do this? +dojox.data.jsonPathStore.byId=dojox.data.jsonPathStore.fetchItemByIdentity; + +} |