X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/25ef3491bd9ae007ff1fc2b0d7943e6eaaccf775..6a7e4474cba9d8be4b2ec445e10f1691f7277c50:/pkgs/data-foundation-debug.js diff --git a/pkgs/data-foundation-debug.js b/pkgs/data-foundation-debug.js index 8164b7dc..4690ee8f 100644 --- a/pkgs/data-foundation-debug.js +++ b/pkgs/data-foundation-debug.js @@ -1,6 +1,6 @@ /*! - * Ext JS Library 3.0.3 - * Copyright(c) 2006-2009 Ext JS, LLC + * Ext JS Library 3.2.0 + * Copyright(c) 2006-2010 Ext JS, Inc. * licensing@extjs.com * http://www.extjs.com/license */ @@ -65,8 +65,7 @@ restActions : { /** * Returns true if supplied action-name is a valid API action defined in {@link #actions} constants - * @param {String} action - * @param {String[]}(Optional) List of available CRUD actions. Pass in list when executing multiple times for efficiency. + * @param {String} action Action to test for availability. * @return {Boolean} */ isAction : function(action) { @@ -98,7 +97,7 @@ restActions : { /** * Returns true if the supplied API is valid; that is, check that all keys match defined actions * otherwise returns an array of mistakes. - * @return {String[]||true} + * @return {String[]|true} */ isValid : function(api){ var invalid = []; @@ -169,7 +168,8 @@ new Ext.data.HttpProxy({ proxy.api[action] = proxy.api[action] || proxy.url || proxy.directFn; if (typeof(proxy.api[action]) == 'string') { proxy.api[action] = { - url: proxy.api[action] + url: proxy.api[action], + method: (proxy.restful === true) ? Ext.data.Api.restActions[action] : undefined }; } } @@ -183,7 +183,8 @@ new Ext.data.HttpProxy({ restify : function(proxy) { proxy.restful = true; for (var verb in this.restActions) { - proxy.api[this.actions[verb]].method = this.restActions[verb]; + proxy.api[this.actions[verb]].method || + (proxy.api[this.actions[verb]].method = this.restActions[verb]); } // TODO: perhaps move this interceptor elsewhere? like into DataProxy, perhaps? Placed here // to satisfy initial 3.0 final release of REST features. @@ -195,16 +196,18 @@ new Ext.data.HttpProxy({ }); switch (response.status) { - case 200: // standard 200 response, send control back to HttpProxy#onWrite + case 200: // standard 200 response, send control back to HttpProxy#onWrite by returning true from this intercepted #onWrite return true; break; case 201: // entity created but no response returned - //res[reader.meta.successProperty] = true; - res.success = true; + if (Ext.isEmpty(res.raw.responseText)) { + res.success = true; + } else { + //if the response contains data, treat it like a 200 + return true; + } break; case 204: // no-content. Create a fake response. - //res[reader.meta.successProperty] = true; - //res[reader.meta.root] = null; res.success = true; res.data = null; break; @@ -212,13 +215,6 @@ new Ext.data.HttpProxy({ return true; break; } - /* - if (res[reader.meta.successProperty] === true) { - this.fireEvent("write", this, action, res[reader.meta.root], res, rs, o.request.arg); - } else { - this.fireEvent('exception', this, 'remote', action, o, res, rs); - } - */ if (res.success === true) { this.fireEvent("write", this, action, res.data, res, rs, o.request.arg); } else { @@ -255,7 +251,7 @@ Ext.data.Response.prototype = { return this.success; }, getStatus : function() { - return this.status + return this.status; }, getRoot : function() { return this.root; @@ -287,90 +283,90 @@ Ext.apply(Ext.data.Api.Error.prototype, { }); - -/** - * @class Ext.data.SortTypes - * @singleton - * Defines the default sorting (casting?) comparison functions used when sorting data. - */ -Ext.data.SortTypes = { - /** - * Default sort that does nothing - * @param {Mixed} s The value being converted - * @return {Mixed} The comparison value - */ - none : function(s){ - return s; - }, - - /** - * The regular expression used to strip tags - * @type {RegExp} - * @property - */ - stripTagsRE : /<\/?[^>]+>/gi, - - /** - * Strips all HTML tags to sort on text only - * @param {Mixed} s The value being converted - * @return {String} The comparison value - */ - asText : function(s){ - return String(s).replace(this.stripTagsRE, ""); - }, - - /** - * Strips all HTML tags to sort on text only - Case insensitive - * @param {Mixed} s The value being converted - * @return {String} The comparison value - */ - asUCText : function(s){ - return String(s).toUpperCase().replace(this.stripTagsRE, ""); - }, - - /** - * Case insensitive string - * @param {Mixed} s The value being converted - * @return {String} The comparison value - */ - asUCString : function(s) { - return String(s).toUpperCase(); - }, - - /** - * Date sorting - * @param {Mixed} s The value being converted - * @return {Number} The comparison value - */ - asDate : function(s) { - if(!s){ - return 0; - } - if(Ext.isDate(s)){ - return s.getTime(); - } - return Date.parse(String(s)); - }, - - /** - * Float sorting - * @param {Mixed} s The value being converted - * @return {Float} The comparison value - */ - asFloat : function(s) { - var val = parseFloat(String(s).replace(/,/g, "")); - return isNaN(val) ? 0 : val; - }, - - /** - * Integer sorting - * @param {Mixed} s The value being converted - * @return {Number} The comparison value - */ - asInt : function(s) { - var val = parseInt(String(s).replace(/,/g, ""), 10); - return isNaN(val) ? 0 : val; - } + +/** + * @class Ext.data.SortTypes + * @singleton + * Defines the default sorting (casting?) comparison functions used when sorting data. + */ +Ext.data.SortTypes = { + /** + * Default sort that does nothing + * @param {Mixed} s The value being converted + * @return {Mixed} The comparison value + */ + none : function(s){ + return s; + }, + + /** + * The regular expression used to strip tags + * @type {RegExp} + * @property + */ + stripTagsRE : /<\/?[^>]+>/gi, + + /** + * Strips all HTML tags to sort on text only + * @param {Mixed} s The value being converted + * @return {String} The comparison value + */ + asText : function(s){ + return String(s).replace(this.stripTagsRE, ""); + }, + + /** + * Strips all HTML tags to sort on text only - Case insensitive + * @param {Mixed} s The value being converted + * @return {String} The comparison value + */ + asUCText : function(s){ + return String(s).toUpperCase().replace(this.stripTagsRE, ""); + }, + + /** + * Case insensitive string + * @param {Mixed} s The value being converted + * @return {String} The comparison value + */ + asUCString : function(s) { + return String(s).toUpperCase(); + }, + + /** + * Date sorting + * @param {Mixed} s The value being converted + * @return {Number} The comparison value + */ + asDate : function(s) { + if(!s){ + return 0; + } + if(Ext.isDate(s)){ + return s.getTime(); + } + return Date.parse(String(s)); + }, + + /** + * Float sorting + * @param {Mixed} s The value being converted + * @return {Float} The comparison value + */ + asFloat : function(s) { + var val = parseFloat(String(s).replace(/,/g, "")); + return isNaN(val) ? 0 : val; + }, + + /** + * Integer sorting + * @param {Mixed} s The value being converted + * @return {Number} The comparison value + */ + asInt : function(s) { + var val = parseInt(String(s).replace(/,/g, ""), 10); + return isNaN(val) ? 0 : val; + } };/** * @class Ext.data.Record *

Instances of this class encapsulate both Record definition information, and Record @@ -394,10 +390,11 @@ Ext.data.SortTypes = { * @param {Object} data (Optional) An object, the properties of which provide values for the new Record's * fields. If not specified the {@link Ext.data.Field#defaultValue defaultValue} * for each field will be assigned. - * @param {Object} id (Optional) The id of the Record. This id should be unique, and is used by the - * {@link Ext.data.Store} object which owns the Record to index its collection of Records. If - * an id is not specified a {@link #phantom} Record will be created - * with an {@link #Record.id automatically generated id}. + * @param {Object} id (Optional) The id of the Record. The id is used by the + * {@link Ext.data.Store} object which owns the Record to index its collection + * of Records (therefore this id should be unique within each store). If an + * id is not specified a {@link #phantom} + * Record will be created with an {@link #Record.id automatically generated id}. */ Ext.data.Record = function(data, id){ // if no id, call the auto id method @@ -440,7 +437,7 @@ var myNewRecord = new TopicRecord( myStore.{@link Ext.data.Store#add add}(myNewRecord); * @method create - * @return {function} A constructor which is used to create new Records according + * @return {Function} A constructor which is used to create new Records according * to the definition. The constructor has the same signature as {@link #Record}. * @static */ @@ -503,22 +500,34 @@ Ext.data.Record.prototype = { * @property id * @type {Object} */ + /** + *

Only present if this Record was created by an {@link Ext.data.XmlReader XmlReader}.

+ *

The XML element which was the source of the data for this Record.

+ * @property node + * @type {XMLElement} + */ + /** + *

Only present if this Record was created by an {@link Ext.data.ArrayReader ArrayReader} or a {@link Ext.data.JsonReader JsonReader}.

+ *

The Array or object which was the source of the data for this Record.

+ * @property json + * @type {Array|Object} + */ /** * Readonly flag - true if this Record has been modified. * @type Boolean */ dirty : false, editing : false, - error: null, + error : null, /** * This object contains a key and value storing the original values of all modified * fields or is null if no fields have been modified. * @property modified * @type {Object} */ - modified: null, + modified : null, /** - * false when the record does not yet exist in a server-side database (see + * true when the record does not yet exist in a server-side database (see * {@link #markDirty}). Any record which has a real database pk set as its id property * is NOT a phantom -- it's real. * @property phantom @@ -556,7 +565,7 @@ rec.{@link #commit}(); // update the record in the store, bypass setting dirty flag, // and do not store the change in the {@link Ext.data.Store#getModifiedRecords modified records} -rec.{@link #data}['firstname'] = 'Wilma'); // updates record, but not the view +rec.{@link #data}['firstname'] = 'Wilma'; // updates record, but not the view rec.{@link #commit}(); // updates the view * * Notes:
* @param {String} name The {@link Ext.data.Field#name name of the field} to set. - * @param {Object} value The value to set the field to. + * @param {String/Object/Array} value The value to set the field to. */ set : function(name, value){ - var isObj = (typeof value === 'object'); - if(!isObj && String(this.data[name]) === String(value)){ - return; - } else if (isObj && Ext.encode(this.data[name]) === Ext.encode(value)) { + var encode = Ext.isPrimitive(value) ? String : Ext.encode; + if(encode(this.data[name]) == encode(value)) { return; - } + } this.dirty = true; if(!this.modified){ this.modified = {}; } - if(typeof this.modified[name] == 'undefined'){ + if(this.modified[name] === undefined){ this.modified[name] = this.data[name]; } this.data[name] = value; @@ -591,21 +598,21 @@ rec.{@link #commit}(); // updates the view }, // private - afterEdit: function(){ - if(this.store){ + afterEdit : function(){ + if (this.store != undefined && typeof this.store.afterEdit == "function") { this.store.afterEdit(this); } }, // private - afterReject: function(){ + afterReject : function(){ if(this.store){ this.store.afterReject(this); } }, // private - afterCommit: function(){ + afterCommit : function(){ if(this.store){ this.store.afterCommit(this); } @@ -715,10 +722,13 @@ rec.{@link #commit}(); // updates the view }, /** - * Creates a copy of this Record. - * @param {String} id (optional) A new Record id, defaults to {@link #Record.id autogenerating an id}. - * Note: if an id is not specified the copy created will be a - * {@link #phantom} Record. + * Creates a copy (clone) of this Record. + * @param {String} id (optional) A new Record id, defaults to the id + * of the record being copied. See {@link #id}. + * To generate a phantom record with a new id use:

+var rec = record.copy(); // clone the record
+Ext.data.Record.id(rec); // automatically generate a unique sequential id
+     * 
* @return {Record} */ copy : function(newId) { @@ -765,7 +775,8 @@ rec.{@link #commit}(); // updates the view this.modified[f.name] = this.data[f.name]; },this); } -};/** +}; +/** * @class Ext.StoreMgr * @extends Ext.util.MixedCollection * The default global group of stores. @@ -902,282 +913,7 @@ myStore.{@link #insert}(0, r); // insert a new record into the store (also see { * and read the data into Records. * @xtype store */ -Ext.data.Store = function(config){ - this.data = new Ext.util.MixedCollection(false); - this.data.getKey = function(o){ - return o.id; - }; - /** - * See the {@link #baseParams corresponding configuration option} - * for a description of this property. - * To modify this property see {@link #setBaseParam}. - * @property - */ - this.baseParams = {}; - - // temporary removed-records cache - this.removed = []; - - if(config && config.data){ - this.inlineData = config.data; - delete config.data; - } - - Ext.apply(this, config); - - this.paramNames = Ext.applyIf(this.paramNames || {}, this.defaultParamNames); - - if(this.url && !this.proxy){ - this.proxy = new Ext.data.HttpProxy({url: this.url}); - } - // If Store is RESTful, so too is the DataProxy - if (this.restful === true && this.proxy) { - // When operating RESTfully, a unique transaction is generated for each record. - this.batch = false; - Ext.data.Api.restify(this.proxy); - } - - if(this.reader){ // reader passed - if(!this.recordType){ - this.recordType = this.reader.recordType; - } - if(this.reader.onMetaChange){ - //this.reader.onMetaChange = this.onMetaChange.createDelegate(this); - this.reader.onMetaChange = this.reader.onMetaChange.createSequence(this.onMetaChange, this); - } - if (this.writer) { // writer passed - this.writer.meta = this.reader.meta; - this.pruneModifiedRecords = true; - } - } - - /** - * The {@link Ext.data.Record Record} constructor as supplied to (or created by) the - * {@link Ext.data.DataReader Reader}. Read-only. - *

If the Reader was constructed by passing in an Array of {@link Ext.data.Field} definition objects, - * instead of a Record constructor, it will implicitly create a Record constructor from that Array (see - * {@link Ext.data.Record}.{@link Ext.data.Record#create create} for additional details).

- *

This property may be used to create new Records of the type held in this Store, for example:


-// create the data store
-var store = new Ext.data.ArrayStore({
-    autoDestroy: true,
-    fields: [
-       {name: 'company'},
-       {name: 'price', type: 'float'},
-       {name: 'change', type: 'float'},
-       {name: 'pctChange', type: 'float'},
-       {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
-    ]
-});
-store.loadData(myData);
-
-// create the Grid
-var grid = new Ext.grid.EditorGridPanel({
-    store: store,
-    colModel: new Ext.grid.ColumnModel({
-        columns: [
-            {id:'company', header: 'Company', width: 160, dataIndex: 'company'},
-            {header: 'Price', renderer: 'usMoney', dataIndex: 'price'},
-            {header: 'Change', renderer: change, dataIndex: 'change'},
-            {header: '% Change', renderer: pctChange, dataIndex: 'pctChange'},
-            {header: 'Last Updated', width: 85,
-                renderer: Ext.util.Format.dateRenderer('m/d/Y'),
-                dataIndex: 'lastChange'}
-        ],
-        defaults: {
-            sortable: true,
-            width: 75
-        }
-    }),
-    autoExpandColumn: 'company', // match the id specified in the column model
-    height:350,
-    width:600,
-    title:'Array Grid',
-    tbar: [{
-        text: 'Add Record',
-        handler : function(){
-            var defaultData = {
-                change: 0,
-                company: 'New Company',
-                lastChange: (new Date()).clearTime(),
-                pctChange: 0,
-                price: 10
-            };
-            var recId = 3; // provide unique id
-            var p = new store.recordType(defaultData, recId); // create new record
-            grid.stopEditing();
-            store.{@link #insert}(0, p); // insert a new record into the store (also see {@link #add})
-            grid.startEditing(0, 0);
-        }
-    }]
-});
-     * 
- * @property recordType - * @type Function - */ - - if(this.recordType){ - /** - * A {@link Ext.util.MixedCollection MixedCollection} containing the defined {@link Ext.data.Field Field}s - * for the {@link Ext.data.Record Records} stored in this Store. Read-only. - * @property fields - * @type Ext.util.MixedCollection - */ - this.fields = this.recordType.prototype.fields; - } - this.modified = []; - - this.addEvents( - /** - * @event datachanged - * Fires when the data cache has changed in a bulk manner (e.g., it has been sorted, filtered, etc.) and a - * widget that is using this Store as a Record cache should refresh its view. - * @param {Store} this - */ - 'datachanged', - /** - * @event metachange - * Fires when this store's reader provides new metadata (fields). This is currently only supported for JsonReaders. - * @param {Store} this - * @param {Object} meta The JSON metadata - */ - 'metachange', - /** - * @event add - * Fires when Records have been {@link #add}ed to the Store - * @param {Store} this - * @param {Ext.data.Record[]} records The array of Records added - * @param {Number} index The index at which the record(s) were added - */ - 'add', - /** - * @event remove - * Fires when a Record has been {@link #remove}d from the Store - * @param {Store} this - * @param {Ext.data.Record} record The Record that was removed - * @param {Number} index The index at which the record was removed - */ - 'remove', - /** - * @event update - * Fires when a Record has been updated - * @param {Store} this - * @param {Ext.data.Record} record The Record that was updated - * @param {String} operation The update operation being performed. Value may be one of: - *

- Ext.data.Record.EDIT
- Ext.data.Record.REJECT
- Ext.data.Record.COMMIT
-         * 
- */ - 'update', - /** - * @event clear - * Fires when the data cache has been cleared. - * @param {Store} this - * @param {Record[]} The records that were cleared. - */ - 'clear', - /** - * @event exception - *

Fires if an exception occurs in the Proxy during a remote request. - * This event is relayed through the corresponding {@link Ext.data.DataProxy}. - * See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} - * for additional details. - * @param {misc} misc See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} - * for description. - */ - 'exception', - /** - * @event beforeload - * Fires before a request is made for a new data object. If the beforeload handler returns - * false the {@link #load} action will be canceled. - * @param {Store} this - * @param {Object} options The loading options that were specified (see {@link #load} for details) - */ - 'beforeload', - /** - * @event load - * Fires after a new set of Records has been loaded. - * @param {Store} this - * @param {Ext.data.Record[]} records The Records that were loaded - * @param {Object} options The loading options that were specified (see {@link #load} for details) - */ - 'load', - /** - * @event loadexception - *

This event is deprecated in favor of the catch-all {@link #exception} - * event instead.

- *

This event is relayed through the corresponding {@link Ext.data.DataProxy}. - * See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#loadexception loadexception} - * for additional details. - * @param {misc} misc See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#loadexception loadexception} - * for description. - */ - 'loadexception', - /** - * @event beforewrite - * @param {Ext.data.Store} store - * @param {String} action [Ext.data.Api.actions.create|update|destroy] - * @param {Record/Array[Record]} rs - * @param {Object} options The loading options that were specified. Edit options.params to add Http parameters to the request. (see {@link #save} for details) - * @param {Object} arg The callback's arg object passed to the {@link #request} function - */ - 'beforewrite', - /** - * @event write - * Fires if the server returns 200 after an Ext.data.Api.actions CRUD action. - * Success of the action is determined in the result['successProperty']property (NOTE for RESTful stores, - * a simple 20x response is sufficient for the actions "destroy" and "update". The "create" action should should return 200 along with a database pk). - * @param {Ext.data.Store} store - * @param {String} action [Ext.data.Api.actions.create|update|destroy] - * @param {Object} result The 'data' picked-out out of the response for convenience. - * @param {Ext.Direct.Transaction} res - * @param {Record/Record[]} rs Store's records, the subject(s) of the write-action - */ - 'write' - ); - - if(this.proxy){ - this.relayEvents(this.proxy, ['loadexception', 'exception']); - } - // With a writer set for the Store, we want to listen to add/remove events to remotely create/destroy records. - if (this.writer) { - this.on({ - scope: this, - add: this.createRecords, - remove: this.destroyRecord, - update: this.updateRecord, - clear: this.onClear - }); - } - - this.sortToggle = {}; - if(this.sortField){ - this.setDefaultSort(this.sortField, this.sortDir); - }else if(this.sortInfo){ - this.setDefaultSort(this.sortInfo.field, this.sortInfo.direction); - } - - Ext.data.Store.superclass.constructor.call(this); - - if(this.id){ - this.storeId = this.id; - delete this.id; - } - if(this.storeId){ - Ext.StoreMgr.register(this); - } - if(this.inlineData){ - this.loadData(this.inlineData); - delete this.inlineData; - }else if(this.autoLoad){ - this.load.defer(10, this, [ - typeof this.autoLoad == 'object' ? - this.autoLoad : undefined]); - } -}; -Ext.extend(Ext.data.Store, Ext.util.Observable, { +Ext.data.Store = Ext.extend(Ext.util.Observable, { /** * @cfg {String} storeId If passed, the id to use to register with the {@link Ext.StoreMgr StoreMgr}. *

Note: if a (deprecated) {@link #id} is specified it will supersede the storeId @@ -1359,6 +1095,348 @@ sortInfo: { dir : 'dir' }, + /** + * @property isDestroyed + * @type Boolean + * True if the store has been destroyed already. Read only + */ + isDestroyed: false, + + /** + * @property hasMultiSort + * @type Boolean + * True if this store is currently sorted by more than one field/direction combination. + */ + hasMultiSort: false, + + // private + batchKey : '_ext_batch_', + + constructor : function(config){ + this.data = new Ext.util.MixedCollection(false); + this.data.getKey = function(o){ + return o.id; + }; + + + // temporary removed-records cache + this.removed = []; + + if(config && config.data){ + this.inlineData = config.data; + delete config.data; + } + + Ext.apply(this, config); + + /** + * See the {@link #baseParams corresponding configuration option} + * for a description of this property. + * To modify this property see {@link #setBaseParam}. + * @property + */ + this.baseParams = Ext.isObject(this.baseParams) ? this.baseParams : {}; + + this.paramNames = Ext.applyIf(this.paramNames || {}, this.defaultParamNames); + + if((this.url || this.api) && !this.proxy){ + this.proxy = new Ext.data.HttpProxy({url: this.url, api: this.api}); + } + // If Store is RESTful, so too is the DataProxy + if (this.restful === true && this.proxy) { + // When operating RESTfully, a unique transaction is generated for each record. + // TODO might want to allow implemention of faux REST where batch is possible using RESTful routes only. + this.batch = false; + Ext.data.Api.restify(this.proxy); + } + + if(this.reader){ // reader passed + if(!this.recordType){ + this.recordType = this.reader.recordType; + } + if(this.reader.onMetaChange){ + this.reader.onMetaChange = this.reader.onMetaChange.createSequence(this.onMetaChange, this); + } + if (this.writer) { // writer passed + if (this.writer instanceof(Ext.data.DataWriter) === false) { // <-- config-object instead of instance. + this.writer = this.buildWriter(this.writer); + } + this.writer.meta = this.reader.meta; + this.pruneModifiedRecords = true; + } + } + + /** + * The {@link Ext.data.Record Record} constructor as supplied to (or created by) the + * {@link Ext.data.DataReader Reader}. Read-only. + *

If the Reader was constructed by passing in an Array of {@link Ext.data.Field} definition objects, + * instead of a Record constructor, it will implicitly create a Record constructor from that Array (see + * {@link Ext.data.Record}.{@link Ext.data.Record#create create} for additional details).

+ *

This property may be used to create new Records of the type held in this Store, for example:


+    // create the data store
+    var store = new Ext.data.ArrayStore({
+        autoDestroy: true,
+        fields: [
+           {name: 'company'},
+           {name: 'price', type: 'float'},
+           {name: 'change', type: 'float'},
+           {name: 'pctChange', type: 'float'},
+           {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
+        ]
+    });
+    store.loadData(myData);
+
+    // create the Grid
+    var grid = new Ext.grid.EditorGridPanel({
+        store: store,
+        colModel: new Ext.grid.ColumnModel({
+            columns: [
+                {id:'company', header: 'Company', width: 160, dataIndex: 'company'},
+                {header: 'Price', renderer: 'usMoney', dataIndex: 'price'},
+                {header: 'Change', renderer: change, dataIndex: 'change'},
+                {header: '% Change', renderer: pctChange, dataIndex: 'pctChange'},
+                {header: 'Last Updated', width: 85,
+                    renderer: Ext.util.Format.dateRenderer('m/d/Y'),
+                    dataIndex: 'lastChange'}
+            ],
+            defaults: {
+                sortable: true,
+                width: 75
+            }
+        }),
+        autoExpandColumn: 'company', // match the id specified in the column model
+        height:350,
+        width:600,
+        title:'Array Grid',
+        tbar: [{
+            text: 'Add Record',
+            handler : function(){
+                var defaultData = {
+                    change: 0,
+                    company: 'New Company',
+                    lastChange: (new Date()).clearTime(),
+                    pctChange: 0,
+                    price: 10
+                };
+                var recId = 3; // provide unique id
+                var p = new store.recordType(defaultData, recId); // create new record
+                grid.stopEditing();
+                store.{@link #insert}(0, p); // insert a new record into the store (also see {@link #add})
+                grid.startEditing(0, 0);
+            }
+        }]
+    });
+         * 
+ * @property recordType + * @type Function + */ + + if(this.recordType){ + /** + * A {@link Ext.util.MixedCollection MixedCollection} containing the defined {@link Ext.data.Field Field}s + * for the {@link Ext.data.Record Records} stored in this Store. Read-only. + * @property fields + * @type Ext.util.MixedCollection + */ + this.fields = this.recordType.prototype.fields; + } + this.modified = []; + + this.addEvents( + /** + * @event datachanged + * Fires when the data cache has changed in a bulk manner (e.g., it has been sorted, filtered, etc.) and a + * widget that is using this Store as a Record cache should refresh its view. + * @param {Store} this + */ + 'datachanged', + /** + * @event metachange + * Fires when this store's reader provides new metadata (fields). This is currently only supported for JsonReaders. + * @param {Store} this + * @param {Object} meta The JSON metadata + */ + 'metachange', + /** + * @event add + * Fires when Records have been {@link #add}ed to the Store + * @param {Store} this + * @param {Ext.data.Record[]} records The array of Records added + * @param {Number} index The index at which the record(s) were added + */ + 'add', + /** + * @event remove + * Fires when a Record has been {@link #remove}d from the Store + * @param {Store} this + * @param {Ext.data.Record} record The Record that was removed + * @param {Number} index The index at which the record was removed + */ + 'remove', + /** + * @event update + * Fires when a Record has been updated + * @param {Store} this + * @param {Ext.data.Record} record The Record that was updated + * @param {String} operation The update operation being performed. Value may be one of: + *

+     Ext.data.Record.EDIT
+     Ext.data.Record.REJECT
+     Ext.data.Record.COMMIT
+             * 
+ */ + 'update', + /** + * @event clear + * Fires when the data cache has been cleared. + * @param {Store} this + * @param {Record[]} The records that were cleared. + */ + 'clear', + /** + * @event exception + *

Fires if an exception occurs in the Proxy during a remote request. + * This event is relayed through the corresponding {@link Ext.data.DataProxy}. + * See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} + * for additional details. + * @param {misc} misc See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} + * for description. + */ + 'exception', + /** + * @event beforeload + * Fires before a request is made for a new data object. If the beforeload handler returns + * false the {@link #load} action will be canceled. + * @param {Store} this + * @param {Object} options The loading options that were specified (see {@link #load} for details) + */ + 'beforeload', + /** + * @event load + * Fires after a new set of Records has been loaded. + * @param {Store} this + * @param {Ext.data.Record[]} records The Records that were loaded + * @param {Object} options The loading options that were specified (see {@link #load} for details) + */ + 'load', + /** + * @event loadexception + *

This event is deprecated in favor of the catch-all {@link #exception} + * event instead.

+ *

This event is relayed through the corresponding {@link Ext.data.DataProxy}. + * See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#loadexception loadexception} + * for additional details. + * @param {misc} misc See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#loadexception loadexception} + * for description. + */ + 'loadexception', + /** + * @event beforewrite + * @param {Ext.data.Store} store + * @param {String} action [Ext.data.Api.actions.create|update|destroy] + * @param {Record/Record[]} rs The Record(s) being written. + * @param {Object} options The loading options that were specified. Edit options.params to add Http parameters to the request. (see {@link #save} for details) + * @param {Object} arg The callback's arg object passed to the {@link #request} function + */ + 'beforewrite', + /** + * @event write + * Fires if the server returns 200 after an Ext.data.Api.actions CRUD action. + * Success of the action is determined in the result['successProperty']property (NOTE for RESTful stores, + * a simple 20x response is sufficient for the actions "destroy" and "update". The "create" action should should return 200 along with a database pk). + * @param {Ext.data.Store} store + * @param {String} action [Ext.data.Api.actions.create|update|destroy] + * @param {Object} result The 'data' picked-out out of the response for convenience. + * @param {Ext.Direct.Transaction} res + * @param {Record/Record[]} rs Store's records, the subject(s) of the write-action + */ + 'write', + /** + * @event beforesave + * Fires before a save action is called. A save encompasses destroying records, updating records and creating records. + * @param {Ext.data.Store} store + * @param {Object} data An object containing the data that is to be saved. The object will contain a key for each appropriate action, + * with an array of records for each action. + */ + 'beforesave', + /** + * @event save + * Fires after a save is completed. A save encompasses destroying records, updating records and creating records. + * @param {Ext.data.Store} store + * @param {Number} batch The identifier for the batch that was saved. + * @param {Object} data An object containing the data that is to be saved. The object will contain a key for each appropriate action, + * with an array of records for each action. + */ + 'save' + + ); + + if(this.proxy){ + // TODO remove deprecated loadexception with ext-3.0.1 + this.relayEvents(this.proxy, ['loadexception', 'exception']); + } + // With a writer set for the Store, we want to listen to add/remove events to remotely create/destroy records. + if (this.writer) { + this.on({ + scope: this, + add: this.createRecords, + remove: this.destroyRecord, + update: this.updateRecord, + clear: this.onClear + }); + } + + this.sortToggle = {}; + if(this.sortField){ + this.setDefaultSort(this.sortField, this.sortDir); + }else if(this.sortInfo){ + this.setDefaultSort(this.sortInfo.field, this.sortInfo.direction); + } + + Ext.data.Store.superclass.constructor.call(this); + + if(this.id){ + this.storeId = this.id; + delete this.id; + } + if(this.storeId){ + Ext.StoreMgr.register(this); + } + if(this.inlineData){ + this.loadData(this.inlineData); + delete this.inlineData; + }else if(this.autoLoad){ + this.load.defer(10, this, [ + typeof this.autoLoad == 'object' ? + this.autoLoad : undefined]); + } + // used internally to uniquely identify a batch + this.batchCounter = 0; + this.batches = {}; + }, + + /** + * builds a DataWriter instance when Store constructor is provided with a writer config-object instead of an instace. + * @param {Object} config Writer configuration + * @return {Ext.data.DataWriter} + * @private + */ + buildWriter : function(config) { + var klass = undefined, + type = (config.format || 'json').toLowerCase(); + switch (type) { + case 'json': + klass = Ext.data.JsonWriter; + break; + case 'xml': + klass = Ext.data.XmlWriter; + break; + default: + klass = Ext.data.JsonWriter; + } + return new klass(config); + }, + /** * Destroys the store. */ @@ -1410,20 +1488,27 @@ sortInfo: { }, /** - * Remove a Record from the Store and fires the {@link #remove} event. - * @param {Ext.data.Record} record The Ext.data.Record object to remove from the cache. + * Remove Records from the Store and fires the {@link #remove} event. + * @param {Ext.data.Record/Ext.data.Record[]} record The record object or array of records to remove from the cache. */ remove : function(record){ + if(Ext.isArray(record)){ + Ext.each(record, function(r){ + this.remove(r); + }, this); + } var index = this.data.indexOf(record); if(index > -1){ record.join(null); this.data.removeAt(index); - if(this.pruneModifiedRecords){ - this.modified.remove(record); - } - if(this.snapshot){ - this.snapshot.remove(record); - } + } + if(this.pruneModifiedRecords){ + this.modified.remove(record); + } + if(this.snapshot){ + this.snapshot.remove(record); + } + if(index > -1){ this.fireEvent('remove', this, record, index); } }, @@ -1438,8 +1523,9 @@ sortInfo: { /** * Remove all Records from the Store and fires the {@link #clear} event. + * @param {Boolean} silent [false] Defaults to false. Set true to not fire clear event. */ - removeAll : function(){ + removeAll : function(silent){ var items = []; this.each(function(rec){ items.push(rec); @@ -1451,7 +1537,9 @@ sortInfo: { if(this.pruneModifiedRecords){ this.modified = []; } - this.fireEvent('clear', this, items); + if (silent !== true) { // <-- prevents write-actions when we just want to clear a store. + this.fireEvent('clear', this, items); + } }, // private @@ -1473,6 +1561,9 @@ sortInfo: { this.data.insert(index, records[i]); records[i].join(this); } + if(this.snapshot){ + this.snapshot.addAll(records); + } this.fireEvent('add', this, records, index); }, @@ -1500,7 +1591,7 @@ sortInfo: { * @return {Ext.data.Record} The Record with the passed id. Returns undefined if not found. */ getById : function(id){ - return this.data.key(id); + return (this.snapshot || this.data).key(id); }, /** @@ -1556,25 +1647,25 @@ sortInfo: { * parameters to a remote data source. Note: params will override any * {@link #baseParams} of the same name.

*

Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode}.

- *
  • callback : Function

    A function to be called after the Records - * have been loaded. The callback is called after the load event and is passed the following arguments:

  • - *
  • scope : Object

    Scope with which to call the callback (defaults + *

  • callback : Function

    A function to be called after the Records + * have been loaded. The callback is called after the load event is fired, and is passed the following arguments:

  • + *
  • scope : Object

    Scope with which to call the callback (defaults * to the Store object)

  • - *
  • add : Boolean

    Indicator to append loaded records rather than + *

  • add : Boolean

    Indicator to append loaded records rather than * replace the current cache. Note: see note for {@link #loadData}

  • * * @return {Boolean} If the developer provided {@link #beforeload} event handler returns * false, the load call will abort and will return false; otherwise will return true. */ load : function(options) { - options = options || {}; + options = Ext.apply({}, options); this.storeOptions(options); if(this.sortInfo && this.remoteSort){ var pn = this.paramNames; - options.params = options.params || {}; + options.params = Ext.apply({}, options.params); options.params[pn.sort] = this.sortInfo.field; options.params[pn.dir] = this.sortInfo.direction; } @@ -1595,7 +1686,7 @@ sortInfo: { * @private */ updateRecord : function(store, record, action) { - if (action == Ext.data.Record.EDIT && this.autoSave === true && (!record.phantom || (record.phantom && record.isValid))) { + if (action == Ext.data.Record.EDIT && this.autoSave === true && (!record.phantom || (record.phantom && record.isValid()))) { this.save(); } }, @@ -1620,9 +1711,9 @@ sortInfo: { }, /** - * Destroys a record or records. Should not be used directly. It's called by Store#remove if a Writer is set. - * @param {Store} this - * @param {Ext.data.Record/Ext.data.Record[]} + * Destroys a Record. Should not be used directly. It's called by Store#remove if a Writer is set. + * @param {Store} store this + * @param {Ext.data.Record} record * @param {Number} index * @private */ @@ -1654,23 +1745,25 @@ sortInfo: { * @throws Error * @private */ - execute : function(action, rs, options) { + execute : function(action, rs, options, /* private */ batch) { // blow up if action not Ext.data.CREATE, READ, UPDATE, DESTROY if (!Ext.data.Api.isAction(action)) { throw new Ext.data.Api.Error('execute', action); } - // make sure options has a params key + // make sure options has a fresh, new params hash options = Ext.applyIf(options||{}, { params: {} }); - + if(batch !== undefined){ + this.addToBatch(batch); + } // have to separate before-events since load has a different signature than create,destroy and save events since load does not // include the rs (record resultset) parameter. Capture return values from the beforeaction into doRequest flag. var doRequest = true; if (action === 'read') { - Ext.applyIf(options.params, this.baseParams); doRequest = this.fireEvent('beforeload', this, options); + Ext.applyIf(options.params, this.baseParams); } else { // if Writer is configured as listful, force single-record rs to be [{}] instead of {} @@ -1684,19 +1777,20 @@ sortInfo: { } // Write the action to options.params if ((doRequest = this.fireEvent('beforewrite', this, action, rs, options)) !== false) { - this.writer.write(action, options.params, rs); + this.writer.apply(options.params, this.baseParams, action, rs); } } if (doRequest !== false) { // Send request to proxy. - var params = Ext.apply({}, options.params, this.baseParams); if (this.writer && this.proxy.url && !this.proxy.restful && !Ext.data.Api.hasUniqueUrl(this.proxy, action)) { - params.xaction = action; // <-- really old, probaby unecessary. + options.params.xaction = action; // <-- really old, probaby unecessary. } // Note: Up until this point we've been dealing with 'action' as a key from Ext.data.Api.actions. // We'll flip it now and send the value into DataProxy#request, since it's the value which maps to // the user's configured DataProxy#api - this.proxy.request(Ext.data.Api.actions[action], rs, params, this.reader, this.createCallback(action, rs), this, options); + // TODO Refactor all Proxies to accept an instance of Ext.data.Request (not yet defined) instead of this looooooong list + // of params. This method is an artifact from Ext2. + this.proxy.request(Ext.data.Api.actions[action], rs, options.params, this.reader, this.createCallback(action, rs, batch), this, options); } return doRequest; }, @@ -1713,68 +1807,125 @@ sortInfo: { * * @TODO: Create extensions of Error class and send associated Record with thrown exceptions. * e.g.: Ext.data.DataReader.Error or Ext.data.Error or Ext.data.DataProxy.Error, etc. + * @return {Number} batch Returns a number to uniquely identify the "batch" of saves occurring. -1 will be returned + * if there are no items to save or the save was cancelled. */ save : function() { if (!this.writer) { throw new Ext.data.Store.Error('writer-undefined'); } + var queue = [], + len, + trans, + batch, + data = {}; // DESTROY: First check for removed records. Records in this.removed are guaranteed non-phantoms. @see Store#remove - if (this.removed.length) { - this.doTransaction('destroy', this.removed); + if(this.removed.length){ + queue.push(['destroy', this.removed]); } // Check for modified records. Use a copy so Store#rejectChanges will work if server returns error. var rs = [].concat(this.getModifiedRecords()); - if (!rs.length) { // Bail-out if empty... - return true; - } - - // CREATE: Next check for phantoms within rs. splice-off and execute create. - var phantoms = []; - for (var i = rs.length-1; i >= 0; i--) { - if (rs[i].phantom === true) { - var rec = rs.splice(i, 1).shift(); - if (rec.isValid()) { - phantoms.push(rec); + if(rs.length){ + // CREATE: Next check for phantoms within rs. splice-off and execute create. + var phantoms = []; + for(var i = rs.length-1; i >= 0; i--){ + if(rs[i].phantom === true){ + var rec = rs.splice(i, 1).shift(); + if(rec.isValid()){ + phantoms.push(rec); + } + }else if(!rs[i].isValid()){ // <-- while we're here, splice-off any !isValid real records + rs.splice(i,1); } - } else if (!rs[i].isValid()) { // <-- while we're here, splice-off any !isValid real records - rs.splice(i,1); } - } - // If we have valid phantoms, create them... - if (phantoms.length) { - this.doTransaction('create', phantoms); - } + // If we have valid phantoms, create them... + if(phantoms.length){ + queue.push(['create', phantoms]); + } - // UPDATE: And finally, if we're still here after splicing-off phantoms and !isValid real records, update the rest... - if (rs.length) { - this.doTransaction('update', rs); + // UPDATE: And finally, if we're still here after splicing-off phantoms and !isValid real records, update the rest... + if(rs.length){ + queue.push(['update', rs]); + } + } + len = queue.length; + if(len){ + batch = ++this.batchCounter; + for(var i = 0; i < len; ++i){ + trans = queue[i]; + data[trans[0]] = trans[1]; + } + if(this.fireEvent('beforesave', this, data) !== false){ + for(var i = 0; i < len; ++i){ + trans = queue[i]; + this.doTransaction(trans[0], trans[1], batch); + } + return batch; + } } - return true; + return -1; }, // private. Simply wraps call to Store#execute in try/catch. Defers to Store#handleException on error. Loops if batch: false - doTransaction : function(action, rs) { + doTransaction : function(action, rs, batch) { function transaction(records) { - try { - this.execute(action, records); - } catch (e) { + try{ + this.execute(action, records, undefined, batch); + }catch (e){ this.handleException(e); } } - if (this.batch === false) { - for (var i = 0, len = rs.length; i < len; i++) { + if(this.batch === false){ + for(var i = 0, len = rs.length; i < len; i++){ transaction.call(this, rs[i]); } - } else { + }else{ transaction.call(this, rs); } }, + // private + addToBatch : function(batch){ + var b = this.batches, + key = this.batchKey + batch, + o = b[key]; + + if(!o){ + b[key] = o = { + id: batch, + count: 0, + data: {} + }; + } + ++o.count; + }, + + removeFromBatch : function(batch, action, data){ + var b = this.batches, + key = this.batchKey + batch, + o = b[key], + data, + arr; + + + if(o){ + arr = o.data[action] || []; + o.data[action] = arr.concat(data); + if(o.count === 1){ + data = o.data; + delete b[key]; + this.fireEvent('save', this, batch, data); + }else{ + --o.count; + } + } + }, + // @private callback-handler for remote CRUD actions // Do not override -- override loadRecords, onCreateRecords, onDestroyRecords and onUpdateRecords instead. - createCallback : function(action, rs) { + createCallback : function(action, rs, batch) { var actions = Ext.data.Api.actions; return (action == 'read') ? this.loadRecords : function(data, response, success) { // calls: onCreateRecords | onUpdateRecords | onDestroyRecords @@ -1783,6 +1934,7 @@ sortInfo: { if (success === true) { this.fireEvent('write', this, action, data, response, rs); } + this.removeFromBatch(batch, action, data); }; }, @@ -1869,12 +2021,22 @@ sortInfo: { }, /** - *

    Reloads the Record cache from the configured Proxy using the configured {@link Ext.data.Reader Reader} and - * the options from the last load operation performed.

    + *

    Reloads the Record cache from the configured Proxy using the configured + * {@link Ext.data.Reader Reader} and the options from the last load operation + * performed.

    *

    Note: see the Important note in {@link #load}.

    - * @param {Object} options (optional) An Object containing {@link #load loading options} which may - * override the options used in the last {@link #load} operation. See {@link #load} for details (defaults to - * null, in which case the {@link #lastOptions} are used). + * @param {Object} options

    (optional) An Object containing + * {@link #load loading options} which may override the {@link #lastOptions options} + * used in the last {@link #load} operation. See {@link #load} for details + * (defaults to null, in which case the {@link #lastOptions} are + * used).

    + *

    To add new params to the existing params:

    
    +lastOptions = myStore.lastOptions;
    +Ext.apply(lastOptions.params, {
    +    myNewParam: true
    +});
    +myStore.reload(lastOptions);
    +     * 
    */ reload : function(options){ this.load(Ext.applyIf(options||{}, this.lastOptions)); @@ -1883,6 +2045,9 @@ sortInfo: { // private // Called as a callback by the Reader during a load operation. loadRecords : function(o, options, success){ + if (this.isDestroyed === true) { + return; + } if(!o || success === false){ if(success !== false){ this.fireEvent('load', this, [], options); @@ -1974,34 +2139,95 @@ sortInfo: { return this.sortInfo; }, - // private + /** + * @private + * Invokes sortData if we have sortInfo to sort on and are not sorting remotely + */ applySort : function(){ - if(this.sortInfo && !this.remoteSort){ - var s = this.sortInfo, f = s.field; - this.sortData(f, s.direction); + if ((this.sortInfo || this.multiSortInfo) && !this.remoteSort) { + this.sortData(); } }, - // private - sortData : function(f, direction){ - direction = direction || 'ASC'; - var st = this.fields.get(f).sortType; - var fn = function(r1, r2){ - var v1 = st(r1.data[f]), v2 = st(r2.data[f]); - return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0); - }; + /** + * @private + * Performs the actual sorting of data. This checks to see if we currently have a multi sort or not. It applies + * each sorter field/direction pair in turn by building an OR'ed master sorting function and running it against + * the full dataset + */ + sortData : function() { + var sortInfo = this.hasMultiSort ? this.multiSortInfo : this.sortInfo, + direction = sortInfo.direction || "ASC", + sorters = sortInfo.sorters, + sortFns = []; + + //if we just have a single sorter, pretend it's the first in an array + if (!this.hasMultiSort) { + sorters = [{direction: direction, field: sortInfo.field}]; + } + + //create a sorter function for each sorter field/direction combo + for (var i=0, j = sorters.length; i < j; i++) { + sortFns.push(this.createSortFunction(sorters[i].field, sorters[i].direction)); + } + + if (sortFns.length == 0) { + return; + } + + //the direction modifier is multiplied with the result of the sorting functions to provide overall sort direction + //(as opposed to direction per field) + var directionModifier = direction.toUpperCase() == "DESC" ? -1 : 1; + + //create a function which ORs each sorter together to enable multi-sort + var fn = function(r1, r2) { + var result = sortFns[0].call(this, r1, r2); + + //if we have more than one sorter, OR any additional sorter functions together + if (sortFns.length > 1) { + for (var i=1, j = sortFns.length; i < j; i++) { + result = result || sortFns[i].call(this, r1, r2); + } + } + + return directionModifier * result; + }; + + //sort the data this.data.sort(direction, fn); - if(this.snapshot && this.snapshot != this.data){ + if (this.snapshot && this.snapshot != this.data) { this.snapshot.sort(direction, fn); } }, + /** + * Creates and returns a function which sorts an array by the given field and direction + * @param {String} field The field to create the sorter for + * @param {String} direction The direction to sort by (defaults to "ASC") + * @return {Function} A function which sorts by the field/direction combination provided + */ + createSortFunction: function(field, direction) { + direction = direction || "ASC"; + var directionModifier = direction.toUpperCase() == "DESC" ? -1 : 1; + + var sortType = this.fields.get(field).sortType; + + //create a comparison function. Takes 2 records, returns 1 if record 1 is greater, + //-1 if record 2 is greater or 0 if they are equal + return function(r1, r2) { + var v1 = sortType(r1.data[field]), + v2 = sortType(r2.data[field]); + + return directionModifier * (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)); + }; + }, + /** * Sets the default sort column and order to be used by the next {@link #load} operation. * @param {String} fieldName The name of the field to sort by. * @param {String} dir (optional) The sort order, 'ASC' or 'DESC' (case-sensitive, defaults to 'ASC') */ - setDefaultSort : function(field, dir){ + setDefaultSort : function(field, dir) { dir = dir ? dir.toUpperCase() : 'ASC'; this.sortInfo = {field: field, direction: dir}; this.sortToggle[field] = dir; @@ -2011,38 +2237,109 @@ sortInfo: { * Sort the Records. * If remote sorting is used, the sort is performed on the server, and the cache is reloaded. If local * sorting is used, the cache is sorted internally. See also {@link #remoteSort} and {@link #paramNames}. - * @param {String} fieldName The name of the field to sort by. + * This function accepts two call signatures - pass in a field name as the first argument to sort on a single + * field, or pass in an array of sort configuration objects to sort by multiple fields. + * Single sort example: + * store.sort('name', 'ASC'); + * Multi sort example: + * store.sort([ + * { + * field : 'name', + * direction: 'ASC' + * }, + * { + * field : 'salary', + * direction: 'DESC' + * } + * ], 'ASC'); + * In this second form, the sort configs are applied in order, with later sorters sorting within earlier sorters' results. + * For example, if two records with the same name are present they will also be sorted by salary if given the sort configs + * above. Any number of sort configs can be added. + * @param {String/Array} fieldName The name of the field to sort by, or an array of ordered sort configs * @param {String} dir (optional) The sort order, 'ASC' or 'DESC' (case-sensitive, defaults to 'ASC') */ - sort : function(fieldName, dir){ - var f = this.fields.get(fieldName); - if(!f){ - return false; + sort : function(fieldName, dir) { + if (Ext.isArray(arguments[0])) { + return this.multiSort.call(this, fieldName, dir); + } else { + return this.singleSort(fieldName, dir); } - if(!dir){ - if(this.sortInfo && this.sortInfo.field == f.name){ // toggle sort dir - dir = (this.sortToggle[f.name] || 'ASC').toggle('ASC', 'DESC'); - }else{ - dir = f.sortDir; + }, + + /** + * Sorts the store contents by a single field and direction. This is called internally by {@link sort} and would + * not usually be called manually + * @param {String} fieldName The name of the field to sort by. + * @param {String} dir (optional) The sort order, 'ASC' or 'DESC' (case-sensitive, defaults to 'ASC') + */ + singleSort: function(fieldName, dir) { + var field = this.fields.get(fieldName); + if (!field) return false; + + var name = field.name, + sortInfo = this.sortInfo || null, + sortToggle = this.sortToggle ? this.sortToggle[name] : null; + + if (!dir) { + if (sortInfo && sortInfo.field == name) { // toggle sort dir + dir = (this.sortToggle[name] || 'ASC').toggle('ASC', 'DESC'); + } else { + dir = field.sortDir; } } - var st = (this.sortToggle) ? this.sortToggle[f.name] : null; - var si = (this.sortInfo) ? this.sortInfo : null; - this.sortToggle[f.name] = dir; - this.sortInfo = {field: f.name, direction: dir}; - if(!this.remoteSort){ - this.applySort(); - this.fireEvent('datachanged', this); - }else{ + this.sortToggle[name] = dir; + this.sortInfo = {field: name, direction: dir}; + this.hasMultiSort = false; + + if (this.remoteSort) { if (!this.load(this.lastOptions)) { - if (st) { - this.sortToggle[f.name] = st; + if (sortToggle) { + this.sortToggle[name] = sortToggle; } - if (si) { - this.sortInfo = si; + if (sortInfo) { + this.sortInfo = sortInfo; } } + } else { + this.applySort(); + this.fireEvent('datachanged', this); + } + }, + + /** + * Sorts the contents of this store by multiple field/direction sorters. This is called internally by {@link sort} + * and would not usually be called manually. + * Multi sorting only currently applies to local datasets - multiple sort data is not currently sent to a proxy + * if remoteSort is used. + * @param {Array} sorters Array of sorter objects (field and direction) + * @param {String} direction Overall direction to sort the ordered results by (defaults to "ASC") + */ + multiSort: function(sorters, direction) { + this.hasMultiSort = true; + direction = direction || "ASC"; + + //toggle sort direction + if (this.multiSortInfo && direction == this.multiSortInfo.direction) { + direction = direction.toggle("ASC", "DESC"); + } + + /** + * @property multiSortInfo + * @type Object + * Object containing overall sort direction and an ordered array of sorter configs used when sorting on multiple fields + */ + this.multiSortInfo = { + sorters : sorters, + direction: direction + }; + + if (this.remoteSort) { + this.singleSort(sorters[0].field, sorters[0].direction); + + } else { + this.applySort(); + this.fireEvent('datachanged', this); } }, @@ -2050,7 +2347,8 @@ sortInfo: { * Calls the specified function for each of the {@link Ext.data.Record Records} in the cache. * @param {Function} fn The function to call. The {@link Ext.data.Record Record} is passed as the first parameter. * Returning false aborts and exits the iteration. - * @param {Object} scope (optional) The scope in which to call the function (defaults to the {@link Ext.data.Record Record}). + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. + * Defaults to the current {@link Ext.data.Record Record} in the iteration. */ each : function(fn, scope){ this.data.each(fn, scope); @@ -2069,17 +2367,6 @@ sortInfo: { return this.modified; }, - // private - createFilterFn : function(property, value, anyMatch, caseSensitive){ - if(Ext.isEmpty(value, false)){ - return false; - } - value = this.data.createValueMatcher(value, anyMatch, caseSensitive); - return function(r){ - return value.test(r.data[property]); - }; - }, - /** * Sums the value of property for each {@link Ext.data.Record record} between start * and end and returns the result. @@ -2100,15 +2387,105 @@ sortInfo: { }, /** - * Filter the {@link Ext.data.Record records} by a specified property. - * @param {String} field A field on your records + * @private + * Returns a filter function used to test a the given property's value. Defers most of the work to + * Ext.util.MixedCollection's createValueMatcher function + * @param {String} property The property to create the filter function for + * @param {String/RegExp} value The string/regex to compare the property value to + * @param {Boolean} anyMatch True if we don't care if the filter value is not the full value (defaults to false) + * @param {Boolean} caseSensitive True to create a case-sensitive regex (defaults to false) + * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. Ignored if anyMatch is true. + */ + createFilterFn : function(property, value, anyMatch, caseSensitive, exactMatch){ + if(Ext.isEmpty(value, false)){ + return false; + } + value = this.data.createValueMatcher(value, anyMatch, caseSensitive, exactMatch); + return function(r) { + return value.test(r.data[property]); + }; + }, + + /** + * Given an array of filter functions (each with optional scope), constructs and returns a single function that returns + * the result of all of the filters ANDed together + * @param {Array} filters The array of filter objects (each object should contain an 'fn' and optional scope) + * @return {Function} The multiple filter function + */ + createMultipleFilterFn: function(filters) { + return function(record) { + var isMatch = true; + + for (var i=0, j = filters.length; i < j; i++) { + var filter = filters[i], + fn = filter.fn, + scope = filter.scope; + + isMatch = isMatch && fn.call(scope, record); + } + + return isMatch; + }; + }, + + /** + * Filter the {@link Ext.data.Record records} by a specified property. Alternatively, pass an array of filter + * options to filter by more than one property. + * Single filter example: + * store.filter('name', 'Ed', true, true); //finds all records containing the substring 'Ed' + * Multiple filter example: + * store.filter([ + * { + * property : 'name', + * value : 'Ed', + * anyMatch : true, //optional, defaults to true + * caseSensitive: true //optional, defaults to true + * }, + * + * //filter functions can also be passed + * { + * fn : function(record) { + * return record.get('age') == 24 + * }, + * scope: this + * } + * ]); + * @param {String|Array} field A field on your records, or an array containing multiple filter options * @param {String/RegExp} value Either a string that the field should begin with, or a RegExp to test * against the field. * @param {Boolean} anyMatch (optional) true to match any part not just the beginning * @param {Boolean} caseSensitive (optional) true for case sensitive comparison + * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. Ignored if anyMatch is true. */ - filter : function(property, value, anyMatch, caseSensitive){ - var fn = this.createFilterFn(property, value, anyMatch, caseSensitive); + filter : function(property, value, anyMatch, caseSensitive, exactMatch){ + //we can accept an array of filter objects, or a single filter object - normalize them here + if (Ext.isObject(property)) { + property = [property]; + } + + if (Ext.isArray(property)) { + var filters = []; + + //normalize the filters passed into an array of filter functions + for (var i=0, j = property.length; i < j; i++) { + var filter = property[i], + func = filter.fn, + scope = filter.scope || this; + + //if we weren't given a filter function, construct one now + if (!Ext.isFunction(func)) { + func = this.createFilterFn(filter.property, filter.value, filter.anyMatch, filter.caseSensitive, filter.exactMatch); + } + + filters.push({fn: func, scope: scope}); + } + + var fn = this.createMultipleFilterFn(filters); + } else { + //classic single property filter + var fn = this.createFilterFn(property, value, anyMatch, caseSensitive, exactMatch); + } + return fn ? this.filterBy(fn) : this.clearFilter(); }, @@ -2121,7 +2498,7 @@ sortInfo: { * to test for filtering. Access field values using {@link Ext.data.Record#get}.

    *
  • id : Object

    The ID of the Record passed.

  • * - * @param {Object} scope (optional) The scope of the function (defaults to this) + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. */ filterBy : function(fn, scope){ this.snapshot = this.snapshot || this.data; @@ -2129,6 +2506,29 @@ sortInfo: { this.fireEvent('datachanged', this); }, + /** + * Revert to a view of the Record cache with no filtering applied. + * @param {Boolean} suppressEvent If true the filter is cleared silently without firing the + * {@link #datachanged} event. + */ + clearFilter : function(suppressEvent){ + if(this.isFiltered()){ + this.data = this.snapshot; + delete this.snapshot; + if(suppressEvent !== true){ + this.fireEvent('datachanged', this); + } + } + }, + + /** + * Returns true if this store is currently filtered + * @return {Boolean} + */ + isFiltered : function(){ + return !!this.snapshot && this.snapshot != this.data; + }, + /** * Query the records by a specified property. * @param {String} field A field on your records @@ -2152,7 +2552,7 @@ sortInfo: { * to test for filtering. Access field values using {@link Ext.data.Record#get}.

    *
  • id : Object

    The ID of the Record passed.

  • * - * @param {Object} scope (optional) The scope of the function (defaults to this) + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. * @return {MixedCollection} Returns an Ext.util.MixedCollection of the matched records **/ queryBy : function(fn, scope){ @@ -2161,10 +2561,10 @@ sortInfo: { }, /** - * Finds the index of the first matching record in this store by a specific property/value. - * @param {String} property A property on your objects - * @param {String/RegExp} value Either a string that the property value - * should begin with, or a RegExp to test against the property. + * Finds the index of the first matching Record in this store by a specific field value. + * @param {String} fieldName The name of the Record field to test. + * @param {String/RegExp} value Either a string that the field value + * should begin with, or a RegExp to test against the field. * @param {Number} startIndex (optional) The index to start searching at * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning * @param {Boolean} caseSensitive (optional) True for case sensitive comparison @@ -2176,9 +2576,9 @@ sortInfo: { }, /** - * Finds the index of the first matching record in this store by a specific property/value. - * @param {String} property A property on your objects - * @param {String/RegExp} value The value to match against + * Finds the index of the first matching Record in this store by a specific field value. + * @param {String} fieldName The name of the Record field to test. + * @param {Mixed} value The value to match the field against. * @param {Number} startIndex (optional) The index to start searching at * @return {Number} The matched index or -1 */ @@ -2196,7 +2596,7 @@ sortInfo: { * to test for filtering. Access field values using {@link Ext.data.Record#get}.

    *
  • id : Object

    The ID of the Record passed.

  • * - * @param {Object} scope (optional) The scope of the function (defaults to this) + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. * @param {Number} startIndex (optional) The index to start searching at * @return {Number} The matched index or -1 */ @@ -2226,29 +2626,6 @@ sortInfo: { return r; }, - /** - * Revert to a view of the Record cache with no filtering applied. - * @param {Boolean} suppressEvent If true the filter is cleared silently without firing the - * {@link #datachanged} event. - */ - clearFilter : function(suppressEvent){ - if(this.isFiltered()){ - this.data = this.snapshot; - delete this.snapshot; - if(suppressEvent !== true){ - this.fireEvent('datachanged', this); - } - } - }, - - /** - * Returns true if this store is currently filtered - * @return {Boolean} - */ - isFiltered : function(){ - return this.snapshot && this.snapshot != this.data; - }, - // private afterEdit : function(record){ if(this.modified.indexOf(record) == -1){ @@ -2357,7 +2734,6 @@ Ext.apply(Ext.data.Store.Error.prototype, { 'writer-undefined' : 'Attempted to execute a write-action without a DataWriter installed.' } }); - /** * @class Ext.data.Field *

    This class encapsulates the field definition information specified in the field definition objects @@ -2365,107 +2741,49 @@ Ext.apply(Ext.data.Store.Error.prototype, { *

    Developers do not need to instantiate this class. Instances are created by {@link Ext.data.Record.create} * and cached in the {@link Ext.data.Record#fields fields} property of the created Record constructor's prototype.

    */ -Ext.data.Field = function(config){ - if(typeof config == "string"){ - config = {name: config}; - } - Ext.apply(this, config); - - if(!this.type){ - this.type = "auto"; - } - - var st = Ext.data.SortTypes; - // named sortTypes are supported, here we look them up - if(typeof this.sortType == "string"){ - this.sortType = st[this.sortType]; - } - - // set default sortType for strings and dates - if(!this.sortType){ - switch(this.type){ - case "string": - this.sortType = st.asUCString; - break; - case "date": - this.sortType = st.asDate; - break; - default: - this.sortType = st.none; +Ext.data.Field = Ext.extend(Object, { + + constructor : function(config){ + if(Ext.isString(config)){ + config = {name: config}; + } + Ext.apply(this, config); + + var types = Ext.data.Types, + st = this.sortType, + t; + + if(this.type){ + if(Ext.isString(this.type)){ + this.type = Ext.data.Types[this.type.toUpperCase()] || types.AUTO; + } + }else{ + this.type = types.AUTO; } - } - - // define once - var stripRe = /[\$,%]/g; - - // prebuilt conversion function for this field, instead of - // switching every time we're reading a value - if(!this.convert){ - var cv, dateFormat = this.dateFormat; - switch(this.type){ - case "": - case "auto": - case undefined: - cv = function(v){ return v; }; - break; - case "string": - cv = function(v){ return (v === undefined || v === null) ? '' : String(v); }; - break; - case "int": - cv = function(v){ - return v !== undefined && v !== null && v !== '' ? - parseInt(String(v).replace(stripRe, ""), 10) : ''; - }; - break; - case "float": - cv = function(v){ - return v !== undefined && v !== null && v !== '' ? - parseFloat(String(v).replace(stripRe, ""), 10) : ''; - }; - break; - case "bool": - case "boolean": - cv = function(v){ return v === true || v === "true" || v == 1; }; - break; - case "date": - cv = function(v){ - if(!v){ - return ''; - } - if(Ext.isDate(v)){ - return v; - } - if(dateFormat){ - if(dateFormat == "timestamp"){ - return new Date(v*1000); - } - if(dateFormat == "time"){ - return new Date(parseInt(v, 10)); - } - return Date.parseDate(v, dateFormat); - } - var parsed = Date.parse(v); - return parsed ? new Date(parsed) : null; - }; - break; + // named sortTypes are supported, here we look them up + if(Ext.isString(st)){ + this.sortType = Ext.data.SortTypes[st]; + }else if(Ext.isEmpty(st)){ + this.sortType = this.type.sortType; } - this.convert = cv; - } -}; -Ext.data.Field.prototype = { + if(!this.convert){ + this.convert = this.type.convert; + } + }, + /** * @cfg {String} name * The name by which the field is referenced within the Record. This is referenced by, for example, - * the dataIndex property in column definition objects passed to {@link Ext.grid.ColumnModel}. - *

    Note: In the simplest case, if no properties other than name are required, a field + * the dataIndex property in column definition objects passed to {@link Ext.grid.ColumnModel}. + *

    Note: In the simplest case, if no properties other than name are required, a field * definition may consist of just a String for the field name.

    */ /** - * @cfg {String} type - * (Optional) The data type for conversion to displayable value if {@link Ext.data.Field#convert convert} - * has not been specified. Possible values are + * @cfg {Mixed} type + * (Optional) The data type for automatic conversion from received data to the stored value if {@link Ext.data.Field#convert convert} + * has not been specified. This may be specified as a string value. Possible values are *
    + *

    This may also be specified by referencing a member of the {@link Ext.data.Types} class.

    + *

    Developers may create their own application-specific data types by defining new members of the + * {@link Ext.data.Types} class.

    */ /** * @cfg {Function} convert * (Optional) A function which converts the value provided by the Reader into an object that will be stored * in the Record. It is passed the following parameters:
    *
  • rec : Mixed
    The data object containing the row as read by the Reader. * Depending on the Reader type, this could be an Array ({@link Ext.data.ArrayReader ArrayReader}), an object * ({@link Ext.data.JsonReader JsonReader}), or an XML element ({@link Ext.data.XMLReader XMLReader}).
  • @@ -2508,7 +2829,7 @@ var store = new Ext.data.Store({ reader: new Ext.data.JsonReader( { idProperty: 'key', - root: 'daRoot', + root: 'daRoot', totalProperty: 'total' }, Dude // recordType @@ -2533,15 +2854,16 @@ var myData = [ */ /** * @cfg {String} dateFormat - * (Optional) A format string for the {@link Date#parseDate Date.parseDate} function, or "timestamp" if the + *

    (Optional) Used when converting received data into a Date when the {@link #type} is specified as "date".

    + *

    A format string for the {@link Date#parseDate Date.parseDate} function, or "timestamp" if the * value provided by the Reader is a UNIX timestamp, or "time" if the value provided by the Reader is a - * javascript millisecond timestamp. + * javascript millisecond timestamp. See {@link Date}

    */ dateFormat: null, /** * @cfg {Mixed} defaultValue * (Optional) The default value used when a Record is being created by a {@link Ext.data.Reader Reader} - * when the item referenced by the {@link Ext.data.Field#mapping mapping} does not exist in the data + * when the item referenced by the {@link Ext.data.Field#mapping mapping} does not exist in the data * object (i.e. undefined). (defaults to "") */ defaultValue: "", @@ -2589,340 +2911,381 @@ sortType: function(value) { sortType : null, /** * @cfg {String} sortDir - * (Optional) Initial direction to sort ("ASC" or "DESC"). Defaults to - * "ASC". + * (Optional) Initial direction to sort ("ASC" or "DESC"). Defaults to + * "ASC". */ sortDir : "ASC", - /** - * @cfg {Boolean} allowBlank - * (Optional) Used for validating a {@link Ext.data.Record record}, defaults to true. - * An empty value here will cause {@link Ext.data.Record}.{@link Ext.data.Record#isValid isValid} - * to evaluate to false. - */ - allowBlank : true -};/** - * @class Ext.data.DataReader - * Abstract base class for reading structured data from a data source and converting - * it into an object containing {@link Ext.data.Record} objects and metadata for use - * by an {@link Ext.data.Store}. This class is intended to be extended and should not - * be created directly. For existing implementations, see {@link Ext.data.ArrayReader}, - * {@link Ext.data.JsonReader} and {@link Ext.data.XmlReader}. - * @constructor Create a new DataReader - * @param {Object} meta Metadata configuration options (implementation-specific). - * @param {Array/Object} recordType - *

    Either an Array of {@link Ext.data.Field Field} definition objects (which - * will be passed to {@link Ext.data.Record#create}, or a {@link Ext.data.Record Record} - * constructor created using {@link Ext.data.Record#create}.

    - */ -Ext.data.DataReader = function(meta, recordType){ - /** - * This DataReader's configured metadata as passed to the constructor. - * @type Mixed - * @property meta - */ - this.meta = meta; - /** - * @cfg {Array/Object} fields - *

    Either an Array of {@link Ext.data.Field Field} definition objects (which - * will be passed to {@link Ext.data.Record#create}, or a {@link Ext.data.Record Record} - * constructor created from {@link Ext.data.Record#create}.

    - */ - this.recordType = Ext.isArray(recordType) ? - Ext.data.Record.create(recordType) : recordType; - - // if recordType defined make sure extraction functions are defined - if (this.recordType){ - this.buildExtractors(); - } -}; - -Ext.data.DataReader.prototype = { - /** - * @cfg {String} messageProperty [undefined] Optional name of a property within a server-response that represents a user-feedback message. - */ - /** - * Abstract method created in extension's buildExtractors impl. - */ - getTotal: Ext.emptyFn, - /** - * Abstract method created in extension's buildExtractors impl. - */ - getRoot: Ext.emptyFn, - /** - * Abstract method created in extension's buildExtractors impl. - */ - getMessage: Ext.emptyFn, - /** - * Abstract method created in extension's buildExtractors impl. - */ - getSuccess: Ext.emptyFn, - /** - * Abstract method created in extension's buildExtractors impl. - */ - getId: Ext.emptyFn, - /** - * Abstract method, overridden in DataReader extensions such as {@link Ext.data.JsonReader} and {@link Ext.data.XmlReader} - */ - buildExtractors : Ext.emptyFn, - /** - * Abstract method overridden in DataReader extensions such as {@link Ext.data.JsonReader} and {@link Ext.data.XmlReader} - */ - extractData : Ext.emptyFn, - /** - * Abstract method overridden in DataReader extensions such as {@link Ext.data.JsonReader} and {@link Ext.data.XmlReader} - */ - extractValues : Ext.emptyFn, - - /** - * Used for un-phantoming a record after a successful database insert. Sets the records pk along with new data from server. - * You must return at least the database pk using the idProperty defined in your DataReader configuration. The incoming - * data from server will be merged with the data in the local record. - * In addition, you must return record-data from the server in the same order received. - * Will perform a commit as well, un-marking dirty-fields. Store's "update" event will be suppressed. - * @param {Record/Record[]} record The phantom record to be realized. - * @param {Object/Object[]} data The new record data to apply. Must include the primary-key from database defined in idProperty field. - */ - realize: function(rs, data){ - if (Ext.isArray(rs)) { - for (var i = rs.length - 1; i >= 0; i--) { - // recurse - if (Ext.isArray(data)) { - this.realize(rs.splice(i,1).shift(), data.splice(i,1).shift()); - } - else { - // weird...rs is an array but data isn't?? recurse but just send in the whole invalid data object. - // the else clause below will detect !this.isData and throw exception. - this.realize(rs.splice(i,1).shift(), data); - } - } - } - else { - // If rs is NOT an array but data IS, see if data contains just 1 record. If so extract it and carry on. - if (Ext.isArray(data) && data.length == 1) { - data = data.shift(); - } - if (!this.isData(data)) { - // TODO: Let exception-handler choose to commit or not rather than blindly rs.commit() here. - //rs.commit(); - throw new Ext.data.DataReader.Error('realize', rs); - } - rs.phantom = false; // <-- That's what it's all about - rs._phid = rs.id; // <-- copy phantom-id -> _phid, so we can remap in Store#onCreateRecords - rs.id = this.getId(data); - rs.data = data; - rs.commit(); - } - }, - - /** - * Used for updating a non-phantom or "real" record's data with fresh data from server after remote-save. - * If returning data from multiple-records after a batch-update, you must return record-data from the server in - * the same order received. Will perform a commit as well, un-marking dirty-fields. Store's "update" event will be - * suppressed as the record receives fresh new data-hash - * @param {Record/Record[]} rs - * @param {Object/Object[]} data - */ - update : function(rs, data) { - if (Ext.isArray(rs)) { - for (var i=rs.length-1; i >= 0; i--) { - if (Ext.isArray(data)) { - this.update(rs.splice(i,1).shift(), data.splice(i,1).shift()); - } - else { - // weird...rs is an array but data isn't?? recurse but just send in the whole data object. - // the else clause below will detect !this.isData and throw exception. - this.update(rs.splice(i,1).shift(), data); - } - } - } - else { - // If rs is NOT an array but data IS, see if data contains just 1 record. If so extract it and carry on. - if (Ext.isArray(data) && data.length == 1) { - data = data.shift(); - } - if (this.isData(data)) { - rs.data = Ext.apply(rs.data, data); - } - rs.commit(); - } - }, - - /** - * Returns true if the supplied data-hash looks and quacks like data. Checks to see if it has a key - * corresponding to idProperty defined in your DataReader config containing non-empty pk. - * @param {Object} data - * @return {Boolean} - */ - isData : function(data) { - return (data && Ext.isObject(data) && !Ext.isEmpty(this.getId(data))) ? true : false; - }, - - // private function a store will createSequence upon - onMetaChange : function(meta){ - delete this.ef; - this.meta = meta; - this.recordType = Ext.data.Record.create(meta.fields); - this.buildExtractors(); - } -}; - -/** - * @class Ext.data.DataReader.Error - * @extends Ext.Error - * General error class for Ext.data.DataReader - */ -Ext.data.DataReader.Error = Ext.extend(Ext.Error, { - constructor : function(message, arg) { - this.arg = arg; - Ext.Error.call(this, message); - }, - name: 'Ext.data.DataReader' -}); -Ext.apply(Ext.data.DataReader.Error.prototype, { - lang : { - 'update': "#update received invalid data from server. Please see docs for DataReader#update and review your DataReader configuration.", - 'realize': "#realize was called with invalid remote-data. Please see the docs for DataReader#realize and review your DataReader configuration.", - 'invalid-response': "#readResponse received an invalid response from the server." - } -}); - - -/** - * Ext.data.Response - * A generic response class to normalize response-handling internally to the framework. - * TODO move to own file, add to jsb. - */ -Ext.data.Response = function(params) { - Ext.apply(this, params); -}; -Ext.data.Response.prototype = { - /** - * @property {String} action {@link Ext.data.Api#actions} - */ - action: undefined, - /** - * @property {Boolean} success - */ - success : undefined, - /** - * @property {String} message - */ - message : undefined, - /** - * @property {Array/Object} data - */ - data: undefined, - /** - * @property {Object} raw The raw response returned from server-code - */ - raw: undefined, - /** - * @property {Ext.data.Record/Ext.data.Record[]} record(s) related to the Request action - */ - records: undefined -} -/** - * @class Ext.data.DataWriter - *

    Ext.data.DataWriter facilitates create, update, and destroy actions between - * an Ext.data.Store and a server-side framework. A Writer enabled Store will - * automatically manage the Ajax requests to perform CRUD actions on a Store.

    - *

    Ext.data.DataWriter is an abstract base class which is intended to be extended - * and should not be created directly. For existing implementations, see - * {@link Ext.data.JsonWriter}.

    - *

    Creating a writer is simple:

    - *
    
    -var writer = new Ext.data.JsonWriter();
    - * 
    - *

    The proxy for a writer enabled store can be configured with a simple url:

    - *
    
    -// Create a standard HttpProxy instance.
    -var proxy = new Ext.data.HttpProxy({
    -    url: 'app.php/users'
    -});
    - * 
    - *

    For finer grained control, the proxy may also be configured with an api:

    - *
    
    -// Use the api specification
    -var proxy = new Ext.data.HttpProxy({
    -    api: {
    -        read    : 'app.php/users/read',
    -        create  : 'app.php/users/create',
    -        update  : 'app.php/users/update',
    -        destroy : 'app.php/users/destroy'
    -    }
    -});
    - * 
    - *

    Creating a Writer enabled store:

    - *
    
    -var store = new Ext.data.Store({
    -    proxy: proxy,
    -    reader: reader,
    -    writer: writer
    +    /**
    +     * @cfg {Boolean} allowBlank
    +     * (Optional) Used for validating a {@link Ext.data.Record record}, defaults to true.
    +     * An empty value here will cause {@link Ext.data.Record}.{@link Ext.data.Record#isValid isValid}
    +     * to evaluate to false.
    +     */
    +    allowBlank : true
     });
    - * 
    - * @constructor Create a new DataWriter - * @param {Object} meta Metadata configuration options (implementation-specific) - * @param {Object} recordType Either an Array of field definition objects as specified - * in {@link Ext.data.Record#create}, or an {@link Ext.data.Record} object created - * using {@link Ext.data.Record#create}. +/** + * @class Ext.data.DataReader + * Abstract base class for reading structured data from a data source and converting + * it into an object containing {@link Ext.data.Record} objects and metadata for use + * by an {@link Ext.data.Store}. This class is intended to be extended and should not + * be created directly. For existing implementations, see {@link Ext.data.ArrayReader}, + * {@link Ext.data.JsonReader} and {@link Ext.data.XmlReader}. + * @constructor Create a new DataReader + * @param {Object} meta Metadata configuration options (implementation-specific). + * @param {Array/Object} recordType + *

    Either an Array of {@link Ext.data.Field Field} definition objects (which + * will be passed to {@link Ext.data.Record#create}, or a {@link Ext.data.Record Record} + * constructor created using {@link Ext.data.Record#create}.

    */ -Ext.data.DataWriter = function(config){ - Ext.apply(this, config); -}; -Ext.data.DataWriter.prototype = { - +Ext.data.DataReader = function(meta, recordType){ /** - * @cfg {Boolean} writeAllFields - * false by default. Set true to have DataWriter return ALL fields of a modified - * record -- not just those that changed. - * false to have DataWriter only request modified fields from a record. + * This DataReader's configured metadata as passed to the constructor. + * @type Mixed + * @property meta */ - writeAllFields : false, + this.meta = meta; /** - * @cfg {Boolean} listful - * false by default. Set true to have the DataWriter always write HTTP params as a list, - * even when acting upon a single record. + * @cfg {Array/Object} fields + *

    Either an Array of {@link Ext.data.Field Field} definition objects (which + * will be passed to {@link Ext.data.Record#create}, or a {@link Ext.data.Record Record} + * constructor created from {@link Ext.data.Record#create}.

    */ - listful : false, // <-- listful is actually not used internally here in DataWriter. @see Ext.data.Store#execute. + this.recordType = Ext.isArray(recordType) ? + Ext.data.Record.create(recordType) : recordType; + // if recordType defined make sure extraction functions are defined + if (this.recordType){ + this.buildExtractors(); + } +}; + +Ext.data.DataReader.prototype = { /** - * Writes data in preparation for server-write action. Simply proxies to DataWriter#update, DataWriter#create - * DataWriter#destroy. - * @param {String} action [CREATE|UPDATE|DESTROY] - * @param {Object} params The params-hash to write-to - * @param {Record/Record[]} rs The recordset write. + * @cfg {String} messageProperty [undefined] Optional name of a property within a server-response that represents a user-feedback message. */ - write : function(action, params, rs) { - var data = [], - renderer = action + 'Record'; - // TODO implement @cfg listful here - if (Ext.isArray(rs)) { - Ext.each(rs, function(rec){ - data.push(this[renderer](rec)); - }, this); - } - else if (rs instanceof Ext.data.Record) { - data = this[renderer](rs); - } - this.render(action, rs, params, data); - }, - /** - * abstract method meant to be overridden by all DataWriter extensions. It's the extension's job to apply the "data" to the "params". - * The data-object provided to render is populated with data according to the meta-info defined in the user's DataReader config, - * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] - * @param {Record[]} rs Store recordset - * @param {Object} params Http params to be sent to server. - * @param {Object} data object populated according to DataReader meta-data. + * Abstract method created in extension's buildExtractors impl. */ - render : Ext.emptyFn, - + getTotal: Ext.emptyFn, /** - * @cfg {Function} updateRecord Abstract method that should be implemented in all subclasses - * (e.g.: {@link Ext.data.JsonWriter#updateRecord JsonWriter.updateRecord} + * Abstract method created in extension's buildExtractors impl. */ - updateRecord : Ext.emptyFn, - + getRoot: Ext.emptyFn, + /** + * Abstract method created in extension's buildExtractors impl. + */ + getMessage: Ext.emptyFn, + /** + * Abstract method created in extension's buildExtractors impl. + */ + getSuccess: Ext.emptyFn, + /** + * Abstract method created in extension's buildExtractors impl. + */ + getId: Ext.emptyFn, + /** + * Abstract method, overridden in DataReader extensions such as {@link Ext.data.JsonReader} and {@link Ext.data.XmlReader} + */ + buildExtractors : Ext.emptyFn, + /** + * Abstract method overridden in DataReader extensions such as {@link Ext.data.JsonReader} and {@link Ext.data.XmlReader} + */ + extractValues : Ext.emptyFn, + + /** + * Used for un-phantoming a record after a successful database insert. Sets the records pk along with new data from server. + * You must return at least the database pk using the idProperty defined in your DataReader configuration. The incoming + * data from server will be merged with the data in the local record. + * In addition, you must return record-data from the server in the same order received. + * Will perform a commit as well, un-marking dirty-fields. Store's "update" event will be suppressed. + * @param {Record/Record[]} record The phantom record to be realized. + * @param {Object/Object[]} data The new record data to apply. Must include the primary-key from database defined in idProperty field. + */ + realize: function(rs, data){ + if (Ext.isArray(rs)) { + for (var i = rs.length - 1; i >= 0; i--) { + // recurse + if (Ext.isArray(data)) { + this.realize(rs.splice(i,1).shift(), data.splice(i,1).shift()); + } + else { + // weird...rs is an array but data isn't?? recurse but just send in the whole invalid data object. + // the else clause below will detect !this.isData and throw exception. + this.realize(rs.splice(i,1).shift(), data); + } + } + } + else { + // If rs is NOT an array but data IS, see if data contains just 1 record. If so extract it and carry on. + if (Ext.isArray(data) && data.length == 1) { + data = data.shift(); + } + if (!this.isData(data)) { + // TODO: Let exception-handler choose to commit or not rather than blindly rs.commit() here. + //rs.commit(); + throw new Ext.data.DataReader.Error('realize', rs); + } + rs.phantom = false; // <-- That's what it's all about + rs._phid = rs.id; // <-- copy phantom-id -> _phid, so we can remap in Store#onCreateRecords + rs.id = this.getId(data); + rs.data = data; + + rs.commit(); + } + }, + + /** + * Used for updating a non-phantom or "real" record's data with fresh data from server after remote-save. + * If returning data from multiple-records after a batch-update, you must return record-data from the server in + * the same order received. Will perform a commit as well, un-marking dirty-fields. Store's "update" event will be + * suppressed as the record receives fresh new data-hash + * @param {Record/Record[]} rs + * @param {Object/Object[]} data + */ + update : function(rs, data) { + if (Ext.isArray(rs)) { + for (var i=rs.length-1; i >= 0; i--) { + if (Ext.isArray(data)) { + this.update(rs.splice(i,1).shift(), data.splice(i,1).shift()); + } + else { + // weird...rs is an array but data isn't?? recurse but just send in the whole data object. + // the else clause below will detect !this.isData and throw exception. + this.update(rs.splice(i,1).shift(), data); + } + } + } + else { + // If rs is NOT an array but data IS, see if data contains just 1 record. If so extract it and carry on. + if (Ext.isArray(data) && data.length == 1) { + data = data.shift(); + } + if (this.isData(data)) { + rs.data = Ext.apply(rs.data, data); + } + rs.commit(); + } + }, + + /** + * returns extracted, type-cast rows of data. Iterates to call #extractValues for each row + * @param {Object[]/Object} data-root from server response + * @param {Boolean} returnRecords [false] Set true to return instances of Ext.data.Record + * @private + */ + extractData : function(root, returnRecords) { + // A bit ugly this, too bad the Record's raw data couldn't be saved in a common property named "raw" or something. + var rawName = (this instanceof Ext.data.JsonReader) ? 'json' : 'node'; + + var rs = []; + + // Had to add Check for XmlReader, #isData returns true if root is an Xml-object. Want to check in order to re-factor + // #extractData into DataReader base, since the implementations are almost identical for JsonReader, XmlReader + if (this.isData(root) && !(this instanceof Ext.data.XmlReader)) { + root = [root]; + } + var f = this.recordType.prototype.fields, + fi = f.items, + fl = f.length, + rs = []; + if (returnRecords === true) { + var Record = this.recordType; + for (var i = 0; i < root.length; i++) { + var n = root[i]; + var record = new Record(this.extractValues(n, fi, fl), this.getId(n)); + record[rawName] = n; // <-- There's implementation of ugly bit, setting the raw record-data. + rs.push(record); + } + } + else { + for (var i = 0; i < root.length; i++) { + var data = this.extractValues(root[i], fi, fl); + data[this.meta.idProperty] = this.getId(root[i]); + rs.push(data); + } + } + return rs; + }, + + /** + * Returns true if the supplied data-hash looks and quacks like data. Checks to see if it has a key + * corresponding to idProperty defined in your DataReader config containing non-empty pk. + * @param {Object} data + * @return {Boolean} + */ + isData : function(data) { + return (data && Ext.isObject(data) && !Ext.isEmpty(this.getId(data))) ? true : false; + }, + + // private function a store will createSequence upon + onMetaChange : function(meta){ + delete this.ef; + this.meta = meta; + this.recordType = Ext.data.Record.create(meta.fields); + this.buildExtractors(); + } +}; + +/** + * @class Ext.data.DataReader.Error + * @extends Ext.Error + * General error class for Ext.data.DataReader + */ +Ext.data.DataReader.Error = Ext.extend(Ext.Error, { + constructor : function(message, arg) { + this.arg = arg; + Ext.Error.call(this, message); + }, + name: 'Ext.data.DataReader' +}); +Ext.apply(Ext.data.DataReader.Error.prototype, { + lang : { + 'update': "#update received invalid data from server. Please see docs for DataReader#update and review your DataReader configuration.", + 'realize': "#realize was called with invalid remote-data. Please see the docs for DataReader#realize and review your DataReader configuration.", + 'invalid-response': "#readResponse received an invalid response from the server." + } +}); +/** + * @class Ext.data.DataWriter + *

    Ext.data.DataWriter facilitates create, update, and destroy actions between + * an Ext.data.Store and a server-side framework. A Writer enabled Store will + * automatically manage the Ajax requests to perform CRUD actions on a Store.

    + *

    Ext.data.DataWriter is an abstract base class which is intended to be extended + * and should not be created directly. For existing implementations, see + * {@link Ext.data.JsonWriter}.

    + *

    Creating a writer is simple:

    + *
    
    +var writer = new Ext.data.JsonWriter({
    +    encode: false   // <--- false causes data to be printed to jsonData config-property of Ext.Ajax#reqeust
    +});
    + * 
    + * *

    Same old JsonReader as Ext-2.x:

    + *
    
    +var reader = new Ext.data.JsonReader({idProperty: 'id'}, [{name: 'first'}, {name: 'last'}, {name: 'email'}]);
    + * 
    + * + *

    The proxy for a writer enabled store can be configured with a simple url:

    + *
    
    +// Create a standard HttpProxy instance.
    +var proxy = new Ext.data.HttpProxy({
    +    url: 'app.php/users'    // <--- Supports "provides"-type urls, such as '/users.json', '/products.xml' (Hello Rails/Merb)
    +});
    + * 
    + *

    For finer grained control, the proxy may also be configured with an API:

    + *
    
    +// Maximum flexibility with the API-configuration
    +var proxy = new Ext.data.HttpProxy({
    +    api: {
    +        read    : 'app.php/users/read',
    +        create  : 'app.php/users/create',
    +        update  : 'app.php/users/update',
    +        destroy : {  // <--- Supports object-syntax as well
    +            url: 'app.php/users/destroy',
    +            method: "DELETE"
    +        }
    +    }
    +});
    + * 
    + *

    Pulling it all together into a Writer-enabled Store:

    + *
    
    +var store = new Ext.data.Store({
    +    proxy: proxy,
    +    reader: reader,
    +    writer: writer,
    +    autoLoad: true,
    +    autoSave: true  // -- Cell-level updates.
    +});
    + * 
    + *

    Initiating write-actions automatically, using the existing Ext2.0 Store/Record API:

    + *
    
    +var rec = store.getAt(0);
    +rec.set('email', 'foo@bar.com');  // <--- Immediately initiates an UPDATE action through configured proxy.
    +
    +store.remove(rec);  // <---- Immediately initiates a DESTROY action through configured proxy.
    + * 
    + *

    For record/batch updates, use the Store-configuration {@link Ext.data.Store#autoSave autoSave:false}

    + *
    
    +var store = new Ext.data.Store({
    +    proxy: proxy,
    +    reader: reader,
    +    writer: writer,
    +    autoLoad: true,
    +    autoSave: false  // -- disable cell-updates
    +});
    +
    +var urec = store.getAt(0);
    +urec.set('email', 'foo@bar.com');
    +
    +var drec = store.getAt(1);
    +store.remove(drec);
    +
    +// Push the button!
    +store.save();
    + * 
    + * @constructor Create a new DataWriter + * @param {Object} meta Metadata configuration options (implementation-specific) + * @param {Object} recordType Either an Array of field definition objects as specified + * in {@link Ext.data.Record#create}, or an {@link Ext.data.Record} object created + * using {@link Ext.data.Record#create}. + */ +Ext.data.DataWriter = function(config){ + Ext.apply(this, config); +}; +Ext.data.DataWriter.prototype = { + + /** + * @cfg {Boolean} writeAllFields + * false by default. Set true to have DataWriter return ALL fields of a modified + * record -- not just those that changed. + * false to have DataWriter only request modified fields from a record. + */ + writeAllFields : false, + /** + * @cfg {Boolean} listful + * false by default. Set true to have the DataWriter always write HTTP params as a list, + * even when acting upon a single record. + */ + listful : false, // <-- listful is actually not used internally here in DataWriter. @see Ext.data.Store#execute. + + /** + * Compiles a Store recordset into a data-format defined by an extension such as {@link Ext.data.JsonWriter} or {@link Ext.data.XmlWriter} in preparation for a {@link Ext.data.Api#actions server-write action}. The first two params are similar similar in nature to {@link Ext#apply}, + * Where the first parameter is the receiver of paramaters and the second, baseParams, the source. + * @param {Object} params The request-params receiver. + * @param {Object} baseParams as defined by {@link Ext.data.Store#baseParams}. The baseParms must be encoded by the extending class, eg: {@link Ext.data.JsonWriter}, {@link Ext.data.XmlWriter}. + * @param {String} action [{@link Ext.data.Api#actions create|update|destroy}] + * @param {Record/Record[]} rs The recordset to write, the subject(s) of the write action. + */ + apply : function(params, baseParams, action, rs) { + var data = [], + renderer = action + 'Record'; + // TODO implement @cfg listful here + if (Ext.isArray(rs)) { + Ext.each(rs, function(rec){ + data.push(this[renderer](rec)); + }, this); + } + else if (rs instanceof Ext.data.Record) { + data = this[renderer](rs); + } + this.render(params, baseParams, data); + }, + + /** + * abstract method meant to be overridden by all DataWriter extensions. It's the extension's job to apply the "data" to the "params". + * The data-object provided to render is populated with data according to the meta-info defined in the user's DataReader config, + * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] + * @param {Record[]} rs Store recordset + * @param {Object} params Http params to be sent to server. + * @param {Object} data object populated according to DataReader meta-data. + */ + render : Ext.emptyFn, + + /** + * @cfg {Function} updateRecord Abstract method that should be implemented in all subclasses + * (e.g.: {@link Ext.data.JsonWriter#updateRecord JsonWriter.updateRecord} + */ + updateRecord : Ext.emptyFn, + /** * @cfg {Function} createRecord Abstract method that should be implemented in all subclasses * (e.g.: {@link Ext.data.JsonWriter#createRecord JsonWriter.createRecord}) @@ -2936,11 +3299,16 @@ Ext.data.DataWriter.prototype = { destroyRecord : Ext.emptyFn, /** - * Converts a Record to a hash. - * @param {Record} - * @private + * Converts a Record to a hash, taking into account the state of the Ext.data.Record along with configuration properties + * related to its rendering, such as {@link #writeAllFields}, {@link Ext.data.Record#phantom phantom}, {@link Ext.data.Record#getChanges getChanges} and + * {@link Ext.data.DataReader#idProperty idProperty} + * @param {Ext.data.Record} rec The Record from which to create a hash. + * @param {Object} config NOT YET IMPLEMENTED. Will implement an exlude/only configuration for fine-control over which fields do/don't get rendered. + * @return {Object} + * @protected + * TODO Implement excludes/only configuration with 2nd param? */ - toHash : function(rec) { + toHash : function(rec, config) { var map = rec.fields.map, data = {}, raw = (this.writeAllFields === false && rec.phantom === false) ? rec.getChanges() : rec.data, @@ -2961,1060 +3329,1390 @@ Ext.data.DataWriter.prototype = { data[this.meta.idProperty] = rec.id } return data; + }, + + /** + * Converts a {@link Ext.data.DataWriter#toHash Hashed} {@link Ext.data.Record} to fields-array array suitable + * for encoding to xml via XTemplate, eg: +
    <tpl for="."><{name}>{value}</{name}</tpl>
    + * eg, non-phantom: +
    {id: 1, first: 'foo', last: 'bar'} --> [{name: 'id', value: 1}, {name: 'first', value: 'foo'}, {name: 'last', value: 'bar'}]
    + * {@link Ext.data.Record#phantom Phantom} records will have had their idProperty omitted in {@link #toHash} if determined to be auto-generated. + * Non AUTOINCREMENT pks should have been protected. + * @param {Hash} data Hashed by Ext.data.DataWriter#toHash + * @return {[Object]} Array of attribute-objects. + * @protected + */ + toArray : function(data) { + var fields = []; + Ext.iterate(data, function(k, v) {fields.push({name: k, value: v});},this); + return fields; + } +};/** + * @class Ext.data.DataProxy + * @extends Ext.util.Observable + *

    Abstract base class for implementations which provide retrieval of unformatted data objects. + * This class is intended to be extended and should not be created directly. For existing implementations, + * see {@link Ext.data.DirectProxy}, {@link Ext.data.HttpProxy}, {@link Ext.data.ScriptTagProxy} and + * {@link Ext.data.MemoryProxy}.

    + *

    DataProxy implementations are usually used in conjunction with an implementation of {@link Ext.data.DataReader} + * (of the appropriate type which knows how to parse the data object) to provide a block of + * {@link Ext.data.Records} to an {@link Ext.data.Store}.

    + *

    The parameter to a DataProxy constructor may be an {@link Ext.data.Connection} or can also be the + * config object to an {@link Ext.data.Connection}.

    + *

    Custom implementations must implement either the doRequest method (preferred) or the + * load method (deprecated). See + * {@link Ext.data.HttpProxy}.{@link Ext.data.HttpProxy#doRequest doRequest} or + * {@link Ext.data.HttpProxy}.{@link Ext.data.HttpProxy#load load} for additional details.

    + *

    Example 1

    + *
    
    +proxy: new Ext.data.ScriptTagProxy({
    +    {@link Ext.data.Connection#url url}: 'http://extjs.com/forum/topics-remote.php'
    +}),
    + * 
    + *

    Example 2

    + *
    
    +proxy : new Ext.data.HttpProxy({
    +    {@link Ext.data.Connection#method method}: 'GET',
    +    {@link Ext.data.HttpProxy#prettyUrls prettyUrls}: false,
    +    {@link Ext.data.Connection#url url}: 'local/default.php', // see options parameter for {@link Ext.Ajax#request}
    +    {@link #api}: {
    +        // all actions except the following will use above url
    +        create  : 'local/new.php',
    +        update  : 'local/update.php'
    +    }
    +}),
    + * 
    + *

    And new in Ext version 3, attach centralized event-listeners upon the DataProxy class itself! This is a great place + * to implement a messaging system to centralize your application's user-feedback and error-handling.

    + *
    
    +// Listen to all "beforewrite" event fired by all proxies.
    +Ext.data.DataProxy.on('beforewrite', function(proxy, action) {
    +    console.log('beforewrite: ', action);
    +});
    +
    +// Listen to "write" event fired by all proxies
    +Ext.data.DataProxy.on('write', function(proxy, action, data, res, rs) {
    +    console.info('write: ', action);
    +});
    +
    +// Listen to "exception" event fired by all proxies
    +Ext.data.DataProxy.on('exception', function(proxy, type, action) {
    +    console.error(type + action + ' exception);
    +});
    + * 
    + * Note: These three events are all fired with the signature of the corresponding DataProxy instance event {@link #beforewrite beforewrite}, {@link #write write} and {@link #exception exception}. + */ +Ext.data.DataProxy = function(conn){ + // make sure we have a config object here to support ux proxies. + // All proxies should now send config into superclass constructor. + conn = conn || {}; + + // This line caused a bug when people use custom Connection object having its own request method. + // http://extjs.com/forum/showthread.php?t=67194. Have to set DataProxy config + //Ext.applyIf(this, conn); + + this.api = conn.api; + this.url = conn.url; + this.restful = conn.restful; + this.listeners = conn.listeners; + + // deprecated + this.prettyUrls = conn.prettyUrls; + + /** + * @cfg {Object} api + * Specific urls to call on CRUD action methods "read", "create", "update" and "destroy". + * Defaults to:
    
    +api: {
    +    read    : undefined,
    +    create  : undefined,
    +    update  : undefined,
    +    destroy : undefined
    +}
    +     * 
    + *

    The url is built based upon the action being executed [load|create|save|destroy] + * using the commensurate {@link #api} property, or if undefined default to the + * configured {@link Ext.data.Store}.{@link Ext.data.Store#url url}.


    + *

    For example:

    + *
    
    +api: {
    +    load :    '/controller/load',
    +    create :  '/controller/new',  // Server MUST return idProperty of new record
    +    save :    '/controller/update',
    +    destroy : '/controller/destroy_action'
    +}
    +
    +// Alternatively, one can use the object-form to specify each API-action
    +api: {
    +    load: {url: 'read.php', method: 'GET'},
    +    create: 'create.php',
    +    destroy: 'destroy.php',
    +    save: 'update.php'
    +}
    +     * 
    + *

    If the specific URL for a given CRUD action is undefined, the CRUD action request + * will be directed to the configured {@link Ext.data.Connection#url url}.

    + *

    Note: To modify the URL for an action dynamically the appropriate API + * property should be modified before the action is requested using the corresponding before + * action event. For example to modify the URL associated with the load action: + *

    
    +// modify the url for the action
    +myStore.on({
    +    beforeload: {
    +        fn: function (store, options) {
    +            // use {@link Ext.data.HttpProxy#setUrl setUrl} to change the URL for *just* this request.
    +            store.proxy.setUrl('changed1.php');
    +
    +            // set optional second parameter to true to make this URL change
    +            // permanent, applying this URL for all subsequent requests.
    +            store.proxy.setUrl('changed1.php', true);
    +
    +            // Altering the proxy API should be done using the public
    +            // method {@link Ext.data.DataProxy#setApi setApi}.
    +            store.proxy.setApi('read', 'changed2.php');
    +
    +            // Or set the entire API with a config-object.
    +            // When using the config-object option, you must redefine the entire
    +            // API -- not just a specific action of it.
    +            store.proxy.setApi({
    +                read    : 'changed_read.php',
    +                create  : 'changed_create.php',
    +                update  : 'changed_update.php',
    +                destroy : 'changed_destroy.php'
    +            });
    +        }
    +    }
    +});
    +     * 
    + *

    + */ + + this.addEvents( + /** + * @event exception + *

    Fires if an exception occurs in the Proxy during a remote request. This event is relayed + * through a corresponding {@link Ext.data.Store}.{@link Ext.data.Store#exception exception}, + * so any Store instance may observe this event.

    + *

    In addition to being fired through the DataProxy instance that raised the event, this event is also fired + * through the Ext.data.DataProxy class to allow for centralized processing of exception events from all + * DataProxies by attaching a listener to the Ext.data.Proxy class itself.

    + *

    This event can be fired for one of two reasons:

    + *
    + *

    This event fires with two different contexts based upon the 2nd + * parameter type [remote|response]. The first four parameters + * are identical between the two contexts -- only the final two parameters + * differ.

    + * @param {DataProxy} this The proxy that sent the request + * @param {String} type + *

    The value of this parameter will be either 'response' or 'remote'.

    + *
    + * @param {String} action Name of the action (see {@link Ext.data.Api#actions}. + * @param {Object} options The options for the action that were specified in the {@link #request}. + * @param {Object} response + *

    The value of this parameter depends on the value of the type parameter:

    + *
    + * @param {Mixed} arg + *

    The type and value of this parameter depends on the value of the type parameter:

    + *
    + */ + 'exception', + /** + * @event beforeload + * Fires before a request to retrieve a data object. + * @param {DataProxy} this The proxy for the request + * @param {Object} params The params object passed to the {@link #request} function + */ + 'beforeload', + /** + * @event load + * Fires before the load method's callback is called. + * @param {DataProxy} this The proxy for the request + * @param {Object} o The request transaction object + * @param {Object} options The callback's options property as passed to the {@link #request} function + */ + 'load', + /** + * @event loadexception + *

    This event is deprecated. The signature of the loadexception event + * varies depending on the proxy, use the catch-all {@link #exception} event instead. + * This event will fire in addition to the {@link #exception} event.

    + * @param {misc} misc See {@link #exception}. + * @deprecated + */ + 'loadexception', + /** + * @event beforewrite + *

    Fires before a request is generated for one of the actions Ext.data.Api.actions.create|update|destroy

    + *

    In addition to being fired through the DataProxy instance that raised the event, this event is also fired + * through the Ext.data.DataProxy class to allow for centralized processing of beforewrite events from all + * DataProxies by attaching a listener to the Ext.data.Proxy class itself.

    + * @param {DataProxy} this The proxy for the request + * @param {String} action [Ext.data.Api.actions.create|update|destroy] + * @param {Record/Record[]} rs The Record(s) to create|update|destroy. + * @param {Object} params The request params object. Edit params to add parameters to the request. + */ + 'beforewrite', + /** + * @event write + *

    Fires before the request-callback is called

    + *

    In addition to being fired through the DataProxy instance that raised the event, this event is also fired + * through the Ext.data.DataProxy class to allow for centralized processing of write events from all + * DataProxies by attaching a listener to the Ext.data.Proxy class itself.

    + * @param {DataProxy} this The proxy that sent the request + * @param {String} action [Ext.data.Api.actions.create|upate|destroy] + * @param {Object} data The data object extracted from the server-response + * @param {Object} response The decoded response from server + * @param {Record/Record[]} rs The Record(s) from Store + * @param {Object} options The callback's options property as passed to the {@link #request} function + */ + 'write' + ); + Ext.data.DataProxy.superclass.constructor.call(this); + + // Prepare the proxy api. Ensures all API-actions are defined with the Object-form. + try { + Ext.data.Api.prepare(this); + } catch (e) { + if (e instanceof Ext.data.Api.Error) { + e.toConsole(); + } } -};/** - * @class Ext.data.DataProxy - * @extends Ext.util.Observable - *

    Abstract base class for implementations which provide retrieval of unformatted data objects. - * This class is intended to be extended and should not be created directly. For existing implementations, - * see {@link Ext.data.DirectProxy}, {@link Ext.data.HttpProxy}, {@link Ext.data.ScriptTagProxy} and - * {@link Ext.data.MemoryProxy}.

    - *

    DataProxy implementations are usually used in conjunction with an implementation of {@link Ext.data.DataReader} - * (of the appropriate type which knows how to parse the data object) to provide a block of - * {@link Ext.data.Records} to an {@link Ext.data.Store}.

    - *

    The parameter to a DataProxy constructor may be an {@link Ext.data.Connection} or can also be the - * config object to an {@link Ext.data.Connection}.

    - *

    Custom implementations must implement either the doRequest method (preferred) or the - * load method (deprecated). See - * {@link Ext.data.HttpProxy}.{@link Ext.data.HttpProxy#doRequest doRequest} or - * {@link Ext.data.HttpProxy}.{@link Ext.data.HttpProxy#load load} for additional details.

    - *

    Example 1

    - *
    
    -proxy: new Ext.data.ScriptTagProxy({
    -    {@link Ext.data.Connection#url url}: 'http://extjs.com/forum/topics-remote.php'
    -}),
    - * 
    - *

    Example 2

    - *
    
    -proxy : new Ext.data.HttpProxy({
    -    {@link Ext.data.Connection#method method}: 'GET',
    -    {@link Ext.data.HttpProxy#prettyUrls prettyUrls}: false,
    -    {@link Ext.data.Connection#url url}: 'local/default.php', // see options parameter for {@link Ext.Ajax#request}
    -    {@link #api}: {
    -        // all actions except the following will use above url
    -        create  : 'local/new.php',
    -        update  : 'local/update.php'
    -    }
    -}),
    - * 
    - */ -Ext.data.DataProxy = function(conn){ - // make sure we have a config object here to support ux proxies. - // All proxies should now send config into superclass constructor. - conn = conn || {}; - - // This line caused a bug when people use custom Connection object having its own request method. - // http://extjs.com/forum/showthread.php?t=67194. Have to set DataProxy config - //Ext.applyIf(this, conn); - - this.api = conn.api; - this.url = conn.url; - this.restful = conn.restful; - this.listeners = conn.listeners; - - // deprecated - this.prettyUrls = conn.prettyUrls; - - /** - * @cfg {Object} api - * Specific urls to call on CRUD action methods "read", "create", "update" and "destroy". - * Defaults to:
    
    -api: {
    -    read    : undefined,
    -    create  : undefined,
    -    update  : undefined,
    -    destroy : undefined
    -}
    -     * 
    - *

    The url is built based upon the action being executed [load|create|save|destroy] - * using the commensurate {@link #api} property, or if undefined default to the - * configured {@link Ext.data.Store}.{@link Ext.data.Store#url url}.


    - *

    For example:

    - *
    
    -api: {
    -    load :    '/controller/load',
    -    create :  '/controller/new',  // Server MUST return idProperty of new record
    -    save :    '/controller/update',
    -    destroy : '/controller/destroy_action'
    -}
    -
    -// Alternatively, one can use the object-form to specify each API-action
    -api: {
    -    load: {url: 'read.php', method: 'GET'},
    -    create: 'create.php',
    -    destroy: 'destroy.php',
    -    save: 'update.php'
    -}
    -     * 
    - *

    If the specific URL for a given CRUD action is undefined, the CRUD action request - * will be directed to the configured {@link Ext.data.Connection#url url}.

    - *

    Note: To modify the URL for an action dynamically the appropriate API - * property should be modified before the action is requested using the corresponding before - * action event. For example to modify the URL associated with the load action: - *

    
    -// modify the url for the action
    -myStore.on({
    -    beforeload: {
    -        fn: function (store, options) {
    -            // use {@link Ext.data.HttpProxy#setUrl setUrl} to change the URL for *just* this request.
    -            store.proxy.setUrl('changed1.php');
    -
    -            // set optional second parameter to true to make this URL change
    -            // permanent, applying this URL for all subsequent requests.
    -            store.proxy.setUrl('changed1.php', true);
    -
    -            // Altering the proxy API should be done using the public
    -            // method {@link Ext.data.DataProxy#setApi setApi}.
    -            store.proxy.setApi('read', 'changed2.php');
    -
    -            // Or set the entire API with a config-object.
    -            // When using the config-object option, you must redefine the entire
    -            // API -- not just a specific action of it.
    -            store.proxy.setApi({
    -                read    : 'changed_read.php',
    -                create  : 'changed_create.php',
    -                update  : 'changed_update.php',
    -                destroy : 'changed_destroy.php'
    -            });
    -        }
    -    }
    -});
    -     * 
    - *

    - */ - - this.addEvents( - /** - * @event exception - *

    Fires if an exception occurs in the Proxy during a remote request. This event is relayed - * through a corresponding {@link Ext.data.Store}.{@link Ext.data.Store#exception exception}, - * so any Store instance may observe this event.

    - *

    In addition to being fired through the DataProxy instance that raised the event, this event is also fired - * through the Ext.data.DataProxy class to allow for centralized processing of exception events from all - * DataProxies by attaching a listener to the Ext.data.Proxy class itself.

    - *

    This event can be fired for one of two reasons:

    - *
    - *

    This event fires with two different contexts based upon the 2nd - * parameter type [remote|response]. The first four parameters - * are identical between the two contexts -- only the final two parameters - * differ.

    - * @param {DataProxy} this The proxy that sent the request - * @param {String} type - *

    The value of this parameter will be either 'response' or 'remote'.

    - *
    - * @param {String} action Name of the action (see {@link Ext.data.Api#actions}. - * @param {Object} options The options for the action that were specified in the {@link #request}. - * @param {Object} response - *

    The value of this parameter depends on the value of the type parameter:

    - *
    - * @param {Mixed} arg - *

    The type and value of this parameter depends on the value of the type parameter:

    - *
    - */ - 'exception', - /** - * @event beforeload - * Fires before a request to retrieve a data object. - * @param {DataProxy} this The proxy for the request - * @param {Object} params The params object passed to the {@link #request} function - */ - 'beforeload', - /** - * @event load - * Fires before the load method's callback is called. - * @param {DataProxy} this The proxy for the request - * @param {Object} o The request transaction object - * @param {Object} options The callback's options property as passed to the {@link #request} function - */ - 'load', - /** - * @event loadexception - *

    This event is deprecated. The signature of the loadexception event - * varies depending on the proxy, use the catch-all {@link #exception} event instead. - * This event will fire in addition to the {@link #exception} event.

    - * @param {misc} misc See {@link #exception}. - * @deprecated - */ - 'loadexception', - /** - * @event beforewrite - *

    Fires before a request is generated for one of the actions Ext.data.Api.actions.create|update|destroy

    - *

    In addition to being fired through the DataProxy instance that raised the event, this event is also fired - * through the Ext.data.DataProxy class to allow for centralized processing of beforewrite events from all - * DataProxies by attaching a listener to the Ext.data.Proxy class itself.

    - * @param {DataProxy} this The proxy for the request - * @param {String} action [Ext.data.Api.actions.create|update|destroy] - * @param {Record/Array[Record]} rs The Record(s) to create|update|destroy. - * @param {Object} params The request params object. Edit params to add parameters to the request. - */ - 'beforewrite', - /** - * @event write - *

    Fires before the request-callback is called

    - *

    In addition to being fired through the DataProxy instance that raised the event, this event is also fired - * through the Ext.data.DataProxy class to allow for centralized processing of write events from all - * DataProxies by attaching a listener to the Ext.data.Proxy class itself.

    - * @param {DataProxy} this The proxy that sent the request - * @param {String} action [Ext.data.Api.actions.create|upate|destroy] - * @param {Object} data The data object extracted from the server-response - * @param {Object} response The decoded response from server - * @param {Record/Record{}} rs The records from Store - * @param {Object} options The callback's options property as passed to the {@link #request} function - */ - 'write' - ); - Ext.data.DataProxy.superclass.constructor.call(this); - - // Prepare the proxy api. Ensures all API-actions are defined with the Object-form. - try { - Ext.data.Api.prepare(this); - } catch (e) { - if (e instanceof Ext.data.Api.Error) { - e.toConsole(); - } - } - // relay each proxy's events onto Ext.data.DataProxy class for centralized Proxy-listening - Ext.data.DataProxy.relayEvents(this, ['beforewrite', 'write', 'exception']); -}; - -Ext.extend(Ext.data.DataProxy, Ext.util.Observable, { - /** - * @cfg {Boolean} restful - *

    Defaults to false. Set to true to operate in a RESTful manner.

    - *

    Note: this parameter will automatically be set to true if the - * {@link Ext.data.Store} it is plugged into is set to restful: true. If the - * Store is RESTful, there is no need to set this option on the proxy.

    - *

    RESTful implementations enable the serverside framework to automatically route - * actions sent to one url based upon the HTTP method, for example: - *

    
    -store: new Ext.data.Store({
    -    restful: true,
    -    proxy: new Ext.data.HttpProxy({url:'/users'}); // all requests sent to /users
    -    ...
    -)}
    -     * 
    - * If there is no {@link #api} specified in the configuration of the proxy, - * all requests will be marshalled to a single RESTful url (/users) so the serverside - * framework can inspect the HTTP Method and act accordingly: - *
    -Method   url        action
    -POST     /users     create
    -GET      /users     read
    -PUT      /users/23  update
    -DESTROY  /users/23  delete
    -     * 

    - *

    If set to true, a {@link Ext.data.Record#phantom non-phantom} record's - * {@link Ext.data.Record#id id} will be appended to the url. Some MVC (e.g., Ruby on Rails, - * Merb and Django) support segment based urls where the segments in the URL follow the - * Model-View-Controller approach:

    
    -     * someSite.com/controller/action/id
    -     * 
    - * Where the segments in the url are typically:

    - *

    Refer to {@link Ext.data.DataProxy#api} for additional information.

    - */ - restful: false, - - /** - *

    Redefines the Proxy's API or a single action of an API. Can be called with two method signatures.

    - *

    If called with an object as the only parameter, the object should redefine the entire API, e.g.:

    
    -proxy.setApi({
    -    read    : '/users/read',
    -    create  : '/users/create',
    -    update  : '/users/update',
    -    destroy : '/users/destroy'
    -});
    -
    - *

    If called with two parameters, the first parameter should be a string specifying the API action to - * redefine and the second parameter should be the URL (or function if using DirectProxy) to call for that action, e.g.:

    
    -proxy.setApi(Ext.data.Api.actions.read, '/users/new_load_url');
    -
    - * @param {String/Object} api An API specification object, or the name of an action. - * @param {String/Function} url The URL (or function if using DirectProxy) to call for the action. - */ - setApi : function() { - if (arguments.length == 1) { - var valid = Ext.data.Api.isValid(arguments[0]); - if (valid === true) { - this.api = arguments[0]; - } - else { - throw new Ext.data.Api.Error('invalid', valid); - } - } - else if (arguments.length == 2) { - if (!Ext.data.Api.isAction(arguments[0])) { - throw new Ext.data.Api.Error('invalid', arguments[0]); - } - this.api[arguments[0]] = arguments[1]; - } - Ext.data.Api.prepare(this); - }, - - /** - * Returns true if the specified action is defined as a unique action in the api-config. - * request. If all API-actions are routed to unique urls, the xaction parameter is unecessary. However, if no api is defined - * and all Proxy actions are routed to DataProxy#url, the server-side will require the xaction parameter to perform a switch to - * the corresponding code for CRUD action. - * @param {String [Ext.data.Api.CREATE|READ|UPDATE|DESTROY]} action - * @return {Boolean} - */ - isApiAction : function(action) { - return (this.api[action]) ? true : false; - }, - - /** - * All proxy actions are executed through this method. Automatically fires the "before" + action event - * @param {String} action Name of the action - * @param {Ext.data.Record/Ext.data.Record[]/null} rs Will be null when action is 'load' - * @param {Object} params - * @param {Ext.data.DataReader} reader - * @param {Function} callback - * @param {Object} scope Scope with which to call the callback (defaults to the Proxy object) - * @param {Object} options Any options specified for the action (e.g. see {@link Ext.data.Store#load}. - */ - request : function(action, rs, params, reader, callback, scope, options) { - if (!this.api[action] && !this.load) { - throw new Ext.data.DataProxy.Error('action-undefined', action); - } - params = params || {}; - if ((action === Ext.data.Api.actions.read) ? this.fireEvent("beforeload", this, params) : this.fireEvent("beforewrite", this, action, rs, params) !== false) { - this.doRequest.apply(this, arguments); - } - else { - callback.call(scope || this, null, options, false); - } - }, - - - /** - * Deprecated load method using old method signature. See {@doRequest} for preferred method. - * @deprecated - * @param {Object} params - * @param {Object} reader - * @param {Object} callback - * @param {Object} scope - * @param {Object} arg - */ - load : null, - - /** - * @cfg {Function} doRequest Abstract method that should be implemented in all subclasses - * (e.g.: {@link Ext.data.HttpProxy#doRequest HttpProxy.doRequest}, - * {@link Ext.data.DirectProxy#doRequest DirectProxy.doRequest}). - */ - doRequest : function(action, rs, params, reader, callback, scope, options) { - // default implementation of doRequest for backwards compatibility with 2.0 proxies. - // If we're executing here, the action is probably "load". - // Call with the pre-3.0 method signature. - this.load(params, reader, callback, scope, options); - }, - - /** - * buildUrl - * Sets the appropriate url based upon the action being executed. If restful is true, and only a single record is being acted upon, - * url will be built Rails-style, as in "/controller/action/32". restful will aply iff the supplied record is an - * instance of Ext.data.Record rather than an Array of them. - * @param {String} action The api action being executed [read|create|update|destroy] - * @param {Ext.data.Record/Array[Ext.data.Record]} The record or Array of Records being acted upon. - * @return {String} url - * @private - */ - buildUrl : function(action, record) { - record = record || null; - var url = (this.api[action]) ? this.api[action].url : this.url; - if (!url) { - throw new Ext.data.Api.Error('invalid-url', action); - } - - // look for urls having "provides" suffix (from Rails/Merb and others...), - // e.g.: /users.json, /users.xml, etc - // with restful routes, we need urls like: - // PUT /users/1.json - // DELETE /users/1.json - var format = null; - var m = url.match(/(.*)(\.json|\.xml|\.html)$/); - if (m) { - format = m[2]; // eg ".json" - url = m[1]; // eg: "/users" - } - // prettyUrls is deprectated in favor of restful-config - if ((this.prettyUrls === true || this.restful === true) && record instanceof Ext.data.Record && !record.phantom) { - url += '/' + record.id; - } - if (format) { // <-- append the request format if exists (ie: /users/update/69[.json]) - url += format; - } - return url; - }, - - /** - * Destroys the proxy by purging any event listeners and cancelling any active requests. - */ - destroy: function(){ - this.purgeListeners(); - } -}); - -// Apply the Observable prototype to the DataProxy class so that proxy instances can relay their -// events to the class. Allows for centralized listening of all proxy instances upon the DataProxy class. -Ext.apply(Ext.data.DataProxy, Ext.util.Observable.prototype); -Ext.util.Observable.call(Ext.data.DataProxy); - -/** - * @class Ext.data.DataProxy.Error - * @extends Ext.Error - * DataProxy Error extension. - * constructor - * @param {String} name - * @param {Record/Array[Record]/Array} - */ -Ext.data.DataProxy.Error = Ext.extend(Ext.Error, { - constructor : function(message, arg) { - this.arg = arg; - Ext.Error.call(this, message); - }, - name: 'Ext.data.DataProxy' -}); -Ext.apply(Ext.data.DataProxy.Error.prototype, { - lang: { - 'action-undefined': "DataProxy attempted to execute an API-action but found an undefined url / function. Please review your Proxy url/api-configuration.", - 'api-invalid': 'Recieved an invalid API-configuration. Please ensure your proxy API-configuration contains only the actions from Ext.data.Api.actions.' - } -}); - - -/** - * @class Ext.data.ScriptTagProxy - * @extends Ext.data.DataProxy - * An implementation of Ext.data.DataProxy that reads a data object from a URL which may be in a domain - * other than the originating domain of the running page.
    - *

    - * Note that if you are retrieving data from a page that is in a domain that is NOT the same as the originating domain - * of the running page, you must use this class, rather than HttpProxy.
    - *

    - * The content passed back from a server resource requested by a ScriptTagProxy must be executable JavaScript - * source code because it is used as the source inside a <script> tag.
    - *

    - * In order for the browser to process the returned data, the server must wrap the data object - * with a call to a callback function, the name of which is passed as a parameter by the ScriptTagProxy. - * Below is a Java example for a servlet which returns data for either a ScriptTagProxy, or an HttpProxy - * depending on whether the callback name was passed: - *

    - *

    
    -boolean scriptTag = false;
    -String cb = request.getParameter("callback");
    -if (cb != null) {
    -    scriptTag = true;
    -    response.setContentType("text/javascript");
    -} else {
    -    response.setContentType("application/x-json");
    -}
    -Writer out = response.getWriter();
    -if (scriptTag) {
    -    out.write(cb + "(");
    -}
    -out.print(dataBlock.toJsonString());
    -if (scriptTag) {
    -    out.write(");");
    -}
    -
    - * - * @constructor - * @param {Object} config A configuration object. - */ -Ext.data.ScriptTagProxy = function(config){ - Ext.apply(this, config); - - Ext.data.ScriptTagProxy.superclass.constructor.call(this, config); - - this.head = document.getElementsByTagName("head")[0]; - - /** - * @event loadexception - * Deprecated in favor of 'exception' event. - * Fires if an exception occurs in the Proxy during data loading. This event can be fired for one of two reasons: - * - * Note that this event is also relayed through {@link Ext.data.Store}, so you can listen for it directly - * on any Store instance. - * @param {Object} this - * @param {Object} options The loading options that were specified (see {@link #load} for details). If the load - * call timed out, this parameter will be null. - * @param {Object} arg The callback's arg object passed to the {@link #load} function - * @param {Error} e The JavaScript Error object caught if the configured Reader could not read the data. - * If the remote request returns success: false, this parameter will be null. - */ -}; - -Ext.data.ScriptTagProxy.TRANS_ID = 1000; - -Ext.extend(Ext.data.ScriptTagProxy, Ext.data.DataProxy, { - /** - * @cfg {String} url The URL from which to request the data object. - */ - /** - * @cfg {Number} timeout (optional) The number of milliseconds to wait for a response. Defaults to 30 seconds. - */ - timeout : 30000, - /** - * @cfg {String} callbackParam (Optional) The name of the parameter to pass to the server which tells - * the server the name of the callback function set up by the load call to process the returned data object. - * Defaults to "callback".

    The server-side processing must read this parameter value, and generate - * javascript output which calls this named function passing the data object as its only parameter. - */ - callbackParam : "callback", - /** - * @cfg {Boolean} nocache (optional) Defaults to true. Disable caching by adding a unique parameter - * name to the request. - */ - nocache : true, - - /** - * HttpProxy implementation of DataProxy#doRequest - * @param {String} action - * @param {Ext.data.Record/Ext.data.Record[]} rs If action is read, rs will be null - * @param {Object} params An object containing properties which are to be used as HTTP parameters - * for the request to the remote server. - * @param {Ext.data.DataReader} reader The Reader object which converts the data - * object into a block of Ext.data.Records. - * @param {Function} callback The function into which to pass the block of Ext.data.Records. - * The function must be passed

    - * @param {Object} scope The scope in which to call the callback - * @param {Object} arg An optional argument which is passed to the callback as its second parameter. - */ - doRequest : function(action, rs, params, reader, callback, scope, arg) { - var p = Ext.urlEncode(Ext.apply(params, this.extraParams)); - - var url = this.buildUrl(action, rs); - if (!url) { - throw new Ext.data.Api.Error('invalid-url', url); - } - url = Ext.urlAppend(url, p); - - if(this.nocache){ - url = Ext.urlAppend(url, '_dc=' + (new Date().getTime())); - } - var transId = ++Ext.data.ScriptTagProxy.TRANS_ID; - var trans = { - id : transId, - action: action, - cb : "stcCallback"+transId, - scriptId : "stcScript"+transId, - params : params, - arg : arg, - url : url, - callback : callback, - scope : scope, - reader : reader - }; - window[trans.cb] = this.createCallback(action, rs, trans); - url += String.format("&{0}={1}", this.callbackParam, trans.cb); - if(this.autoAbort !== false){ - this.abort(); - } - - trans.timeoutId = this.handleFailure.defer(this.timeout, this, [trans]); - - var script = document.createElement("script"); - script.setAttribute("src", url); - script.setAttribute("type", "text/javascript"); - script.setAttribute("id", trans.scriptId); - this.head.appendChild(script); - - this.trans = trans; - }, - - // @private createCallback - createCallback : function(action, rs, trans) { - var self = this; - return function(res) { - self.trans = false; - self.destroyTrans(trans, true); - if (action === Ext.data.Api.actions.read) { - self.onRead.call(self, action, trans, res); - } else { - self.onWrite.call(self, action, trans, res, rs); - } - }; - }, - /** - * Callback for read actions - * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] - * @param {Object} trans The request transaction object - * @param {Object} res The server response - * @private - */ - onRead : function(action, trans, res) { - var result; - try { - result = trans.reader.readRecords(res); - }catch(e){ - // @deprecated: fire loadexception - this.fireEvent("loadexception", this, trans, res, e); - - this.fireEvent('exception', this, 'response', action, trans, res, e); - trans.callback.call(trans.scope||window, null, trans.arg, false); - return; - } - if (result.success === false) { - // @deprecated: fire old loadexception for backwards-compat. - this.fireEvent('loadexception', this, trans, res); - - this.fireEvent('exception', this, 'remote', action, trans, res, null); - } else { - this.fireEvent("load", this, res, trans.arg); - } - trans.callback.call(trans.scope||window, result, trans.arg, result.success); - }, - /** - * Callback for write actions - * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] - * @param {Object} trans The request transaction object - * @param {Object} res The server response - * @private - */ - onWrite : function(action, trans, response, rs) { - var reader = trans.reader; - try { - // though we already have a response object here in STP, run through readResponse to catch any meta-data exceptions. - var res = reader.readResponse(action, response); - } catch (e) { - this.fireEvent('exception', this, 'response', action, trans, res, e); - trans.callback.call(trans.scope||window, null, res, false); - return; - } - if(!res.success === true){ - this.fireEvent('exception', this, 'remote', action, trans, res, rs); - trans.callback.call(trans.scope||window, null, res, false); - return; - } - this.fireEvent("write", this, action, res.data, res, rs, trans.arg ); - trans.callback.call(trans.scope||window, res.data, res, true); - }, - - // private - isLoading : function(){ - return this.trans ? true : false; - }, - - /** - * Abort the current server request. - */ - abort : function(){ - if(this.isLoading()){ - this.destroyTrans(this.trans); - } - }, - - // private - destroyTrans : function(trans, isLoaded){ - this.head.removeChild(document.getElementById(trans.scriptId)); - clearTimeout(trans.timeoutId); - if(isLoaded){ - window[trans.cb] = undefined; - try{ - delete window[trans.cb]; - }catch(e){} - }else{ - // if hasn't been loaded, wait for load to remove it to prevent script error - window[trans.cb] = function(){ - window[trans.cb] = undefined; - try{ - delete window[trans.cb]; - }catch(e){} - }; - } - }, - - // private - handleFailure : function(trans){ - this.trans = false; - this.destroyTrans(trans, false); - if (trans.action === Ext.data.Api.actions.read) { - // @deprecated firing loadexception - this.fireEvent("loadexception", this, null, trans.arg); - } - - this.fireEvent('exception', this, 'response', trans.action, { - response: null, - options: trans.arg - }); - trans.callback.call(trans.scope||window, null, trans.arg, false); - }, - - // inherit docs - destroy: function(){ - this.abort(); - Ext.data.ScriptTagProxy.superclass.destroy.call(this); - } -});/** - * @class Ext.data.HttpProxy - * @extends Ext.data.DataProxy - *

    An implementation of {@link Ext.data.DataProxy} that processes data requests within the same - * domain of the originating page.

    - *

    Note: this class cannot be used to retrieve data from a domain other - * than the domain from which the running page was served. For cross-domain requests, use a - * {@link Ext.data.ScriptTagProxy ScriptTagProxy}.

    - *

    Be aware that to enable the browser to parse an XML document, the server must set - * the Content-Type header in the HTTP response to "text/xml".

    - * @constructor - * @param {Object} conn - * An {@link Ext.data.Connection} object, or options parameter to {@link Ext.Ajax#request}. - *

    Note that if this HttpProxy is being used by a (@link Ext.data.Store Store}, then the - * Store's call to {@link #load} will override any specified callback and params - * options. In this case, use the Store's {@link Ext.data.Store#events events} to modify parameters, - * or react to loading events. The Store's {@link Ext.data.Store#baseParams baseParams} may also be - * used to pass parameters known at instantiation time.

    - *

    If an options parameter is passed, the singleton {@link Ext.Ajax} object will be used to make - * the request.

    - */ -Ext.data.HttpProxy = function(conn){ - Ext.data.HttpProxy.superclass.constructor.call(this, conn); - - /** - * The Connection object (Or options parameter to {@link Ext.Ajax#request}) which this HttpProxy - * uses to make requests to the server. Properties of this object may be changed dynamically to - * change the way data is requested. - * @property - */ - this.conn = conn; - - // nullify the connection url. The url param has been copied to 'this' above. The connection - // url will be set during each execution of doRequest when buildUrl is called. This makes it easier for users to override the - // connection url during beforeaction events (ie: beforeload, beforewrite, etc). - // Url is always re-defined during doRequest. - this.conn.url = null; - - this.useAjax = !conn || !conn.events; - - // A hash containing active requests, keyed on action [Ext.data.Api.actions.create|read|update|destroy] - var actions = Ext.data.Api.actions; - this.activeRequest = {}; - for (var verb in actions) { - this.activeRequest[actions[verb]] = undefined; - } -}; - -Ext.extend(Ext.data.HttpProxy, Ext.data.DataProxy, { - /** - * Return the {@link Ext.data.Connection} object being used by this Proxy. - * @return {Connection} The Connection object. This object may be used to subscribe to events on - * a finer-grained basis than the DataProxy events. - */ - getConnection : function() { - return this.useAjax ? Ext.Ajax : this.conn; - }, - - /** - * Used for overriding the url used for a single request. Designed to be called during a beforeaction event. Calling setUrl - * will override any urls set via the api configuration parameter. Set the optional parameter makePermanent to set the url for - * all subsequent requests. If not set to makePermanent, the next request will use the same url or api configuration defined - * in the initial proxy configuration. - * @param {String} url - * @param {Boolean} makePermanent (Optional) [false] - * - * (e.g.: beforeload, beforesave, etc). - */ - setUrl : function(url, makePermanent) { - this.conn.url = url; - if (makePermanent === true) { - this.url = url; - this.api = null; - Ext.data.Api.prepare(this); - } - }, - - /** - * HttpProxy implementation of DataProxy#doRequest - * @param {String} action The crud action type (create, read, update, destroy) - * @param {Ext.data.Record/Ext.data.Record[]} rs If action is load, rs will be null - * @param {Object} params An object containing properties which are to be used as HTTP parameters - * for the request to the remote server. - * @param {Ext.data.DataReader} reader The Reader object which converts the data - * object into a block of Ext.data.Records. - * @param {Function} callback - *

    A function to be called after the request. - * The callback is passed the following arguments:

    - * @param {Object} scope The scope in which to call the callback - * @param {Object} arg An optional argument which is passed to the callback as its second parameter. - */ - doRequest : function(action, rs, params, reader, cb, scope, arg) { - var o = { - method: (this.api[action]) ? this.api[action]['method'] : undefined, - request: { - callback : cb, - scope : scope, - arg : arg - }, - reader: reader, - callback : this.createCallback(action, rs), - scope: this - }; - - // If possible, transmit data using jsonData || xmlData on Ext.Ajax.request (An installed DataWriter would have written it there.). - // Use std HTTP params otherwise. - // TODO wrap into 1 Ext.apply now? - if (params.jsonData) { - o.jsonData = params.jsonData; - } else if (params.xmlData) { - o.xmlData = params.xmlData; - } else { - o.params = params || {}; - } - // Set the connection url. If this.conn.url is not null here, - // the user may have overridden the url during a beforeaction event-handler. - // this.conn.url is nullified after each request. - if (this.conn.url === null) { - this.conn.url = this.buildUrl(action, rs); - } - else if (this.restful === true && rs instanceof Ext.data.Record && !rs.phantom) { // <-- user must have intervened with #setApi or #setUrl - this.conn.url += '/' + rs.id; - } - if(this.useAjax){ - - Ext.applyIf(o, this.conn); - - // If a currently running request is found for this action, abort it. - if (this.activeRequest[action]) { - //// - // Disabled aborting activeRequest while implementing REST. activeRequest[action] will have to become an array - // TODO ideas anyone? - // - //Ext.Ajax.abort(this.activeRequest[action]); - } - this.activeRequest[action] = Ext.Ajax.request(o); - }else{ - this.conn.request(o); - } - // request is sent, nullify the connection url in preparation for the next request - this.conn.url = null; - }, - - /** - * Returns a callback function for a request. Note a special case is made for the - * read action vs all the others. - * @param {String} action [create|update|delete|load] - * @param {Ext.data.Record[]} rs The Store-recordset being acted upon - * @private - */ - createCallback : function(action, rs) { - return function(o, success, response) { - this.activeRequest[action] = undefined; - if (!success) { - if (action === Ext.data.Api.actions.read) { - // @deprecated: fire loadexception for backwards compat. - // TODO remove in 3.1 - this.fireEvent('loadexception', this, o, response); - } - this.fireEvent('exception', this, 'response', action, o, response); - o.request.callback.call(o.request.scope, null, o.request.arg, false); - return; - } - if (action === Ext.data.Api.actions.read) { - this.onRead(action, o, response); - } else { - this.onWrite(action, o, response, rs); - } - } - }, - - /** - * Callback for read action - * @param {String} action Action name as per {@link Ext.data.Api.actions#read}. - * @param {Object} o The request transaction object - * @param {Object} res The server response - * @fires loadexception (deprecated) - * @fires exception - * @fires load - * @private - */ - onRead : function(action, o, response) { - var result; - try { - result = o.reader.read(response); - }catch(e){ - // @deprecated: fire old loadexception for backwards-compat. - // TODO remove in 3.1 - this.fireEvent('loadexception', this, o, response, e); - - this.fireEvent('exception', this, 'response', action, o, response, e); - o.request.callback.call(o.request.scope, null, o.request.arg, false); - return; - } - if (result.success === false) { - // @deprecated: fire old loadexception for backwards-compat. - // TODO remove in 3.1 - this.fireEvent('loadexception', this, o, response); - - // Get DataReader read-back a response-object to pass along to exception event - var res = o.reader.readResponse(action, response); - this.fireEvent('exception', this, 'remote', action, o, res, null); - } - else { - this.fireEvent('load', this, o, o.request.arg); - } - // TODO refactor onRead, onWrite to be more generalized now that we're dealing with Ext.data.Response instance - // the calls to request.callback(...) in each will have to be made identical. - // NOTE reader.readResponse does not currently return Ext.data.Response - o.request.callback.call(o.request.scope, result, o.request.arg, result.success); - }, - /** - * Callback for write actions - * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] - * @param {Object} trans The request transaction object - * @param {Object} res The server response - * @fires exception - * @fires write - * @private - */ - onWrite : function(action, o, response, rs) { - var reader = o.reader; - var res; - try { - res = reader.readResponse(action, response); - } catch (e) { - this.fireEvent('exception', this, 'response', action, o, response, e); - o.request.callback.call(o.request.scope, null, o.request.arg, false); - return; - } - if (res.success === false) { - this.fireEvent('exception', this, 'remote', action, o, res, rs); - } else { - this.fireEvent('write', this, action, res.data, res, rs, o.request.arg); - } - // TODO refactor onRead, onWrite to be more generalized now that we're dealing with Ext.data.Response instance - // the calls to request.callback(...) in each will have to be made similar. - // NOTE reader.readResponse does not currently return Ext.data.Response - o.request.callback.call(o.request.scope, res.data, res, res.success); - }, - - // inherit docs - destroy: function(){ - if(!this.useAjax){ - this.conn.abort(); - }else if(this.activeRequest){ - var actions = Ext.data.Api.actions; - for (var verb in actions) { - if(this.activeRequest[actions[verb]]){ - Ext.Ajax.abort(this.activeRequest[actions[verb]]); - } - } - } - Ext.data.HttpProxy.superclass.destroy.call(this); - } -});/** - * @class Ext.data.MemoryProxy - * @extends Ext.data.DataProxy - * An implementation of Ext.data.DataProxy that simply passes the data specified in its constructor - * to the Reader when its load method is called. - * @constructor - * @param {Object} data The data object which the Reader uses to construct a block of Ext.data.Records. - */ -Ext.data.MemoryProxy = function(data){ - // Must define a dummy api with "read" action to satisfy DataProxy#doRequest and Ext.data.Api#prepare *before* calling super - var api = {}; - api[Ext.data.Api.actions.read] = true; - Ext.data.MemoryProxy.superclass.constructor.call(this, { - api: api - }); - this.data = data; -}; - -Ext.extend(Ext.data.MemoryProxy, Ext.data.DataProxy, { - /** - * @event loadexception - * Fires if an exception occurs in the Proxy during data loading. Note that this event is also relayed - * through {@link Ext.data.Store}, so you can listen for it directly on any Store instance. - * @param {Object} this - * @param {Object} arg The callback's arg object passed to the {@link #load} function - * @param {Object} null This parameter does not apply and will always be null for MemoryProxy - * @param {Error} e The JavaScript Error object caught if the configured Reader could not read the data - */ - - /** - * MemoryProxy implementation of DataProxy#doRequest - * @param {String} action - * @param {Ext.data.Record/Ext.data.Record[]} rs If action is load, rs will be null - * @param {Object} params An object containing properties which are to be used as HTTP parameters - * for the request to the remote server. - * @param {Ext.data.DataReader} reader The Reader object which converts the data - * object into a block of Ext.data.Records. - * @param {Function} callback The function into which to pass the block of Ext.data.Records. - * The function must be passed - * @param {Object} scope The scope in which to call the callback - * @param {Object} arg An optional argument which is passed to the callback as its second parameter. - */ - doRequest : function(action, rs, params, reader, callback, scope, arg) { - // No implementation for CRUD in MemoryProxy. Assumes all actions are 'load' - params = params || {}; - var result; - try { - result = reader.readRecords(this.data); - }catch(e){ - // @deprecated loadexception - this.fireEvent("loadexception", this, null, arg, e); - - this.fireEvent('exception', this, 'response', action, arg, null, e); - callback.call(scope, null, arg, false); - return; - } - callback.call(scope, result, arg, true); - } -}); \ No newline at end of file + // relay each proxy's events onto Ext.data.DataProxy class for centralized Proxy-listening + Ext.data.DataProxy.relayEvents(this, ['beforewrite', 'write', 'exception']); +}; + +Ext.extend(Ext.data.DataProxy, Ext.util.Observable, { + /** + * @cfg {Boolean} restful + *

    Defaults to false. Set to true to operate in a RESTful manner.

    + *

    Note: this parameter will automatically be set to true if the + * {@link Ext.data.Store} it is plugged into is set to restful: true. If the + * Store is RESTful, there is no need to set this option on the proxy.

    + *

    RESTful implementations enable the serverside framework to automatically route + * actions sent to one url based upon the HTTP method, for example: + *

    
    +store: new Ext.data.Store({
    +    restful: true,
    +    proxy: new Ext.data.HttpProxy({url:'/users'}); // all requests sent to /users
    +    ...
    +)}
    +     * 
    + * If there is no {@link #api} specified in the configuration of the proxy, + * all requests will be marshalled to a single RESTful url (/users) so the serverside + * framework can inspect the HTTP Method and act accordingly: + *
    +Method   url        action
    +POST     /users     create
    +GET      /users     read
    +PUT      /users/23  update
    +DESTROY  /users/23  delete
    +     * 

    + *

    If set to true, a {@link Ext.data.Record#phantom non-phantom} record's + * {@link Ext.data.Record#id id} will be appended to the url. Some MVC (e.g., Ruby on Rails, + * Merb and Django) support segment based urls where the segments in the URL follow the + * Model-View-Controller approach:

    
    +     * someSite.com/controller/action/id
    +     * 
    + * Where the segments in the url are typically:

    + *

    Refer to {@link Ext.data.DataProxy#api} for additional information.

    + */ + restful: false, + + /** + *

    Redefines the Proxy's API or a single action of an API. Can be called with two method signatures.

    + *

    If called with an object as the only parameter, the object should redefine the entire API, e.g.:

    
    +proxy.setApi({
    +    read    : '/users/read',
    +    create  : '/users/create',
    +    update  : '/users/update',
    +    destroy : '/users/destroy'
    +});
    +
    + *

    If called with two parameters, the first parameter should be a string specifying the API action to + * redefine and the second parameter should be the URL (or function if using DirectProxy) to call for that action, e.g.:

    
    +proxy.setApi(Ext.data.Api.actions.read, '/users/new_load_url');
    +
    + * @param {String/Object} api An API specification object, or the name of an action. + * @param {String/Function} url The URL (or function if using DirectProxy) to call for the action. + */ + setApi : function() { + if (arguments.length == 1) { + var valid = Ext.data.Api.isValid(arguments[0]); + if (valid === true) { + this.api = arguments[0]; + } + else { + throw new Ext.data.Api.Error('invalid', valid); + } + } + else if (arguments.length == 2) { + if (!Ext.data.Api.isAction(arguments[0])) { + throw new Ext.data.Api.Error('invalid', arguments[0]); + } + this.api[arguments[0]] = arguments[1]; + } + Ext.data.Api.prepare(this); + }, + + /** + * Returns true if the specified action is defined as a unique action in the api-config. + * request. If all API-actions are routed to unique urls, the xaction parameter is unecessary. However, if no api is defined + * and all Proxy actions are routed to DataProxy#url, the server-side will require the xaction parameter to perform a switch to + * the corresponding code for CRUD action. + * @param {String [Ext.data.Api.CREATE|READ|UPDATE|DESTROY]} action + * @return {Boolean} + */ + isApiAction : function(action) { + return (this.api[action]) ? true : false; + }, + + /** + * All proxy actions are executed through this method. Automatically fires the "before" + action event + * @param {String} action Name of the action + * @param {Ext.data.Record/Ext.data.Record[]/null} rs Will be null when action is 'load' + * @param {Object} params + * @param {Ext.data.DataReader} reader + * @param {Function} callback + * @param {Object} scope The scope (this reference) in which the callback function is executed. Defaults to the Proxy object. + * @param {Object} options Any options specified for the action (e.g. see {@link Ext.data.Store#load}. + */ + request : function(action, rs, params, reader, callback, scope, options) { + if (!this.api[action] && !this.load) { + throw new Ext.data.DataProxy.Error('action-undefined', action); + } + params = params || {}; + if ((action === Ext.data.Api.actions.read) ? this.fireEvent("beforeload", this, params) : this.fireEvent("beforewrite", this, action, rs, params) !== false) { + this.doRequest.apply(this, arguments); + } + else { + callback.call(scope || this, null, options, false); + } + }, + + + /** + * Deprecated load method using old method signature. See {@doRequest} for preferred method. + * @deprecated + * @param {Object} params + * @param {Object} reader + * @param {Object} callback + * @param {Object} scope + * @param {Object} arg + */ + load : null, + + /** + * @cfg {Function} doRequest Abstract method that should be implemented in all subclasses. Note: Should only be used by custom-proxy developers. + * (e.g.: {@link Ext.data.HttpProxy#doRequest HttpProxy.doRequest}, + * {@link Ext.data.DirectProxy#doRequest DirectProxy.doRequest}). + */ + doRequest : function(action, rs, params, reader, callback, scope, options) { + // default implementation of doRequest for backwards compatibility with 2.0 proxies. + // If we're executing here, the action is probably "load". + // Call with the pre-3.0 method signature. + this.load(params, reader, callback, scope, options); + }, + + /** + * @cfg {Function} onRead Abstract method that should be implemented in all subclasses. Note: Should only be used by custom-proxy developers. Callback for read {@link Ext.data.Api#actions action}. + * @param {String} action Action name as per {@link Ext.data.Api.actions#read}. + * @param {Object} o The request transaction object + * @param {Object} res The server response + * @fires loadexception (deprecated) + * @fires exception + * @fires load + * @protected + */ + onRead : Ext.emptyFn, + /** + * @cfg {Function} onWrite Abstract method that should be implemented in all subclasses. Note: Should only be used by custom-proxy developers. Callback for create, update and destroy {@link Ext.data.Api#actions actions}. + * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] + * @param {Object} trans The request transaction object + * @param {Object} res The server response + * @fires exception + * @fires write + * @protected + */ + onWrite : Ext.emptyFn, + /** + * buildUrl + * Sets the appropriate url based upon the action being executed. If restful is true, and only a single record is being acted upon, + * url will be built Rails-style, as in "/controller/action/32". restful will aply iff the supplied record is an + * instance of Ext.data.Record rather than an Array of them. + * @param {String} action The api action being executed [read|create|update|destroy] + * @param {Ext.data.Record/Ext.data.Record[]} record The record or Array of Records being acted upon. + * @return {String} url + * @private + */ + buildUrl : function(action, record) { + record = record || null; + + // conn.url gets nullified after each request. If it's NOT null here, that means the user must have intervened with a call + // to DataProxy#setUrl or DataProxy#setApi and changed it before the request was executed. If that's the case, use conn.url, + // otherwise, build the url from the api or this.url. + var url = (this.conn && this.conn.url) ? this.conn.url : (this.api[action]) ? this.api[action].url : this.url; + if (!url) { + throw new Ext.data.Api.Error('invalid-url', action); + } + + // look for urls having "provides" suffix used in some MVC frameworks like Rails/Merb and others. The provides suffice informs + // the server what data-format the client is dealing with and returns data in the same format (eg: application/json, application/xml, etc) + // e.g.: /users.json, /users.xml, etc. + // with restful routes, we need urls like: + // PUT /users/1.json + // DELETE /users/1.json + var provides = null; + var m = url.match(/(.*)(\.json|\.xml|\.html)$/); + if (m) { + provides = m[2]; // eg ".json" + url = m[1]; // eg: "/users" + } + // prettyUrls is deprectated in favor of restful-config + if ((this.restful === true || this.prettyUrls === true) && record instanceof Ext.data.Record && !record.phantom) { + url += '/' + record.id; + } + return (provides === null) ? url : url + provides; + }, + + /** + * Destroys the proxy by purging any event listeners and cancelling any active requests. + */ + destroy: function(){ + this.purgeListeners(); + } +}); + +// Apply the Observable prototype to the DataProxy class so that proxy instances can relay their +// events to the class. Allows for centralized listening of all proxy instances upon the DataProxy class. +Ext.apply(Ext.data.DataProxy, Ext.util.Observable.prototype); +Ext.util.Observable.call(Ext.data.DataProxy); + +/** + * @class Ext.data.DataProxy.Error + * @extends Ext.Error + * DataProxy Error extension. + * constructor + * @param {String} message Message describing the error. + * @param {Record/Record[]} arg + */ +Ext.data.DataProxy.Error = Ext.extend(Ext.Error, { + constructor : function(message, arg) { + this.arg = arg; + Ext.Error.call(this, message); + }, + name: 'Ext.data.DataProxy' +}); +Ext.apply(Ext.data.DataProxy.Error.prototype, { + lang: { + 'action-undefined': "DataProxy attempted to execute an API-action but found an undefined url / function. Please review your Proxy url/api-configuration.", + 'api-invalid': 'Recieved an invalid API-configuration. Please ensure your proxy API-configuration contains only the actions from Ext.data.Api.actions.' + } +}); + + +/** + * @class Ext.data.Request + * A simple Request class used internally to the data package to provide more generalized remote-requests + * to a DataProxy. + * TODO Not yet implemented. Implement in Ext.data.Store#execute + */ +Ext.data.Request = function(params) { + Ext.apply(this, params); +}; +Ext.data.Request.prototype = { + /** + * @cfg {String} action + */ + action : undefined, + /** + * @cfg {Ext.data.Record[]/Ext.data.Record} rs The Store recordset associated with the request. + */ + rs : undefined, + /** + * @cfg {Object} params HTTP request params + */ + params: undefined, + /** + * @cfg {Function} callback The function to call when request is complete + */ + callback : Ext.emptyFn, + /** + * @cfg {Object} scope The scope of the callback funtion + */ + scope : undefined, + /** + * @cfg {Ext.data.DataReader} reader The DataReader instance which will parse the received response + */ + reader : undefined +}; +/** + * @class Ext.data.Response + * A generic response class to normalize response-handling internally to the framework. + */ +Ext.data.Response = function(params) { + Ext.apply(this, params); +}; +Ext.data.Response.prototype = { + /** + * @cfg {String} action {@link Ext.data.Api#actions} + */ + action: undefined, + /** + * @cfg {Boolean} success + */ + success : undefined, + /** + * @cfg {String} message + */ + message : undefined, + /** + * @cfg {Array/Object} data + */ + data: undefined, + /** + * @cfg {Object} raw The raw response returned from server-code + */ + raw: undefined, + /** + * @cfg {Ext.data.Record/Ext.data.Record[]} records related to the Request action + */ + records: undefined +}; +/** + * @class Ext.data.ScriptTagProxy + * @extends Ext.data.DataProxy + * An implementation of Ext.data.DataProxy that reads a data object from a URL which may be in a domain + * other than the originating domain of the running page.
    + *

    + * Note that if you are retrieving data from a page that is in a domain that is NOT the same as the originating domain + * of the running page, you must use this class, rather than HttpProxy.
    + *

    + * The content passed back from a server resource requested by a ScriptTagProxy must be executable JavaScript + * source code because it is used as the source inside a <script> tag.
    + *

    + * In order for the browser to process the returned data, the server must wrap the data object + * with a call to a callback function, the name of which is passed as a parameter by the ScriptTagProxy. + * Below is a Java example for a servlet which returns data for either a ScriptTagProxy, or an HttpProxy + * depending on whether the callback name was passed: + *

    + *

    
    +boolean scriptTag = false;
    +String cb = request.getParameter("callback");
    +if (cb != null) {
    +    scriptTag = true;
    +    response.setContentType("text/javascript");
    +} else {
    +    response.setContentType("application/x-json");
    +}
    +Writer out = response.getWriter();
    +if (scriptTag) {
    +    out.write(cb + "(");
    +}
    +out.print(dataBlock.toJsonString());
    +if (scriptTag) {
    +    out.write(");");
    +}
    +
    + *

    Below is a PHP example to do the same thing:

    
    +$callback = $_REQUEST['callback'];
    +
    +// Create the output object.
    +$output = array('a' => 'Apple', 'b' => 'Banana');
    +
    +//start output
    +if ($callback) {
    +    header('Content-Type: text/javascript');
    +    echo $callback . '(' . json_encode($output) . ');';
    +} else {
    +    header('Content-Type: application/x-json');
    +    echo json_encode($output);
    +}
    +
    + *

    Below is the ASP.Net code to do the same thing:

    
    +String jsonString = "{success: true}";
    +String cb = Request.Params.Get("callback");
    +String responseString = "";
    +if (!String.IsNullOrEmpty(cb)) {
    +    responseString = cb + "(" + jsonString + ")";
    +} else {
    +    responseString = jsonString;
    +}
    +Response.Write(responseString);
    +
    + * + * @constructor + * @param {Object} config A configuration object. + */ +Ext.data.ScriptTagProxy = function(config){ + Ext.apply(this, config); + + Ext.data.ScriptTagProxy.superclass.constructor.call(this, config); + + this.head = document.getElementsByTagName("head")[0]; + + /** + * @event loadexception + * Deprecated in favor of 'exception' event. + * Fires if an exception occurs in the Proxy during data loading. This event can be fired for one of two reasons: + * + * Note that this event is also relayed through {@link Ext.data.Store}, so you can listen for it directly + * on any Store instance. + * @param {Object} this + * @param {Object} options The loading options that were specified (see {@link #load} for details). If the load + * call timed out, this parameter will be null. + * @param {Object} arg The callback's arg object passed to the {@link #load} function + * @param {Error} e The JavaScript Error object caught if the configured Reader could not read the data. + * If the remote request returns success: false, this parameter will be null. + */ +}; + +Ext.data.ScriptTagProxy.TRANS_ID = 1000; + +Ext.extend(Ext.data.ScriptTagProxy, Ext.data.DataProxy, { + /** + * @cfg {String} url The URL from which to request the data object. + */ + /** + * @cfg {Number} timeout (optional) The number of milliseconds to wait for a response. Defaults to 30 seconds. + */ + timeout : 30000, + /** + * @cfg {String} callbackParam (Optional) The name of the parameter to pass to the server which tells + * the server the name of the callback function set up by the load call to process the returned data object. + * Defaults to "callback".

    The server-side processing must read this parameter value, and generate + * javascript output which calls this named function passing the data object as its only parameter. + */ + callbackParam : "callback", + /** + * @cfg {Boolean} nocache (optional) Defaults to true. Disable caching by adding a unique parameter + * name to the request. + */ + nocache : true, + + /** + * HttpProxy implementation of DataProxy#doRequest + * @param {String} action + * @param {Ext.data.Record/Ext.data.Record[]} rs If action is read, rs will be null + * @param {Object} params An object containing properties which are to be used as HTTP parameters + * for the request to the remote server. + * @param {Ext.data.DataReader} reader The Reader object which converts the data + * object into a block of Ext.data.Records. + * @param {Function} callback The function into which to pass the block of Ext.data.Records. + * The function must be passed

    + * @param {Object} scope The scope (this reference) in which the callback function is executed. Defaults to the browser window. + * @param {Object} arg An optional argument which is passed to the callback as its second parameter. + */ + doRequest : function(action, rs, params, reader, callback, scope, arg) { + var p = Ext.urlEncode(Ext.apply(params, this.extraParams)); + + var url = this.buildUrl(action, rs); + if (!url) { + throw new Ext.data.Api.Error('invalid-url', url); + } + url = Ext.urlAppend(url, p); + + if(this.nocache){ + url = Ext.urlAppend(url, '_dc=' + (new Date().getTime())); + } + var transId = ++Ext.data.ScriptTagProxy.TRANS_ID; + var trans = { + id : transId, + action: action, + cb : "stcCallback"+transId, + scriptId : "stcScript"+transId, + params : params, + arg : arg, + url : url, + callback : callback, + scope : scope, + reader : reader + }; + window[trans.cb] = this.createCallback(action, rs, trans); + url += String.format("&{0}={1}", this.callbackParam, trans.cb); + if(this.autoAbort !== false){ + this.abort(); + } + + trans.timeoutId = this.handleFailure.defer(this.timeout, this, [trans]); + + var script = document.createElement("script"); + script.setAttribute("src", url); + script.setAttribute("type", "text/javascript"); + script.setAttribute("id", trans.scriptId); + this.head.appendChild(script); + + this.trans = trans; + }, + + // @private createCallback + createCallback : function(action, rs, trans) { + var self = this; + return function(res) { + self.trans = false; + self.destroyTrans(trans, true); + if (action === Ext.data.Api.actions.read) { + self.onRead.call(self, action, trans, res); + } else { + self.onWrite.call(self, action, trans, res, rs); + } + }; + }, + /** + * Callback for read actions + * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] + * @param {Object} trans The request transaction object + * @param {Object} res The server response + * @protected + */ + onRead : function(action, trans, res) { + var result; + try { + result = trans.reader.readRecords(res); + }catch(e){ + // @deprecated: fire loadexception + this.fireEvent("loadexception", this, trans, res, e); + + this.fireEvent('exception', this, 'response', action, trans, res, e); + trans.callback.call(trans.scope||window, null, trans.arg, false); + return; + } + if (result.success === false) { + // @deprecated: fire old loadexception for backwards-compat. + this.fireEvent('loadexception', this, trans, res); + + this.fireEvent('exception', this, 'remote', action, trans, res, null); + } else { + this.fireEvent("load", this, res, trans.arg); + } + trans.callback.call(trans.scope||window, result, trans.arg, result.success); + }, + /** + * Callback for write actions + * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] + * @param {Object} trans The request transaction object + * @param {Object} res The server response + * @protected + */ + onWrite : function(action, trans, response, rs) { + var reader = trans.reader; + try { + // though we already have a response object here in STP, run through readResponse to catch any meta-data exceptions. + var res = reader.readResponse(action, response); + } catch (e) { + this.fireEvent('exception', this, 'response', action, trans, res, e); + trans.callback.call(trans.scope||window, null, res, false); + return; + } + if(!res.success === true){ + this.fireEvent('exception', this, 'remote', action, trans, res, rs); + trans.callback.call(trans.scope||window, null, res, false); + return; + } + this.fireEvent("write", this, action, res.data, res, rs, trans.arg ); + trans.callback.call(trans.scope||window, res.data, res, true); + }, + + // private + isLoading : function(){ + return this.trans ? true : false; + }, + + /** + * Abort the current server request. + */ + abort : function(){ + if(this.isLoading()){ + this.destroyTrans(this.trans); + } + }, + + // private + destroyTrans : function(trans, isLoaded){ + this.head.removeChild(document.getElementById(trans.scriptId)); + clearTimeout(trans.timeoutId); + if(isLoaded){ + window[trans.cb] = undefined; + try{ + delete window[trans.cb]; + }catch(e){} + }else{ + // if hasn't been loaded, wait for load to remove it to prevent script error + window[trans.cb] = function(){ + window[trans.cb] = undefined; + try{ + delete window[trans.cb]; + }catch(e){} + }; + } + }, + + // private + handleFailure : function(trans){ + this.trans = false; + this.destroyTrans(trans, false); + if (trans.action === Ext.data.Api.actions.read) { + // @deprecated firing loadexception + this.fireEvent("loadexception", this, null, trans.arg); + } + + this.fireEvent('exception', this, 'response', trans.action, { + response: null, + options: trans.arg + }); + trans.callback.call(trans.scope||window, null, trans.arg, false); + }, + + // inherit docs + destroy: function(){ + this.abort(); + Ext.data.ScriptTagProxy.superclass.destroy.call(this); + } +});/** + * @class Ext.data.HttpProxy + * @extends Ext.data.DataProxy + *

    An implementation of {@link Ext.data.DataProxy} that processes data requests within the same + * domain of the originating page.

    + *

    Note: this class cannot be used to retrieve data from a domain other + * than the domain from which the running page was served. For cross-domain requests, use a + * {@link Ext.data.ScriptTagProxy ScriptTagProxy}.

    + *

    Be aware that to enable the browser to parse an XML document, the server must set + * the Content-Type header in the HTTP response to "text/xml".

    + * @constructor + * @param {Object} conn + * An {@link Ext.data.Connection} object, or options parameter to {@link Ext.Ajax#request}. + *

    Note that if this HttpProxy is being used by a {@link Ext.data.Store Store}, then the + * Store's call to {@link #load} will override any specified callback and params + * options. In this case, use the Store's {@link Ext.data.Store#events events} to modify parameters, + * or react to loading events. The Store's {@link Ext.data.Store#baseParams baseParams} may also be + * used to pass parameters known at instantiation time.

    + *

    If an options parameter is passed, the singleton {@link Ext.Ajax} object will be used to make + * the request.

    + */ +Ext.data.HttpProxy = function(conn){ + Ext.data.HttpProxy.superclass.constructor.call(this, conn); + + /** + * The Connection object (Or options parameter to {@link Ext.Ajax#request}) which this HttpProxy + * uses to make requests to the server. Properties of this object may be changed dynamically to + * change the way data is requested. + * @property + */ + this.conn = conn; + + // nullify the connection url. The url param has been copied to 'this' above. The connection + // url will be set during each execution of doRequest when buildUrl is called. This makes it easier for users to override the + // connection url during beforeaction events (ie: beforeload, beforewrite, etc). + // Url is always re-defined during doRequest. + this.conn.url = null; + + this.useAjax = !conn || !conn.events; + + // A hash containing active requests, keyed on action [Ext.data.Api.actions.create|read|update|destroy] + var actions = Ext.data.Api.actions; + this.activeRequest = {}; + for (var verb in actions) { + this.activeRequest[actions[verb]] = undefined; + } +}; + +Ext.extend(Ext.data.HttpProxy, Ext.data.DataProxy, { + /** + * Return the {@link Ext.data.Connection} object being used by this Proxy. + * @return {Connection} The Connection object. This object may be used to subscribe to events on + * a finer-grained basis than the DataProxy events. + */ + getConnection : function() { + return this.useAjax ? Ext.Ajax : this.conn; + }, + + /** + * Used for overriding the url used for a single request. Designed to be called during a beforeaction event. Calling setUrl + * will override any urls set via the api configuration parameter. Set the optional parameter makePermanent to set the url for + * all subsequent requests. If not set to makePermanent, the next request will use the same url or api configuration defined + * in the initial proxy configuration. + * @param {String} url + * @param {Boolean} makePermanent (Optional) [false] + * + * (e.g.: beforeload, beforesave, etc). + */ + setUrl : function(url, makePermanent) { + this.conn.url = url; + if (makePermanent === true) { + this.url = url; + this.api = null; + Ext.data.Api.prepare(this); + } + }, + + /** + * HttpProxy implementation of DataProxy#doRequest + * @param {String} action The crud action type (create, read, update, destroy) + * @param {Ext.data.Record/Ext.data.Record[]} rs If action is load, rs will be null + * @param {Object} params An object containing properties which are to be used as HTTP parameters + * for the request to the remote server. + * @param {Ext.data.DataReader} reader The Reader object which converts the data + * object into a block of Ext.data.Records. + * @param {Function} callback + *

    A function to be called after the request. + * The callback is passed the following arguments:

    + * @param {Object} scope The scope (this reference) in which the callback function is executed. Defaults to the browser window. + * @param {Object} arg An optional argument which is passed to the callback as its second parameter. + * @protected + */ + doRequest : function(action, rs, params, reader, cb, scope, arg) { + var o = { + method: (this.api[action]) ? this.api[action]['method'] : undefined, + request: { + callback : cb, + scope : scope, + arg : arg + }, + reader: reader, + callback : this.createCallback(action, rs), + scope: this + }; + + // If possible, transmit data using jsonData || xmlData on Ext.Ajax.request (An installed DataWriter would have written it there.). + // Use std HTTP params otherwise. + if (params.jsonData) { + o.jsonData = params.jsonData; + } else if (params.xmlData) { + o.xmlData = params.xmlData; + } else { + o.params = params || {}; + } + // Set the connection url. If this.conn.url is not null here, + // the user must have overridden the url during a beforewrite/beforeload event-handler. + // this.conn.url is nullified after each request. + this.conn.url = this.buildUrl(action, rs); + + if(this.useAjax){ + + Ext.applyIf(o, this.conn); + + // If a currently running request is found for this action, abort it. + if (this.activeRequest[action]) { + //// + // Disabled aborting activeRequest while implementing REST. activeRequest[action] will have to become an array + // TODO ideas anyone? + // + //Ext.Ajax.abort(this.activeRequest[action]); + } + this.activeRequest[action] = Ext.Ajax.request(o); + }else{ + this.conn.request(o); + } + // request is sent, nullify the connection url in preparation for the next request + this.conn.url = null; + }, + + /** + * Returns a callback function for a request. Note a special case is made for the + * read action vs all the others. + * @param {String} action [create|update|delete|load] + * @param {Ext.data.Record[]} rs The Store-recordset being acted upon + * @private + */ + createCallback : function(action, rs) { + return function(o, success, response) { + this.activeRequest[action] = undefined; + if (!success) { + if (action === Ext.data.Api.actions.read) { + // @deprecated: fire loadexception for backwards compat. + // TODO remove + this.fireEvent('loadexception', this, o, response); + } + this.fireEvent('exception', this, 'response', action, o, response); + o.request.callback.call(o.request.scope, null, o.request.arg, false); + return; + } + if (action === Ext.data.Api.actions.read) { + this.onRead(action, o, response); + } else { + this.onWrite(action, o, response, rs); + } + }; + }, + + /** + * Callback for read action + * @param {String} action Action name as per {@link Ext.data.Api.actions#read}. + * @param {Object} o The request transaction object + * @param {Object} res The server response + * @fires loadexception (deprecated) + * @fires exception + * @fires load + * @protected + */ + onRead : function(action, o, response) { + var result; + try { + result = o.reader.read(response); + }catch(e){ + // @deprecated: fire old loadexception for backwards-compat. + // TODO remove + this.fireEvent('loadexception', this, o, response, e); + + this.fireEvent('exception', this, 'response', action, o, response, e); + o.request.callback.call(o.request.scope, null, o.request.arg, false); + return; + } + if (result.success === false) { + // @deprecated: fire old loadexception for backwards-compat. + // TODO remove + this.fireEvent('loadexception', this, o, response); + + // Get DataReader read-back a response-object to pass along to exception event + var res = o.reader.readResponse(action, response); + this.fireEvent('exception', this, 'remote', action, o, res, null); + } + else { + this.fireEvent('load', this, o, o.request.arg); + } + // TODO refactor onRead, onWrite to be more generalized now that we're dealing with Ext.data.Response instance + // the calls to request.callback(...) in each will have to be made identical. + // NOTE reader.readResponse does not currently return Ext.data.Response + o.request.callback.call(o.request.scope, result, o.request.arg, result.success); + }, + /** + * Callback for write actions + * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] + * @param {Object} trans The request transaction object + * @param {Object} res The server response + * @fires exception + * @fires write + * @protected + */ + onWrite : function(action, o, response, rs) { + var reader = o.reader; + var res; + try { + res = reader.readResponse(action, response); + } catch (e) { + this.fireEvent('exception', this, 'response', action, o, response, e); + o.request.callback.call(o.request.scope, null, o.request.arg, false); + return; + } + if (res.success === true) { + this.fireEvent('write', this, action, res.data, res, rs, o.request.arg); + } else { + this.fireEvent('exception', this, 'remote', action, o, res, rs); + } + // TODO refactor onRead, onWrite to be more generalized now that we're dealing with Ext.data.Response instance + // the calls to request.callback(...) in each will have to be made similar. + // NOTE reader.readResponse does not currently return Ext.data.Response + o.request.callback.call(o.request.scope, res.data, res, res.success); + }, + + // inherit docs + destroy: function(){ + if(!this.useAjax){ + this.conn.abort(); + }else if(this.activeRequest){ + var actions = Ext.data.Api.actions; + for (var verb in actions) { + if(this.activeRequest[actions[verb]]){ + Ext.Ajax.abort(this.activeRequest[actions[verb]]); + } + } + } + Ext.data.HttpProxy.superclass.destroy.call(this); + } +});/** + * @class Ext.data.MemoryProxy + * @extends Ext.data.DataProxy + * An implementation of Ext.data.DataProxy that simply passes the data specified in its constructor + * to the Reader when its load method is called. + * @constructor + * @param {Object} data The data object which the Reader uses to construct a block of Ext.data.Records. + */ +Ext.data.MemoryProxy = function(data){ + // Must define a dummy api with "read" action to satisfy DataProxy#doRequest and Ext.data.Api#prepare *before* calling super + var api = {}; + api[Ext.data.Api.actions.read] = true; + Ext.data.MemoryProxy.superclass.constructor.call(this, { + api: api + }); + this.data = data; +}; + +Ext.extend(Ext.data.MemoryProxy, Ext.data.DataProxy, { + /** + * @event loadexception + * Fires if an exception occurs in the Proxy during data loading. Note that this event is also relayed + * through {@link Ext.data.Store}, so you can listen for it directly on any Store instance. + * @param {Object} this + * @param {Object} arg The callback's arg object passed to the {@link #load} function + * @param {Object} null This parameter does not apply and will always be null for MemoryProxy + * @param {Error} e The JavaScript Error object caught if the configured Reader could not read the data + */ + + /** + * MemoryProxy implementation of DataProxy#doRequest + * @param {String} action + * @param {Ext.data.Record/Ext.data.Record[]} rs If action is load, rs will be null + * @param {Object} params An object containing properties which are to be used as HTTP parameters + * for the request to the remote server. + * @param {Ext.data.DataReader} reader The Reader object which converts the data + * object into a block of Ext.data.Records. + * @param {Function} callback The function into which to pass the block of Ext.data.Records. + * The function must be passed + * @param {Object} scope The scope (this reference) in which the callback function is executed. Defaults to the browser window. + * @param {Object} arg An optional argument which is passed to the callback as its second parameter. + */ + doRequest : function(action, rs, params, reader, callback, scope, arg) { + // No implementation for CRUD in MemoryProxy. Assumes all actions are 'load' + params = params || {}; + var result; + try { + result = reader.readRecords(this.data); + }catch(e){ + // @deprecated loadexception + this.fireEvent("loadexception", this, null, arg, e); + + this.fireEvent('exception', this, 'response', action, arg, null, e); + callback.call(scope, null, arg, false); + return; + } + callback.call(scope, result, arg, true); + } +});/** + * @class Ext.data.Types + *

    This is s static class containing the system-supplied data types which may be given to a {@link Ext.data.Field Field}.

    + *

    The properties in this class are used as type indicators in the {@link Ext.data.Field Field} class, so to + * test whether a Field is of a certain type, compare the {@link Ext.data.Field#type type} property against properties + * of this class.

    + *

    Developers may add their own application-specific data types to this class. Definition names must be UPPERCASE. + * each type definition must contain three properties:

    + *
    + *

    For example, to create a VELatLong field (See the Microsoft Bing Mapping API) containing the latitude/longitude value of a datapoint on a map from a JsonReader data block + * which contained the properties lat and long, you would define a new data type like this:

    + *
    
    +// Add a new Field data type which stores a VELatLong object in the Record.
    +Ext.data.Types.VELATLONG = {
    +    convert: function(v, data) {
    +        return new VELatLong(data.lat, data.long);
    +    },
    +    sortType: function(v) {
    +        return v.Latitude;  // When sorting, order by latitude
    +    },
    +    type: 'VELatLong'
    +};
    +
    + *

    Then, when declaring a Record, use

    
    +var types = Ext.data.Types; // allow shorthand type access
    +UnitRecord = Ext.data.Record.create([
    +    { name: 'unitName', mapping: 'UnitName' },
    +    { name: 'curSpeed', mapping: 'CurSpeed', type: types.INT },
    +    { name: 'latitude', mapping: 'lat', type: types.FLOAT },
    +    { name: 'latitude', mapping: 'lat', type: types.FLOAT },
    +    { name: 'position', type: types.VELATLONG }
    +]);
    +
    + * @singleton + */ +Ext.data.Types = new function(){ + var st = Ext.data.SortTypes; + Ext.apply(this, { + /** + * @type Regexp + * @property stripRe + * A regular expression for stripping non-numeric characters from a numeric value. Defaults to /[\$,%]/g. + * This should be overridden for localization. + */ + stripRe: /[\$,%]/g, + + /** + * @type Object. + * @property AUTO + * This data type means that no conversion is applied to the raw data before it is placed into a Record. + */ + AUTO: { + convert: function(v){ return v; }, + sortType: st.none, + type: 'auto' + }, + + /** + * @type Object. + * @property STRING + * This data type means that the raw data is converted into a String before it is placed into a Record. + */ + STRING: { + convert: function(v){ return (v === undefined || v === null) ? '' : String(v); }, + sortType: st.asUCString, + type: 'string' + }, + + /** + * @type Object. + * @property INT + * This data type means that the raw data is converted into an integer before it is placed into a Record. + *

    The synonym INTEGER is equivalent.

    + */ + INT: { + convert: function(v){ + return v !== undefined && v !== null && v !== '' ? + parseInt(String(v).replace(Ext.data.Types.stripRe, ''), 10) : 0; + }, + sortType: st.none, + type: 'int' + }, + + /** + * @type Object. + * @property FLOAT + * This data type means that the raw data is converted into a number before it is placed into a Record. + *

    The synonym NUMBER is equivalent.

    + */ + FLOAT: { + convert: function(v){ + return v !== undefined && v !== null && v !== '' ? + parseFloat(String(v).replace(Ext.data.Types.stripRe, ''), 10) : 0; + }, + sortType: st.none, + type: 'float' + }, + + /** + * @type Object. + * @property BOOL + *

    This data type means that the raw data is converted into a boolean before it is placed into + * a Record. The string "true" and the number 1 are converted to boolean true.

    + *

    The synonym BOOLEAN is equivalent.

    + */ + BOOL: { + convert: function(v){ return v === true || v === 'true' || v == 1; }, + sortType: st.none, + type: 'bool' + }, + + /** + * @type Object. + * @property DATE + * This data type means that the raw data is converted into a Date before it is placed into a Record. + * The date format is specified in the constructor of the {@link Ext.data.Field} to which this type is + * being applied. + */ + DATE: { + convert: function(v){ + var df = this.dateFormat; + if(!v){ + return null; + } + if(Ext.isDate(v)){ + return v; + } + if(df){ + if(df == 'timestamp'){ + return new Date(v*1000); + } + if(df == 'time'){ + return new Date(parseInt(v, 10)); + } + return Date.parseDate(v, df); + } + var parsed = Date.parse(v); + return parsed ? new Date(parsed) : null; + }, + sortType: st.asDate, + type: 'date' + } + }); + + Ext.apply(this, { + /** + * @type Object. + * @property BOOLEAN + *

    This data type means that the raw data is converted into a boolean before it is placed into + * a Record. The string "true" and the number 1 are converted to boolean true.

    + *

    The synonym BOOL is equivalent.

    + */ + BOOLEAN: this.BOOL, + /** + * @type Object. + * @property INTEGER + * This data type means that the raw data is converted into an integer before it is placed into a Record. + *

    The synonym INT is equivalent.

    + */ + INTEGER: this.INT, + /** + * @type Object. + * @property NUMBER + * This data type means that the raw data is converted into a number before it is placed into a Record. + *

    The synonym FLOAT is equivalent.

    + */ + NUMBER: this.FLOAT + }); +}; \ No newline at end of file