X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/25ef3491bd9ae007ff1fc2b0d7943e6eaaccf775..2e847cf21b8ab9d15fa167b315ca5b2fa92638fc:/ext-all-debug.js diff --git a/ext-all-debug.js b/ext-all-debug.js index d38b19f1..8eb0f03e 100644 --- a/ext-all-debug.js +++ b/ext-all-debug.js @@ -1,6 +1,6 @@ /*! - * Ext JS Library 3.0.3 - * Copyright(c) 2006-2009 Ext JS, LLC + * Ext JS Library 3.1.1 + * Copyright(c) 2006-2010 Ext JS, LLC * licensing@extjs.com * http://www.extjs.com/license */ @@ -167,9 +167,11 @@ Ext.DomHelper = function(){ if(Ext.isString(o)){ b = o; } else if (Ext.isArray(o)) { - Ext.each(o, function(v) { - b += createHtml(v); - }); + for (var i=0; i < o.length; i++) { + if(o[i]) { + b += createHtml(o[i]); + } + }; } else { b += '<' + (o.tag = o.tag || 'div'); Ext.iterate(o, function(attr, val){ @@ -974,6 +976,7 @@ All selectors, attribute filters and pseudos below can be combined infinitely in
  • E:has(S) an E element that has a descendent that matches simple selector S
  • E:next(S) an E element whose next sibling matches simple selector S
  • E:prev(S) an E element whose previous sibling matches simple selector S
  • +
  • E:any(S1|S2|S2) an E element which matches any of the simple selectors S1, S2 or S3//\\
  • CSS Value Selectors:

    * @return {Boolean} If the developer provided {@link #beforeload} event handler returns @@ -31470,7 +33215,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(); } }, @@ -31529,23 +33274,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 {} @@ -31559,19 +33306,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; }, @@ -31588,68 +33336,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]); + } } - return true; + 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 -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 @@ -31658,6 +33463,7 @@ sortInfo: { if (success === true) { this.fireEvent('write', this, action, data, response, rs); } + this.removeFromBatch(batch, action, data); }; }, @@ -31744,12 +33550,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)); @@ -31758,6 +33574,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); @@ -31925,7 +33744,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); @@ -31996,7 +33816,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; @@ -32027,7 +33847,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){ @@ -32036,10 +33856,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 @@ -32051,9 +33871,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 */ @@ -32071,7 +33891,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 */ @@ -32232,7 +34052,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 @@ -32289,17 +34108,16 @@ Ext.data.Field = function(config){ case "int": cv = function(v){ return v !== undefined && v !== null && v !== '' ? - parseInt(String(v).replace(stripRe, ""), 10) : ''; + parseInt(String(v).replace(stripRe, ""), 10) : ''; }; break; case "float": cv = function(v){ return v !== undefined && v !== null && v !== '' ? - parseFloat(String(v).replace(stripRe, ""), 10) : ''; + parseFloat(String(v).replace(stripRe, ""), 10) : ''; }; break; case "bool": - case "boolean": cv = function(v){ return v === true || v === "true" || v == 1; }; break; case "date": @@ -32322,7 +34140,10 @@ Ext.data.Field = function(config){ var parsed = Date.parse(v); return parsed ? new Date(parsed) : null; }; - break; + break; + default: + cv = function(v){ return v; }; + break; } this.convert = cv; @@ -32383,7 +34204,7 @@ var store = new Ext.data.Store({ reader: new Ext.data.JsonReader( { idProperty: 'key', - root: 'daRoot', + root: 'daRoot', totalProperty: 'total' }, Dude // recordType @@ -32468,13 +34289,13 @@ sortType: function(value) { * "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 + /** + * @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 @@ -32584,7 +34405,12 @@ Ext.data.DataReader.prototype = { 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.fields.each(function(f) { + if (data[f.name] !== f.defaultValue) { + rs.data[f.name] = data[f.name]; + } + }); rs.commit(); } }, @@ -32616,12 +34442,56 @@ Ext.data.DataReader.prototype = { data = data.shift(); } if (this.isData(data)) { - rs.data = Ext.apply(rs.data, data); + rs.fields.each(function(f) { + if (data[f.name] !== f.defaultValue) { + rs.data[f.name] = data[f.name]; + } + }); } 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. @@ -32660,42 +34530,6 @@ Ext.apply(Ext.data.DataReader.Error.prototype, { '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 @@ -32706,34 +34540,72 @@ Ext.data.Response.prototype = { * {@link Ext.data.JsonWriter}.

    *

    Creating a writer is simple:

    *
    
    -var writer = new Ext.data.JsonWriter();
    +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'
    +    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:

    + *

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

    *
    
    -// Use the api specification
    +// 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 : 'app.php/users/destroy'
    +        destroy : {  // <--- Supports object-syntax as well
    +            url: 'app.php/users/destroy',
    +            method: "DELETE"
    +        }
         }
     });
      * 
    - *

    Creating a Writer enabled store:

    + *

    Pulling it all together into a Writer-enabled Store:

    *
    
     var store = new Ext.data.Store({
         proxy: proxy,
         reader: reader,
    -    writer: writer
    +    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) @@ -32761,13 +34633,14 @@ Ext.data.DataWriter.prototype = { listful : false, // <-- listful is actually not used internally here in DataWriter. @see Ext.data.Store#execute. /** - * 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. + * 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. */ - write : function(action, params, rs) { + apply : function(params, baseParams, action, rs) { var data = [], renderer = action + 'Record'; // TODO implement @cfg listful here @@ -32779,7 +34652,7 @@ Ext.data.DataWriter.prototype = { else if (rs instanceof Ext.data.Record) { data = this[renderer](rs); } - this.render(action, rs, params, data); + this.render(params, baseParams, data); }, /** @@ -32811,11 +34684,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} + * @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, @@ -32836,6 +34714,24 @@ 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 @@ -32872,6 +34768,25 @@ proxy : new Ext.data.HttpProxy({ } }), * + *

    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. @@ -33186,7 +35101,7 @@ proxy.setApi(Ext.data.Api.actions.read, '/users/new_load_url'); * @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} 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) { @@ -33215,7 +35130,7 @@ proxy.setApi(Ext.data.Api.actions.read, '/users/new_load_url'); load : null, /** - * @cfg {Function} doRequest Abstract method that should be implemented in all subclasses + * @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}). */ @@ -33226,6 +35141,27 @@ proxy.setApi(Ext.data.Api.actions.read, '/users/new_load_url'); 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, @@ -33238,30 +35174,32 @@ proxy.setApi(Ext.data.Api.actions.read, '/users/new_load_url'); */ buildUrl : function(action, record) { record = record || null; - var url = (this.api[action]) ? this.api[action].url : this.url; + + // 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 (from Rails/Merb and others...), - // e.g.: /users.json, /users.xml, etc + // 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 format = null; + var provides = null; var m = url.match(/(.*)(\.json|\.xml|\.html)$/); if (m) { - format = m[2]; // eg ".json" - url = m[1]; // eg: "/users" + provides = 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) { + if ((this.restful === true || this.prettyUrls === 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; + return (provides === null) ? url : url + provides; }, /** @@ -33300,6 +35238,74 @@ Ext.apply(Ext.data.DataProxy.Error.prototype, { }); +/** + * @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 @@ -33334,6 +35340,32 @@ 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 @@ -33404,7 +35436,7 @@ Ext.extend(Ext.data.ScriptTagProxy, Ext.data.DataProxy, { *
  • The "arg" argument from the load function
  • *
  • A boolean success indicator
  • * - * @param {Object} scope The scope in which to call the callback + * @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) { @@ -33467,7 +35499,7 @@ Ext.extend(Ext.data.ScriptTagProxy, Ext.data.DataProxy, { * @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 + * @protected */ onRead : function(action, trans, res) { var result; @@ -33496,7 +35528,7 @@ Ext.extend(Ext.data.ScriptTagProxy, Ext.data.DataProxy, { * @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 + * @protected */ onWrite : function(action, trans, response, rs) { var reader = trans.reader; @@ -33585,7 +35617,7 @@ Ext.extend(Ext.data.ScriptTagProxy, Ext.data.DataProxy, { * @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 + *

    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 @@ -33663,8 +35695,9 @@ Ext.extend(Ext.data.HttpProxy, Ext.data.DataProxy, { *

  • r : Ext.data.Record[] The block of Ext.data.Records.
  • *
  • options: Options object from the action request
  • *
  • success: Boolean success indicator
  • - * @param {Object} scope The scope in which to call the callback + * @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 = { @@ -33681,7 +35714,6 @@ Ext.extend(Ext.data.HttpProxy, Ext.data.DataProxy, { // 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) { @@ -33690,14 +35722,10 @@ Ext.extend(Ext.data.HttpProxy, Ext.data.DataProxy, { 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. + // the user must have overridden the url during a beforewrite/beforeload 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; - } + this.conn.url = this.buildUrl(action, rs); + if(this.useAjax){ Ext.applyIf(o, this.conn); @@ -33743,7 +35771,7 @@ Ext.extend(Ext.data.HttpProxy, Ext.data.DataProxy, { } else { this.onWrite(action, o, response, rs); } - } + }; }, /** @@ -33754,7 +35782,7 @@ Ext.extend(Ext.data.HttpProxy, Ext.data.DataProxy, { * @fires loadexception (deprecated) * @fires exception * @fires load - * @private + * @protected */ onRead : function(action, o, response) { var result; @@ -33793,7 +35821,7 @@ Ext.extend(Ext.data.HttpProxy, Ext.data.DataProxy, { * @param {Object} res The server response * @fires exception * @fires write - * @private + * @protected */ onWrite : function(action, o, response, rs) { var reader = o.reader; @@ -33805,10 +35833,10 @@ Ext.extend(Ext.data.HttpProxy, Ext.data.DataProxy, { 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 { + 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. @@ -33873,7 +35901,7 @@ Ext.extend(Ext.data.MemoryProxy, Ext.data.DataProxy, { *
  • The "arg" argument from the load function
  • *
  • A boolean success indicator
  • * - * @param {Object} scope The scope in which to call the callback + * @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) { @@ -33897,20 +35925,7 @@ Ext.extend(Ext.data.MemoryProxy, Ext.data.DataProxy, { * @extends Ext.data.DataWriter * DataWriter extension for writing an array or single {@link Ext.data.Record} object(s) in preparation for executing a remote CRUD action. */ -Ext.data.JsonWriter = function(config) { - Ext.data.JsonWriter.superclass.constructor.call(this, config); - - // careful to respect "returnJson", renamed to "encode" - // TODO: remove after v3 final release - if (this.returnJson != undefined) { - this.encode = this.returnJson; - } -} -Ext.extend(Ext.data.JsonWriter, Ext.data.DataWriter, { - /** - * @cfg {Boolean} returnJson Deprecated. Use {@link Ext.data.JsonWriter#encode} instead. - */ - returnJson : undefined, +Ext.data.JsonWriter = Ext.extend(Ext.data.DataWriter, { /** * @cfg {Boolean} encode true to {@link Ext.util.JSON#encode encode} the * {@link Ext.data.DataWriter#toHash hashed data}. Defaults to true. When using @@ -33922,20 +35937,34 @@ Ext.extend(Ext.data.JsonWriter, Ext.data.DataWriter, { * let the lower-level connection object (eg: Ext.Ajax) do the encoding. */ encode : true, + /** + * @cfg {Boolean} encodeDelete False to send only the id to the server on delete, true to encode it in an object + * literal, eg:
    
    +{id: 1}
    + * 
    Defaults to false + */ + encodeDelete: false, + + constructor : function(config){ + Ext.data.JsonWriter.superclass.constructor.call(this, config); + }, /** * Final action of a write event. Apply the written data-object to params. - * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] - * @param {Record[]} rs - * @param {Object} http params - * @param {Object} data object populated according to DataReader meta-data "root" and "idProperty" + * @param {Object} http params-object to write-to. + * @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 {Object/Object[]} data Data-object representing compiled Store-recordset. */ - render : function(action, rs, params, data) { + render : function(params, baseParams, data) { if (this.encode === true) { + // Encode here now. + Ext.apply(params, baseParams); params[this.meta.root] = Ext.encode(data); } else { - params.jsonData = {}; - params.jsonData[this.meta.root] = data; + // defer encoding for some other layer, probably in {@link Ext.Ajax#request}. Place everything into "jsonData" key. + var jdata = Ext.apply({}, baseParams); + jdata[this.meta.root] = data; + params.jsonData = jdata; } }, /** @@ -33963,8 +35992,14 @@ Ext.extend(Ext.data.JsonWriter, Ext.data.DataWriter, { * @param {Ext.data.Record} rec * @return {Object} */ - destroyRecord : function(rec) { - return rec.id; + destroyRecord : function(rec){ + if(this.encodeDelete){ + var data = {}; + data[this.meta.idProperty] = rec.id; + return data; + }else{ + return rec.id; + } } });/** * @class Ext.data.JsonReader @@ -34155,7 +36190,7 @@ Ext.extend(Ext.data.JsonReader, Ext.data.DataReader, { var res = new Ext.data.Response({ action: action, success: this.getSuccess(o), - data: this.extractData(root), + data: (root) ? this.extractData(root, false) : [], message: this.getMessage(o), raw: o }); @@ -34260,50 +36295,22 @@ Ext.extend(Ext.data.JsonReader, Ext.data.DataReader, { createAccessor : function(){ var re = /[\[\.]/; return function(expr) { - try { - return(re.test(expr)) ? - new Function('obj', 'return obj.' + expr) : - function(obj){ - return obj[expr]; - }; - } catch(e){} - return Ext.emptyFn; - }; - }(), - - /** - * 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) { - var rs = undefined; - if (this.isData(root)) { - root = [root]; - } - if (Ext.isArray(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.json = n; - rs.push(record); - } + if(Ext.isEmpty(expr)){ + return Ext.emptyFn; } - else { - for (var i = 0; i < root.length; i++) { - rs.push(this.extractValues(root[i], fi, fl)); - } + if(Ext.isFunction(expr)){ + return expr; } - } - return rs; - }, + var i = String(expr).search(re); + if(i >= 0){ + return new Function('obj', 'return obj' + (i > 0 ? '.' : '') + expr); + } + return function(obj){ + return obj[expr]; + }; + + }; + }(), /** * type-casts a single row of raw-data from server @@ -34394,18 +36401,12 @@ Ext.data.ArrayReader = Ext.extend(Ext.data.JsonReader, { this.arrayData = o; var s = this.meta, sid = s ? Ext.num(s.idIndex, s.id) : null, - recordType = this.recordType, + recordType = this.recordType, fields = recordType.prototype.fields, records = [], + success = true, v; - if(!this.getRoot) { - this.getRoot = s.root ? this.getJsonAccessor(s.root) : function(p) {return p;}; - if(s.totalProperty) { - this.getTotal = this.getJsonAccessor(s.totalProperty); - } - } - var root = this.getRoot(o); for(var i = 0, len = root.length; i < len; i++) { @@ -34432,8 +36433,15 @@ Ext.data.ArrayReader = Ext.extend(Ext.data.JsonReader, { totalRecords = v; } } + if(s.successProperty){ + v = this.getSuccess(o); + if(v === false || v === 'false'){ + success = false; + } + } return { + success : success, records : records, totalRecords : totalRecords }; @@ -34547,14 +36555,31 @@ Ext.reg('jsonstore', Ext.data.JsonStore);/** * @class Ext.data.XmlWriter * @extends Ext.data.DataWriter * DataWriter extension for writing an array or single {@link Ext.data.Record} object(s) in preparation for executing a remote CRUD action via XML. + * XmlWriter uses an instance of {@link Ext.XTemplate} for maximum flexibility in defining your own custom XML schema if the default schema is not appropriate for your needs. + * See the {@link #tpl} configuration-property. */ Ext.data.XmlWriter = function(params) { Ext.data.XmlWriter.superclass.constructor.apply(this, arguments); - this.tpl = new Ext.XTemplate(this.tpl).compile(); + // compile the XTemplate for rendering XML documents. + this.tpl = (typeof(this.tpl) === 'string') ? new Ext.XTemplate(this.tpl).compile() : this.tpl.compile(); }; Ext.extend(Ext.data.XmlWriter, Ext.data.DataWriter, { /** - * @cfg {String} root [records] The name of the root element when writing multiple records to the server. Each + * @cfg {String} documentRoot [xrequest] (Optional) The name of the XML document root-node. Note: + * this parameter is required
    only when sending extra {@link Ext.data.Store#baseParams baseParams} to the server + * during a write-request -- if no baseParams are set, the {@link Ext.data.XmlReader#record} meta-property can + * suffice as the XML document root-node for write-actions involving just a single record. For requests + * involving multiple records and NO baseParams, the {@link Ext.data.XmlWriter#root} property can + * act as the XML document root. + */ + documentRoot: 'xrequest', + /** + * @cfg {Boolean} forceDocumentRoot [false] Set to true to force XML documents having a root-node as defined + * by {@link #documentRoot}, even with no baseParams defined. + */ + forceDocumentRoot: false, + /** + * @cfg {String} root [records] The name of the containing element which will contain the nodes of an write-action involving multiple records. Each * xml-record written to the server will be wrapped in an element named after {@link Ext.data.XmlReader#record} property. * eg:
    @@ -34571,7 +36596,7 @@ Ext.extend(Ext.data.XmlWriter, Ext.data.DataWriter, {
             <records><first>Barney</first></user>
         </records>
     
    - * Defaults to records + * Defaults to records. Do not confuse the nature of this property with that of {@link #documentRoot} */ root: 'records', /** @@ -34585,88 +36610,96 @@ Ext.extend(Ext.data.XmlWriter, Ext.data.DataWriter, { */ xmlEncoding: 'ISO-8859-15', /** - * @cfg {String} tpl The xml template. Defaults to + * @cfg {String/Ext.XTemplate} tpl The XML template used to render {@link Ext.data.Api#actions write-actions} to your server. + *

    One can easily provide his/her own custom {@link Ext.XTemplate#constructor template-definition} if the default does not suffice.

    + *

    Defaults to:

     <?xml version="{version}" encoding="{encoding}"?>
    -    <tpl if="{[values.nodes.length>1]}"><{root}}>',
    +    <tpl if="documentRoot"><{documentRoot}>
    +    <tpl for="baseParams">
    +        <tpl for=".">
    +            <{name}>{value}</{name}>
    +        </tpl>
    +    </tpl>
    +    <tpl if="records.length > 1"><{root}>',
         <tpl for="records">
             <{parent.record}>
    -        <tpl for="fields">
    +        <tpl for=".">
                 <{name}>{value}</{name}>
             </tpl>
             </{parent.record}>
         </tpl>
    -    <tpl if="{[values.records.length>1]}"></{root}}></tpl>
    +    <tpl if="records.length > 1"></{root}></tpl>
    +    <tpl if="documentRoot"></{documentRoot}></tpl>
     
    + *

    Templates will be called with the following API

    + * */ // Break up encoding here in case it's being included by some kind of page that will parse it (eg. PHP) tpl: '<' + '?xml version="{version}" encoding="{encoding}"?' + '><{documentRoot}><{name}>{value}<{root}><{parent.record}><{name}>{value}', /** - * Final action of a write event. Apply the written data-object to params. - * @param {String} action [Ext.data.Api.create|read|update|destroy] - * @param {Ext.data.Record/Ext.data.Record[]} rs - * @param {Object} http params - * @param {Object/Object[]} rendered data. + * XmlWriter implementation of the final stage of a write action. + * @param {Object} params Transport-proxy's (eg: {@link Ext.Ajax#request}) params-object to write-to. + * @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 {Object/Object[]} data Data-object representing the compiled Store-recordset. */ - render : function(action, rs, params, data) { + render : function(params, baseParams, data) { + baseParams = this.toArray(baseParams); params.xmlData = this.tpl.applyTemplate({ version: this.xmlVersion, encoding: this.xmlEncoding, + documentRoot: (baseParams.length > 0 || this.forceDocumentRoot === true) ? this.documentRoot : false, record: this.meta.record, root: this.root, - records: (Ext.isArray(rs)) ? data : [data] + baseParams: baseParams, + records: (Ext.isArray(data[0])) ? data : [data] }); }, - /** - * Converts an Ext.data.Record to xml - * @param {Ext.data.Record} rec - * @return {String} rendered xml-element - * @private - */ - toXml : function(data) { - var fields = []; - Ext.iterate(data, function(k, v) { - fields.push({ - name: k, - value: v - }); - },this); - return { - fields: fields - }; - }, - /** * createRecord + * @protected * @param {Ext.data.Record} rec - * @return {String} xml element - * @private + * @return {Array} Array of name:value pairs for attributes of the {@link Ext.data.Record}. See {@link Ext.data.DataWriter#toHash}. */ createRecord : function(rec) { - return this.toXml(this.toHash(rec)); + return this.toArray(this.toHash(rec)); }, /** * updateRecord + * @protected * @param {Ext.data.Record} rec - * @return {String} xml element - * @private + * @return {Array} Array of {name:value} pairs for attributes of the {@link Ext.data.Record}. See {@link Ext.data.DataWriter#toHash}. */ updateRecord : function(rec) { - return this.toXml(this.toHash(rec)); + return this.toArray(this.toHash(rec)); }, /** * destroyRecord + * @protected * @param {Ext.data.Record} rec - * @return {String} xml element + * @return {Array} Array containing a attribute-object (name/value pair) representing the {@link Ext.data.DataReader#idProperty idProperty}. */ destroyRecord : function(rec) { var data = {}; data[this.meta.idProperty] = rec.id; - return this.toXml(data); + return this.toArray(data); } }); @@ -34725,8 +36758,11 @@ var myReader = new Ext.data.XmlReader({ Ext.data.XmlReader = function(meta, recordType){ meta = meta || {}; - // backwards compat, convert idPath to idProperty - meta.idProperty = meta.idProperty || meta.idPath; + // backwards compat, convert idPath or id / success + Ext.applyIf(meta, { + idProperty: meta.idProperty || meta.idPath || meta.id, + successProperty: meta.successProperty || meta.success + }); Ext.data.XmlReader.superclass.constructor.call(this, meta, recordType || meta.fields); }; @@ -34784,17 +36820,19 @@ Ext.extend(Ext.data.XmlReader, Ext.data.DataReader, { /** * Decode a json response from server. * @param {String} action [{@link Ext.data.Api#actions} create|read|update|destroy] - * @param {Ext.data.Response} response Returns an instance of {@link Ext.data.Response} + * @param {Object} response HTTP Response object from browser. + * @return {Ext.data.Response} response Returns an instance of {@link Ext.data.Response} */ readResponse : function(action, response) { var q = Ext.DomQuery, doc = response.responseXML; + // create general Response instance. var res = new Ext.data.Response({ action: action, success : this.getSuccess(doc), message: this.getMessage(doc), - data: this.extractData(q.select(this.meta.record, doc) || q.select(this.meta.root, doc)), + data: this.extractData(q.select(this.meta.record, doc) || q.select(this.meta.root, doc), false), raw: doc }); @@ -34802,6 +36840,7 @@ Ext.extend(Ext.data.XmlReader, Ext.data.DataReader, { throw new Ext.data.DataReader.Error('successProperty-response', this.meta.successProperty); } + // Create actions from a response having status 200 must return pk if (action === Ext.data.Api.actions.create) { var def = Ext.isDefined(res.data); if (def && Ext.isEmpty(res.data)) { @@ -34895,36 +36934,6 @@ Ext.extend(Ext.data.XmlReader, Ext.data.DataReader, { }; }(), - /** - * Extracts rows of record-data from server. iterates and calls #extractValues - * TODO I don't care much for method-names of #extractData, #extractValues. - * @param {Array} root - * @param {Boolean} returnRecords When true, will return instances of Ext.data.Record; otherwise just hashes. - * @private - * @ignore - */ - extractData : function(root, returnRecords) { - var Record = this.recordType, - records = [], - f = Record.prototype.fields, - fi = f.items, - fl = f.length; - if (returnRecords === true) { - for (var i = 0, len = root.length; i < len; i++) { - var data = root[i], - record = new Record(this.extractValues(data, fi, fl), this.getId(data)); - - record.node = data; - records.push(record); - } - } else { - for (var i = 0, len = root.length; i < len; i++) { - records.push(this.extractValues(root[i], fi, fl)); - } - } - return records; - }, - /** * extracts values and type-casts a row of data from server, extracted by #extractData * @param {Hash} data @@ -35023,13 +37032,13 @@ Ext.reg('xmlstore', Ext.data.XmlStore);/** * @xtype groupingstore */ Ext.data.GroupingStore = Ext.extend(Ext.data.Store, { - + //inherit docs constructor: function(config){ Ext.data.GroupingStore.superclass.constructor.call(this, config); this.applyGroupField(); }, - + /** * @cfg {String} groupField * The field name by which to sort the store's data (defaults to ''). @@ -35048,21 +37057,25 @@ Ext.data.GroupingStore = Ext.extend(Ext.data.Store, { */ groupOnSort:false, - groupDir : 'ASC', - + groupDir : 'ASC', + /** * Clears any existing grouping and refreshes the data using the default sort. */ clearGrouping : function(){ this.groupField = false; + if(this.remoteGroup){ if(this.baseParams){ delete this.baseParams.groupBy; + delete this.baseParams.groupDir; } var lo = this.lastOptions; if(lo && lo.params){ delete lo.params.groupBy; + delete lo.params.groupDir; } + this.reload(); }else{ this.applySort(); @@ -35077,12 +37090,12 @@ Ext.data.GroupingStore = Ext.extend(Ext.data.Store, { * in is the same as the current grouping field, false to skip grouping on the same field (defaults to false) */ groupBy : function(field, forceRegroup, direction){ - direction = direction ? (String(direction).toUpperCase() == 'DESC' ? 'DESC' : 'ASC') : this.groupDir; + direction = direction ? (String(direction).toUpperCase() == 'DESC' ? 'DESC' : 'ASC') : this.groupDir; if(this.groupField == field && this.groupDir == direction && !forceRegroup){ return; // already grouped by this field } this.groupField = field; - this.groupDir = direction; + this.groupDir = direction; this.applyGroupField(); if(this.groupOnSort){ this.sort(field, direction); @@ -35092,7 +37105,7 @@ Ext.data.GroupingStore = Ext.extend(Ext.data.Store, { this.reload(); }else{ var si = this.sortInfo || {}; - if(si.field != field || si.direction != direction){ + if(forceRegroup || si.field != field || si.direction != direction){ this.applySort(); }else{ this.sortData(field, direction); @@ -35100,15 +37113,25 @@ Ext.data.GroupingStore = Ext.extend(Ext.data.Store, { this.fireEvent('datachanged', this); } }, - + // private applyGroupField: function(){ if(this.remoteGroup){ if(!this.baseParams){ this.baseParams = {}; } - this.baseParams.groupBy = this.groupField; - this.baseParams.groupDir = this.groupDir; + Ext.apply(this.baseParams, { + groupBy : this.groupField, + groupDir : this.groupDir + }); + + var lo = this.lastOptions; + if(lo && lo.params){ + Ext.apply(lo.params, { + groupBy : this.groupField, + groupDir : this.groupDir + }); + } } }, @@ -35183,14 +37206,31 @@ paramOrder: 'param1|param2|param' */ directFn : undefined, - // protected + /** + * DirectProxy implementation of {@link Ext.data.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, callback, scope, options) { - var args = []; - var directFn = this.api[action] || this.directFn; + var args = [], + directFn = this.api[action] || this.directFn; switch (action) { case Ext.data.Api.actions.create: - args.push(params.jsonData[reader.meta.root]); // <-- create(Hash) + args.push(params.jsonData); // <-- create(Hash) break; case Ext.data.Api.actions.read: // If the method has no parameters, ignore the paramOrder/paramsAsHash. @@ -35205,10 +37245,10 @@ paramOrder: 'param1|param2|param' } break; case Ext.data.Api.actions.update: - args.push(params.jsonData[reader.meta.root]); // <-- update(Hash/Hash[]) + args.push(params.jsonData); // <-- update(Hash/Hash[]) break; case Ext.data.Api.actions.destroy: - args.push(params.jsonData[reader.meta.root]); // <-- destroy(Int/Int[]) + args.push(params.jsonData); // <-- destroy(Int/Int[]) break; } @@ -35249,8 +37289,9 @@ paramOrder: 'param1|param2|param' * Callback for read actions * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] * @param {Object} trans The request transaction object + * @param {Object} result Data object picked out of the server-response. * @param {Object} res The server response - * @private + * @protected */ onRead : function(action, trans, result, res) { var records; @@ -35270,13 +37311,15 @@ paramOrder: 'param1|param2|param' }, /** * Callback for write actions - * @param {String} action [Ext.data.Api.actions.create|read|update|destroy] + * @param {String} action [{@link Ext.data.Api#actions create|read|update|destroy}] * @param {Object} trans The request transaction object + * @param {Object} result Data object picked out of the server-response. * @param {Object} res The server response - * @private + * @param {Ext.data.Record/[Ext.data.Record]} rs The Store resultset associated with the action. + * @protected */ onWrite : function(action, trans, result, res, rs) { - var data = trans.reader.extractData(result); + var data = trans.reader.extractData(result, false); this.fireEvent("write", this, action, data, res, rs, trans.request.arg); trans.request.callback.call(trans.request.scope, data, res, true); } @@ -35317,16 +37360,18 @@ paramOrder: 'param1|param2|param' * @constructor * @param {Object} config */ -Ext.data.DirectStore = function(c){ - // each transaction upon a singe record will generatie a distinct Direct transaction since Direct queues them into one Ajax request. - c.batchTransactions = false; - - Ext.data.DirectStore.superclass.constructor.call(this, Ext.apply(c, { - proxy: (typeof(c.proxy) == 'undefined') ? new Ext.data.DirectProxy(Ext.copyTo({}, c, 'paramOrder,paramsAsHash,directFn,api')) : c.proxy, - reader: (typeof(c.reader) == 'undefined' && typeof(c.fields) == 'object') ? new Ext.data.JsonReader(Ext.copyTo({}, c, 'totalProperty,root,idProperty'), c.fields) : c.reader - })); -}; -Ext.extend(Ext.data.DirectStore, Ext.data.Store, {}); +Ext.data.DirectStore = Ext.extend(Ext.data.Store, { + constructor : function(config){ + // each transaction upon a singe record will generate a distinct Direct transaction since Direct queues them into one Ajax request. + var c = Ext.apply({}, { + batchTransactions: false + }, config); + Ext.data.DirectStore.superclass.constructor.call(this, Ext.apply(c, { + proxy: Ext.isDefined(c.proxy) ? c.proxy : new Ext.data.DirectProxy(Ext.copyTo({}, c, 'paramOrder,paramsAsHash,directFn,api')), + reader: (!Ext.isDefined(c.reader) && c.fields) ? new Ext.data.JsonReader(Ext.copyTo({}, c, 'totalProperty,root,idProperty'), c.fields) : c.reader + })); + } +}); Ext.reg('directstore', Ext.data.DirectStore); /** * @class Ext.Direct @@ -36266,7 +38311,13 @@ TestAction.multiply( } } }); -Ext.Direct.PROVIDERS['remoting'] = Ext.direct.RemotingProvider;/** +Ext.Direct.PROVIDERS['remoting'] = Ext.direct.RemotingProvider;/*! + * Ext JS Library 3.1.1 + * Copyright(c) 2006-2010 Ext JS, LLC + * licensing@extjs.com + * http://www.extjs.com/license + */ +/** * @class Ext.Resizable * @extends Ext.util.Observable *

    Applies drag handles to an element to make it resizable. The drag handles are inserted into the element @@ -36306,132 +38357,134 @@ resizer.on('resize', myHandler); * @param {Mixed} el The id or element to resize * @param {Object} config configuration options */ -Ext.Resizable = function(el, config){ - this.el = Ext.get(el); +Ext.Resizable = Ext.extend(Ext.util.Observable, { - if(config && config.wrap){ - config.resizeChild = this.el; - this.el = this.el.wrap(typeof config.wrap == 'object' ? config.wrap : {cls:'xresizable-wrap'}); - this.el.id = this.el.dom.id = config.resizeChild.id + '-rzwrap'; - this.el.setStyle('overflow', 'hidden'); - this.el.setPositioning(config.resizeChild.getPositioning()); - config.resizeChild.clearPositioning(); - if(!config.width || !config.height){ - var csize = config.resizeChild.getSize(); - this.el.setSize(csize.width, csize.height); - } - if(config.pinned && !config.adjustments){ - config.adjustments = 'auto'; + constructor: function(el, config){ + this.el = Ext.get(el); + if(config && config.wrap){ + config.resizeChild = this.el; + this.el = this.el.wrap(typeof config.wrap == 'object' ? config.wrap : {cls:'xresizable-wrap'}); + this.el.id = this.el.dom.id = config.resizeChild.id + '-rzwrap'; + this.el.setStyle('overflow', 'hidden'); + this.el.setPositioning(config.resizeChild.getPositioning()); + config.resizeChild.clearPositioning(); + if(!config.width || !config.height){ + var csize = config.resizeChild.getSize(); + this.el.setSize(csize.width, csize.height); + } + if(config.pinned && !config.adjustments){ + config.adjustments = 'auto'; + } } - } - - /** - * The proxy Element that is resized in place of the real Element during the resize operation. - * This may be queried using {@link Ext.Element#getBox} to provide the new area to resize to. - * Read only. - * @type Ext.Element. - * @property proxy - */ - this.proxy = this.el.createProxy({tag: 'div', cls: 'x-resizable-proxy', id: this.el.id + '-rzproxy'}, Ext.getBody()); - this.proxy.unselectable(); - this.proxy.enableDisplayMode('block'); - - Ext.apply(this, config); - if(this.pinned){ - this.disableTrackOver = true; - this.el.addClass('x-resizable-pinned'); - } - // if the element isn't positioned, make it relative - var position = this.el.getStyle('position'); - if(position != 'absolute' && position != 'fixed'){ - this.el.setStyle('position', 'relative'); - } - if(!this.handles){ // no handles passed, must be legacy style - this.handles = 's,e,se'; - if(this.multiDirectional){ - this.handles += ',n,w'; + /** + * The proxy Element that is resized in place of the real Element during the resize operation. + * This may be queried using {@link Ext.Element#getBox} to provide the new area to resize to. + * Read only. + * @type Ext.Element. + * @property proxy + */ + this.proxy = this.el.createProxy({tag: 'div', cls: 'x-resizable-proxy', id: this.el.id + '-rzproxy'}, Ext.getBody()); + this.proxy.unselectable(); + this.proxy.enableDisplayMode('block'); + + Ext.apply(this, config); + + if(this.pinned){ + this.disableTrackOver = true; + this.el.addClass('x-resizable-pinned'); } - } - if(this.handles == 'all'){ - this.handles = 'n s e w ne nw se sw'; - } - var hs = this.handles.split(/\s*?[,;]\s*?| /); - var ps = Ext.Resizable.positions; - for(var i = 0, len = hs.length; i < len; i++){ - if(hs[i] && ps[hs[i]]){ - var pos = ps[hs[i]]; - this[pos] = new Ext.Resizable.Handle(this, pos, this.disableTrackOver, this.transparent); + // if the element isn't positioned, make it relative + var position = this.el.getStyle('position'); + if(position != 'absolute' && position != 'fixed'){ + this.el.setStyle('position', 'relative'); } - } - // legacy - this.corner = this.southeast; - - if(this.handles.indexOf('n') != -1 || this.handles.indexOf('w') != -1){ - this.updateBox = true; - } - - this.activeHandle = null; - - if(this.resizeChild){ - if(typeof this.resizeChild == 'boolean'){ - this.resizeChild = Ext.get(this.el.dom.firstChild, true); + if(!this.handles){ // no handles passed, must be legacy style + this.handles = 's,e,se'; + if(this.multiDirectional){ + this.handles += ',n,w'; + } + } + if(this.handles == 'all'){ + this.handles = 'n s e w ne nw se sw'; + } + var hs = this.handles.split(/\s*?[,;]\s*?| /); + var ps = Ext.Resizable.positions; + for(var i = 0, len = hs.length; i < len; i++){ + if(hs[i] && ps[hs[i]]){ + var pos = ps[hs[i]]; + this[pos] = new Ext.Resizable.Handle(this, pos, this.disableTrackOver, this.transparent, this.handleCls); + } + } + // legacy + this.corner = this.southeast; + + if(this.handles.indexOf('n') != -1 || this.handles.indexOf('w') != -1){ + this.updateBox = true; + } + + this.activeHandle = null; + + if(this.resizeChild){ + if(typeof this.resizeChild == 'boolean'){ + this.resizeChild = Ext.get(this.el.dom.firstChild, true); + }else{ + this.resizeChild = Ext.get(this.resizeChild, true); + } + } + + if(this.adjustments == 'auto'){ + var rc = this.resizeChild; + var hw = this.west, he = this.east, hn = this.north, hs = this.south; + if(rc && (hw || hn)){ + rc.position('relative'); + rc.setLeft(hw ? hw.el.getWidth() : 0); + rc.setTop(hn ? hn.el.getHeight() : 0); + } + this.adjustments = [ + (he ? -he.el.getWidth() : 0) + (hw ? -hw.el.getWidth() : 0), + (hn ? -hn.el.getHeight() : 0) + (hs ? -hs.el.getHeight() : 0) -1 + ]; + } + + if(this.draggable){ + this.dd = this.dynamic ? + this.el.initDD(null) : this.el.initDDProxy(null, {dragElId: this.proxy.id}); + this.dd.setHandleElId(this.resizeChild ? this.resizeChild.id : this.el.id); + if(this.constrainTo){ + this.dd.constrainTo(this.constrainTo); + } + } + + this.addEvents( + /** + * @event beforeresize + * Fired before resize is allowed. Set {@link #enabled} to false to cancel resize. + * @param {Ext.Resizable} this + * @param {Ext.EventObject} e The mousedown event + */ + 'beforeresize', + /** + * @event resize + * Fired after a resize. + * @param {Ext.Resizable} this + * @param {Number} width The new width + * @param {Number} height The new height + * @param {Ext.EventObject} e The mouseup event + */ + 'resize' + ); + + if(this.width !== null && this.height !== null){ + this.resizeTo(this.width, this.height); }else{ - this.resizeChild = Ext.get(this.resizeChild, true); + this.updateChildSize(); } - } - - if(this.adjustments == 'auto'){ - var rc = this.resizeChild; - var hw = this.west, he = this.east, hn = this.north, hs = this.south; - if(rc && (hw || hn)){ - rc.position('relative'); - rc.setLeft(hw ? hw.el.getWidth() : 0); - rc.setTop(hn ? hn.el.getHeight() : 0); - } - this.adjustments = [ - (he ? -he.el.getWidth() : 0) + (hw ? -hw.el.getWidth() : 0), - (hn ? -hn.el.getHeight() : 0) + (hs ? -hs.el.getHeight() : 0) -1 - ]; - } - - if(this.draggable){ - this.dd = this.dynamic ? - this.el.initDD(null) : this.el.initDDProxy(null, {dragElId: this.proxy.id}); - this.dd.setHandleElId(this.resizeChild ? this.resizeChild.id : this.el.id); - } - - this.addEvents( - /** - * @event beforeresize - * Fired before resize is allowed. Set {@link #enabled} to false to cancel resize. - * @param {Ext.Resizable} this - * @param {Ext.EventObject} e The mousedown event - */ - 'beforeresize', - /** - * @event resize - * Fired after a resize. - * @param {Ext.Resizable} this - * @param {Number} width The new width - * @param {Number} height The new height - * @param {Ext.EventObject} e The mouseup event - */ - 'resize' - ); - - if(this.width !== null && this.height !== null){ - this.resizeTo(this.width, this.height); - }else{ - this.updateChildSize(); - } - if(Ext.isIE){ - this.el.dom.style.zoom = 1; - } - Ext.Resizable.superclass.constructor.call(this); -}; - -Ext.extend(Ext.Resizable, Ext.util.Observable, { + if(Ext.isIE){ + this.el.dom.style.zoom = 1; + } + Ext.Resizable.superclass.constructor.call(this); + }, /** * @cfg {Array/String} adjustments String 'auto' or an array [width, height] with values to be added to the @@ -36549,6 +38602,9 @@ Ext.extend(Ext.Resizable, Ext.util.Observable, { * @cfg {Boolean} wrap True to wrap an element with a div if needed (required for textareas and images, defaults to false) * in favor of the handles config option (defaults to false) */ + /** + * @cfg {String} handleCls A css class to add to each handle. Defaults to ''. + */ /** @@ -36719,6 +38775,10 @@ new Ext.Panel({ if(!this.dynamic){ this.proxy.hide(); } + if(this.draggable && this.constrainTo){ + this.dd.resetConstraints(); + this.dd.constrainTo(this.constrainTo); + } return box; }, @@ -36969,35 +39029,36 @@ Ext.Resizable.positions = { n: 'north', s: 'south', e: 'east', w: 'west', se: 'southeast', sw: 'southwest', nw: 'northwest', ne: 'northeast' }; -// private -Ext.Resizable.Handle = function(rz, pos, disableTrackOver, transparent){ - if(!this.tpl){ - // only initialize the template if resizable is used - var tpl = Ext.DomHelper.createTemplate( - {tag: 'div', cls: 'x-resizable-handle x-resizable-handle-{0}'} - ); - tpl.compile(); - Ext.Resizable.Handle.prototype.tpl = tpl; - } - this.position = pos; - this.rz = rz; - this.el = this.tpl.append(rz.el.dom, [this.position], true); - this.el.unselectable(); - if(transparent){ - this.el.setOpacity(0); - } - this.el.on('mousedown', this.onMouseDown, this); - if(!disableTrackOver){ - this.el.on({ - scope: this, - mouseover: this.onMouseOver, - mouseout: this.onMouseOut - }); - } -}; - -// private -Ext.Resizable.Handle.prototype = { +Ext.Resizable.Handle = Ext.extend(Object, { + constructor : function(rz, pos, disableTrackOver, transparent, cls){ + if(!this.tpl){ + // only initialize the template if resizable is used + var tpl = Ext.DomHelper.createTemplate( + {tag: 'div', cls: 'x-resizable-handle x-resizable-handle-{0}'} + ); + tpl.compile(); + Ext.Resizable.Handle.prototype.tpl = tpl; + } + this.position = pos; + this.rz = rz; + this.el = this.tpl.append(rz.el.dom, [this.position], true); + this.el.unselectable(); + if(transparent){ + this.el.setOpacity(0); + } + if(!Ext.isEmpty(cls)){ + this.el.addClass(cls); + } + this.el.on('mousedown', this.onMouseDown, this); + if(!disableTrackOver){ + this.el.on({ + scope: this, + mouseover: this.onMouseOver, + mouseout: this.onMouseOut + }); + } + }, + // private afterResize : function(rz){ // do nothing @@ -37019,7 +39080,7 @@ Ext.Resizable.Handle.prototype = { Ext.destroy(this.el); this.el = null; } -}; +}); /** * @class Ext.Window * @extends Ext.Panel @@ -37066,8 +39127,13 @@ Ext.Window = Ext.extend(Ext.Panel, { * A reference to the WindowGroup that should manage this window (defaults to {@link Ext.WindowMgr}). */ /** - * @cfg {String/Number/Button} defaultButton - * The id / index of a button or a button instance to focus when this window received the focus. + * @cfg {String/Number/Component} defaultButton + *

    Specifies a Component to receive focus when this Window is focussed.

    + *

    This may be one of:

    */ /** * @cfg {Function} onEsc @@ -37199,19 +39265,13 @@ Ext.Window = Ext.extend(Ext.Panel, { * @deprecated */ initHidden : undefined, - + /** * @cfg {Boolean} hidden * Render this component hidden (default is true). If true, the * {@link #hide} method will be called internally. */ hidden : true, - - /** - * @cfg {Boolean} monitorResize @hide - * This is automatically managed based on the value of constrain and constrainToHeader - */ - monitorResize : true, // The following configs are set to provide the basic functionality of a window. // Changing them would require additional code to handle correctly and should @@ -37322,7 +39382,8 @@ Ext.Window = Ext.extend(Ext.Panel, { minHeight:this.minHeight, handles: this.resizeHandles || 'all', pinned: true, - resizeElement : this.resizerAction + resizeElement : this.resizerAction, + handleCls: 'x-window-handle' }); this.resizer.window = this; this.mon(this.resizer, 'beforeresize', this.beforeResize, this); @@ -37356,7 +39417,8 @@ Ext.Window = Ext.extend(Ext.Panel, { }, // private - onEsc : function(){ + onEsc : function(k, e){ + e.stopEvent(); this[this.closeAction](); }, @@ -37507,7 +39569,7 @@ Ext.Window = Ext.extend(Ext.Panel, { * @param {String/Element} animateTarget (optional) The target element or id from which the window should * animate while opening (defaults to null with no animation) * @param {Function} callback (optional) A callback function to call after the window is displayed - * @param {Object} scope (optional) The scope in which to execute the callback + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this Window. * @return {Ext.Window} this */ show : function(animateTarget, cb, scope){ @@ -37539,13 +39601,16 @@ Ext.Window = Ext.extend(Ext.Panel, { // private afterShow : function(isAnim){ + if (this.isDestroyed){ + return false; + } this.proxy.hide(); this.el.setStyle('display', 'block'); this.el.show(); if(this.maximized){ this.fitContainer(); } - if(Ext.isMac && Ext.isGecko){ // work around stupid FF 2.0/Mac scroll bar bug + if(Ext.isMac && Ext.isGecko2){ // work around stupid FF 2.0/Mac scroll bar bug this.cascade(this.setAutoScroll); } @@ -37563,6 +39628,7 @@ Ext.Window = Ext.extend(Ext.Panel, { var sz = this.getSize(); this.onResize(sz.width, sz.height); } + this.onShow(); this.fireEvent('show', this); }, @@ -37587,7 +39653,7 @@ Ext.Window = Ext.extend(Ext.Panel, { * @param {String/Element} animateTarget (optional) The target element or id to which the window should * animate while hiding (defaults to null with no animation) * @param {Function} callback (optional) A callback function to call after the window is hidden - * @param {Object} scope (optional) The scope in which to execute the callback + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this Window. * @return {Ext.Window} this */ hide : function(animateTarget, cb, scope){ @@ -37623,6 +39689,7 @@ Ext.Window = Ext.extend(Ext.Panel, { if(this.keyMap){ this.keyMap.disable(); } + this.onHide(); this.fireEvent('hide', this); }, @@ -37642,6 +39709,18 @@ Ext.Window = Ext.extend(Ext.Panel, { })); }, + /** + * Method that is called immediately before the show event is fired. + * Defaults to Ext.emptyFn. + */ + onShow : Ext.emptyFn, + + /** + * Method that is called immediately before the hide event is fired. + * Defaults to Ext.emptyFn. + */ + onHide : Ext.emptyFn, + // private onWindowResize : function(){ if(this.maximized){ @@ -37699,7 +39778,7 @@ Ext.Window = Ext.extend(Ext.Panel, { if(show !== false){ this.el.show(); this.focus(); - if(Ext.isMac && Ext.isGecko){ // work around stupid FF 2.0/Mac scroll bar bug + if(Ext.isMac && Ext.isGecko2){ // work around stupid FF 2.0/Mac scroll bar bug this.cascade(this.setAutoScroll); } } @@ -37739,7 +39818,7 @@ Ext.Window = Ext.extend(Ext.Panel, { } } }, - + // private doClose : function(){ this.fireEvent('close', this); @@ -37789,9 +39868,14 @@ Ext.Window = Ext.extend(Ext.Panel, { */ restore : function(){ if(this.maximized){ + var t = this.tools; this.el.removeClass('x-window-maximized'); - this.tools.restore.hide(); - this.tools.maximize.show(); + if(t.restore){ + t.restore.hide(); + } + if(t.maximize){ + t.maximize.show(); + } this.setPosition(this.restorePos[0], this.restorePos[1]); this.setSize(this.restoreSize.width, this.restoreSize.height); delete this.restorePos; @@ -37802,8 +39886,8 @@ Ext.Window = Ext.extend(Ext.Panel, { if(this.dd){ this.dd.unlock(); } - if(this.collapsible){ - this.tools.toggle.show(); + if(this.collapsible && t.toggle){ + t.toggle.show(); } this.container.removeClass('x-window-maximized-ct'); @@ -37824,7 +39908,7 @@ Ext.Window = Ext.extend(Ext.Panel, { // private fitContainer : function(){ - var vs = this.container.getViewSize(); + var vs = this.container.getViewSize(false); this.setSize(vs.width, vs.height); }, @@ -37847,7 +39931,7 @@ Ext.Window = Ext.extend(Ext.Panel, { /** * Aligns the window to the specified element * @param {Mixed} element The element to align to. - * @param {String} position The position to align to (see {@link Ext.Element#alignTo} for more details). + * @param {String} position (optional, defaults to "tl-bl?") The position to align to (see {@link Ext.Element#alignTo} for more details). * @param {Array} offsets (optional) Offset the positioning by [x, y] * @return {Ext.Window} this */ @@ -37881,7 +39965,6 @@ Ext.Window = Ext.extend(Ext.Panel, { Ext.EventManager.on(window, 'scroll', this.doAnchor, this, {buffer: tm == 'number' ? monitorScroll : 50}); } - this.doAnchor(); return this; }, @@ -37977,7 +40060,7 @@ Ext.extend(Ext.Window.DD, Ext.dd.DD, { }); /** * @class Ext.WindowGroup - * An object that represents a group of {@link Ext.Window} instances and provides z-order management + * An object that manages a group of {@link Ext.Window} instances and provides z-order management * and window activation behavior. * @constructor */ @@ -38034,20 +40117,42 @@ Ext.WindowGroup = function(){ return { /** - * The starting z-index for windows (defaults to 9000) + * The starting z-index for windows in this WindowGroup (defaults to 9000) * @type Number The z-index value */ zseed : 9000, - // private + /** + *

    Registers a {@link Ext.Window Window} with this WindowManager. This should not + * need to be called under normal circumstances. Windows are automatically registered + * with a {@link Ext.Window#manager manager} at construction time.

    + *

    Where this may be useful is moving Windows between two WindowManagers. For example, + * to bring the Ext.MessageBox dialog under the same manager as the Desktop's + * WindowManager in the desktop sample app:

    +var msgWin = Ext.MessageBox.getDialog();
    +MyDesktop.getDesktop().getManager().register(msgWin);
    +
    + * @param {Window} win The Window to register. + */ register : function(win){ + if(win.manager){ + win.manager.unregister(win); + } + win.manager = this; + list[win.id] = win; accessList.push(win); win.on('hide', activateLast); }, - // private + /** + *

    Unregisters a {@link Ext.Window Window} from this WindowManager. This should not + * need to be called. Windows are automatically unregistered upon destruction. + * See {@link #register}.

    + * @param {Window} win The Window to unregister. + */ unregister : function(win){ + delete win.manager; delete list[win.id]; win.un('hide', activateLast); accessList.remove(win); @@ -38063,7 +40168,7 @@ Ext.WindowGroup = function(){ }, /** - * Brings the specified window to the front of any other active windows. + * Brings the specified window to the front of any other active windows in this WindowGroup. * @param {String/Object} win The id of the window or a {@link Ext.Window} instance * @return {Boolean} True if the dialog was brought to the front, else false * if it was already in front @@ -38079,7 +40184,7 @@ Ext.WindowGroup = function(){ }, /** - * Sends the specified window to the back of other active windows. + * Sends the specified window to the back of other active windows in this WindowGroup. * @param {String/Object} win The id of the window or a {@link Ext.Window} instance * @return {Ext.Window} The window */ @@ -38091,7 +40196,7 @@ Ext.WindowGroup = function(){ }, /** - * Hides all windows in the group. + * Hides all windows in this WindowGroup. */ hideAll : function(){ for(var id in list){ @@ -38102,7 +40207,7 @@ Ext.WindowGroup = function(){ }, /** - * Gets the currently-active window in the group. + * Gets the currently-active window in this WindowGroup. * @return {Ext.Window} The active window */ getActive : function(){ @@ -38110,11 +40215,11 @@ Ext.WindowGroup = function(){ }, /** - * Returns zero or more windows in the group using the custom search function passed to this method. + * Returns zero or more windows in this WindowGroup using the custom search function passed to this method. * The function should accept a single {@link Ext.Window} reference as its only argument and should * return true if the window matches the search criteria, otherwise it should return false. * @param {Function} fn The search function - * @param {Object} scope (optional) The scope in which to execute the function (defaults to the window + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the Window being tested. * that gets passed to the function if not specified) * @return {Array} An array of zero or more matching windows */ @@ -38130,10 +40235,10 @@ Ext.WindowGroup = function(){ }, /** - * Executes the specified function once for every window in the group, passing each + * Executes the specified function once for every window in this WindowGroup, passing each * window as the only parameter. Returning false from the function will stop the iteration. * @param {Function} fn The function to execute for each item - * @param {Object} scope (optional) The scope in which to execute the function + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Window in the iteration. */ each : function(fn, scope){ for(var id in list){ @@ -38189,10 +40294,12 @@ Ext.Msg.show({ Ext.MessageBox = function(){ var dlg, opt, mask, waitTimer, bodyEl, msgEl, textboxEl, textareaEl, progressBar, pp, iconEl, spacerEl, - buttons, activeTextEl, bwidth, bufferIcon = '', iconCls = ''; + buttons, activeTextEl, bwidth, bufferIcon = '', iconCls = '', + buttonNames = ['ok', 'yes', 'no', 'cancel']; // private var handleButton = function(button){ + buttons[button].blur(); if(dlg.isVisible()){ dlg.hide(); handleHide(); @@ -38205,7 +40312,7 @@ Ext.MessageBox = function(){ if(opt && opt.cls){ dlg.el.removeClass(opt.cls); } - progressBar.reset(); + progressBar.reset(); }; // private @@ -38221,26 +40328,25 @@ Ext.MessageBox = function(){ // private var updateButtons = function(b){ - var width = 0; + var width = 0, + cfg; if(!b){ - buttons["ok"].hide(); - buttons["cancel"].hide(); - buttons["yes"].hide(); - buttons["no"].hide(); + Ext.each(buttonNames, function(name){ + buttons[name].hide(); + }); return width; } dlg.footer.dom.style.display = ''; - for(var k in buttons){ - if(!Ext.isFunction(buttons[k])){ - if(b[k]){ - buttons[k].show(); - buttons[k].setText(Ext.isString(b[k]) ? b[k] : Ext.MessageBox.buttonText[k]); - width += buttons[k].el.getWidth()+15; - }else{ - buttons[k].hide(); - } + Ext.iterate(buttons, function(name, btn){ + cfg = b[name]; + if(cfg){ + btn.show(); + btn.setText(Ext.isString(cfg) ? cfg : Ext.MessageBox.buttonText[name]); + width += btn.getEl().getWidth() + 15; + }else{ + btn.hide(); } - } + }); return width; }; @@ -38251,6 +40357,16 @@ Ext.MessageBox = function(){ */ getDialog : function(titleText){ if(!dlg){ + var btns = []; + + buttons = {}; + Ext.each(buttonNames, function(name){ + btns.push(buttons[name] = new Ext.Button({ + text: this.buttonText[name], + handler: handleButton.createCallback(name), + hideMode: 'offsets' + })); + }, this); dlg = new Ext.Window({ autoCreate : true, title:titleText, @@ -38275,16 +40391,12 @@ Ext.MessageBox = function(){ }else{ handleButton("cancel"); } - } + }, + fbar: new Ext.Toolbar({ + items: btns, + enableOverflow: false + }) }); - buttons = {}; - var bt = this.buttonText; - //TODO: refactor this block into a buttons config to pass into the Window constructor - buttons["ok"] = dlg.addButton(bt["ok"], handleButton.createCallback("ok")); - buttons["yes"] = dlg.addButton(bt["yes"], handleButton.createCallback("yes")); - buttons["no"] = dlg.addButton(bt["no"], handleButton.createCallback("no")); - buttons["cancel"] = dlg.addButton(bt["cancel"], handleButton.createCallback("cancel")); - buttons["ok"].hideMode = buttons["yes"].hideMode = buttons["no"].hideMode = buttons["cancel"].hideMode = 'offsets'; dlg.render(document.body); dlg.getEl().addClass('x-window-dlg'); mask = dlg.mask; @@ -38327,17 +40439,19 @@ Ext.MessageBox = function(){ } msgEl.update(text || ' '); - var iw = iconCls != '' ? (iconEl.getWidth() + iconEl.getMargins('lr')) : 0; - var mw = msgEl.getWidth() + msgEl.getMargins('lr'); - var fw = dlg.getFrameWidth('lr'); - var bw = dlg.body.getFrameWidth('lr'); + var iw = iconCls != '' ? (iconEl.getWidth() + iconEl.getMargins('lr')) : 0, + mw = msgEl.getWidth() + msgEl.getMargins('lr'), + fw = dlg.getFrameWidth('lr'), + bw = dlg.body.getFrameWidth('lr'), + w; + if (Ext.isIE && iw > 0){ //3 pixels get subtracted in the icon CSS for an IE margin issue, //so we have to add it back here for the overall width to be consistent iw += 3; } - var w = Math.max(Math.min(opt.width || iw+mw+fw+bw, this.maxWidth), - Math.max(opt.minWidth || this.minWidth, bwidth || 0)); + w = Math.max(Math.min(opt.width || iw+mw+fw+bw, opt.maxWidth || this.maxWidth), + Math.max(opt.minWidth || this.minWidth, bwidth || 0)); if(opt.prompt === true){ activeTextEl.setWidth(w-iw-fw-bw); @@ -38524,18 +40638,16 @@ Ext.Msg.show({ // force it to the end of the z-index stack so it gets a cursor in FF document.body.appendChild(dlg.el.dom); d.setAnimateTarget(opt.animEl); + //workaround for window internally enabling keymap in afterShow + d.on('show', function(){ + if(allowClose === true){ + d.keyMap.enable(); + }else{ + d.keyMap.disable(); + } + }, this, {single:true}); d.show(opt.animEl); } - - //workaround for window internally enabling keymap in afterShow - d.on('show', function(){ - if(allowClose === true){ - d.keyMap.enable(); - }else{ - d.keyMap.disable(); - } - }, this, {single:true}); - if(opt.wait === true){ progressBar.wait(opt.waitConfig); } @@ -38628,7 +40740,7 @@ Ext.MessageBox.ERROR * @param {String} title The title bar text * @param {String} msg The message box body text * @param {Function} fn (optional) The callback function invoked after the message box is closed - * @param {Object} scope (optional) The scope of the callback function + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to the browser wnidow. * @return {Ext.MessageBox} this */ alert : function(title, msg, fn, scope){ @@ -38637,7 +40749,8 @@ Ext.MessageBox.ERROR msg : msg, buttons: this.OK, fn: fn, - scope : scope + scope : scope, + minWidth: this.minWidth }); return this; }, @@ -38650,7 +40763,7 @@ Ext.MessageBox.ERROR * @param {String} title The title bar text * @param {String} msg The message box body text * @param {Function} fn (optional) The callback function invoked after the message box is closed - * @param {Object} scope (optional) The scope of the callback function + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to the browser wnidow. * @return {Ext.MessageBox} this */ confirm : function(title, msg, fn, scope){ @@ -38660,7 +40773,8 @@ Ext.MessageBox.ERROR buttons: this.YESNO, fn: fn, scope : scope, - icon: this.QUESTION + icon: this.QUESTION, + minWidth: this.minWidth }); return this; }, @@ -38673,7 +40787,7 @@ Ext.MessageBox.ERROR * @param {String} title The title bar text * @param {String} msg The message box body text * @param {Function} fn (optional) The callback function invoked after the message box is closed - * @param {Object} scope (optional) The scope of the callback function + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to the browser wnidow. * @param {Boolean/Number} multiline (optional) True to create a multiline textbox using the defaultTextHeight * property, or the height in pixels to create the textbox (defaults to false / single-line) * @param {String} value (optional) Default value of the text input element (defaults to '') @@ -38685,7 +40799,7 @@ Ext.MessageBox.ERROR msg : msg, buttons: this.OKCANCEL, fn: fn, - minWidth:250, + minWidth: this.minPromptWidth, scope : scope, prompt:true, multiline: multiline, @@ -38757,10 +40871,16 @@ Ext.MessageBox.ERROR minWidth : 100, /** * The minimum width in pixels of the message box if it is a progress-style dialog. This is useful - * for setting a different minimum width than text-only dialogs may need (defaults to 250) + * for setting a different minimum width than text-only dialogs may need (defaults to 250). * @type Number */ minProgressWidth : 250, + /** + * The minimum width in pixels of the message box if it is a prompt dialog. This is useful + * for setting a different minimum width than text-only dialogs may need (defaults to 250). + * @type Number + */ + minPromptWidth: 250, /** * An object containing the default button text strings that can be overriden for localized language support. * Supported properties are: ok, cancel, yes and no. Generally you should include a locale-specific @@ -39346,7 +41466,7 @@ Ext.DataView = Ext.extend(Ext.BoxComponent, { "click", /** * @event mouseenter - * Fires when the mouse enters a template node. trackOver:true or an overCls must be set to enable this event. + * Fires when the mouse enters a template node. trackOver:true or an overClass must be set to enable this event. * @param {Ext.DataView} this * @param {Number} index The index of the target node * @param {HTMLElement} node The target node @@ -39355,7 +41475,7 @@ Ext.DataView = Ext.extend(Ext.BoxComponent, { "mouseenter", /** * @event mouseleave - * Fires when the mouse leaves a template node. trackOver:true or an overCls must be set to enable this event. + * Fires when the mouse leaves a template node. trackOver:true or an overClass must be set to enable this event. * @param {Ext.DataView} this * @param {Number} index The index of the target node * @param {HTMLElement} node The target node @@ -39933,6 +42053,8 @@ Ext.DataView = Ext.extend(Ext.BoxComponent, { }, onDestroy : function(){ + this.all.clear(); + this.selected.clear(); Ext.DataView.superclass.onDestroy.call(this); this.bindStore(null); } @@ -39944,10 +42066,11 @@ Ext.DataView = Ext.extend(Ext.BoxComponent, { */ Ext.DataView.prototype.setStore = Ext.DataView.prototype.bindStore; -Ext.reg('dataview', Ext.DataView);/** - * @class Ext.ListView +Ext.reg('dataview', Ext.DataView); +/** + * @class Ext.list.ListView * @extends Ext.DataView - *

    Ext.ListView is a fast and light-weight implentation of a + *

    Ext.list.ListView is a fast and light-weight implentation of a * {@link Ext.grid.GridPanel Grid} like view with the following characteristics:

    *
    *

    Creating TabPanels from Code

    *

    TabPanels can be created and rendered completely in code, as in this example:

    @@ -40588,11 +42877,6 @@ Ext.TabPanel = Ext.extend(Ext.Panel, { * class name applied to the tab strip item representing the child Component, allowing special * styling to be applied. */ - /** - * @cfg {Boolean} monitorResize True to automatically monitor window resize events and rerender the layout on - * browser resize (defaults to true). - */ - monitorResize : true, /** * @cfg {Boolean} deferredRender *

    true by default to defer the rendering of child {@link Ext.Container#items items} @@ -40705,9 +42989,9 @@ var tabs = new Ext.TabPanel({ autoTabSelector : 'div.x-tab', /** * @cfg {String/Number} activeTab A string id or the numeric index of the tab that should be initially - * activated on render (defaults to none). + * activated on render (defaults to undefined). */ - activeTab : null, + activeTab : undefined, /** * @cfg {Number} tabMargin The number of pixels of space to calculate into the sizing and scrolling of * tabs. If you change the margin in CSS, you will need to update this value so calculations are correct @@ -40810,10 +43094,11 @@ var tabs = new Ext.TabPanel({ tag:'ul', cls:'x-tab-strip x-tab-strip-'+this.tabPosition}}); var beforeEl = (this.tabPosition=='bottom' ? this.stripWrap : null); - this.stripSpacer = st.createChild({cls:'x-tab-strip-spacer'}, beforeEl); + st.createChild({cls:'x-tab-strip-spacer'}, beforeEl); this.strip = new Ext.Element(this.stripWrap.dom.firstChild); - this.edge = this.strip.createChild({tag:'li', cls:'x-tab-edge'}); + // create an empty span with class x-tab-strip-text to force the height of the header element when there's no tabs. + this.edge = this.strip.createChild({tag:'li', cls:'x-tab-edge', cn: [{tag: 'span', cls: 'x-tab-strip-text', cn: ' '}]}); this.strip.createChild({cls:'x-clear'}); this.body.addClass('x-tab-panel-body-'+this.tabPosition); @@ -40843,9 +43128,9 @@ new Ext.TabPanel({ itemTpl: new Ext.XTemplate( '<li class="{cls}" id="{id}" style="overflow:hidden">', '<tpl if="closable">', - '<a class="x-tab-strip-close" onclick="return false;"></a>', + '<a class="x-tab-strip-close"></a>', '</tpl>', - '<a class="x-tab-right" href="#" onclick="return false;" style="padding-left:6px">', + '<a class="x-tab-right" href="#" style="padding-left:6px">', '<em class="x-tab-left">', '<span class="x-tab-strip-inner">', '<img src="{src}" style="float:left;margin:3px 3px 0 0">', @@ -40882,8 +43167,8 @@ new Ext.TabPanel({ */ if(!this.itemTpl){ var tt = new Ext.Template( - '

  • ', - '', + '
  • ', + '', '{text}', '
  • ' ); @@ -40923,8 +43208,9 @@ new Ext.TabPanel({ // private findTargets : function(e){ - var item = null; - var itemEl = e.getTarget('li', this.strip); + var item = null, + itemEl = e.getTarget('li:not(.x-tab-edge)', this.strip); + if(itemEl){ item = this.getComponent(itemEl.id.split(this.idDelimiter)[1]); if(item.disabled){ @@ -40952,7 +43238,6 @@ new Ext.TabPanel({ if(t.close){ if (t.item.fireEvent('beforeclose', t.item) !== false) { t.item.fireEvent('close', t.item); - delete t.item.tabEl; this.remove(t.item); } return; @@ -40984,8 +43269,8 @@ new Ext.TabPanel({ } var tabs = this.el.query(this.autoTabSelector); for(var i = 0, len = tabs.length; i < len; i++){ - var tab = tabs[i]; - var title = tab.getAttribute('title'); + var tab = tabs[i], + title = tab.getAttribute('title'); tab.removeAttribute('title'); this.add({ title: title, @@ -40996,26 +43281,46 @@ new Ext.TabPanel({ // private initTab : function(item, index){ - var before = this.strip.dom.childNodes[index]; - var p = this.getTemplateArgs(item); - var el = before ? + var before = this.strip.dom.childNodes[index], + p = this.getTemplateArgs(item), + el = before ? this.itemTpl.insertBefore(before, p) : - this.itemTpl.append(this.strip, p); + this.itemTpl.append(this.strip, p), + cls = 'x-tab-strip-over', + tabEl = Ext.get(el); - Ext.fly(el).addClassOnOver('x-tab-strip-over'); + tabEl.hover(function(){ + if(!item.disabled){ + tabEl.addClass(cls); + } + }, function(){ + tabEl.removeClass(cls); + }); if(item.tabTip){ - Ext.fly(el).child('span.x-tab-strip-text', true).qtip = item.tabTip; + tabEl.child('span.x-tab-strip-text', true).qtip = item.tabTip; } item.tabEl = el; - item.on('disable', this.onItemDisabled, this); - item.on('enable', this.onItemEnabled, this); - item.on('titlechange', this.onItemTitleChanged, this); - item.on('iconchange', this.onItemIconChanged, this); - item.on('beforeshow', this.onBeforeShowItem, this); + // Route *keyboard triggered* click events to the tab strip mouse handler. + tabEl.select('a').on('click', function(e){ + if(!e.getPageX()){ + this.onStripMouseDown(e); + } + }, this, {preventDefault: true}); + + item.on({ + scope: this, + disable: this.onItemDisabled, + enable: this.onItemEnabled, + titlechange: this.onItemTitleChanged, + iconchange: this.onItemIconChanged, + beforeshow: this.onBeforeShowItem + }); }, + + /** *

    Provides template arguments for rendering a tab selector item in the tab strip.

    *

    This method returns an object hash containing properties used by the TabPanel's {@link #itemTpl} @@ -41077,9 +43382,15 @@ new Ext.TabPanel({ // private onRemove : function(c){ + var te = Ext.get(c.tabEl); + // check if the tabEl exists, it won't if the tab isn't rendered + if(te){ + te.select('a').removeAllListeners(); + Ext.destroy(te); + } Ext.TabPanel.superclass.onRemove.call(this, c); - Ext.destroy(Ext.get(this.getTabEl(c))); this.stack.remove(c); + delete c.tabEl; c.un('disable', this.onItemDisabled, this); c.un('enable', this.onItemEnabled, this); c.un('titlechange', this.onItemTitleChanged, this); @@ -41092,10 +43403,12 @@ new Ext.TabPanel({ }else if(this.items.getCount() > 0){ this.setActiveTab(0); }else{ - this.activeTab = null; + this.setActiveTab(null); } } - this.delegateUpdates(); + if(!this.destroying){ + this.delegateUpdates(); + } }, // private @@ -41148,7 +43461,8 @@ new Ext.TabPanel({ * @return {HTMLElement} The DOM node */ getTabEl : function(item){ - return document.getElementById(this.id + this.idDelimiter + this.getComponent(item).getItemId()); + var c = this.getComponent(item); + return c ? c.tabEl : null; }, // private @@ -41214,10 +43528,10 @@ new Ext.TabPanel({ // private autoSizeTabs : function(){ - var count = this.items.length; - var ce = this.tabPosition != 'bottom' ? 'header' : 'footer'; - var ow = this[ce].dom.offsetWidth; - var aw = this[ce].dom.clientWidth; + var count = this.items.length, + ce = this.tabPosition != 'bottom' ? 'header' : 'footer', + ow = this[ce].dom.offsetWidth, + aw = this[ce].dom.clientWidth; if(!this.resizeTabs || count < 1 || !aw){ // !aw for display:none return; @@ -41225,12 +43539,12 @@ new Ext.TabPanel({ var each = Math.max(Math.min(Math.floor((aw-4) / count) - this.tabMargin, this.tabWidth), this.minTabWidth); // -4 for float errors in IE this.lastTabWidth = each; - var lis = this.strip.query("li:not([className^=x-tab-edge])"); + var lis = this.strip.query('li:not(.x-tab-edge)'); for(var i = 0, len = lis.length; i < len; i++) { - var li = lis[i]; - var inner = Ext.fly(li).child('.x-tab-strip-inner', true); - var tw = li.offsetWidth; - var iw = inner.offsetWidth; + var li = lis[i], + inner = Ext.fly(li).child('.x-tab-strip-inner', true), + tw = li.offsetWidth, + iw = inner.offsetWidth; inner.style.width = (each - (tw-iw)) + 'px'; } }, @@ -41261,7 +43575,7 @@ new Ext.TabPanel({ */ setActiveTab : function(item){ item = this.getComponent(item); - if(!item || this.fireEvent('beforetabchange', this, item, this.activeTab) === false){ + if(this.fireEvent('beforetabchange', this, item, this.activeTab) === false){ return; } if(!this.rendered){ @@ -41274,26 +43588,27 @@ new Ext.TabPanel({ if(oldEl){ Ext.fly(oldEl).removeClass('x-tab-strip-active'); } - this.activeTab.fireEvent('deactivate', this.activeTab); } - var el = this.getTabEl(item); - Ext.fly(el).addClass('x-tab-strip-active'); - this.activeTab = item; - this.stack.add(item); - - this.layout.setActiveItem(item); - if(this.scrolling){ - this.scrollToTab(item, this.animScroll); + if(item){ + var el = this.getTabEl(item); + Ext.fly(el).addClass('x-tab-strip-active'); + this.activeTab = item; + this.stack.add(item); + + this.layout.setActiveItem(item); + if(this.scrolling){ + this.scrollToTab(item, this.animScroll); + } } - - item.fireEvent('activate', item); this.fireEvent('tabchange', this, item); } }, /** - * Gets the currently active tab. - * @return {Panel} The active tab + * Returns the Component which is the currently active tab. Note that before the TabPanel + * first activates a child Component, this method will return whatever was configured in the + * {@link #activeTab} config option. + * @return {BoxComponent} The currently active child Component if one is active, or the {@link #activeTab} config value. */ getActiveTab : function(){ return this.activeTab || null; @@ -41311,15 +43626,14 @@ new Ext.TabPanel({ // private autoScrollTabs : function(){ this.pos = this.tabPosition=='bottom' ? this.footer : this.header; - var count = this.items.length; - var ow = this.pos.dom.offsetWidth; - var tw = this.pos.dom.clientWidth; - - var wrap = this.stripWrap; - var wd = wrap.dom; - var cw = wd.offsetWidth; - var pos = this.getScrollPos(); - var l = this.edge.getOffsetsTo(this.stripWrap)[0] + pos; + var count = this.items.length, + ow = this.pos.dom.offsetWidth, + tw = this.pos.dom.clientWidth, + wrap = this.stripWrap, + wd = wrap.dom, + cw = wd.offsetWidth, + pos = this.getScrollPos(), + l = this.edge.getOffsetsTo(this.stripWrap)[0] + pos; if(!this.enableTabScroll || count < 1 || cw < 20){ // 20 to prevent display:none issues return; @@ -41431,11 +43745,14 @@ new Ext.TabPanel({ */ scrollToTab : function(item, animate){ - if(!item){ return; } - var el = this.getTabEl(item); - var pos = this.getScrollPos(), area = this.getScrollArea(); - var left = Ext.fly(el).getOffsetsTo(this.stripWrap)[0] + pos; - var right = left + el.offsetWidth; + if(!item){ + return; + } + var el = this.getTabEl(item), + pos = this.getScrollPos(), + area = this.getScrollArea(), + left = Ext.fly(el).getOffsetsTo(this.stripWrap)[0] + pos, + right = left + el.offsetWidth; if(left < pos){ this.scrollTo(left, animate); }else if(right > (pos + area)){ @@ -41455,9 +43772,9 @@ new Ext.TabPanel({ var d = e.getWheelDelta()*this.wheelIncrement*-1; e.stopEvent(); - var pos = this.getScrollPos(); - var newpos = pos + d; - var sw = this.getScrollWidth()-this.getScrollArea(); + var pos = this.getScrollPos(), + newpos = pos + d, + sw = this.getScrollWidth()-this.getScrollArea(); var s = Math.max(0, Math.min(sw, newpos)); if(s != pos){ @@ -41467,9 +43784,9 @@ new Ext.TabPanel({ // private onScrollRight : function(){ - var sw = this.getScrollWidth()-this.getScrollArea(); - var pos = this.getScrollPos(); - var s = Math.min(sw, pos + this.getScrollIncrement()); + var sw = this.getScrollWidth()-this.getScrollArea(), + pos = this.getScrollPos(), + s = Math.min(sw, pos + this.getScrollIncrement()); if(s != pos){ this.scrollTo(s, this.animScroll); } @@ -41477,8 +43794,8 @@ new Ext.TabPanel({ // private onScrollLeft : function(){ - var pos = this.getScrollPos(); - var s = Math.max(0, pos - this.getScrollIncrement()); + var pos = this.getScrollPos(), + s = Math.max(0, pos - this.getScrollIncrement()); if(s != pos){ this.scrollTo(s, this.animScroll); } @@ -41493,17 +43810,9 @@ new Ext.TabPanel({ // private beforeDestroy : function() { - if(this.items){ - this.items.each(function(item){ - if(item && item.tabEl){ - Ext.get(item.tabEl).removeAllListeners(); - item.tabEl = null; - } - }, this); - } - if(this.strip){ - this.strip.removeAllListeners(); - } + Ext.destroy(this.leftRepeater, this.rightRepeater); + this.deleteMembers('strip', 'edge', 'scrollLeft', 'scrollRight', 'stripWrap'); + this.activeTab = null; Ext.TabPanel.superclass.beforeDestroy.apply(this); } @@ -41592,768 +43901,824 @@ Ext.TabPanel.AccessStack = function(){ } }; }; -/** - * @class Ext.Button - * @extends Ext.BoxComponent - * Simple Button class - * @cfg {String} text The button text to be used as innerHTML (html tags are accepted) - * @cfg {String} icon The path to an image to display in the button (the image will be set as the background-image - * CSS property of the button by default, so if you want a mixed icon/text button, set cls:'x-btn-text-icon') - * @cfg {Function} handler A function called when the button is clicked (can be used instead of click event). - * The handler is passed the following parameters:

      - *
    • b : Button
      This Button.
    • - *
    • e : EventObject
      The click event.
    • - *
    - * @cfg {Number} minWidth The minimum width for this button (used to give a set of buttons a common width). - * See also {@link Ext.Panel}.{@link Ext.Panel#minButtonWidth minButtonWidth}. - * @cfg {String/Object} tooltip The tooltip for the button - can be a string to be used as innerHTML (html tags are accepted) or QuickTips config object - * @cfg {Boolean} hidden True to start hidden (defaults to false) - * @cfg {Boolean} disabled True to start disabled (defaults to false) - * @cfg {Boolean} pressed True to start pressed (only if enableToggle = true) - * @cfg {String} toggleGroup The group this toggle button is a member of (only 1 per group can be pressed) - * @cfg {Boolean/Object} repeat True to repeat fire the click event while the mouse is down. This can also be - * a {@link Ext.util.ClickRepeater ClickRepeater} config object (defaults to false). - * @constructor - * Create a new button - * @param {Object} config The config object - * @xtype button - */ -Ext.Button = Ext.extend(Ext.BoxComponent, { - /** - * Read-only. True if this button is hidden - * @type Boolean - */ - hidden : false, - /** - * Read-only. True if this button is disabled - * @type Boolean - */ - disabled : false, - /** - * Read-only. True if this button is pressed (only if enableToggle = true) - * @type Boolean - */ - pressed : false, - - /** - * @cfg {Number} tabIndex Set a DOM tabIndex for this button (defaults to undefined) - */ - - /** - * @cfg {Boolean} allowDepress - * False to not allow a pressed Button to be depressed (defaults to undefined). Only valid when {@link #enableToggle} is true. - */ - - /** - * @cfg {Boolean} enableToggle - * True to enable pressed/not pressed toggling (defaults to false) - */ - enableToggle : false, - /** - * @cfg {Function} toggleHandler - * Function called when a Button with {@link #enableToggle} set to true is clicked. Two arguments are passed:
      - *
    • button : Ext.Button
      this Button object
    • - *
    • state : Boolean
      The next state if the Button, true means pressed.
    • - *
    - */ - /** - * @cfg {Mixed} menu - * Standard menu attribute consisting of a reference to a menu object, a menu id or a menu config blob (defaults to undefined). - */ - /** - * @cfg {String} menuAlign - * The position to align the menu to (see {@link Ext.Element#alignTo} for more details, defaults to 'tl-bl?'). - */ - menuAlign : 'tl-bl?', - - /** - * @cfg {String} overflowText If used in a {@link Ext.Toolbar Toolbar}, the - * text to be used if this item is shown in the overflow menu. See also - * {@link Ext.Toolbar.Item}.{@link Ext.Toolbar.Item#overflowText overflowText}. - */ - /** - * @cfg {String} iconCls - * A css class which sets a background image to be used as the icon for this button - */ - /** - * @cfg {String} type - * submit, reset or button - defaults to 'button' - */ - type : 'button', - - // private - menuClassTarget : 'tr:nth(2)', - - /** - * @cfg {String} clickEvent - * The DOM event that will fire the handler of the button. This can be any valid event name (dblclick, contextmenu). - * Defaults to 'click'. - */ - clickEvent : 'click', - - /** - * @cfg {Boolean} handleMouseEvents - * False to disable visual cues on mouseover, mouseout and mousedown (defaults to true) - */ - handleMouseEvents : true, - - /** - * @cfg {String} tooltipType - * The type of tooltip to use. Either 'qtip' (default) for QuickTips or 'title' for title attribute. - */ - tooltipType : 'qtip', - - /** - * @cfg {String} buttonSelector - *

    (Optional) A {@link Ext.DomQuery DomQuery} selector which is used to extract the active, clickable element from the - * DOM structure created.

    - *

    When a custom {@link #template} is used, you must ensure that this selector results in the selection of - * a focussable element.

    - *

    Defaults to 'button:first-child'.

    - */ - buttonSelector : 'button:first-child', - - /** - * @cfg {String} scale - *

    (Optional) The size of the Button. Three values are allowed:

    - *
      - *
    • 'small'
      Results in the button element being 16px high.
    • - *
    • 'medium'
      Results in the button element being 24px high.
    • - *
    • 'large'
      Results in the button element being 32px high.
    • - *
    - *

    Defaults to 'small'.

    - */ - scale : 'small', - - /** - * @cfg {Object} scope The scope (this reference) in which the - * {@link #handler} and {@link #toggleHandler} is - * executed. Defaults to this Button. - */ - - /** - * @cfg {String} iconAlign - *

    (Optional) The side of the Button box to render the icon. Four values are allowed:

    - *
      - *
    • 'top'
    • - *
    • 'right'
    • - *
    • 'bottom'
    • - *
    • 'left'
    • - *
    - *

    Defaults to 'left'.

    - */ - iconAlign : 'left', - - /** - * @cfg {String} arrowAlign - *

    (Optional) The side of the Button box to render the arrow if the button has an associated {@link #menu}. - * Two values are allowed:

    - *
      - *
    • 'right'
    • - *
    • 'bottom'
    • - *
    - *

    Defaults to 'right'.

    - */ - arrowAlign : 'right', - - /** - * @cfg {Ext.Template} template (Optional) - *

    A {@link Ext.Template Template} used to create the Button's DOM structure.

    - * Instances, or subclasses which need a different DOM structure may provide a different - * template layout in conjunction with an implementation of {@link #getTemplateArgs}. - * @type Ext.Template - * @property template - */ - /** - * @cfg {String} cls - * A CSS class string to apply to the button's main element. - */ - /** - * @property menu - * @type Menu - * The {@link Ext.menu.Menu Menu} object associated with this Button when configured with the {@link #menu} config option. - */ - - initComponent : function(){ - Ext.Button.superclass.initComponent.call(this); - - this.addEvents( - /** - * @event click - * Fires when this button is clicked - * @param {Button} this - * @param {EventObject} e The click event - */ - 'click', - /** - * @event toggle - * Fires when the 'pressed' state of this button changes (only if enableToggle = true) - * @param {Button} this - * @param {Boolean} pressed - */ - 'toggle', - /** - * @event mouseover - * Fires when the mouse hovers over the button - * @param {Button} this - * @param {Event} e The event object - */ - 'mouseover', - /** - * @event mouseout - * Fires when the mouse exits the button - * @param {Button} this - * @param {Event} e The event object - */ - 'mouseout', - /** - * @event menushow - * If this button has a menu, this event fires when it is shown - * @param {Button} this - * @param {Menu} menu - */ - 'menushow', - /** - * @event menuhide - * If this button has a menu, this event fires when it is hidden - * @param {Button} this - * @param {Menu} menu - */ - 'menuhide', - /** - * @event menutriggerover - * If this button has a menu, this event fires when the mouse enters the menu triggering element - * @param {Button} this - * @param {Menu} menu - * @param {EventObject} e - */ - 'menutriggerover', - /** - * @event menutriggerout - * If this button has a menu, this event fires when the mouse leaves the menu triggering element - * @param {Button} this - * @param {Menu} menu - * @param {EventObject} e - */ - 'menutriggerout' - ); - if(this.menu){ - this.menu = Ext.menu.MenuMgr.get(this.menu); - } - if(Ext.isString(this.toggleGroup)){ - this.enableToggle = true; - } - }, - -/** - *

    This method returns an object which provides substitution parameters for the {@link #template Template} used - * to create this Button's DOM structure.

    - *

    Instances or subclasses which use a different Template to create a different DOM structure may need to provide their - * own implementation of this method.

    - *

    The default implementation which provides data for the default {@link #template} returns an Array containing the - * following items:

      - *
    • The Button's {@link #text}
    • - *
    • The <button>'s {@link #type}
    • - *
    • The {@link iconCls} applied to the <button> {@link #btnEl element}
    • - *
    • The {@link #cls} applied to the Button's main {@link #getEl Element}
    • - *
    • A CSS class name controlling the Button's {@link #scale} and {@link #iconAlign icon alignment}
    • - *
    • A CSS class name which applies an arrow to the Button if configured with a {@link #menu}
    • - *
    - * @return {Object} Substitution data for a Template. - */ - getTemplateArgs : function(){ - var cls = (this.cls || ''); - cls += (this.iconCls || this.icon) ? (this.text ? ' x-btn-text-icon' : ' x-btn-icon') : ' x-btn-noicon'; - if(this.pressed){ - cls += ' x-btn-pressed'; - } - return [this.text || ' ', this.type, this.iconCls || '', cls, 'x-btn-' + this.scale + ' x-btn-icon-' + this.scale + '-' + this.iconAlign, this.getMenuClass()]; - }, - - // protected - getMenuClass : function(){ - return this.menu ? (this.arrowAlign != 'bottom' ? 'x-btn-arrow' : 'x-btn-arrow-bottom') : ''; - }, - - // private - onRender : function(ct, position){ - if(!this.template){ - if(!Ext.Button.buttonTemplate){ - // hideous table template - Ext.Button.buttonTemplate = new Ext.Template( - '', - '', - '', - '', - "
      
      
      
    "); - Ext.Button.buttonTemplate.compile(); - } - this.template = Ext.Button.buttonTemplate; - } - - var btn, targs = this.getTemplateArgs(); - - if(position){ - btn = this.template.insertBefore(position, targs, true); - }else{ - btn = this.template.append(ct, targs, true); - } - /** - * An {@link Ext.Element Element} encapsulating the Button's clickable element. By default, - * this references a <button> element. Read only. - * @type Ext.Element - * @property btnEl - */ - this.btnEl = btn.child(this.buttonSelector); - this.mon(this.btnEl, { - scope: this, - focus: this.onFocus, - blur: this.onBlur - }); - - this.initButtonEl(btn, this.btnEl); - - Ext.ButtonToggleMgr.register(this); - }, - - // private - initButtonEl : function(btn, btnEl){ - this.el = btn; - - if(this.id){ - var d = this.el.dom, - c = Ext.Element.cache; - - delete c[d.id]; - d.id = this.el.id = this.id; - c[d.id] = this.el; - } - if(this.icon){ - btnEl.setStyle('background-image', 'url(' +this.icon +')'); - } - if(this.tabIndex !== undefined){ - btnEl.dom.tabIndex = this.tabIndex; - } - if(this.tooltip){ - this.setTooltip(this.tooltip, true); - } - - if(this.handleMouseEvents){ - this.mon(btn, { - scope: this, - mouseover: this.onMouseOver, - mousedown: this.onMouseDown - }); - - // new functionality for monitoring on the document level - //this.mon(btn, 'mouseout', this.onMouseOut, this); - } - - if(this.menu){ - this.mon(this.menu, { - scope: this, - show: this.onMenuShow, - hide: this.onMenuHide - }); - } - - if(this.repeat){ - var repeater = new Ext.util.ClickRepeater(btn, Ext.isObject(this.repeat) ? this.repeat : {}); - this.mon(repeater, 'click', this.onClick, this); - } - this.mon(btn, this.clickEvent, this.onClick, this); - }, - - // private - afterRender : function(){ - Ext.Button.superclass.afterRender.call(this); - this.doAutoWidth(); - }, - - /** - * Sets the CSS class that provides a background image to use as the button's icon. This method also changes - * the value of the {@link iconCls} config internally. - * @param {String} cls The CSS class providing the icon image - * @return {Ext.Button} this - */ - setIconClass : function(cls){ - if(this.el){ - this.btnEl.replaceClass(this.iconCls, cls); - } - this.iconCls = cls; - return this; - }, - - /** - * Sets the tooltip for this Button. - * @param {String/Object} tooltip. This may be:
      - *
    • String : A string to be used as innerHTML (html tags are accepted) to show in a tooltip
    • - *
    • Object : A configuration object for {@link Ext.QuickTips#register}.
    • - *
    - * @return {Ext.Button} this - */ - setTooltip : function(tooltip, /* private */ initial){ - if(this.rendered){ - if(!initial){ - this.clearTip(); - } - if(Ext.isObject(tooltip)){ - Ext.QuickTips.register(Ext.apply({ - target: this.btnEl.id - }, tooltip)); - this.tooltip = tooltip; - }else{ - this.btnEl.dom[this.tooltipType] = tooltip; - } - }else{ - this.tooltip = tooltip; - } - return this; - }, - - // private - clearTip : function(){ - if(Ext.isObject(this.tooltip)){ - Ext.QuickTips.unregister(this.btnEl); - } - }, - - // private - beforeDestroy : function(){ - if(this.rendered){ - this.clearTip(); - } - Ext.destroy(this.menu, this.repeater); - }, - - // private - onDestroy : function(){ - var doc = Ext.getDoc(); - doc.un('mouseover', this.monitorMouseOver, this); - doc.un('mouseup', this.onMouseUp, this); - if(this.rendered){ - Ext.ButtonToggleMgr.unregister(this); - } - }, - - // private - doAutoWidth : function(){ - if(this.el && this.text && this.width === undefined){ - this.el.setWidth('auto'); - if(Ext.isIE7 && Ext.isStrict){ - var ib = this.btnEl; - if(ib && ib.getWidth() > 20){ - ib.clip(); - ib.setWidth(Ext.util.TextMetrics.measure(ib, this.text).width+ib.getFrameWidth('lr')); - } - } - if(this.minWidth){ - if(this.el.getWidth() < this.minWidth){ - this.el.setWidth(this.minWidth); - } - } - } - }, - - /** - * Assigns this Button's click handler - * @param {Function} handler The function to call when the button is clicked - * @param {Object} scope (optional) Scope for the function passed in. Defaults to this Button. - * @return {Ext.Button} this - */ - setHandler : function(handler, scope){ - this.handler = handler; - this.scope = scope; - return this; - }, - - /** - * Sets this Button's text - * @param {String} text The button text - * @return {Ext.Button} this - */ - setText : function(text){ - this.text = text; - if(this.el){ - this.el.child('td.x-btn-mc ' + this.buttonSelector).update(text); - } - this.doAutoWidth(); - return this; - }, - - /** - * Gets the text for this Button - * @return {String} The button text - */ - getText : function(){ - return this.text; - }, - - /** - * If a state it passed, it becomes the pressed state otherwise the current state is toggled. - * @param {Boolean} state (optional) Force a particular state - * @param {Boolean} supressEvent (optional) True to stop events being fired when calling this method. - * @return {Ext.Button} this - */ - toggle : function(state, suppressEvent){ - state = state === undefined ? !this.pressed : !!state; - if(state != this.pressed){ - if(this.rendered){ - this.el[state ? 'addClass' : 'removeClass']('x-btn-pressed'); - } - this.pressed = state; - if(!suppressEvent){ - this.fireEvent('toggle', this, state); - if(this.toggleHandler){ - this.toggleHandler.call(this.scope || this, this, state); - } - } - } - return this; - }, - - /** - * Focus the button - */ - focus : function(){ - this.btnEl.focus(); - }, - - // private - onDisable : function(){ - this.onDisableChange(true); - }, - - // private - onEnable : function(){ - this.onDisableChange(false); - }, - - onDisableChange : function(disabled){ - if(this.el){ - if(!Ext.isIE6 || !this.text){ - this.el[disabled ? 'addClass' : 'removeClass'](this.disabledClass); - } - this.el.dom.disabled = disabled; - } - this.disabled = disabled; - }, - - /** - * Show this button's menu (if it has one) - */ - showMenu : function(){ - if(this.rendered && this.menu){ - if(this.tooltip){ - Ext.QuickTips.getQuickTip().cancelShow(this.btnEl); - } - this.menu.show(this.el, this.menuAlign); - } - return this; - }, - - /** - * Hide this button's menu (if it has one) - */ - hideMenu : function(){ - if(this.menu){ - this.menu.hide(); - } - return this; - }, - - /** - * Returns true if the button has a menu and it is visible - * @return {Boolean} - */ - hasVisibleMenu : function(){ - return this.menu && this.menu.isVisible(); - }, - - // private - onClick : function(e){ - if(e){ - e.preventDefault(); - } - if(e.button !== 0){ - return; - } - if(!this.disabled){ - if(this.enableToggle && (this.allowDepress !== false || !this.pressed)){ - this.toggle(); - } - if(this.menu && !this.menu.isVisible() && !this.ignoreNextClick){ - this.showMenu(); - } - this.fireEvent('click', this, e); - if(this.handler){ - //this.el.removeClass('x-btn-over'); - this.handler.call(this.scope || this, this, e); - } - } - }, - - // private - isMenuTriggerOver : function(e, internal){ - return this.menu && !internal; - }, - - // private - isMenuTriggerOut : function(e, internal){ - return this.menu && !internal; - }, - - // private - onMouseOver : function(e){ - if(!this.disabled){ - var internal = e.within(this.el, true); - if(!internal){ - this.el.addClass('x-btn-over'); - if(!this.monitoringMouseOver){ - Ext.getDoc().on('mouseover', this.monitorMouseOver, this); - this.monitoringMouseOver = true; - } - this.fireEvent('mouseover', this, e); - } - if(this.isMenuTriggerOver(e, internal)){ - this.fireEvent('menutriggerover', this, this.menu, e); - } - } - }, - - // private - monitorMouseOver : function(e){ - if(e.target != this.el.dom && !e.within(this.el)){ - if(this.monitoringMouseOver){ - Ext.getDoc().un('mouseover', this.monitorMouseOver, this); - this.monitoringMouseOver = false; - } - this.onMouseOut(e); - } - }, - - // private - onMouseOut : function(e){ - var internal = e.within(this.el) && e.target != this.el.dom; - this.el.removeClass('x-btn-over'); - this.fireEvent('mouseout', this, e); - if(this.isMenuTriggerOut(e, internal)){ - this.fireEvent('menutriggerout', this, this.menu, e); - } - }, - // private - onFocus : function(e){ - if(!this.disabled){ - this.el.addClass('x-btn-focus'); - } - }, - // private - onBlur : function(e){ - this.el.removeClass('x-btn-focus'); - }, - - // private - getClickEl : function(e, isUp){ - return this.el; - }, - - // private - onMouseDown : function(e){ - if(!this.disabled && e.button === 0){ - this.getClickEl(e).addClass('x-btn-click'); - Ext.getDoc().on('mouseup', this.onMouseUp, this); - } - }, - // private - onMouseUp : function(e){ - if(e.button === 0){ - this.getClickEl(e, true).removeClass('x-btn-click'); - Ext.getDoc().un('mouseup', this.onMouseUp, this); - } - }, - // private - onMenuShow : function(e){ - this.ignoreNextClick = 0; - this.el.addClass('x-btn-menu-active'); - this.fireEvent('menushow', this, this.menu); - }, - // private - onMenuHide : function(e){ - this.el.removeClass('x-btn-menu-active'); - this.ignoreNextClick = this.restoreClick.defer(250, this); - this.fireEvent('menuhide', this, this.menu); - }, - - // private - restoreClick : function(){ - this.ignoreNextClick = 0; - } - - - - /** - * @cfg {String} autoEl @hide - */ -}); -Ext.reg('button', Ext.Button); - -// Private utility class used by Button -Ext.ButtonToggleMgr = function(){ - var groups = {}; - - function toggleGroup(btn, state){ - if(state){ - var g = groups[btn.toggleGroup]; - for(var i = 0, l = g.length; i < l; i++){ - if(g[i] != btn){ - g[i].toggle(false); - } - } - } - } - - return { - register : function(btn){ - if(!btn.toggleGroup){ - return; - } - var g = groups[btn.toggleGroup]; - if(!g){ - g = groups[btn.toggleGroup] = []; - } - g.push(btn); - btn.on('toggle', toggleGroup); - }, - - unregister : function(btn){ - if(!btn.toggleGroup){ - return; - } - var g = groups[btn.toggleGroup]; - if(g){ - g.remove(btn); - btn.un('toggle', toggleGroup); - } - }, - - /** - * Gets the pressed button in the passed group or null - * @param {String} group - * @return Button - */ - getPressed : function(group){ - var g = groups[group]; - if(g){ - for(var i = 0, len = g.length; i < len; i++){ - if(g[i].pressed === true){ - return g[i]; - } - } - } - return null; - } - }; -}();/** +/** + * @class Ext.Button + * @extends Ext.BoxComponent + * Simple Button class + * @cfg {String} text The button text to be used as innerHTML (html tags are accepted) + * @cfg {String} icon The path to an image to display in the button (the image will be set as the background-image + * CSS property of the button by default, so if you want a mixed icon/text button, set cls:'x-btn-text-icon') + * @cfg {Function} handler A function called when the button is clicked (can be used instead of click event). + * The handler is passed the following parameters:
      + *
    • b : Button
      This Button.
    • + *
    • e : EventObject
      The click event.
    • + *
    + * @cfg {Number} minWidth The minimum width for this button (used to give a set of buttons a common width). + * See also {@link Ext.Panel}.{@link Ext.Panel#minButtonWidth minButtonWidth}. + * @cfg {String/Object} tooltip The tooltip for the button - can be a string to be used as innerHTML (html tags are accepted) or QuickTips config object + * @cfg {Boolean} hidden True to start hidden (defaults to false) + * @cfg {Boolean} disabled True to start disabled (defaults to false) + * @cfg {Boolean} pressed True to start pressed (only if enableToggle = true) + * @cfg {String} toggleGroup The group this toggle button is a member of (only 1 per group can be pressed) + * @cfg {Boolean/Object} repeat True to repeat fire the click event while the mouse is down. This can also be + * a {@link Ext.util.ClickRepeater ClickRepeater} config object (defaults to false). + * @constructor + * Create a new button + * @param {Object} config The config object + * @xtype button + */ +Ext.Button = Ext.extend(Ext.BoxComponent, { + /** + * Read-only. True if this button is hidden + * @type Boolean + */ + hidden : false, + /** + * Read-only. True if this button is disabled + * @type Boolean + */ + disabled : false, + /** + * Read-only. True if this button is pressed (only if enableToggle = true) + * @type Boolean + */ + pressed : false, + + /** + * @cfg {Number} tabIndex Set a DOM tabIndex for this button (defaults to undefined) + */ + + /** + * @cfg {Boolean} allowDepress + * False to not allow a pressed Button to be depressed (defaults to undefined). Only valid when {@link #enableToggle} is true. + */ + + /** + * @cfg {Boolean} enableToggle + * True to enable pressed/not pressed toggling (defaults to false) + */ + enableToggle : false, + /** + * @cfg {Function} toggleHandler + * Function called when a Button with {@link #enableToggle} set to true is clicked. Two arguments are passed:
      + *
    • button : Ext.Button
      this Button object
    • + *
    • state : Boolean
      The next state of the Button, true means pressed.
    • + *
    + */ + /** + * @cfg {Mixed} menu + * Standard menu attribute consisting of a reference to a menu object, a menu id or a menu config blob (defaults to undefined). + */ + /** + * @cfg {String} menuAlign + * The position to align the menu to (see {@link Ext.Element#alignTo} for more details, defaults to 'tl-bl?'). + */ + menuAlign : 'tl-bl?', + + /** + * @cfg {String} overflowText If used in a {@link Ext.Toolbar Toolbar}, the + * text to be used if this item is shown in the overflow menu. See also + * {@link Ext.Toolbar.Item}.{@link Ext.Toolbar.Item#overflowText overflowText}. + */ + /** + * @cfg {String} iconCls + * A css class which sets a background image to be used as the icon for this button + */ + /** + * @cfg {String} type + * submit, reset or button - defaults to 'button' + */ + type : 'button', + + // private + menuClassTarget : 'tr:nth(2)', + + /** + * @cfg {String} clickEvent + * The DOM event that will fire the handler of the button. This can be any valid event name (dblclick, contextmenu). + * Defaults to 'click'. + */ + clickEvent : 'click', + + /** + * @cfg {Boolean} handleMouseEvents + * False to disable visual cues on mouseover, mouseout and mousedown (defaults to true) + */ + handleMouseEvents : true, + + /** + * @cfg {String} tooltipType + * The type of tooltip to use. Either 'qtip' (default) for QuickTips or 'title' for title attribute. + */ + tooltipType : 'qtip', + + /** + * @cfg {String} buttonSelector + *

    (Optional) A {@link Ext.DomQuery DomQuery} selector which is used to extract the active, clickable element from the + * DOM structure created.

    + *

    When a custom {@link #template} is used, you must ensure that this selector results in the selection of + * a focussable element.

    + *

    Defaults to 'button:first-child'.

    + */ + buttonSelector : 'button:first-child', + + /** + * @cfg {String} scale + *

    (Optional) The size of the Button. Three values are allowed:

    + *
      + *
    • 'small'
      Results in the button element being 16px high.
    • + *
    • 'medium'
      Results in the button element being 24px high.
    • + *
    • 'large'
      Results in the button element being 32px high.
    • + *
    + *

    Defaults to 'small'.

    + */ + scale : 'small', + + /** + * @cfg {Object} scope The scope (this reference) in which the + * {@link #handler} and {@link #toggleHandler} is + * executed. Defaults to this Button. + */ + + /** + * @cfg {String} iconAlign + *

    (Optional) The side of the Button box to render the icon. Four values are allowed:

    + *
      + *
    • 'top'
    • + *
    • 'right'
    • + *
    • 'bottom'
    • + *
    • 'left'
    • + *
    + *

    Defaults to 'left'.

    + */ + iconAlign : 'left', + + /** + * @cfg {String} arrowAlign + *

    (Optional) The side of the Button box to render the arrow if the button has an associated {@link #menu}. + * Two values are allowed:

    + *
      + *
    • 'right'
    • + *
    • 'bottom'
    • + *
    + *

    Defaults to 'right'.

    + */ + arrowAlign : 'right', + + /** + * @cfg {Ext.Template} template (Optional) + *

    A {@link Ext.Template Template} used to create the Button's DOM structure.

    + * Instances, or subclasses which need a different DOM structure may provide a different + * template layout in conjunction with an implementation of {@link #getTemplateArgs}. + * @type Ext.Template + * @property template + */ + /** + * @cfg {String} cls + * A CSS class string to apply to the button's main element. + */ + /** + * @property menu + * @type Menu + * The {@link Ext.menu.Menu Menu} object associated with this Button when configured with the {@link #menu} config option. + */ + + initComponent : function(){ + Ext.Button.superclass.initComponent.call(this); + + this.addEvents( + /** + * @event click + * Fires when this button is clicked + * @param {Button} this + * @param {EventObject} e The click event + */ + 'click', + /** + * @event toggle + * Fires when the 'pressed' state of this button changes (only if enableToggle = true) + * @param {Button} this + * @param {Boolean} pressed + */ + 'toggle', + /** + * @event mouseover + * Fires when the mouse hovers over the button + * @param {Button} this + * @param {Event} e The event object + */ + 'mouseover', + /** + * @event mouseout + * Fires when the mouse exits the button + * @param {Button} this + * @param {Event} e The event object + */ + 'mouseout', + /** + * @event menushow + * If this button has a menu, this event fires when it is shown + * @param {Button} this + * @param {Menu} menu + */ + 'menushow', + /** + * @event menuhide + * If this button has a menu, this event fires when it is hidden + * @param {Button} this + * @param {Menu} menu + */ + 'menuhide', + /** + * @event menutriggerover + * If this button has a menu, this event fires when the mouse enters the menu triggering element + * @param {Button} this + * @param {Menu} menu + * @param {EventObject} e + */ + 'menutriggerover', + /** + * @event menutriggerout + * If this button has a menu, this event fires when the mouse leaves the menu triggering element + * @param {Button} this + * @param {Menu} menu + * @param {EventObject} e + */ + 'menutriggerout' + ); + if(this.menu){ + this.menu = Ext.menu.MenuMgr.get(this.menu); + } + if(Ext.isString(this.toggleGroup)){ + this.enableToggle = true; + } + }, + +/** + *

    This method returns an Array which provides substitution parameters for the {@link #template Template} used + * to create this Button's DOM structure.

    + *

    Instances or subclasses which use a different Template to create a different DOM structure may need to provide their + * own implementation of this method.

    + *

    The default implementation which provides data for the default {@link #template} returns an Array containing the + * following items:

      + *
    • The <button>'s {@link #type}
    • + *
    • A CSS class name applied to the Button's main <tbody> element which determines the button's scale and icon alignment.
    • + *
    • A CSS class to determine the presence and position of an arrow icon. ('x-btn-arrow' or 'x-btn-arrow-bottom' or '')
    • + *
    • The {@link #cls} CSS class name applied to the button's wrapping <table> element.
    • + *
    • The Component id which is applied to the button's wrapping <table> element.
    • + *
    + * @return {Array} Substitution data for a Template. + */ + getTemplateArgs : function(){ + return [this.type, 'x-btn-' + this.scale + ' x-btn-icon-' + this.scale + '-' + this.iconAlign, this.getMenuClass(), this.cls, this.id]; + }, + + // private + setButtonClass : function(){ + if(this.useSetClass){ + if(!Ext.isEmpty(this.oldCls)){ + this.el.removeClass([this.oldCls, 'x-btn-pressed']); + } + this.oldCls = (this.iconCls || this.icon) ? (this.text ? ' x-btn-text-icon' : ' x-btn-icon') : ' x-btn-noicon'; + this.el.addClass([this.oldCls, this.pressed ? 'x-btn-pressed' : null]); + } + }, + + // protected + getMenuClass : function(){ + return this.menu ? (this.arrowAlign != 'bottom' ? 'x-btn-arrow' : 'x-btn-arrow-bottom') : ''; + }, + + // private + onRender : function(ct, position){ + if(!this.template){ + if(!Ext.Button.buttonTemplate){ + // hideous table template + Ext.Button.buttonTemplate = new Ext.Template( + '', + '', + '', + '', + '
      
      
      
    '); + Ext.Button.buttonTemplate.compile(); + } + this.template = Ext.Button.buttonTemplate; + } + + var btn, targs = this.getTemplateArgs(); + + if(position){ + btn = this.template.insertBefore(position, targs, true); + }else{ + btn = this.template.append(ct, targs, true); + } + /** + * An {@link Ext.Element Element} encapsulating the Button's clickable element. By default, + * this references a <button> element. Read only. + * @type Ext.Element + * @property btnEl + */ + this.btnEl = btn.child(this.buttonSelector); + this.mon(this.btnEl, { + scope: this, + focus: this.onFocus, + blur: this.onBlur + }); + + this.initButtonEl(btn, this.btnEl); + + Ext.ButtonToggleMgr.register(this); + }, + + // private + initButtonEl : function(btn, btnEl){ + this.el = btn; + this.setIcon(this.icon); + this.setText(this.text); + this.setIconClass(this.iconCls); + if(Ext.isDefined(this.tabIndex)){ + btnEl.dom.tabIndex = this.tabIndex; + } + if(this.tooltip){ + this.setTooltip(this.tooltip, true); + } + + if(this.handleMouseEvents){ + this.mon(btn, { + scope: this, + mouseover: this.onMouseOver, + mousedown: this.onMouseDown + }); + + // new functionality for monitoring on the document level + //this.mon(btn, 'mouseout', this.onMouseOut, this); + } + + if(this.menu){ + this.mon(this.menu, { + scope: this, + show: this.onMenuShow, + hide: this.onMenuHide + }); + } + + if(this.repeat){ + var repeater = new Ext.util.ClickRepeater(btn, Ext.isObject(this.repeat) ? this.repeat : {}); + this.mon(repeater, 'click', this.onClick, this); + } + this.mon(btn, this.clickEvent, this.onClick, this); + }, + + // private + afterRender : function(){ + Ext.Button.superclass.afterRender.call(this); + this.useSetClass = true; + this.setButtonClass(); + this.doc = Ext.getDoc(); + this.doAutoWidth(); + }, + + /** + * Sets the CSS class that provides a background image to use as the button's icon. This method also changes + * the value of the {@link iconCls} config internally. + * @param {String} cls The CSS class providing the icon image + * @return {Ext.Button} this + */ + setIconClass : function(cls){ + this.iconCls = cls; + if(this.el){ + this.btnEl.dom.className = ''; + this.btnEl.addClass(['x-btn-text', cls || '']); + this.setButtonClass(); + } + return this; + }, + + /** + * Sets the tooltip for this Button. + * @param {String/Object} tooltip. This may be:
      + *
    • String : A string to be used as innerHTML (html tags are accepted) to show in a tooltip
    • + *
    • Object : A configuration object for {@link Ext.QuickTips#register}.
    • + *
    + * @return {Ext.Button} this + */ + setTooltip : function(tooltip, /* private */ initial){ + if(this.rendered){ + if(!initial){ + this.clearTip(); + } + if(Ext.isObject(tooltip)){ + Ext.QuickTips.register(Ext.apply({ + target: this.btnEl.id + }, tooltip)); + this.tooltip = tooltip; + }else{ + this.btnEl.dom[this.tooltipType] = tooltip; + } + }else{ + this.tooltip = tooltip; + } + return this; + }, + + // private + clearTip : function(){ + if(Ext.isObject(this.tooltip)){ + Ext.QuickTips.unregister(this.btnEl); + } + }, + + // private + beforeDestroy : function(){ + if(this.rendered){ + this.clearTip(); + } + if(this.menu && this.destroyMenu !== false) { + Ext.destroy(this.menu); + } + Ext.destroy(this.repeater); + }, + + // private + onDestroy : function(){ + if(this.rendered){ + this.doc.un('mouseover', this.monitorMouseOver, this); + this.doc.un('mouseup', this.onMouseUp, this); + delete this.doc; + delete this.btnEl; + Ext.ButtonToggleMgr.unregister(this); + } + Ext.Button.superclass.onDestroy.call(this); + }, + + // private + doAutoWidth : function(){ + if(this.el && this.text && this.width === undefined){ + this.el.setWidth('auto'); + if(Ext.isIE7 && Ext.isStrict){ + var ib = this.btnEl; + if(ib && ib.getWidth() > 20){ + ib.clip(); + ib.setWidth(Ext.util.TextMetrics.measure(ib, this.text).width+ib.getFrameWidth('lr')); + } + } + if(this.minWidth){ + if(this.el.getWidth() < this.minWidth){ + this.el.setWidth(this.minWidth); + } + } + } + }, + + /** + * Assigns this Button's click handler + * @param {Function} handler The function to call when the button is clicked + * @param {Object} scope (optional) The scope (this reference) in which the handler function is executed. + * Defaults to this Button. + * @return {Ext.Button} this + */ + setHandler : function(handler, scope){ + this.handler = handler; + this.scope = scope; + return this; + }, + + /** + * Sets this Button's text + * @param {String} text The button text + * @return {Ext.Button} this + */ + setText : function(text){ + this.text = text; + if(this.el){ + this.btnEl.update(text || ' '); + this.setButtonClass(); + } + this.doAutoWidth(); + return this; + }, + + /** + * Sets the background image (inline style) of the button. This method also changes + * the value of the {@link icon} config internally. + * @param {String} icon The path to an image to display in the button + * @return {Ext.Button} this + */ + setIcon : function(icon){ + this.icon = icon; + if(this.el){ + this.btnEl.setStyle('background-image', icon ? 'url(' + icon + ')' : ''); + this.setButtonClass(); + } + return this; + }, + + /** + * Gets the text for this Button + * @return {String} The button text + */ + getText : function(){ + return this.text; + }, + + /** + * If a state it passed, it becomes the pressed state otherwise the current state is toggled. + * @param {Boolean} state (optional) Force a particular state + * @param {Boolean} supressEvent (optional) True to stop events being fired when calling this method. + * @return {Ext.Button} this + */ + toggle : function(state, suppressEvent){ + state = state === undefined ? !this.pressed : !!state; + if(state != this.pressed){ + if(this.rendered){ + this.el[state ? 'addClass' : 'removeClass']('x-btn-pressed'); + } + this.pressed = state; + if(!suppressEvent){ + this.fireEvent('toggle', this, state); + if(this.toggleHandler){ + this.toggleHandler.call(this.scope || this, this, state); + } + } + } + return this; + }, + + /** + * Focus the button + */ + focus : function(){ + this.btnEl.focus(); + }, + + // private + onDisable : function(){ + this.onDisableChange(true); + }, + + // private + onEnable : function(){ + this.onDisableChange(false); + }, + + onDisableChange : function(disabled){ + if(this.el){ + if(!Ext.isIE6 || !this.text){ + this.el[disabled ? 'addClass' : 'removeClass'](this.disabledClass); + } + this.el.dom.disabled = disabled; + } + this.disabled = disabled; + }, + + /** + * Show this button's menu (if it has one) + */ + showMenu : function(){ + if(this.rendered && this.menu){ + if(this.tooltip){ + Ext.QuickTips.getQuickTip().cancelShow(this.btnEl); + } + if(this.menu.isVisible()){ + this.menu.hide(); + } + this.menu.ownerCt = this; + this.menu.show(this.el, this.menuAlign); + } + return this; + }, + + /** + * Hide this button's menu (if it has one) + */ + hideMenu : function(){ + if(this.hasVisibleMenu()){ + this.menu.hide(); + } + return this; + }, + + /** + * Returns true if the button has a menu and it is visible + * @return {Boolean} + */ + hasVisibleMenu : function(){ + return this.menu && this.menu.ownerCt == this && this.menu.isVisible(); + }, + + // private + onClick : function(e){ + if(e){ + e.preventDefault(); + } + if(e.button !== 0){ + return; + } + if(!this.disabled){ + if(this.enableToggle && (this.allowDepress !== false || !this.pressed)){ + this.toggle(); + } + if(this.menu && !this.hasVisibleMenu() && !this.ignoreNextClick){ + this.showMenu(); + } + this.fireEvent('click', this, e); + if(this.handler){ + //this.el.removeClass('x-btn-over'); + this.handler.call(this.scope || this, this, e); + } + } + }, + + // private + isMenuTriggerOver : function(e, internal){ + return this.menu && !internal; + }, + + // private + isMenuTriggerOut : function(e, internal){ + return this.menu && !internal; + }, + + // private + onMouseOver : function(e){ + if(!this.disabled){ + var internal = e.within(this.el, true); + if(!internal){ + this.el.addClass('x-btn-over'); + if(!this.monitoringMouseOver){ + this.doc.on('mouseover', this.monitorMouseOver, this); + this.monitoringMouseOver = true; + } + this.fireEvent('mouseover', this, e); + } + if(this.isMenuTriggerOver(e, internal)){ + this.fireEvent('menutriggerover', this, this.menu, e); + } + } + }, + + // private + monitorMouseOver : function(e){ + if(e.target != this.el.dom && !e.within(this.el)){ + if(this.monitoringMouseOver){ + this.doc.un('mouseover', this.monitorMouseOver, this); + this.monitoringMouseOver = false; + } + this.onMouseOut(e); + } + }, + + // private + onMouseOut : function(e){ + var internal = e.within(this.el) && e.target != this.el.dom; + this.el.removeClass('x-btn-over'); + this.fireEvent('mouseout', this, e); + if(this.isMenuTriggerOut(e, internal)){ + this.fireEvent('menutriggerout', this, this.menu, e); + } + }, + + focus : function() { + this.btnEl.focus(); + }, + + blur : function() { + this.btnEl.blur(); + }, + + // private + onFocus : function(e){ + if(!this.disabled){ + this.el.addClass('x-btn-focus'); + } + }, + // private + onBlur : function(e){ + this.el.removeClass('x-btn-focus'); + }, + + // private + getClickEl : function(e, isUp){ + return this.el; + }, + + // private + onMouseDown : function(e){ + if(!this.disabled && e.button === 0){ + this.getClickEl(e).addClass('x-btn-click'); + this.doc.on('mouseup', this.onMouseUp, this); + } + }, + // private + onMouseUp : function(e){ + if(e.button === 0){ + this.getClickEl(e, true).removeClass('x-btn-click'); + this.doc.un('mouseup', this.onMouseUp, this); + } + }, + // private + onMenuShow : function(e){ + if(this.menu.ownerCt == this){ + this.menu.ownerCt = this; + this.ignoreNextClick = 0; + this.el.addClass('x-btn-menu-active'); + this.fireEvent('menushow', this, this.menu); + } + }, + // private + onMenuHide : function(e){ + if(this.menu.ownerCt == this){ + this.el.removeClass('x-btn-menu-active'); + this.ignoreNextClick = this.restoreClick.defer(250, this); + this.fireEvent('menuhide', this, this.menu); + delete this.menu.ownerCt; + } + }, + + // private + restoreClick : function(){ + this.ignoreNextClick = 0; + } + + /** + * @cfg {String} autoEl @hide + */ + /** + * @cfg {String/Object} html @hide + */ + /** + * @cfg {String} contentEl @hide + */ + /** + * @cfg {Mixed} data @hide + */ + /** + * @cfg {Mixed} tpl @hide + */ + /** + * @cfg {String} tplWriteMode @hide + */ +}); +Ext.reg('button', Ext.Button); + +// Private utility class used by Button +Ext.ButtonToggleMgr = function(){ + var groups = {}; + + function toggleGroup(btn, state){ + if(state){ + var g = groups[btn.toggleGroup]; + for(var i = 0, l = g.length; i < l; i++){ + if(g[i] != btn){ + g[i].toggle(false); + } + } + } + } + + return { + register : function(btn){ + if(!btn.toggleGroup){ + return; + } + var g = groups[btn.toggleGroup]; + if(!g){ + g = groups[btn.toggleGroup] = []; + } + g.push(btn); + btn.on('toggle', toggleGroup); + }, + + unregister : function(btn){ + if(!btn.toggleGroup){ + return; + } + var g = groups[btn.toggleGroup]; + if(g){ + g.remove(btn); + btn.un('toggle', toggleGroup); + } + }, + + /** + * Gets the pressed button in the passed group or null + * @param {String} group + * @return Button + */ + getPressed : function(group){ + var g = groups[group]; + if(g){ + for(var i = 0, len = g.length; i < len; i++){ + if(g[i].pressed === true){ + return g[i]; + } + } + } + return null; + } + }; +}(); +/** * @class Ext.SplitButton * @extends Ext.Button * A split button that provides a built-in dropdown arrow that can fire an event separately from the default @@ -42430,9 +44795,13 @@ Ext.SplitButton = Ext.extend(Ext.Button, { }, isClickOnArrow : function(e){ - return this.arrowAlign != 'bottom' ? - e.getPageX() > this.el.child(this.buttonSelector).getRegion().right : - e.getPageY() > this.el.child(this.buttonSelector).getRegion().bottom; + if (this.arrowAlign != 'bottom') { + var visBtn = this.el.child('em.x-btn-split'); + var right = visBtn.getRegion().right - visBtn.getPadding('r'); + return e.getPageX() > right; + } else { + return e.getPageY() > this.btnEl.getRegion().bottom; + } }, // private @@ -42461,12 +44830,12 @@ Ext.SplitButton = Ext.extend(Ext.Button, { // private isMenuTriggerOver : function(e){ - return this.menu && e.target.tagName == 'em'; + return this.menu && e.target.tagName == this.arrowSelector; }, // private isMenuTriggerOut : function(e, internal){ - return this.menu && e.target.tagName != 'em'; + return this.menu && e.target.tagName != this.arrowSelector; } }); @@ -42563,7 +44932,7 @@ Ext.CycleButton = Ext.extend(Ext.SplitButton, { } this.activeItem = item; if(!item.checked){ - item.setChecked(true, true); + item.setChecked(true, false); } if(this.forceIcon){ this.setIconClass(this.forceIcon); @@ -42604,7 +44973,7 @@ Ext.CycleButton = Ext.extend(Ext.SplitButton, { this.itemCount = this.items.length; this.menu = {cls:'x-cycle-menu', items:[]}; - var checked; + var checked = 0; Ext.each(this.items, function(item, i){ Ext.apply(item, { group: item.group || this.id, @@ -42615,13 +44984,12 @@ Ext.CycleButton = Ext.extend(Ext.SplitButton, { }); this.menu.items.push(item); if(item.checked){ - checked = item; + checked = i; } }, this); - this.setActiveItem(checked, true); Ext.CycleButton.superclass.initComponent.call(this); - this.on('click', this.toggleSelected, this); + this.setActiveItem(checked, true); }, // private @@ -42657,809 +45025,582 @@ Ext.CycleButton = Ext.extend(Ext.SplitButton, { } } }); -Ext.reg('cycle', Ext.CycleButton);/** - * @class Ext.layout.ToolbarLayout - * @extends Ext.layout.ContainerLayout - * Layout manager implicitly used by Ext.Toolbar. - */ -Ext.layout.ToolbarLayout = Ext.extend(Ext.layout.ContainerLayout, { - monitorResize : true, - triggerWidth : 18, - lastOverflow : false, - - noItemsMenuText : '
    (None)
    ', - // private - onLayout : function(ct, target){ - if(!this.leftTr){ - target.addClass('x-toolbar-layout-ct'); - target.insertHtml('beforeEnd', - '
    '); - this.leftTr = target.child('tr.x-toolbar-left-row', true); - this.rightTr = target.child('tr.x-toolbar-right-row', true); - this.extrasTr = target.child('tr.x-toolbar-extras-row', true); - } - var side = this.leftTr; - var pos = 0; - - var items = ct.items.items; - for(var i = 0, len = items.length, c; i < len; i++, pos++) { - c = items[i]; - if(c.isFill){ - side = this.rightTr; - pos = -1; - }else if(!c.rendered){ - c.render(this.insertCell(c, side, pos)); - }else{ - if(!c.xtbHidden && !this.isValidParent(c, side.childNodes[pos])){ - var td = this.insertCell(c, side, pos); - td.appendChild(c.getDomPositionEl().dom); - c.container = Ext.get(td); - } - } - } - //strip extra empty cells - this.cleanup(this.leftTr); - this.cleanup(this.rightTr); - this.cleanup(this.extrasTr); - this.fitToSize(target); - }, - - cleanup : function(row){ - var cn = row.childNodes; - for(var i = cn.length-1, c; i >= 0 && (c = cn[i]); i--){ - if(!c.firstChild){ - row.removeChild(c); - } - } - }, - - insertCell : function(c, side, pos){ - var td = document.createElement('td'); - td.className='x-toolbar-cell'; - side.insertBefore(td, side.childNodes[pos]||null); - return td; - }, - - hideItem : function(item){ - var h = (this.hiddens = this.hiddens || []); - h.push(item); - item.xtbHidden = true; - item.xtbWidth = item.getDomPositionEl().dom.parentNode.offsetWidth; - item.hide(); - }, - - unhideItem : function(item){ - item.show(); - item.xtbHidden = false; - this.hiddens.remove(item); - if(this.hiddens.length < 1){ - delete this.hiddens; - } - }, - - getItemWidth : function(c){ - return c.hidden ? (c.xtbWidth || 0) : c.getDomPositionEl().dom.parentNode.offsetWidth; - }, - - fitToSize : function(t){ - if(this.container.enableOverflow === false){ - return; - } - var w = t.dom.clientWidth; - var lw = this.lastWidth || 0; - this.lastWidth = w; - var iw = t.dom.firstChild.offsetWidth; - - var clipWidth = w - this.triggerWidth; - var hideIndex = -1; - - if(iw > w || (this.hiddens && w >= lw)){ - var i, items = this.container.items.items, len = items.length, c; - var loopWidth = 0; - for(i = 0; i < len; i++) { - c = items[i]; - if(!c.isFill){ - loopWidth += this.getItemWidth(c); - if(loopWidth > clipWidth){ - if(!c.xtbHidden){ - this.hideItem(c); - } - }else{ - if(c.xtbHidden){ - this.unhideItem(c); - } - } - } - } - } - if(this.hiddens){ - this.initMore(); - if(!this.lastOverflow){ - this.container.fireEvent('overflowchange', this.container, true); - this.lastOverflow = true; - } - }else if(this.more){ - this.clearMenu(); - this.more.destroy(); - delete this.more; - if(this.lastOverflow){ - this.container.fireEvent('overflowchange', this.container, false); - this.lastOverflow = false; - } - } - }, - - createMenuConfig : function(c, hideOnClick){ - var cfg = Ext.apply({}, c.initialConfig), - group = c.toggleGroup; - - Ext.apply(cfg, { - text: c.overflowText || c.text, - iconCls: c.iconCls, - icon: c.icon, - itemId: c.itemId, - disabled: c.disabled, - handler: c.handler, - scope: c.scope, - menu: c.menu, - hideOnClick: hideOnClick - }); - if(group || c.enableToggle){ - Ext.apply(cfg, { - group: group, - checked: c.pressed, - listeners: { - checkchange: function(item, checked){ - c.toggle(checked); - } - } - }); - } - delete cfg.xtype; - delete cfg.id; - return cfg; - }, - - // private - addComponentToMenu : function(m, c){ - if(c instanceof Ext.Toolbar.Separator){ - m.add('-'); - }else if(Ext.isFunction(c.isXType)){ - if(c.isXType('splitbutton')){ - m.add(this.createMenuConfig(c, true)); - }else if(c.isXType('button')){ - m.add(this.createMenuConfig(c, !c.menu)); - }else if(c.isXType('buttongroup')){ - c.items.each(function(item){ - this.addComponentToMenu(m, item); - }, this); - } - } - }, - - clearMenu : function(){ - var m = this.moreMenu; - if(m && m.items){ - m.items.each(function(item){ - delete item.menu; - }); - } - }, - - // private - beforeMoreShow : function(m){ - var h = this.container.items.items, - len = h.length, - c, - prev, - needsSep = function(group, item){ - return group.isXType('buttongroup') && !(item instanceof Ext.Toolbar.Separator); - }; - - this.clearMenu(); - m.removeAll(); - for(var i = 0; i < len; i++){ - c = h[i]; - if(c.xtbHidden){ - if(prev && (needsSep(c, prev) || needsSep(prev, c))){ - m.add('-'); - } - this.addComponentToMenu(m, c); - prev = c; - } - } - // put something so the menu isn't empty - // if no compatible items found - if(m.items.length < 1){ - m.add(this.noItemsMenuText); - } - }, - - initMore : function(){ - if(!this.more){ - this.moreMenu = new Ext.menu.Menu({ - listeners: { - beforeshow: this.beforeMoreShow, - scope: this - } - }); - this.moreMenu.ownerCt = this.container; - this.more = new Ext.Button({ - iconCls: 'x-toolbar-more-icon', - cls: 'x-toolbar-more', - menu: this.moreMenu - }); - var td = this.insertCell(this.more, this.extrasTr, 100); - this.more.render(td); - } - }, - - destroy : function(){ - Ext.destroy(this.more, this.moreMenu); - Ext.layout.ToolbarLayout.superclass.destroy.call(this); - } - /** - * @property activeItem - * @hide - */ -}); - -Ext.Container.LAYOUTS.toolbar = Ext.layout.ToolbarLayout; - -/** - * @class Ext.Toolbar - * @extends Ext.Container - *

    Basic Toolbar class. Although the {@link Ext.Container#defaultType defaultType} for Toolbar - * is {@link Ext.Button button}, Toolbar elements (child items for the Toolbar container) may - * be virtually any type of Component. Toolbar elements can be created explicitly via their constructors, - * or implicitly via their xtypes, and can be {@link #add}ed dynamically.

    - *

    Some items have shortcut strings for creation:

    - *
    -Shortcut  xtype          Class                  Description
    -'->'      'tbfill'       {@link Ext.Toolbar.Fill}       begin using the right-justified button container
    -'-'       'tbseparator'  {@link Ext.Toolbar.Separator}  add a vertical separator bar between toolbar items
    -' '       'tbspacer'     {@link Ext.Toolbar.Spacer}     add horiztonal space between elements
    - * 
    - * - * Example usage of various elements: - *
    
    -var tb = new Ext.Toolbar({
    -    renderTo: document.body,
    -    width: 600,
    -    height: 100,
    -    items: [
    -        {
    -            // xtype: 'button', // default for Toolbars, same as 'tbbutton'
    -            text: 'Button'
    -        },
    -        {
    -            xtype: 'splitbutton', // same as 'tbsplitbutton'
    -            text: 'Split Button'
    -        },
    -        // begin using the right-justified button container
    -        '->', // same as {xtype: 'tbfill'}, // Ext.Toolbar.Fill
    -        {
    -            xtype: 'textfield',
    -            name: 'field1',
    -            emptyText: 'enter search term'
    -        },
    -        // add a vertical separator bar between toolbar items
    -        '-', // same as {xtype: 'tbseparator'} to create Ext.Toolbar.Separator
    -        'text 1', // same as {xtype: 'tbtext', text: 'text1'} to create Ext.Toolbar.TextItem
    -        {xtype: 'tbspacer'},// same as ' ' to create Ext.Toolbar.Spacer
    -        'text 2',
    -        {xtype: 'tbspacer', width: 50}, // add a 50px space
    -        'text 3'
    -    ]
    -});
    - * 
    - * Example adding a ComboBox within a menu of a button: - *
    
    -// ComboBox creation
    -var combo = new Ext.form.ComboBox({
    -    store: new Ext.data.ArrayStore({
    -        autoDestroy: true,
    -        fields: ['initials', 'fullname'],
    -        data : [
    -            ['FF', 'Fred Flintstone'],
    -            ['BR', 'Barney Rubble']
    -        ]
    -    }),
    -    displayField: 'fullname',
    -    typeAhead: true,
    -    mode: 'local',
    -    forceSelection: true,
    -    triggerAction: 'all',
    -    emptyText: 'Select a name...',
    -    selectOnFocus: true,
    -    width: 135,
    -    getListParent: function() {
    -        return this.el.up('.x-menu');
    -    },
    -    iconCls: 'no-icon' //use iconCls if placing within menu to shift to right side of menu
    -});
    -
    -// put ComboBox in a Menu
    -var menu = new Ext.menu.Menu({
    -    id: 'mainMenu',
    -    items: [
    -        combo // A Field in a Menu
    -    ]
    -});
    -
    -// add a Button with the menu
    -tb.add({
    -        text:'Button w/ Menu',
    -        menu: menu  // assign menu by instance
    -    });
    -tb.doLayout();
    - * 
    - * @constructor - * Creates a new Toolbar - * @param {Object/Array} config A config object or an array of buttons to {@link #add} - * @xtype toolbar - */ -Ext.Toolbar = function(config){ - if(Ext.isArray(config)){ - config = {items: config, layout: 'toolbar'}; - } else { - config = Ext.apply({ - layout: 'toolbar' - }, config); - if(config.buttons) { - config.items = config.buttons; - } - } - Ext.Toolbar.superclass.constructor.call(this, config); -}; - -(function(){ - -var T = Ext.Toolbar; - -Ext.extend(T, Ext.Container, { - - defaultType: 'button', - - /** - * @cfg {String/Object} layout - * This class assigns a default layout (layout:'toolbar'). - * Developers may override this configuration option if another layout - * is required (the constructor must be passed a configuration object in this - * case instead of an array). - * See {@link Ext.Container#layout} for additional information. - */ - - trackMenus : true, - internalDefaults: {removeMode: 'container', hideParent: true}, - toolbarCls: 'x-toolbar', - - initComponent : function(){ - T.superclass.initComponent.call(this); - - /** - * @event overflowchange - * Fires after the overflow state has changed. - * @param {Object} c The Container - * @param {Boolean} lastOverflow overflow state - */ - this.addEvents('overflowchange'); - }, - - // private - onRender : function(ct, position){ - if(!this.el){ - if(!this.autoCreate){ - this.autoCreate = { - cls: this.toolbarCls + ' x-small-editor' - }; - } - this.el = ct.createChild(Ext.apply({ id: this.id },this.autoCreate), position); - Ext.Toolbar.superclass.onRender.apply(this, arguments); - } - }, - - /** - *

    Adds element(s) to the toolbar -- this function takes a variable number of - * arguments of mixed type and adds them to the toolbar.

    - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Mixed} arg1 The following types of arguments are all valid:
    - *
      - *
    • {@link Ext.Button} config: A valid button config object (equivalent to {@link #addButton})
    • - *
    • HtmlElement: Any standard HTML element (equivalent to {@link #addElement})
    • - *
    • Field: Any form field (equivalent to {@link #addField})
    • - *
    • Item: Any subclass of {@link Ext.Toolbar.Item} (equivalent to {@link #addItem})
    • - *
    • String: Any generic string (gets wrapped in a {@link Ext.Toolbar.TextItem}, equivalent to {@link #addText}). - * Note that there are a few special strings that are treated differently as explained next.
    • - *
    • '-': Creates a separator element (equivalent to {@link #addSeparator})
    • - *
    • ' ': Creates a spacer element (equivalent to {@link #addSpacer})
    • - *
    • '->': Creates a fill element (equivalent to {@link #addFill})
    • - *
    - * @param {Mixed} arg2 - * @param {Mixed} etc. - * @method add - */ - - // private - lookupComponent : function(c){ - if(Ext.isString(c)){ - if(c == '-'){ - c = new T.Separator(); - }else if(c == ' '){ - c = new T.Spacer(); - }else if(c == '->'){ - c = new T.Fill(); - }else{ - c = new T.TextItem(c); - } - this.applyDefaults(c); - }else{ - if(c.isFormField || c.render){ // some kind of form field, some kind of Toolbar.Item - c = this.constructItem(c); - }else if(c.tag){ // DomHelper spec - c = new T.Item({autoEl: c}); - }else if(c.tagName){ // element - c = new T.Item({el:c}); - }else if(Ext.isObject(c)){ // must be button config? - c = c.xtype ? this.constructItem(c) : this.constructButton(c); - } - } - return c; - }, - - // private - applyDefaults : function(c){ - if(!Ext.isString(c)){ - c = Ext.Toolbar.superclass.applyDefaults.call(this, c); - var d = this.internalDefaults; - if(c.events){ - Ext.applyIf(c.initialConfig, d); - Ext.apply(c, d); - }else{ - Ext.applyIf(c, d); - } - } - return c; - }, - - // private - constructItem : function(item, type){ - return Ext.create(item, type || this.defaultType); - }, - - /** - * Adds a separator - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @return {Ext.Toolbar.Item} The separator {@link Ext.Toolbar.Item item} - */ - addSeparator : function(){ - return this.add(new T.Separator()); - }, - - /** - * Adds a spacer element - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @return {Ext.Toolbar.Spacer} The spacer item - */ - addSpacer : function(){ - return this.add(new T.Spacer()); - }, - - /** - * Forces subsequent additions into the float:right toolbar - *

    Note: See the notes within {@link Ext.Container#add}.

    - */ - addFill : function(){ - this.add(new T.Fill()); - }, - - /** - * Adds any standard HTML element to the toolbar - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Mixed} el The element or id of the element to add - * @return {Ext.Toolbar.Item} The element's item - */ - addElement : function(el){ - return this.addItem(new T.Item({el:el})); - }, - - /** - * Adds any Toolbar.Item or subclass - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Ext.Toolbar.Item} item - * @return {Ext.Toolbar.Item} The item - */ - addItem : function(item){ - return this.add.apply(this, arguments); - }, - - /** - * Adds a button (or buttons). See {@link Ext.Button} for more info on the config. - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Object/Array} config A button config or array of configs - * @return {Ext.Button/Array} - */ - addButton : function(config){ - if(Ext.isArray(config)){ - var buttons = []; - for(var i = 0, len = config.length; i < len; i++) { - buttons.push(this.addButton(config[i])); - } - return buttons; - } - return this.add(this.constructButton(config)); - }, - - /** - * Adds text to the toolbar - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {String} text The text to add - * @return {Ext.Toolbar.Item} The element's item - */ - addText : function(text){ - return this.addItem(new T.TextItem(text)); - }, - - /** - * Adds a new element to the toolbar from the passed {@link Ext.DomHelper} config - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Object} config - * @return {Ext.Toolbar.Item} The element's item - */ - addDom : function(config){ - return this.add(new T.Item({autoEl: config})); - }, - - /** - * Adds a dynamically rendered Ext.form field (TextField, ComboBox, etc). Note: the field should not have - * been rendered yet. For a field that has already been rendered, use {@link #addElement}. - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Ext.form.Field} field - * @return {Ext.Toolbar.Item} - */ - addField : function(field){ - return this.add(field); - }, - - /** - * Inserts any {@link Ext.Toolbar.Item}/{@link Ext.Button} at the specified index. - *

    Note: See the notes within {@link Ext.Container#add}.

    - * @param {Number} index The index where the item is to be inserted - * @param {Object/Ext.Toolbar.Item/Ext.Button/Array} item The button, or button config object to be - * inserted, or an array of buttons/configs. - * @return {Ext.Button/Item} - */ - insertButton : function(index, item){ - if(Ext.isArray(item)){ - var buttons = []; - for(var i = 0, len = item.length; i < len; i++) { - buttons.push(this.insertButton(index + i, item[i])); - } - return buttons; - } - return Ext.Toolbar.superclass.insert.call(this, index, item); - }, - - // private - initMenuTracking : function(item){ - if(this.trackMenus && item.menu){ - this.mon(item, { - 'menutriggerover' : this.onButtonTriggerOver, - 'menushow' : this.onButtonMenuShow, - 'menuhide' : this.onButtonMenuHide, - scope: this - }); - } - }, - - // private - constructButton : function(item){ - var b = item.events ? item : this.constructItem(item, item.split ? 'splitbutton' : this.defaultType); - this.initMenuTracking(b); - return b; - }, - - // private - onDisable : function(){ - this.items.each(function(item){ - if(item.disable){ - item.disable(); - } - }); - }, - - // private - onEnable : function(){ - this.items.each(function(item){ - if(item.enable){ - item.enable(); - } - }); - }, - - // private - onButtonTriggerOver : function(btn){ - if(this.activeMenuBtn && this.activeMenuBtn != btn){ - this.activeMenuBtn.hideMenu(); - btn.showMenu(); - this.activeMenuBtn = btn; - } - }, - - // private - onButtonMenuShow : function(btn){ - this.activeMenuBtn = btn; - }, - - // private - onButtonMenuHide : function(btn){ - delete this.activeMenuBtn; - } -}); -Ext.reg('toolbar', Ext.Toolbar); - -/** - * @class Ext.Toolbar.Item - * @extends Ext.BoxComponent - * The base class that other non-interacting Toolbar Item classes should extend in order to - * get some basic common toolbar item functionality. - * @constructor - * Creates a new Item - * @param {HTMLElement} el - * @xtype tbitem - */ -T.Item = Ext.extend(Ext.BoxComponent, { - hideParent: true, // Hiding a Toolbar.Item hides its containing TD - enable:Ext.emptyFn, - disable:Ext.emptyFn, - focus:Ext.emptyFn - /** - * @cfg {String} overflowText Text to be used for the menu if the item is overflowed. - */ -}); -Ext.reg('tbitem', T.Item); - -/** - * @class Ext.Toolbar.Separator - * @extends Ext.Toolbar.Item - * A simple class that adds a vertical separator bar between toolbar items - * (css class:'xtb-sep'). Example usage: - *
    
    -new Ext.Panel({
    -    tbar : [
    -        'Item 1',
    -        {xtype: 'tbseparator'}, // or '-'
    -        'Item 2'
    -    ]
    -});
    -
    - * @constructor - * Creates a new Separator - * @xtype tbseparator - */ -T.Separator = Ext.extend(T.Item, { - onRender : function(ct, position){ - this.el = ct.createChild({tag:'span', cls:'xtb-sep'}, position); - } -}); -Ext.reg('tbseparator', T.Separator); - -/** - * @class Ext.Toolbar.Spacer - * @extends Ext.Toolbar.Item - * A simple element that adds extra horizontal space between items in a toolbar. - * By default a 2px wide space is added via css specification:
    
    -.x-toolbar .xtb-spacer {
    -    width:2px;
    -}
    - * 
    - *

    Example usage:

    - *
    
    -new Ext.Panel({
    -    tbar : [
    -        'Item 1',
    -        {xtype: 'tbspacer'}, // or ' '
    -        'Item 2',
    -        // space width is also configurable via javascript
    -        {xtype: 'tbspacer', width: 50}, // add a 50px space
    -        'Item 3'
    -    ]
    -});
    -
    - * @constructor - * Creates a new Spacer - * @xtype tbspacer - */ -T.Spacer = Ext.extend(T.Item, { - /** - * @cfg {Number} width - * The width of the spacer in pixels (defaults to 2px via css style .x-toolbar .xtb-spacer). - */ - - onRender : function(ct, position){ - this.el = ct.createChild({tag:'div', cls:'xtb-spacer', style: this.width?'width:'+this.width+'px':''}, position); - } -}); -Ext.reg('tbspacer', T.Spacer); - -/** - * @class Ext.Toolbar.Fill - * @extends Ext.Toolbar.Spacer - * A non-rendering placeholder item which instructs the Toolbar's Layout to begin using - * the right-justified button container. - *
    
    -new Ext.Panel({
    -    tbar : [
    -        'Item 1',
    -        {xtype: 'tbfill'}, // or '->'
    -        'Item 2'
    -    ]
    -});
    -
    - * @constructor - * Creates a new Fill - * @xtype tbfill - */ -T.Fill = Ext.extend(T.Item, { - // private - render : Ext.emptyFn, - isFill : true -}); -Ext.reg('tbfill', T.Fill); - -/** - * @class Ext.Toolbar.TextItem - * @extends Ext.Toolbar.Item - * A simple class that renders text directly into a toolbar - * (with css class:'xtb-text'). Example usage: - *
    
    -new Ext.Panel({
    -    tbar : [
    -        {xtype: 'tbtext', text: 'Item 1'} // or simply 'Item 1'
    -    ]
    -});
    -
    - * @constructor - * Creates a new TextItem - * @param {String/Object} text A text string, or a config object containing a text property - * @xtype tbtext - */ -T.TextItem = Ext.extend(T.Item, { - /** - * @cfg {String} text The text to be used as innerHTML (html tags are accepted) - */ - - constructor: function(config){ - T.TextItem.superclass.constructor.call(this, Ext.isString(config) ? {text: config} : config); - }, - - // private - onRender : function(ct, position) { - this.autoEl = {cls: 'xtb-text', html: this.text || ''}; - T.TextItem.superclass.onRender.call(this, ct, position); - }, - - /** - * Updates this item's text, setting the text to be used as innerHTML. - * @param {String} t The text to display (html accepted). - */ - setText : function(t) { - if(this.rendered){ - this.el.update(t); - }else{ - this.text = t; - } - } -}); -Ext.reg('tbtext', T.TextItem); - -// backwards compat -T.Button = Ext.extend(Ext.Button, {}); -T.SplitButton = Ext.extend(Ext.SplitButton, {}); -Ext.reg('tbbutton', T.Button); -Ext.reg('tbsplit', T.SplitButton); - -})(); +Ext.reg('cycle', Ext.CycleButton);/** + * @class Ext.Toolbar + * @extends Ext.Container + *

    Basic Toolbar class. Although the {@link Ext.Container#defaultType defaultType} for Toolbar + * is {@link Ext.Button button}, Toolbar elements (child items for the Toolbar container) may + * be virtually any type of Component. Toolbar elements can be created explicitly via their constructors, + * or implicitly via their xtypes, and can be {@link #add}ed dynamically.

    + *

    Some items have shortcut strings for creation:

    + *
    +Shortcut  xtype          Class                  Description
    +'->'      'tbfill'       {@link Ext.Toolbar.Fill}       begin using the right-justified button container
    +'-'       'tbseparator'  {@link Ext.Toolbar.Separator}  add a vertical separator bar between toolbar items
    +' '       'tbspacer'     {@link Ext.Toolbar.Spacer}     add horiztonal space between elements
    + * 
    + * + * Example usage of various elements: + *
    
    +var tb = new Ext.Toolbar({
    +    renderTo: document.body,
    +    width: 600,
    +    height: 100,
    +    items: [
    +        {
    +            // xtype: 'button', // default for Toolbars, same as 'tbbutton'
    +            text: 'Button'
    +        },
    +        {
    +            xtype: 'splitbutton', // same as 'tbsplitbutton'
    +            text: 'Split Button'
    +        },
    +        // begin using the right-justified button container
    +        '->', // same as {xtype: 'tbfill'}, // Ext.Toolbar.Fill
    +        {
    +            xtype: 'textfield',
    +            name: 'field1',
    +            emptyText: 'enter search term'
    +        },
    +        // add a vertical separator bar between toolbar items
    +        '-', // same as {xtype: 'tbseparator'} to create Ext.Toolbar.Separator
    +        'text 1', // same as {xtype: 'tbtext', text: 'text1'} to create Ext.Toolbar.TextItem
    +        {xtype: 'tbspacer'},// same as ' ' to create Ext.Toolbar.Spacer
    +        'text 2',
    +        {xtype: 'tbspacer', width: 50}, // add a 50px space
    +        'text 3'
    +    ]
    +});
    + * 
    + * Example adding a ComboBox within a menu of a button: + *
    
    +// ComboBox creation
    +var combo = new Ext.form.ComboBox({
    +    store: new Ext.data.ArrayStore({
    +        autoDestroy: true,
    +        fields: ['initials', 'fullname'],
    +        data : [
    +            ['FF', 'Fred Flintstone'],
    +            ['BR', 'Barney Rubble']
    +        ]
    +    }),
    +    displayField: 'fullname',
    +    typeAhead: true,
    +    mode: 'local',
    +    forceSelection: true,
    +    triggerAction: 'all',
    +    emptyText: 'Select a name...',
    +    selectOnFocus: true,
    +    width: 135,
    +    getListParent: function() {
    +        return this.el.up('.x-menu');
    +    },
    +    iconCls: 'no-icon' //use iconCls if placing within menu to shift to right side of menu
    +});
    +
    +// put ComboBox in a Menu
    +var menu = new Ext.menu.Menu({
    +    id: 'mainMenu',
    +    items: [
    +        combo // A Field in a Menu
    +    ]
    +});
    +
    +// add a Button with the menu
    +tb.add({
    +        text:'Button w/ Menu',
    +        menu: menu  // assign menu by instance
    +    });
    +tb.doLayout();
    + * 
    + * @constructor + * Creates a new Toolbar + * @param {Object/Array} config A config object or an array of buttons to {@link #add} + * @xtype toolbar + */ +Ext.Toolbar = function(config){ + if(Ext.isArray(config)){ + config = {items: config, layout: 'toolbar'}; + } else { + config = Ext.apply({ + layout: 'toolbar' + }, config); + if(config.buttons) { + config.items = config.buttons; + } + } + Ext.Toolbar.superclass.constructor.call(this, config); +}; + +(function(){ + +var T = Ext.Toolbar; + +Ext.extend(T, Ext.Container, { + + defaultType: 'button', + + /** + * @cfg {String/Object} layout + * This class assigns a default layout (layout:'toolbar'). + * Developers may override this configuration option if another layout + * is required (the constructor must be passed a configuration object in this + * case instead of an array). + * See {@link Ext.Container#layout} for additional information. + */ + + enableOverflow : false, + + /** + * @cfg {Boolean} enableOverflow + * Defaults to false. Configure true to make the toolbar provide a button + * which activates a dropdown Menu to show items which overflow the Toolbar's width. + */ + /** + * @cfg {String} buttonAlign + *

    The default position at which to align child items. Defaults to "left"

    + *

    May be specified as "center" to cause items added before a Fill (A "->") item + * to be centered in the Toolbar. Items added after a Fill are still right-aligned.

    + *

    Specify as "right" to right align all child items.

    + */ + + trackMenus : true, + internalDefaults: {removeMode: 'container', hideParent: true}, + toolbarCls: 'x-toolbar', + + initComponent : function(){ + T.superclass.initComponent.call(this); + + /** + * @event overflowchange + * Fires after the overflow state has changed. + * @param {Object} c The Container + * @param {Boolean} lastOverflow overflow state + */ + this.addEvents('overflowchange'); + }, + + // private + onRender : function(ct, position){ + if(!this.el){ + if(!this.autoCreate){ + this.autoCreate = { + cls: this.toolbarCls + ' x-small-editor' + }; + } + this.el = ct.createChild(Ext.apply({ id: this.id },this.autoCreate), position); + Ext.Toolbar.superclass.onRender.apply(this, arguments); + } + }, + + /** + *

    Adds element(s) to the toolbar -- this function takes a variable number of + * arguments of mixed type and adds them to the toolbar.

    + *

    Note: See the notes within {@link Ext.Container#add}.

    + * @param {Mixed} arg1 The following types of arguments are all valid:
    + *
      + *
    • {@link Ext.Button} config: A valid button config object (equivalent to {@link #addButton})
    • + *
    • HtmlElement: Any standard HTML element (equivalent to {@link #addElement})
    • + *
    • Field: Any form field (equivalent to {@link #addField})
    • + *
    • Item: Any subclass of {@link Ext.Toolbar.Item} (equivalent to {@link #addItem})
    • + *
    • String: Any generic string (gets wrapped in a {@link Ext.Toolbar.TextItem}, equivalent to {@link #addText}). + * Note that there are a few special strings that are treated differently as explained next.
    • + *
    • '-': Creates a separator element (equivalent to {@link #addSeparator})
    • + *
    • ' ': Creates a spacer element (equivalent to {@link #addSpacer})
    • + *
    • '->': Creates a fill element (equivalent to {@link #addFill})
    • + *
    + * @param {Mixed} arg2 + * @param {Mixed} etc. + * @method add + */ + + // private + lookupComponent : function(c){ + if(Ext.isString(c)){ + if(c == '-'){ + c = new T.Separator(); + }else if(c == ' '){ + c = new T.Spacer(); + }else if(c == '->'){ + c = new T.Fill(); + }else{ + c = new T.TextItem(c); + } + this.applyDefaults(c); + }else{ + if(c.isFormField || c.render){ // some kind of form field, some kind of Toolbar.Item + c = this.createComponent(c); + }else if(c.tag){ // DomHelper spec + c = new T.Item({autoEl: c}); + }else if(c.tagName){ // element + c = new T.Item({el:c}); + }else if(Ext.isObject(c)){ // must be button config? + c = c.xtype ? this.createComponent(c) : this.constructButton(c); + } + } + return c; + }, + + // private + applyDefaults : function(c){ + if(!Ext.isString(c)){ + c = Ext.Toolbar.superclass.applyDefaults.call(this, c); + var d = this.internalDefaults; + if(c.events){ + Ext.applyIf(c.initialConfig, d); + Ext.apply(c, d); + }else{ + Ext.applyIf(c, d); + } + } + return c; + }, + + /** + * Adds a separator + *

    Note: See the notes within {@link Ext.Container#add}.

    + * @return {Ext.Toolbar.Item} The separator {@link Ext.Toolbar.Item item} + */ + addSeparator : function(){ + return this.add(new T.Separator()); + }, + + /** + * Adds a spacer element + *

    Note: See the notes within {@link Ext.Container#add}.

    + * @return {Ext.Toolbar.Spacer} The spacer item + */ + addSpacer : function(){ + return this.add(new T.Spacer()); + }, + + /** + * Forces subsequent additions into the float:right toolbar + *

    Note: See the notes within {@link Ext.Container#add}.

    + */ + addFill : function(){ + this.add(new T.Fill()); + }, + + /** + * Adds any standard HTML element to the toolbar + *

    Note: See the notes within {@link Ext.Container#add}.

    + * @param {Mixed} el The element or id of the element to add + * @return {Ext.Toolbar.Item} The element's item + */ + addElement : function(el){ + return this.addItem(new T.Item({el:el})); + }, + + /** + * Adds any Toolbar.Item or subclass + *

    Note: See the notes within {@link Ext.Container#add}.

    + * @param {Ext.Toolbar.Item} item + * @return {Ext.Toolbar.Item} The item + */ + addItem : function(item){ + return this.add.apply(this, arguments); + }, + + /** + * Adds a button (or buttons). See {@link Ext.Button} for more info on the config. + *

    Note: See the notes within {@link Ext.Container#add}.

    + * @param {Object/Array} config A button config or array of configs + * @return {Ext.Button/Array} + */ + addButton : function(config){ + if(Ext.isArray(config)){ + var buttons = []; + for(var i = 0, len = config.length; i < len; i++) { + buttons.push(this.addButton(config[i])); + } + return buttons; + } + return this.add(this.constructButton(config)); + }, + + /** + * Adds text to the toolbar + *

    Note: See the notes within {@link Ext.Container#add}.

    + * @param {String} text The text to add + * @return {Ext.Toolbar.Item} The element's item + */ + addText : function(text){ + return this.addItem(new T.TextItem(text)); + }, + + /** + * Adds a new element to the toolbar from the passed {@link Ext.DomHelper} config + *

    Note: See the notes within {@link Ext.Container#add}.

    + * @param {Object} config + * @return {Ext.Toolbar.Item} The element's item + */ + addDom : function(config){ + return this.add(new T.Item({autoEl: config})); + }, + + /** + * Adds a dynamically rendered Ext.form field (TextField, ComboBox, etc). Note: the field should not have + * been rendered yet. For a field that has already been rendered, use {@link #addElement}. + *

    Note: See the notes within {@link Ext.Container#add}.

    + * @param {Ext.form.Field} field + * @return {Ext.Toolbar.Item} + */ + addField : function(field){ + return this.add(field); + }, + + /** + * Inserts any {@link Ext.Toolbar.Item}/{@link Ext.Button} at the specified index. + *

    Note: See the notes within {@link Ext.Container#add}.

    + * @param {Number} index The index where the item is to be inserted + * @param {Object/Ext.Toolbar.Item/Ext.Button/Array} item The button, or button config object to be + * inserted, or an array of buttons/configs. + * @return {Ext.Button/Item} + */ + insertButton : function(index, item){ + if(Ext.isArray(item)){ + var buttons = []; + for(var i = 0, len = item.length; i < len; i++) { + buttons.push(this.insertButton(index + i, item[i])); + } + return buttons; + } + return Ext.Toolbar.superclass.insert.call(this, index, item); + }, + + // private + trackMenu : function(item, remove){ + if(this.trackMenus && item.menu){ + var method = remove ? 'mun' : 'mon'; + this[method](item, 'menutriggerover', this.onButtonTriggerOver, this); + this[method](item, 'menushow', this.onButtonMenuShow, this); + this[method](item, 'menuhide', this.onButtonMenuHide, this); + } + }, + + // private + constructButton : function(item){ + var b = item.events ? item : this.createComponent(item, item.split ? 'splitbutton' : this.defaultType); + return b; + }, + + // private + onAdd : function(c){ + Ext.Toolbar.superclass.onAdd.call(this); + this.trackMenu(c); + if(this.disabled){ + c.disable(); + } + }, + + // private + onRemove : function(c){ + Ext.Toolbar.superclass.onRemove.call(this); + this.trackMenu(c, true); + }, + + // private + onDisable : function(){ + this.items.each(function(item){ + if(item.disable){ + item.disable(); + } + }); + }, + + // private + onEnable : function(){ + this.items.each(function(item){ + if(item.enable){ + item.enable(); + } + }); + }, + + // private + onButtonTriggerOver : function(btn){ + if(this.activeMenuBtn && this.activeMenuBtn != btn){ + this.activeMenuBtn.hideMenu(); + btn.showMenu(); + this.activeMenuBtn = btn; + } + }, + + // private + onButtonMenuShow : function(btn){ + this.activeMenuBtn = btn; + }, + + // private + onButtonMenuHide : function(btn){ + delete this.activeMenuBtn; + } +}); +Ext.reg('toolbar', Ext.Toolbar); + +/** + * @class Ext.Toolbar.Item + * @extends Ext.BoxComponent + * The base class that other non-interacting Toolbar Item classes should extend in order to + * get some basic common toolbar item functionality. + * @constructor + * Creates a new Item + * @param {HTMLElement} el + * @xtype tbitem + */ +T.Item = Ext.extend(Ext.BoxComponent, { + hideParent: true, // Hiding a Toolbar.Item hides its containing TD + enable:Ext.emptyFn, + disable:Ext.emptyFn, + focus:Ext.emptyFn + /** + * @cfg {String} overflowText Text to be used for the menu if the item is overflowed. + */ +}); +Ext.reg('tbitem', T.Item); + +/** + * @class Ext.Toolbar.Separator + * @extends Ext.Toolbar.Item + * A simple class that adds a vertical separator bar between toolbar items + * (css class:'xtb-sep'). Example usage: + *
    
    +new Ext.Panel({
    +    tbar : [
    +        'Item 1',
    +        {xtype: 'tbseparator'}, // or '-'
    +        'Item 2'
    +    ]
    +});
    +
    + * @constructor + * Creates a new Separator + * @xtype tbseparator + */ +T.Separator = Ext.extend(T.Item, { + onRender : function(ct, position){ + this.el = ct.createChild({tag:'span', cls:'xtb-sep'}, position); + } +}); +Ext.reg('tbseparator', T.Separator); + +/** + * @class Ext.Toolbar.Spacer + * @extends Ext.Toolbar.Item + * A simple element that adds extra horizontal space between items in a toolbar. + * By default a 2px wide space is added via css specification:
    
    +.x-toolbar .xtb-spacer {
    +    width:2px;
    +}
    + * 
    + *

    Example usage:

    + *
    
    +new Ext.Panel({
    +    tbar : [
    +        'Item 1',
    +        {xtype: 'tbspacer'}, // or ' '
    +        'Item 2',
    +        // space width is also configurable via javascript
    +        {xtype: 'tbspacer', width: 50}, // add a 50px space
    +        'Item 3'
    +    ]
    +});
    +
    + * @constructor + * Creates a new Spacer + * @xtype tbspacer + */ +T.Spacer = Ext.extend(T.Item, { + /** + * @cfg {Number} width + * The width of the spacer in pixels (defaults to 2px via css style .x-toolbar .xtb-spacer). + */ + + onRender : function(ct, position){ + this.el = ct.createChild({tag:'div', cls:'xtb-spacer', style: this.width?'width:'+this.width+'px':''}, position); + } +}); +Ext.reg('tbspacer', T.Spacer); + +/** + * @class Ext.Toolbar.Fill + * @extends Ext.Toolbar.Spacer + * A non-rendering placeholder item which instructs the Toolbar's Layout to begin using + * the right-justified button container. + *
    
    +new Ext.Panel({
    +    tbar : [
    +        'Item 1',
    +        {xtype: 'tbfill'}, // or '->'
    +        'Item 2'
    +    ]
    +});
    +
    + * @constructor + * Creates a new Fill + * @xtype tbfill + */ +T.Fill = Ext.extend(T.Item, { + // private + render : Ext.emptyFn, + isFill : true +}); +Ext.reg('tbfill', T.Fill); + +/** + * @class Ext.Toolbar.TextItem + * @extends Ext.Toolbar.Item + * A simple class that renders text directly into a toolbar + * (with css class:'xtb-text'). Example usage: + *
    
    +new Ext.Panel({
    +    tbar : [
    +        {xtype: 'tbtext', text: 'Item 1'} // or simply 'Item 1'
    +    ]
    +});
    +
    + * @constructor + * Creates a new TextItem + * @param {String/Object} text A text string, or a config object containing a text property + * @xtype tbtext + */ +T.TextItem = Ext.extend(T.Item, { + /** + * @cfg {String} text The text to be used as innerHTML (html tags are accepted) + */ + + constructor: function(config){ + T.TextItem.superclass.constructor.call(this, Ext.isString(config) ? {text: config} : config); + }, + + // private + onRender : function(ct, position) { + this.autoEl = {cls: 'xtb-text', html: this.text || ''}; + T.TextItem.superclass.onRender.call(this, ct, position); + }, + + /** + * Updates this item's text, setting the text to be used as innerHTML. + * @param {String} t The text to display (html accepted). + */ + setText : function(t) { + if(this.rendered){ + this.el.update(t); + }else{ + this.text = t; + } + } +}); +Ext.reg('tbtext', T.TextItem); + +// backwards compat +T.Button = Ext.extend(Ext.Button, {}); +T.SplitButton = Ext.extend(Ext.SplitButton, {}); +Ext.reg('tbbutton', T.Button); +Ext.reg('tbsplit', T.SplitButton); + +})(); /** * @class Ext.ButtonGroup * @extends Ext.Panel @@ -43500,6 +45641,9 @@ var p = new Ext.Panel({ }] }); *
    + * @constructor + * Create a new ButtonGroup. + * @param {Object} config The config object * @xtype buttongroup */ Ext.ButtonGroup = Ext.extend(Ext.Panel, { @@ -43772,6 +45916,7 @@ Ext.PagingToolbar = Ext.extend(Ext.Toolbar, { allowNegative: false, enableKeyEvents: true, selectOnFocus: true, + submitValue: false, listeners: { scope: this, keydown: this.onPagingKeyDown, @@ -43972,12 +46117,6 @@ Ext.PagingToolbar = Ext.extend(Ext.Toolbar, { return this.paramNames || this.store.paramNames; }, - // private - getParams : function(){ - //retain backwards compat, allow params on the toolbar itself, if they exist. - return this.paramNames || this.store.paramNames; - }, - // private beforeLoad : function(){ if(this.rendered && this.refresh){ @@ -44121,7 +46260,7 @@ Ext.History = (function () { } function updateIFrame (token) { - var html = ['
    ',token,'
    '].join(''); + var html = ['
    ',Ext.util.Format.htmlEncode(token),'
    '].join(''); try { var doc = iframe.contentWindow.document; doc.open(); @@ -44205,14 +46344,14 @@ Ext.History = (function () { * @property */ iframeId: 'x-history-frame', - + events:{}, /** * Initialize the global History instance. * @param {Boolean} onReady (optional) A callback function that will be called once the history * component is fully initialized. - * @param {Object} scope (optional) The callback scope + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to the browser window. */ init: function (onReady, scope) { if(ready) { @@ -44229,7 +46368,20 @@ Ext.History = (function () { if (Ext.isIE) { iframe = Ext.getDom(Ext.History.iframeId); } - this.addEvents('ready', 'change'); + this.addEvents( + /** + * @event ready + * Fires when the Ext.History singleton has been initialized and is ready for use. + * @param {Ext.History} The Ext.History singleton. + */ + 'ready', + /** + * @event change + * Fires when navigation back or forwards within the local page's history occurs. + * @param {String} token An identifier associated with the page state at that point in its history. + */ + 'change' + ); if(onReady){ this.on('ready', onReady, scope, {single:true}); } @@ -44378,12 +46530,13 @@ tip.showAt([50,100]); }, // protected - doAutoWidth : function(){ + doAutoWidth : function(adjust){ + adjust = adjust || 0; var bw = this.body.getTextWidth(); if(this.title){ bw = Math.max(bw, this.header.child('span').getTextWidth(this.title)); } - bw += this.getFrameWidth() + (this.closable ? 20 : 0) + this.body.getPadding("lr"); + bw += this.getFrameWidth() + (this.closable ? 20 : 0) + this.body.getPadding("lr") + adjust; this.setWidth(bw.constrain(this.minWidth, this.maxWidth)); // IE7 repaint bug on initial show @@ -44626,18 +46779,17 @@ myGrid.on('render', function(grid) { } if(this.anchor){ this.targetCounter++; - var offsets = this.getOffsets(); - var xy = (this.anchorToTarget && !this.trackMouse) ? - this.el.getAlignToXY(this.anchorTarget, this.getAnchorAlign()) : - this.targetXY; - - var dw = Ext.lib.Dom.getViewWidth()-5; - var dh = Ext.lib.Dom.getViewHeight()-5; - var scrollX = (document.documentElement.scrollLeft || document.body.scrollLeft || 0)+5; - var scrollY = (document.documentElement.scrollTop || document.body.scrollTop || 0)+5; - - var axy = [xy[0] + offsets[0], xy[1] + offsets[1]]; - var sz = this.getSize(); + var offsets = this.getOffsets(), + xy = (this.anchorToTarget && !this.trackMouse) ? this.el.getAlignToXY(this.anchorTarget, this.getAnchorAlign()) : this.targetXY, + dw = Ext.lib.Dom.getViewWidth() - 5, + dh = Ext.lib.Dom.getViewHeight() - 5, + de = document.documentElement, + bd = document.body, + scrollX = (de.scrollLeft || bd.scrollLeft || 0) + 5, + scrollY = (de.scrollTop || bd.scrollTop || 0) + 5, + axy = [xy[0] + offsets[0], xy[1] + offsets[1]], + sz = this.getSize(); + this.anchorEl.removeClass(this.anchorCls); if(this.targetCounter < 2){ @@ -44726,7 +46878,8 @@ myGrid.on('render', function(grid) { // private getOffsets : function(){ - var offsets, ap = this.getAnchorPosition().charAt(0); + var offsets, + ap = this.getAnchorPosition().charAt(0); if(this.anchorToTarget && !this.trackMouse){ switch(ap){ case 't': @@ -44946,6 +47099,16 @@ myGrid.on('render', function(grid) { } return {x : x, y: y}; }, + + beforeDestroy : function(){ + this.clearTimers(); + Ext.destroy(this.anchorEl); + delete this.anchorEl; + delete this.target; + delete this.anchorTarget; + delete this.triggerElement; + Ext.ToolTip.superclass.beforeDestroy.call(this); + }, // private onDestroy : function(){ @@ -45046,13 +47209,12 @@ Ext.QuickTip = Ext.extend(Ext.ToolTip, { this.clearTimer('show'); } }, - - // private + getTipCfg: function(e) { var t = e.getTarget(), ttp, cfg; - if(this.interceptTitles && t.title){ + if(this.interceptTitles && t.title && Ext.isString(t.title)){ ttp = t.title; t.qtip = ttp; t.removeAttribute("title"); @@ -45435,18 +47597,19 @@ new Ext.Viewport({ */ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { rootVisible : true, - animate: Ext.enableFx, + animate : Ext.enableFx, lines : true, enableDD : false, hlDrop : Ext.enableFx, - pathSeparator: "/", - + pathSeparator : '/', + /** * @cfg {Array} bubbleEvents *

    An array of events that, when fired, should be bubbled to any parent container. - * Defaults to ['add', 'remove']. + * See {@link Ext.util.Observable#enableBubble}. + * Defaults to []. */ - bubbleEvents: [], + bubbleEvents : [], initComponent : function(){ Ext.tree.TreePanel.superclass.initComponent.call(this); @@ -45462,7 +47625,7 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { dataUrl: this.dataUrl, requestMethod: this.requestMethod }); - }else if(typeof l == 'object' && !l.load){ + }else if(Ext.isObject(l) && !l.load){ l = new Ext.tree.TreeLoader(l); } this.loader = l; @@ -45491,7 +47654,7 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { * @param {Node} node The newly appended node * @param {Number} index The index of the newly appended node */ - "append", + 'append', /** * @event remove * Fires when a child node is removed from a node in this tree. @@ -45499,7 +47662,7 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { * @param {Node} parent The parent node * @param {Node} node The child node removed */ - "remove", + 'remove', /** * @event movenode * Fires when a node is moved to a new location in the tree @@ -45509,7 +47672,7 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { * @param {Node} newParent The new parent of this node * @param {Number} index The index it was moved to */ - "movenode", + 'movenode', /** * @event insert * Fires when a new child node is inserted in a node in this tree. @@ -45518,7 +47681,7 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { * @param {Node} node The child node inserted * @param {Node} refNode The child node the node was inserted before */ - "insert", + 'insert', /** * @event beforeappend * Fires before a new child is appended to a node in this tree, return false to cancel the append. @@ -45526,7 +47689,7 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { * @param {Node} parent The parent node * @param {Node} node The child node to be appended */ - "beforeappend", + 'beforeappend', /** * @event beforeremove * Fires before a child is removed from a node in this tree, return false to cancel the remove. @@ -45534,7 +47697,7 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { * @param {Node} parent The parent node * @param {Node} node The child node to be removed */ - "beforeremove", + 'beforeremove', /** * @event beforemovenode * Fires before a node is moved to a new location in the tree. Return false to cancel the move. @@ -45544,7 +47707,7 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { * @param {Node} newParent The new parent the node is moving to * @param {Number} index The index it is being moved to */ - "beforemovenode", + 'beforemovenode', /** * @event beforeinsert * Fires before a new child is inserted in a node in this tree, return false to cancel the insert. @@ -45553,20 +47716,20 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { * @param {Node} node The child node to be inserted * @param {Node} refNode The child node the node is being inserted before */ - "beforeinsert", + 'beforeinsert', /** * @event beforeload * Fires before a node is loaded, return false to cancel * @param {Node} node The node being loaded */ - "beforeload", + 'beforeload', /** * @event load * Fires when a node is loaded * @param {Node} node The node that was loaded */ - "load", + 'load', /** * @event textchange * Fires when the text for a node is changed @@ -45574,7 +47737,7 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { * @param {String} text The new text * @param {String} oldText The old text */ - "textchange", + 'textchange', /** * @event beforeexpandnode * Fires before a node is expanded, return false to cancel. @@ -45582,7 +47745,7 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { * @param {Boolean} deep * @param {Boolean} anim */ - "beforeexpandnode", + 'beforeexpandnode', /** * @event beforecollapsenode * Fires before a node is collapsed, return false to cancel. @@ -45590,61 +47753,75 @@ Ext.tree.TreePanel = Ext.extend(Ext.Panel, { * @param {Boolean} deep * @param {Boolean} anim */ - "beforecollapsenode", + 'beforecollapsenode', /** * @event expandnode * Fires when a node is expanded * @param {Node} node The node */ - "expandnode", + 'expandnode', /** * @event disabledchange * Fires when the disabled status of a node changes * @param {Node} node The node * @param {Boolean} disabled */ - "disabledchange", + 'disabledchange', /** * @event collapsenode * Fires when a node is collapsed * @param {Node} node The node */ - "collapsenode", + 'collapsenode', /** * @event beforeclick * Fires before click processing on a node. Return false to cancel the default action. * @param {Node} node The node * @param {Ext.EventObject} e The event object */ - "beforeclick", + 'beforeclick', /** * @event click * Fires when a node is clicked * @param {Node} node The node * @param {Ext.EventObject} e The event object */ - "click", + 'click', + /** + * @event containerclick + * Fires when the tree container is clicked + * @param {Tree} this + * @param {Ext.EventObject} e The event object + */ + 'containerclick', /** * @event checkchange * Fires when a node with a checkbox's checked property changes * @param {Node} this This node * @param {Boolean} checked */ - "checkchange", + 'checkchange', /** * @event beforedblclick * Fires before double click processing on a node. Return false to cancel the default action. * @param {Node} node The node * @param {Ext.EventObject} e The event object */ - "beforedblclick", + 'beforedblclick', /** * @event dblclick * Fires when a node is double clicked * @param {Node} node The node * @param {Ext.EventObject} e The event object */ - "dblclick", + 'dblclick', + /** + * @event containerdblclick + * Fires when the tree container is double clicked + * @param {Tree} this + * @param {Ext.EventObject} e The event object + */ + 'containerdblclick', /** * @event contextmenu * Fires when a node is right clicked. To display a context menu in response to this @@ -45692,13 +47869,20 @@ new Ext.tree.TreePanel({ * @param {Node} node The node * @param {Ext.EventObject} e The event object */ - "contextmenu", + 'contextmenu', + /** + * @event containercontextmenu + * Fires when the tree container is right clicked + * @param {Tree} this + * @param {Ext.EventObject} e The event object + */ + 'containercontextmenu', /** * @event beforechildrenrendered * Fires right before the child nodes for a node are rendered * @param {Node} node The node */ - "beforechildrenrendered", + 'beforechildrenrendered', /** * @event startdrag * Fires when a node starts being dragged @@ -45706,7 +47890,7 @@ new Ext.tree.TreePanel({ * @param {Ext.tree.TreeNode} node * @param {event} e The raw browser event */ - "startdrag", + 'startdrag', /** * @event enddrag * Fires when a drag operation is complete @@ -45714,7 +47898,7 @@ new Ext.tree.TreePanel({ * @param {Ext.tree.TreeNode} node * @param {event} e The raw browser event */ - "enddrag", + 'enddrag', /** * @event dragdrop * Fires when a dragged node is dropped on a valid DD target @@ -45723,7 +47907,7 @@ new Ext.tree.TreePanel({ * @param {DD} dd The dd it was dropped on * @param {event} e The raw browser event */ - "dragdrop", + 'dragdrop', /** * @event beforenodedrop * Fires when a DD object is dropped on a node in this tree for preprocessing. Return false to cancel the drop. The dropEvent @@ -45739,11 +47923,11 @@ new Ext.tree.TreePanel({ * to be inserted by setting them on this object. *

  • cancel - Set this to true to cancel the drop.
  • *
  • dropStatus - If the default drop action is cancelled but the drop is valid, setting this to true - * will prevent the animated "repair" from appearing.
  • + * will prevent the animated 'repair' from appearing. * * @param {Object} dropEvent */ - "beforenodedrop", + 'beforenodedrop', /** * @event nodedrop * Fires after a DD object is dropped on a node in this tree. The dropEvent @@ -45759,7 +47943,7 @@ new Ext.tree.TreePanel({ * * @param {Object} dropEvent */ - "nodedrop", + 'nodedrop', /** * @event nodedragover * Fires when a tree node is being targeted for a drag drop, return false to signal drop not allowed. The dragOverEvent @@ -45776,10 +47960,10 @@ new Ext.tree.TreePanel({ * * @param {Object} dragOverEvent */ - "nodedragover" + 'nodedragover' ); if(this.singleExpand){ - this.on("beforeexpandnode", this.restrictExpand, this); + this.on('beforeexpandnode', this.restrictExpand, this); } }, @@ -45820,12 +48004,24 @@ new Ext.tree.TreePanel({ var uiP = node.attributes.uiProvider; node.ui = uiP ? new uiP(node) : new Ext.tree.RootTreeNodeUI(node); } - if (this.innerCt) { - this.innerCt.update(''); - this.afterRender(); + if(this.innerCt){ + this.clearInnerCt(); + this.renderRoot(); } return node; }, + + clearInnerCt : function(){ + this.innerCt.update(''); + }, + + // private + renderRoot : function(){ + this.root.render(); + if(!this.rootVisible){ + this.root.renderChildren(); + } + }, /** * Gets a node in this tree by its id @@ -45848,7 +48044,7 @@ new Ext.tree.TreePanel({ // private toString : function(){ - return "[Tree"+(this.id?" "+this.id:"")+"]"; + return '[Tree'+(this.id?' '+this.id:'')+']'; }, // private @@ -45863,7 +48059,7 @@ new Ext.tree.TreePanel({ }, /** - * Retrieve an array of checked nodes, or an array of a specific attribute of checked nodes (e.g. "id") + * Retrieve an array of checked nodes, or an array of a specific attribute of checked nodes (e.g. 'id') * @param {String} attribute (optional) Defaults to null (return the actual nodes) * @param {TreeNode} startNode (optional) The node to start from, defaults to the root * @return {Array} @@ -45880,14 +48076,6 @@ new Ext.tree.TreePanel({ return r; }, - /** - * Returns the container element for this TreePanel. - * @return {Element} The container element for this TreePanel. - */ - getEl : function(){ - return this.el; - }, - /** * Returns the default {@link Ext.tree.TreeLoader} for this TreePanel. * @return {Ext.tree.TreeLoader} The TreeLoader for this TreePanel. @@ -45929,7 +48117,7 @@ new Ext.tree.TreePanel({ * (bSuccess, oLastNode) where bSuccess is if the expand was successful and oLastNode is the last node that was expanded. */ expandPath : function(path, attr, callback){ - attr = attr || "id"; + attr = attr || 'id'; var keys = path.split(this.pathSeparator); var curNode = this.root; if(curNode.attributes[attr] != keys[1]){ // invalid root @@ -45967,7 +48155,7 @@ new Ext.tree.TreePanel({ * (bSuccess, oSelNode) where bSuccess is if the selection was successful and oSelNode is the selected node. */ selectPath : function(path, attr, callback){ - attr = attr || "id"; + attr = attr || 'id'; var keys = path.split(this.pathSeparator), v = keys.pop(); if(keys.length > 1){ @@ -46009,9 +48197,9 @@ new Ext.tree.TreePanel({ onRender : function(ct, position){ Ext.tree.TreePanel.superclass.onRender.call(this, ct, position); this.el.addClass('x-tree'); - this.innerCt = this.body.createChild({tag:"ul", - cls:"x-tree-root-ct " + - (this.useArrows ? 'x-tree-arrows' : this.lines ? "x-tree-lines" : "x-tree-no-lines")}); + this.innerCt = this.body.createChild({tag:'ul', + cls:'x-tree-root-ct ' + + (this.useArrows ? 'x-tree-arrows' : this.lines ? 'x-tree-lines' : 'x-tree-no-lines')}); }, // private @@ -46028,7 +48216,7 @@ new Ext.tree.TreePanel({ * @type Ext.tree.TreeDropZone */ this.dropZone = new Ext.tree.TreeDropZone(this, this.dropConfig || { - ddGroup: this.ddGroup || "TreeDD", appendOnly: this.ddAppendOnly === true + ddGroup: this.ddGroup || 'TreeDD', appendOnly: this.ddAppendOnly === true }); } if((this.enableDD || this.enableDrag) && !this.dragZone){ @@ -46038,7 +48226,7 @@ new Ext.tree.TreePanel({ * @type Ext.tree.TreeDragZone */ this.dragZone = new Ext.tree.TreeDragZone(this, this.dragConfig || { - ddGroup: this.ddGroup || "TreeDD", + ddGroup: this.ddGroup || 'TreeDD', scroll: this.ddScroll }); } @@ -46048,26 +48236,17 @@ new Ext.tree.TreePanel({ // private afterRender : function(){ Ext.tree.TreePanel.superclass.afterRender.call(this); - this.root.render(); - if(!this.rootVisible){ - this.root.renderChildren(); - } + this.renderRoot(); }, - onDestroy : function(){ + beforeDestroy : function(){ if(this.rendered){ - this.body.removeAllListeners(); Ext.dd.ScrollManager.unregister(this.body); - if(this.dropZone){ - this.dropZone.unreg(); - } - if(this.dragZone){ - this.dragZone.unreg(); - } + Ext.destroy(this.dropZone, this.dragZone); } - this.root.destroy(); - this.nodeHash = null; - Ext.tree.TreePanel.superclass.onDestroy.call(this); + Ext.destroy(this.root, this.loader); + this.nodeHash = this.root = this.loader = null; + Ext.tree.TreePanel.superclass.beforeDestroy.call(this); } /** @@ -46209,6 +48388,15 @@ new Ext.tree.TreePanel({ /** * @cfg {String} contentEl @hide */ + /** + * @cfg {Mixed} data @hide + */ + /** + * @cfg {Mixed} tpl @hide + */ + /** + * @cfg {String} tplWriteMode @hide + */ /** * @cfg {String} disabledClass @hide */ @@ -46254,7 +48442,7 @@ Ext.reg('treepanel', Ext.tree.TreePanel);Ext.tree.TreeEventModel = function(tree Ext.tree.TreeEventModel.prototype = { initEvents : function(){ var t = this.tree; - + if(t.trackMouseOver !== false){ t.mon(t.innerCt, { scope: this, @@ -46327,42 +48515,55 @@ Ext.tree.TreeEventModel.prototype = { }, trackExit : function(e){ - if(this.lastOverNode && !e.within(this.lastOverNode.ui.getEl())){ - this.onNodeOut(e, this.lastOverNode); + if(this.lastOverNode){ + if(this.lastOverNode.ui && !e.within(this.lastOverNode.ui.getEl())){ + this.onNodeOut(e, this.lastOverNode); + } delete this.lastOverNode; Ext.getBody().un('mouseover', this.trackExit, this); this.trackingDoc = false; } + }, delegateClick : function(e, t){ - if(!this.beforeEvent(e)){ - return; - } - - if(e.getTarget('input[type=checkbox]', 1)){ - this.onCheckboxClick(e, this.getNode(e)); - } - else if(e.getTarget('.x-tree-ec-icon', 1)){ - this.onIconClick(e, this.getNode(e)); - } - else if(this.getNodeTarget(e)){ - this.onNodeClick(e, this.getNode(e)); + if(this.beforeEvent(e)){ + if(e.getTarget('input[type=checkbox]', 1)){ + this.onCheckboxClick(e, this.getNode(e)); + }else if(e.getTarget('.x-tree-ec-icon', 1)){ + this.onIconClick(e, this.getNode(e)); + }else if(this.getNodeTarget(e)){ + this.onNodeClick(e, this.getNode(e)); + }else{ + this.onContainerEvent(e, 'click'); + } } }, delegateDblClick : function(e, t){ - if(this.beforeEvent(e) && this.getNodeTarget(e)){ - this.onNodeDblClick(e, this.getNode(e)); + if(this.beforeEvent(e)){ + if(this.getNodeTarget(e)){ + this.onNodeDblClick(e, this.getNode(e)); + }else{ + this.onContainerEvent(e, 'dblclick'); + } } }, delegateContextMenu : function(e, t){ - if(this.beforeEvent(e) && this.getNodeTarget(e)){ - this.onNodeContextMenu(e, this.getNode(e)); + if(this.beforeEvent(e)){ + if(this.getNodeTarget(e)){ + this.onNodeContextMenu(e, this.getNode(e)); + }else{ + this.onContainerEvent(e, 'contextmenu'); + } } }, + onContainerEvent: function(e, type){ + this.tree.fireEvent('container' + type, this.tree, e); + }, + onNodeClick : function(e, node){ node.ui.onClick(e); }, @@ -46401,7 +48602,8 @@ Ext.tree.TreeEventModel.prototype = { }, beforeEvent : function(e){ - if(this.disabled){ + var node = this.getNode(e); + if(this.disabled || !node || !node.ui){ e.stopEvent(); return false; } @@ -46430,7 +48632,7 @@ Ext.tree.DefaultSelectionModel = function(config){ * @param {DefaultSelectionModel} this * @param {TreeNode} node the new selection */ - "selectionchange", + 'selectionchange', /** * @event beforeselect @@ -46439,7 +48641,7 @@ Ext.tree.DefaultSelectionModel = function(config){ * @param {TreeNode} node the new selection * @param {TreeNode} node the old selection */ - "beforeselect" + 'beforeselect' ); Ext.apply(this, config); @@ -46450,7 +48652,7 @@ Ext.extend(Ext.tree.DefaultSelectionModel, Ext.util.Observable, { init : function(tree){ this.tree = tree; tree.mon(tree.getTreeEl(), 'keydown', this.onKeyDown, this); - tree.on("click", this.onNodeClick, this); + tree.on('click', this.onNodeClick, this); }, onNodeClick : function(node, e){ @@ -46471,12 +48673,12 @@ Ext.extend(Ext.tree.DefaultSelectionModel, Ext.util.Observable, { if(node == last){ node.ui.onSelectedChange(true); }else if(this.fireEvent('beforeselect', this, node, last) !== false){ - if(last){ + if(last && last.ui){ last.ui.onSelectedChange(false); } this.selNode = node; node.ui.onSelectedChange(true); - this.fireEvent("selectionchange", this, node, last); + this.fireEvent('selectionchange', this, node, last); } return node; }, @@ -46484,22 +48686,26 @@ Ext.extend(Ext.tree.DefaultSelectionModel, Ext.util.Observable, { /** * Deselect a node. * @param {TreeNode} node The node to unselect + * @param {Boolean} silent True to stop the selectionchange event from firing. */ - unselect : function(node){ + unselect : function(node, silent){ if(this.selNode == node){ - this.clearSelections(); + this.clearSelections(silent); } }, /** * Clear all selections + * @param {Boolean} silent True to stop the selectionchange event from firing. */ - clearSelections : function(){ + clearSelections : function(silent){ var n = this.selNode; if(n){ n.ui.onSelectedChange(false); this.selNode = null; - this.fireEvent("selectionchange", this, null); + if(silent !== true){ + this.fireEvent('selectionchange', this, null); + } } return n; }, @@ -46627,7 +48833,7 @@ Ext.tree.MultiSelectionModel = function(config){ * @param {MultiSelectionModel} this * @param {Array} nodes Array of the selected nodes */ - "selectionchange" + 'selectionchange' ); Ext.apply(this, config); Ext.tree.MultiSelectionModel.superclass.constructor.call(this); @@ -46637,7 +48843,7 @@ Ext.extend(Ext.tree.MultiSelectionModel, Ext.util.Observable, { init : function(tree){ this.tree = tree; tree.mon(tree.getTreeEl(), 'keydown', this.onKeyDown, this); - tree.on("click", this.onNodeClick, this); + tree.on('click', this.onNodeClick, this); }, onNodeClick : function(node, e){ @@ -46667,7 +48873,7 @@ Ext.extend(Ext.tree.MultiSelectionModel, Ext.util.Observable, { this.selMap[node.id] = node; this.lastSelNode = node; node.ui.onSelectedChange(true); - this.fireEvent("selectionchange", this, this.selNodes); + this.fireEvent('selectionchange', this, this.selNodes); return node; }, @@ -46684,7 +48890,7 @@ Ext.extend(Ext.tree.MultiSelectionModel, Ext.util.Observable, { this.selNodes.splice(index, 1); } delete this.selMap[node.id]; - this.fireEvent("selectionchange", this, this.selNodes); + this.fireEvent('selectionchange', this, this.selNodes); } }, @@ -46700,7 +48906,7 @@ Ext.extend(Ext.tree.MultiSelectionModel, Ext.util.Observable, { this.selNodes = []; this.selMap = {}; if(suppressEvent !== true){ - this.fireEvent("selectionchange", this, this.selNodes); + this.fireEvent('selectionchange', this, this.selNodes); } } }, @@ -47140,9 +49346,10 @@ Ext.extend(Ext.data.Node, Ext.util.Observable, { /** * Removes a child node from this node. * @param {Node} node The node to remove + * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. * @return {Node} The removed node */ - removeChild : function(node){ + removeChild : function(node, destroy){ var index = this.childNodes.indexOf(node); if(index == -1){ return false; @@ -47170,14 +49377,35 @@ Ext.extend(Ext.data.Node, Ext.util.Observable, { this.setLastChild(node.previousSibling); } - node.setOwnerTree(null); - // clear any references from the node - node.parentNode = null; - node.previousSibling = null; - node.nextSibling = null; + node.clear(); this.fireEvent("remove", this.ownerTree, this, node); + if(destroy){ + node.destroy(); + } return node; }, + + // private + clear : function(destroy){ + // clear any references from the node + this.setOwnerTree(null, destroy); + this.parentNode = this.previousSibling = this.nextSibling = null + if(destroy){ + this.firstChild = this.lastChild = null; + } + }, + + /** + * Destroys the node. + */ + destroy : function(){ + this.purgeListeners(); + this.clear(true); + Ext.each(this.childNodes, function(n){ + n.destroy(); + }); + this.childNodes = null; + }, /** * Inserts the first node before the second node in this nodes childNodes collection. @@ -47237,10 +49465,25 @@ Ext.extend(Ext.data.Node, Ext.util.Observable, { /** * Removes this node from its parent + * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. * @return {Node} this */ - remove : function(){ - this.parentNode.removeChild(this); + remove : function(destroy){ + this.parentNode.removeChild(this, destroy); + return this; + }, + + /** + * Removes all child nodes from this node. + * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. + * @return {Node} this + */ + removeAll : function(destroy){ + var cn = this.childNodes, + n; + while((n = cn[0])){ + this.removeChild(n, destroy); + } return this; }, @@ -47309,16 +49552,18 @@ Ext.extend(Ext.data.Node, Ext.util.Observable, { }, // private - setOwnerTree : function(tree){ + setOwnerTree : function(tree, destroy){ // if it is a move, we need to update everyone if(tree != this.ownerTree){ if(this.ownerTree){ this.ownerTree.unregisterNode(this); } this.ownerTree = tree; - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - cs[i].setOwnerTree(tree); + // If we're destroying, we don't need to recurse since it will be called on each child node + if(destroy !== true){ + Ext.each(this.childNodes, function(n){ + n.setOwnerTree(tree); + }); } if(tree){ tree.registerNode(this); @@ -47365,13 +49610,12 @@ Ext.extend(Ext.data.Node, Ext.util.Observable, { }, /** - * Bubbles up the tree from this node, calling the specified function with each node. The scope (this) of - * function call will be the scope provided or the current node. The arguments to the function + * Bubbles up the tree from this node, calling the specified function with each node. The arguments to the function * will be the args provided or the current node. If the function returns false at any point, * the bubble is stopped. * @param {Function} fn The function to call - * @param {Object} scope (optional) The scope of the function (defaults to current node) - * @param {Array} args (optional) The args to call the function with (default to passing the current node) + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node. + * @param {Array} args (optional) The args to call the function with (default to passing the current Node) */ bubble : function(fn, scope, args){ var p = this; @@ -47384,13 +49628,12 @@ Ext.extend(Ext.data.Node, Ext.util.Observable, { }, /** - * Cascades down the tree from this node, calling the specified function with each node. The scope (this) of - * function call will be the scope provided or the current node. The arguments to the function + * Cascades down the tree from this node, calling the specified function with each node. The arguments to the function * will be the args provided or the current node. If the function returns false at any point, * the cascade is stopped on that branch. * @param {Function} fn The function to call - * @param {Object} scope (optional) The scope of the function (defaults to current node) - * @param {Array} args (optional) The args to call the function with (default to passing the current node) + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node. + * @param {Array} args (optional) The args to call the function with (default to passing the current Node) */ cascade : function(fn, scope, args){ if(fn.apply(scope || this, args || [this]) !== false){ @@ -47402,13 +49645,12 @@ Ext.extend(Ext.data.Node, Ext.util.Observable, { }, /** - * Interates the child nodes of this node, calling the specified function with each node. The scope (this) of - * function call will be the scope provided or the current node. The arguments to the function + * Interates the child nodes of this node, calling the specified function with each node. The arguments to the function * will be the args provided or the current node. If the function returns false at any point, * the iteration stops. * @param {Function} fn The function to call - * @param {Object} scope (optional) The scope of the function (defaults to current node) - * @param {Array} args (optional) The args to call the function with (default to passing the current node) + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node in the iteration. + * @param {Array} args (optional) The args to call the function with (default to passing the current Node) */ eachChild : function(fn, scope, args){ var cs = this.childNodes; @@ -47436,10 +49678,9 @@ Ext.extend(Ext.data.Node, Ext.util.Observable, { }, /** - * Finds the first child by a custom function. The child matches if the function passed - * returns true. - * @param {Function} fn - * @param {Object} scope (optional) + * Finds the first child by a custom function. The child matches if the function passed returns true. + * @param {Function} fn A function which must return true if the passed Node is the required Node. + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the Node being tested. * @return {Node} The found child or null if none was found */ findChildBy : function(fn, scope){ @@ -47453,9 +49694,9 @@ Ext.extend(Ext.data.Node, Ext.util.Observable, { }, /** - * Sorts this nodes children using the supplied sort function - * @param {Function} fn - * @param {Object} scope (optional) + * Sorts this nodes children using the supplied sort function. + * @param {Function} fn A function which, when passed two Nodes, returns -1, 0 or 1 depending upon required sort order. + * @param {Object} scope (optional)The scope (this reference) in which the function is executed. Defaults to the browser window. */ sort : function(fn, scope){ var cs = this.childNodes; @@ -47505,649 +49746,668 @@ Ext.extend(Ext.data.Node, Ext.util.Observable, { toString : function(){ return "[Node"+(this.id?" "+this.id:"")+"]"; } -});/** - * @class Ext.tree.TreeNode - * @extends Ext.data.Node - * @cfg {String} text The text for this node - * @cfg {Boolean} expanded true to start the node expanded - * @cfg {Boolean} allowDrag False to make this node undraggable if {@link #draggable} = true (defaults to true) - * @cfg {Boolean} allowDrop False if this node cannot have child nodes dropped on it (defaults to true) - * @cfg {Boolean} disabled true to start the node disabled - * @cfg {String} icon The path to an icon for the node. The preferred way to do this - * is to use the cls or iconCls attributes and add the icon via a CSS background image. - * @cfg {String} cls A css class to be added to the node - * @cfg {String} iconCls A css class to be added to the nodes icon element for applying css background images - * @cfg {String} href URL of the link used for the node (defaults to #) - * @cfg {String} hrefTarget target frame for the link - * @cfg {Boolean} hidden True to render hidden. (Defaults to false). - * @cfg {String} qtip An Ext QuickTip for the node - * @cfg {Boolean} expandable If set to true, the node will always show a plus/minus icon, even when empty - * @cfg {String} qtipCfg An Ext QuickTip config for the node (used instead of qtip) - * @cfg {Boolean} singleClickExpand True for single click expand on this node - * @cfg {Function} uiProvider A UI class to use for this node (defaults to Ext.tree.TreeNodeUI) - * @cfg {Boolean} checked True to render a checked checkbox for this node, false to render an unchecked checkbox - * (defaults to undefined with no checkbox rendered) - * @cfg {Boolean} draggable True to make this node draggable (defaults to false) - * @cfg {Boolean} isTarget False to not allow this node to act as a drop target (defaults to true) - * @cfg {Boolean} allowChildren False to not allow this node to have child nodes (defaults to true) - * @cfg {Boolean} editable False to not allow this node to be edited by an (@link Ext.tree.TreeEditor} (defaults to true) +});/** + * @class Ext.tree.TreeNode + * @extends Ext.data.Node + * @cfg {String} text The text for this node + * @cfg {Boolean} expanded true to start the node expanded + * @cfg {Boolean} allowDrag False to make this node undraggable if {@link #draggable} = true (defaults to true) + * @cfg {Boolean} allowDrop False if this node cannot have child nodes dropped on it (defaults to true) + * @cfg {Boolean} disabled true to start the node disabled + * @cfg {String} icon The path to an icon for the node. The preferred way to do this + * is to use the cls or iconCls attributes and add the icon via a CSS background image. + * @cfg {String} cls A css class to be added to the node + * @cfg {String} iconCls A css class to be added to the nodes icon element for applying css background images + * @cfg {String} href URL of the link used for the node (defaults to #) + * @cfg {String} hrefTarget target frame for the link + * @cfg {Boolean} hidden True to render hidden. (Defaults to false). + * @cfg {String} qtip An Ext QuickTip for the node + * @cfg {Boolean} expandable If set to true, the node will always show a plus/minus icon, even when empty + * @cfg {String} qtipCfg An Ext QuickTip config for the node (used instead of qtip) + * @cfg {Boolean} singleClickExpand True for single click expand on this node + * @cfg {Function} uiProvider A UI class to use for this node (defaults to Ext.tree.TreeNodeUI) + * @cfg {Boolean} checked True to render a checked checkbox for this node, false to render an unchecked checkbox + * (defaults to undefined with no checkbox rendered) + * @cfg {Boolean} draggable True to make this node draggable (defaults to false) + * @cfg {Boolean} isTarget False to not allow this node to act as a drop target (defaults to true) + * @cfg {Boolean} allowChildren False to not allow this node to have child nodes (defaults to true) + * @cfg {Boolean} editable False to not allow this node to be edited by an {@link Ext.tree.TreeEditor} (defaults to true) + * @constructor + * @param {Object/String} attributes The attributes/config for the node or just a string with the text for the node + */ +Ext.tree.TreeNode = function(attributes){ + attributes = attributes || {}; + if(Ext.isString(attributes)){ + attributes = {text: attributes}; + } + this.childrenRendered = false; + this.rendered = false; + Ext.tree.TreeNode.superclass.constructor.call(this, attributes); + this.expanded = attributes.expanded === true; + this.isTarget = attributes.isTarget !== false; + this.draggable = attributes.draggable !== false && attributes.allowDrag !== false; + this.allowChildren = attributes.allowChildren !== false && attributes.allowDrop !== false; + + /** + * Read-only. The text for this node. To change it use {@link #setText}. + * @type String + */ + this.text = attributes.text; + /** + * True if this node is disabled. + * @type Boolean + */ + this.disabled = attributes.disabled === true; + /** + * True if this node is hidden. + * @type Boolean + */ + this.hidden = attributes.hidden === true; + + this.addEvents( + /** + * @event textchange + * Fires when the text for this node is changed + * @param {Node} this This node + * @param {String} text The new text + * @param {String} oldText The old text + */ + 'textchange', + /** + * @event beforeexpand + * Fires before this node is expanded, return false to cancel. + * @param {Node} this This node + * @param {Boolean} deep + * @param {Boolean} anim + */ + 'beforeexpand', + /** + * @event beforecollapse + * Fires before this node is collapsed, return false to cancel. + * @param {Node} this This node + * @param {Boolean} deep + * @param {Boolean} anim + */ + 'beforecollapse', + /** + * @event expand + * Fires when this node is expanded + * @param {Node} this This node + */ + 'expand', + /** + * @event disabledchange + * Fires when the disabled status of this node changes + * @param {Node} this This node + * @param {Boolean} disabled + */ + 'disabledchange', + /** + * @event collapse + * Fires when this node is collapsed + * @param {Node} this This node + */ + 'collapse', + /** + * @event beforeclick + * Fires before click processing. Return false to cancel the default action. + * @param {Node} this This node + * @param {Ext.EventObject} e The event object + */ + 'beforeclick', + /** + * @event click + * Fires when this node is clicked + * @param {Node} this This node + * @param {Ext.EventObject} e The event object + */ + 'click', + /** + * @event checkchange + * Fires when a node with a checkbox's checked property changes + * @param {Node} this This node + * @param {Boolean} checked + */ + 'checkchange', + /** + * @event beforedblclick + * Fires before double click processing. Return false to cancel the default action. + * @param {Node} this This node + * @param {Ext.EventObject} e The event object + */ + 'beforedblclick', + /** + * @event dblclick + * Fires when this node is double clicked + * @param {Node} this This node + * @param {Ext.EventObject} e The event object + */ + 'dblclick', + /** + * @event contextmenu + * Fires when this node is right clicked + * @param {Node} this This node + * @param {Ext.EventObject} e The event object + */ + 'contextmenu', + /** + * @event beforechildrenrendered + * Fires right before the child nodes for this node are rendered + * @param {Node} this This node + */ + 'beforechildrenrendered' + ); + + var uiClass = this.attributes.uiProvider || this.defaultUI || Ext.tree.TreeNodeUI; + + /** + * Read-only. The UI for this node + * @type TreeNodeUI + */ + this.ui = new uiClass(this); +}; +Ext.extend(Ext.tree.TreeNode, Ext.data.Node, { + preventHScroll : true, + /** + * Returns true if this node is expanded + * @return {Boolean} + */ + isExpanded : function(){ + return this.expanded; + }, + +/** + * Returns the UI object for this node. + * @return {TreeNodeUI} The object which is providing the user interface for this tree + * node. Unless otherwise specified in the {@link #uiProvider}, this will be an instance + * of {@link Ext.tree.TreeNodeUI} + */ + getUI : function(){ + return this.ui; + }, + + getLoader : function(){ + var owner; + return this.loader || ((owner = this.getOwnerTree()) && owner.loader ? owner.loader : (this.loader = new Ext.tree.TreeLoader())); + }, + + // private override + setFirstChild : function(node){ + var of = this.firstChild; + Ext.tree.TreeNode.superclass.setFirstChild.call(this, node); + if(this.childrenRendered && of && node != of){ + of.renderIndent(true, true); + } + if(this.rendered){ + this.renderIndent(true, true); + } + }, + + // private override + setLastChild : function(node){ + var ol = this.lastChild; + Ext.tree.TreeNode.superclass.setLastChild.call(this, node); + if(this.childrenRendered && ol && node != ol){ + ol.renderIndent(true, true); + } + if(this.rendered){ + this.renderIndent(true, true); + } + }, + + // these methods are overridden to provide lazy rendering support + // private override + appendChild : function(n){ + var node, exists; + if(!n.render && !Ext.isArray(n)){ + n = this.getLoader().createNode(n); + }else{ + exists = !n.parentNode; + } + node = Ext.tree.TreeNode.superclass.appendChild.call(this, n); + if(node){ + this.afterAdd(node, exists); + } + this.ui.updateExpandIcon(); + return node; + }, + + // private override + removeChild : function(node, destroy){ + this.ownerTree.getSelectionModel().unselect(node); + Ext.tree.TreeNode.superclass.removeChild.apply(this, arguments); + // if it's been rendered remove dom node + if(node.ui.rendered){ + node.ui.remove(); + } + if(this.childNodes.length < 1){ + this.collapse(false, false); + }else{ + this.ui.updateExpandIcon(); + } + if(!this.firstChild && !this.isHiddenRoot()) { + this.childrenRendered = false; + } + return node; + }, + + // private override + insertBefore : function(node, refNode){ + var newNode, exists; + if(!node.render){ + node = this.getLoader().createNode(node); + } else { + exists = Ext.isObject(node.parentNode); + } + newNode = Ext.tree.TreeNode.superclass.insertBefore.call(this, node, refNode); + if(newNode && refNode){ + this.afterAdd(newNode, exists); + } + this.ui.updateExpandIcon(); + return newNode; + }, + + // private + afterAdd : function(node, exists){ + if(this.childrenRendered){ + // bulk render if the node already exists + node.render(exists); + }else if(exists){ + // make sure we update the indent + node.renderIndent(true, true); + } + }, + + /** + * Sets the text for this node + * @param {String} text + */ + setText : function(text){ + var oldText = this.text; + this.text = this.attributes.text = text; + if(this.rendered){ // event without subscribing + this.ui.onTextChange(this, text, oldText); + } + this.fireEvent('textchange', this, text, oldText); + }, + + /** + * Triggers selection of this node + */ + select : function(){ + var t = this.getOwnerTree(); + if(t){ + t.getSelectionModel().select(this); + } + }, + + /** + * Triggers deselection of this node + * @param {Boolean} silent (optional) True to stop selection change events from firing. + */ + unselect : function(silent){ + var t = this.getOwnerTree(); + if(t){ + t.getSelectionModel().unselect(this, silent); + } + }, + + /** + * Returns true if this node is selected + * @return {Boolean} + */ + isSelected : function(){ + var t = this.getOwnerTree(); + return t ? t.getSelectionModel().isSelected(this) : false; + }, + + /** + * Expand this node. + * @param {Boolean} deep (optional) True to expand all children as well + * @param {Boolean} anim (optional) false to cancel the default animation + * @param {Function} callback (optional) A callback to be called when + * expanding this node completes (does not wait for deep expand to complete). + * Called with 1 parameter, this node. + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this TreeNode. + */ + expand : function(deep, anim, callback, scope){ + if(!this.expanded){ + if(this.fireEvent('beforeexpand', this, deep, anim) === false){ + return; + } + if(!this.childrenRendered){ + this.renderChildren(); + } + this.expanded = true; + if(!this.isHiddenRoot() && (this.getOwnerTree().animate && anim !== false) || anim){ + this.ui.animExpand(function(){ + this.fireEvent('expand', this); + this.runCallback(callback, scope || this, [this]); + if(deep === true){ + this.expandChildNodes(true); + } + }.createDelegate(this)); + return; + }else{ + this.ui.expand(); + this.fireEvent('expand', this); + this.runCallback(callback, scope || this, [this]); + } + }else{ + this.runCallback(callback, scope || this, [this]); + } + if(deep === true){ + this.expandChildNodes(true); + } + }, + + runCallback : function(cb, scope, args){ + if(Ext.isFunction(cb)){ + cb.apply(scope, args); + } + }, + + isHiddenRoot : function(){ + return this.isRoot && !this.getOwnerTree().rootVisible; + }, + + /** + * Collapse this node. + * @param {Boolean} deep (optional) True to collapse all children as well + * @param {Boolean} anim (optional) false to cancel the default animation + * @param {Function} callback (optional) A callback to be called when + * expanding this node completes (does not wait for deep expand to complete). + * Called with 1 parameter, this node. + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this TreeNode. + */ + collapse : function(deep, anim, callback, scope){ + if(this.expanded && !this.isHiddenRoot()){ + if(this.fireEvent('beforecollapse', this, deep, anim) === false){ + return; + } + this.expanded = false; + if((this.getOwnerTree().animate && anim !== false) || anim){ + this.ui.animCollapse(function(){ + this.fireEvent('collapse', this); + this.runCallback(callback, scope || this, [this]); + if(deep === true){ + this.collapseChildNodes(true); + } + }.createDelegate(this)); + return; + }else{ + this.ui.collapse(); + this.fireEvent('collapse', this); + this.runCallback(callback, scope || this, [this]); + } + }else if(!this.expanded){ + this.runCallback(callback, scope || this, [this]); + } + if(deep === true){ + var cs = this.childNodes; + for(var i = 0, len = cs.length; i < len; i++) { + cs[i].collapse(true, false); + } + } + }, + + // private + delayedExpand : function(delay){ + if(!this.expandProcId){ + this.expandProcId = this.expand.defer(delay, this); + } + }, + + // private + cancelExpand : function(){ + if(this.expandProcId){ + clearTimeout(this.expandProcId); + } + this.expandProcId = false; + }, + + /** + * Toggles expanded/collapsed state of the node + */ + toggle : function(){ + if(this.expanded){ + this.collapse(); + }else{ + this.expand(); + } + }, + + /** + * Ensures all parent nodes are expanded, and if necessary, scrolls + * the node into view. + * @param {Function} callback (optional) A function to call when the node has been made visible. + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this TreeNode. + */ + ensureVisible : function(callback, scope){ + var tree = this.getOwnerTree(); + tree.expandPath(this.parentNode ? this.parentNode.getPath() : this.getPath(), false, function(){ + var node = tree.getNodeById(this.id); // Somehow if we don't do this, we lose changes that happened to node in the meantime + tree.getTreeEl().scrollChildIntoView(node.ui.anchor); + this.runCallback(callback, scope || this, [this]); + }.createDelegate(this)); + }, + + /** + * Expand all child nodes + * @param {Boolean} deep (optional) true if the child nodes should also expand their child nodes + */ + expandChildNodes : function(deep){ + var cs = this.childNodes; + for(var i = 0, len = cs.length; i < len; i++) { + cs[i].expand(deep); + } + }, + + /** + * Collapse all child nodes + * @param {Boolean} deep (optional) true if the child nodes should also collapse their child nodes + */ + collapseChildNodes : function(deep){ + var cs = this.childNodes; + for(var i = 0, len = cs.length; i < len; i++) { + cs[i].collapse(deep); + } + }, + + /** + * Disables this node + */ + disable : function(){ + this.disabled = true; + this.unselect(); + if(this.rendered && this.ui.onDisableChange){ // event without subscribing + this.ui.onDisableChange(this, true); + } + this.fireEvent('disabledchange', this, true); + }, + + /** + * Enables this node + */ + enable : function(){ + this.disabled = false; + if(this.rendered && this.ui.onDisableChange){ // event without subscribing + this.ui.onDisableChange(this, false); + } + this.fireEvent('disabledchange', this, false); + }, + + // private + renderChildren : function(suppressEvent){ + if(suppressEvent !== false){ + this.fireEvent('beforechildrenrendered', this); + } + var cs = this.childNodes; + for(var i = 0, len = cs.length; i < len; i++){ + cs[i].render(true); + } + this.childrenRendered = true; + }, + + // private + sort : function(fn, scope){ + Ext.tree.TreeNode.superclass.sort.apply(this, arguments); + if(this.childrenRendered){ + var cs = this.childNodes; + for(var i = 0, len = cs.length; i < len; i++){ + cs[i].render(true); + } + } + }, + + // private + render : function(bulkRender){ + this.ui.render(bulkRender); + if(!this.rendered){ + // make sure it is registered + this.getOwnerTree().registerNode(this); + this.rendered = true; + if(this.expanded){ + this.expanded = false; + this.expand(false, false); + } + } + }, + + // private + renderIndent : function(deep, refresh){ + if(refresh){ + this.ui.childIndent = null; + } + this.ui.renderIndent(); + if(deep === true && this.childrenRendered){ + var cs = this.childNodes; + for(var i = 0, len = cs.length; i < len; i++){ + cs[i].renderIndent(true, refresh); + } + } + }, + + beginUpdate : function(){ + this.childrenRendered = false; + }, + + endUpdate : function(){ + if(this.expanded && this.rendered){ + this.renderChildren(); + } + }, + + destroy : function(){ + this.unselect(true); + Ext.tree.TreeNode.superclass.destroy.call(this); + Ext.destroy(this.ui, this.loader); + this.ui = this.loader = null; + }, + + // private + onIdChange : function(id){ + this.ui.onIdChange(id); + } +}); + +Ext.tree.TreePanel.nodeTypes.node = Ext.tree.TreeNode;/** + * @class Ext.tree.AsyncTreeNode + * @extends Ext.tree.TreeNode + * @cfg {TreeLoader} loader A TreeLoader to be used by this node (defaults to the loader defined on the tree) * @constructor - * @param {Object/String} attributes The attributes/config for the node or just a string with the text for the node + * @param {Object/String} attributes The attributes/config for the node or just a string with the text for the node */ -Ext.tree.TreeNode = function(attributes){ - attributes = attributes || {}; - if(typeof attributes == 'string'){ - attributes = {text: attributes}; - } - this.childrenRendered = false; - this.rendered = false; - Ext.tree.TreeNode.superclass.constructor.call(this, attributes); - this.expanded = attributes.expanded === true; - this.isTarget = attributes.isTarget !== false; - this.draggable = attributes.draggable !== false && attributes.allowDrag !== false; - this.allowChildren = attributes.allowChildren !== false && attributes.allowDrop !== false; - - /** - * Read-only. The text for this node. To change it use {@link #setText}. - * @type String - */ - this.text = attributes.text; + Ext.tree.AsyncTreeNode = function(config){ + this.loaded = config && config.loaded === true; + this.loading = false; + Ext.tree.AsyncTreeNode.superclass.constructor.apply(this, arguments); /** - * True if this node is disabled. - * @type Boolean - */ - this.disabled = attributes.disabled === true; + * @event beforeload + * Fires before this node is loaded, return false to cancel + * @param {Node} this This node + */ + this.addEvents('beforeload', 'load'); /** - * True if this node is hidden. - * @type Boolean - */ - this.hidden = attributes.hidden === true; - - this.addEvents( - /** - * @event textchange - * Fires when the text for this node is changed - * @param {Node} this This node - * @param {String} text The new text - * @param {String} oldText The old text - */ - 'textchange', - /** - * @event beforeexpand - * Fires before this node is expanded, return false to cancel. - * @param {Node} this This node - * @param {Boolean} deep - * @param {Boolean} anim - */ - 'beforeexpand', - /** - * @event beforecollapse - * Fires before this node is collapsed, return false to cancel. - * @param {Node} this This node - * @param {Boolean} deep - * @param {Boolean} anim - */ - 'beforecollapse', - /** - * @event expand - * Fires when this node is expanded - * @param {Node} this This node - */ - 'expand', - /** - * @event disabledchange - * Fires when the disabled status of this node changes - * @param {Node} this This node - * @param {Boolean} disabled - */ - 'disabledchange', - /** - * @event collapse - * Fires when this node is collapsed - * @param {Node} this This node - */ - 'collapse', - /** - * @event beforeclick - * Fires before click processing. Return false to cancel the default action. - * @param {Node} this This node - * @param {Ext.EventObject} e The event object - */ - 'beforeclick', - /** - * @event click - * Fires when this node is clicked - * @param {Node} this This node - * @param {Ext.EventObject} e The event object - */ - 'click', - /** - * @event checkchange - * Fires when a node with a checkbox's checked property changes - * @param {Node} this This node - * @param {Boolean} checked - */ - 'checkchange', - /** - * @event beforedblclick - * Fires before double click processing. Return false to cancel the default action. - * @param {Node} this This node - * @param {Ext.EventObject} e The event object - */ - 'beforedblclick', - /** - * @event dblclick - * Fires when this node is double clicked - * @param {Node} this This node - * @param {Ext.EventObject} e The event object - */ - 'dblclick', - /** - * @event contextmenu - * Fires when this node is right clicked - * @param {Node} this This node - * @param {Ext.EventObject} e The event object - */ - 'contextmenu', - /** - * @event beforechildrenrendered - * Fires right before the child nodes for this node are rendered - * @param {Node} this This node - */ - 'beforechildrenrendered' - ); - - var uiClass = this.attributes.uiProvider || this.defaultUI || Ext.tree.TreeNodeUI; - + * @event load + * Fires when this node is loaded + * @param {Node} this This node + */ /** - * Read-only. The UI for this node - * @type TreeNodeUI + * The loader used by this node (defaults to using the tree's defined loader) + * @type TreeLoader + * @property loader */ - this.ui = new uiClass(this); }; -Ext.extend(Ext.tree.TreeNode, Ext.data.Node, { - preventHScroll : true, +Ext.extend(Ext.tree.AsyncTreeNode, Ext.tree.TreeNode, { + expand : function(deep, anim, callback, scope){ + if(this.loading){ // if an async load is already running, waiting til it's done + var timer; + var f = function(){ + if(!this.loading){ // done loading + clearInterval(timer); + this.expand(deep, anim, callback, scope); + } + }.createDelegate(this); + timer = setInterval(f, 200); + return; + } + if(!this.loaded){ + if(this.fireEvent("beforeload", this) === false){ + return; + } + this.loading = true; + this.ui.beforeLoad(this); + var loader = this.loader || this.attributes.loader || this.getOwnerTree().getLoader(); + if(loader){ + loader.load(this, this.loadComplete.createDelegate(this, [deep, anim, callback, scope]), this); + return; + } + } + Ext.tree.AsyncTreeNode.superclass.expand.call(this, deep, anim, callback, scope); + }, + /** - * Returns true if this node is expanded + * Returns true if this node is currently loading * @return {Boolean} */ - isExpanded : function(){ - return this.expanded; - }, - -/** - * Returns the UI object for this node. - * @return {TreeNodeUI} The object which is providing the user interface for this tree - * node. Unless otherwise specified in the {@link #uiProvider}, this will be an instance - * of {@link Ext.tree.TreeNodeUI} - */ - getUI : function(){ - return this.ui; - }, - - getLoader : function(){ - var owner; - return this.loader || ((owner = this.getOwnerTree()) && owner.loader ? owner.loader : new Ext.tree.TreeLoader()); - }, - - // private override - setFirstChild : function(node){ - var of = this.firstChild; - Ext.tree.TreeNode.superclass.setFirstChild.call(this, node); - if(this.childrenRendered && of && node != of){ - of.renderIndent(true, true); - } - if(this.rendered){ - this.renderIndent(true, true); - } + isLoading : function(){ + return this.loading; }, - - // private override - setLastChild : function(node){ - var ol = this.lastChild; - Ext.tree.TreeNode.superclass.setLastChild.call(this, node); - if(this.childrenRendered && ol && node != ol){ - ol.renderIndent(true, true); - } - if(this.rendered){ - this.renderIndent(true, true); - } + + loadComplete : function(deep, anim, callback, scope){ + this.loading = false; + this.loaded = true; + this.ui.afterLoad(this); + this.fireEvent("load", this); + this.expand(deep, anim, callback, scope); }, - - // these methods are overridden to provide lazy rendering support - // private override - appendChild : function(n){ - if(!n.render && !Ext.isArray(n)){ - n = this.getLoader().createNode(n); - } - var node = Ext.tree.TreeNode.superclass.appendChild.call(this, n); - if(node && this.childrenRendered){ - node.render(); - } - this.ui.updateExpandIcon(); - return node; + + /** + * Returns true if this node has been loaded + * @return {Boolean} + */ + isLoaded : function(){ + return this.loaded; }, - - // private override - removeChild : function(node){ - this.ownerTree.getSelectionModel().unselect(node); - Ext.tree.TreeNode.superclass.removeChild.apply(this, arguments); - // if it's been rendered remove dom node - if(this.childrenRendered){ - node.ui.remove(); - } - if(this.childNodes.length < 1){ - this.collapse(false, false); + + hasChildNodes : function(){ + if(!this.isLeaf() && !this.loaded){ + return true; }else{ - this.ui.updateExpandIcon(); - } - if(!this.firstChild && !this.isHiddenRoot()) { - this.childrenRendered = false; - } - return node; - }, - - // private override - insertBefore : function(node, refNode){ - if(!node.render){ - node = this.getLoader().createNode(node); - } - var newNode = Ext.tree.TreeNode.superclass.insertBefore.call(this, node, refNode); - if(newNode && refNode && this.childrenRendered){ - node.render(); + return Ext.tree.AsyncTreeNode.superclass.hasChildNodes.call(this); } - this.ui.updateExpandIcon(); - return newNode; }, /** - * Sets the text for this node - * @param {String} text + * Trigger a reload for this node + * @param {Function} callback + * @param {Object} scope (optional) The scope (this reference) in which the callback is executed. Defaults to this Node. */ - setText : function(text){ - var oldText = this.text; - this.text = text; - this.attributes.text = text; - if(this.rendered){ // event without subscribing - this.ui.onTextChange(this, text, oldText); - } - this.fireEvent('textchange', this, text, oldText); - }, - - /** - * Triggers selection of this node - */ - select : function(){ - this.getOwnerTree().getSelectionModel().select(this); - }, - - /** - * Triggers deselection of this node - */ - unselect : function(){ - this.getOwnerTree().getSelectionModel().unselect(this); - }, - - /** - * Returns true if this node is selected - * @return {Boolean} - */ - isSelected : function(){ - return this.getOwnerTree().getSelectionModel().isSelected(this); - }, - - /** - * Expand this node. - * @param {Boolean} deep (optional) True to expand all children as well - * @param {Boolean} anim (optional) false to cancel the default animation - * @param {Function} callback (optional) A callback to be called when - * expanding this node completes (does not wait for deep expand to complete). - * Called with 1 parameter, this node. - * @param {Object} scope (optional) The scope in which to execute the callback. - */ - expand : function(deep, anim, callback, scope){ - if(!this.expanded){ - if(this.fireEvent('beforeexpand', this, deep, anim) === false){ - return; - } - if(!this.childrenRendered){ - this.renderChildren(); - } - this.expanded = true; - if(!this.isHiddenRoot() && (this.getOwnerTree().animate && anim !== false) || anim){ - this.ui.animExpand(function(){ - this.fireEvent('expand', this); - this.runCallback(callback, scope || this, [this]); - if(deep === true){ - this.expandChildNodes(true); - } - }.createDelegate(this)); - return; - }else{ - this.ui.expand(); - this.fireEvent('expand', this); - this.runCallback(callback, scope || this, [this]); - } - }else{ - this.runCallback(callback, scope || this, [this]); - } - if(deep === true){ - this.expandChildNodes(true); - } - }, - - runCallback : function(cb, scope, args){ - if(Ext.isFunction(cb)){ - cb.apply(scope, args); - } - }, - - isHiddenRoot : function(){ - return this.isRoot && !this.getOwnerTree().rootVisible; - }, - - /** - * Collapse this node. - * @param {Boolean} deep (optional) True to collapse all children as well - * @param {Boolean} anim (optional) false to cancel the default animation - * @param {Function} callback (optional) A callback to be called when - * expanding this node completes (does not wait for deep expand to complete). - * Called with 1 parameter, this node. - * @param {Object} scope (optional) The scope in which to execute the callback. - */ - collapse : function(deep, anim, callback, scope){ - if(this.expanded && !this.isHiddenRoot()){ - if(this.fireEvent('beforecollapse', this, deep, anim) === false){ - return; - } - this.expanded = false; - if((this.getOwnerTree().animate && anim !== false) || anim){ - this.ui.animCollapse(function(){ - this.fireEvent('collapse', this); - this.runCallback(callback, scope || this, [this]); - if(deep === true){ - this.collapseChildNodes(true); - } - }.createDelegate(this)); - return; - }else{ - this.ui.collapse(); - this.fireEvent('collapse', this); - this.runCallback(callback, scope || this, [this]); - } - }else if(!this.expanded){ - this.runCallback(callback, scope || this, [this]); - } - if(deep === true){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - cs[i].collapse(true, false); - } - } - }, - - // private - delayedExpand : function(delay){ - if(!this.expandProcId){ - this.expandProcId = this.expand.defer(delay, this); - } - }, - - // private - cancelExpand : function(){ - if(this.expandProcId){ - clearTimeout(this.expandProcId); - } - this.expandProcId = false; - }, - - /** - * Toggles expanded/collapsed state of the node - */ - toggle : function(){ - if(this.expanded){ - this.collapse(); - }else{ - this.expand(); - } - }, - - /** - * Ensures all parent nodes are expanded, and if necessary, scrolls - * the node into view. - * @param {Function} callback (optional) A function to call when the node has been made visible. - * @param {Object} scope (optional) The scope in which to execute the callback. - */ - ensureVisible : function(callback, scope){ - var tree = this.getOwnerTree(); - tree.expandPath(this.parentNode ? this.parentNode.getPath() : this.getPath(), false, function(){ - var node = tree.getNodeById(this.id); // Somehow if we don't do this, we lose changes that happened to node in the meantime - tree.getTreeEl().scrollChildIntoView(node.ui.anchor); - this.runCallback(callback, scope || this, [this]); - }.createDelegate(this)); - }, - - /** - * Expand all child nodes - * @param {Boolean} deep (optional) true if the child nodes should also expand their child nodes - */ - expandChildNodes : function(deep){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - cs[i].expand(deep); - } - }, - - /** - * Collapse all child nodes - * @param {Boolean} deep (optional) true if the child nodes should also collapse their child nodes - */ - collapseChildNodes : function(deep){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++) { - cs[i].collapse(deep); - } - }, - - /** - * Disables this node - */ - disable : function(){ - this.disabled = true; - this.unselect(); - if(this.rendered && this.ui.onDisableChange){ // event without subscribing - this.ui.onDisableChange(this, true); - } - this.fireEvent('disabledchange', this, true); - }, - - /** - * Enables this node - */ - enable : function(){ - this.disabled = false; - if(this.rendered && this.ui.onDisableChange){ // event without subscribing - this.ui.onDisableChange(this, false); - } - this.fireEvent('disabledchange', this, false); - }, - - // private - renderChildren : function(suppressEvent){ - if(suppressEvent !== false){ - this.fireEvent('beforechildrenrendered', this); - } - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++){ - cs[i].render(true); - } - this.childrenRendered = true; - }, - - // private - sort : function(fn, scope){ - Ext.tree.TreeNode.superclass.sort.apply(this, arguments); - if(this.childrenRendered){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++){ - cs[i].render(true); - } - } - }, - - // private - render : function(bulkRender){ - this.ui.render(bulkRender); - if(!this.rendered){ - // make sure it is registered - this.getOwnerTree().registerNode(this); - this.rendered = true; - if(this.expanded){ - this.expanded = false; - this.expand(false, false); - } - } - }, - - // private - renderIndent : function(deep, refresh){ - if(refresh){ - this.ui.childIndent = null; - } - this.ui.renderIndent(); - if(deep === true && this.childrenRendered){ - var cs = this.childNodes; - for(var i = 0, len = cs.length; i < len; i++){ - cs[i].renderIndent(true, refresh); - } - } - }, - - beginUpdate : function(){ - this.childrenRendered = false; - }, - - endUpdate : function(){ - if(this.expanded && this.rendered){ - this.renderChildren(); - } - }, - - destroy : function(){ - if(this.childNodes){ - for(var i = 0,l = this.childNodes.length; i < l; i++){ - this.childNodes[i].destroy(); - } - this.childNodes = null; - } - if(this.ui.destroy){ - this.ui.destroy(); - } - }, - - // private - onIdChange : function(id){ - this.ui.onIdChange(id); - } -}); - -Ext.tree.TreePanel.nodeTypes.node = Ext.tree.TreeNode;/** - * @class Ext.tree.AsyncTreeNode - * @extends Ext.tree.TreeNode - * @cfg {TreeLoader} loader A TreeLoader to be used by this node (defaults to the loader defined on the tree) - * @constructor - * @param {Object/String} attributes The attributes/config for the node or just a string with the text for the node - */ - Ext.tree.AsyncTreeNode = function(config){ - this.loaded = config && config.loaded === true; - this.loading = false; - Ext.tree.AsyncTreeNode.superclass.constructor.apply(this, arguments); - /** - * @event beforeload - * Fires before this node is loaded, return false to cancel - * @param {Node} this This node - */ - this.addEvents('beforeload', 'load'); - /** - * @event load - * Fires when this node is loaded - * @param {Node} this This node - */ - /** - * The loader used by this node (defaults to using the tree's defined loader) - * @type TreeLoader - * @property loader - */ -}; -Ext.extend(Ext.tree.AsyncTreeNode, Ext.tree.TreeNode, { - expand : function(deep, anim, callback, scope){ - if(this.loading){ // if an async load is already running, waiting til it's done - var timer; - var f = function(){ - if(!this.loading){ // done loading - clearInterval(timer); - this.expand(deep, anim, callback, scope); - } - }.createDelegate(this); - timer = setInterval(f, 200); - return; - } - if(!this.loaded){ - if(this.fireEvent("beforeload", this) === false){ - return; - } - this.loading = true; - this.ui.beforeLoad(this); - var loader = this.loader || this.attributes.loader || this.getOwnerTree().getLoader(); - if(loader){ - loader.load(this, this.loadComplete.createDelegate(this, [deep, anim, callback, scope]), this); - return; - } - } - Ext.tree.AsyncTreeNode.superclass.expand.call(this, deep, anim, callback, scope); - }, - - /** - * Returns true if this node is currently loading - * @return {Boolean} - */ - isLoading : function(){ - return this.loading; - }, - - loadComplete : function(deep, anim, callback, scope){ - this.loading = false; - this.loaded = true; - this.ui.afterLoad(this); - this.fireEvent("load", this); - this.expand(deep, anim, callback, scope); - }, - - /** - * Returns true if this node has been loaded - * @return {Boolean} - */ - isLoaded : function(){ - return this.loaded; - }, - - hasChildNodes : function(){ - if(!this.isLeaf() && !this.loaded){ - return true; - }else{ - return Ext.tree.AsyncTreeNode.superclass.hasChildNodes.call(this); - } - }, - - /** - * Trigger a reload for this node - * @param {Function} callback - * @param {Object} scope (optional) The scope in which to execute the callback. - */ - reload : function(callback, scope){ - this.collapse(false, false); - while(this.firstChild){ - this.removeChild(this.firstChild).destroy(); + reload : function(callback, scope){ + this.collapse(false, false); + while(this.firstChild){ + this.removeChild(this.firstChild).destroy(); } this.childrenRendered = false; this.loaded = false; @@ -48185,7 +50445,7 @@ Ext.tree.TreeNodeUI.prototype = { removeChild : function(node){ if(this.rendered){ this.ctNode.removeChild(node.ui.getEl()); - } + } }, // private @@ -48208,14 +50468,14 @@ Ext.tree.TreeNodeUI.prototype = { // private onDisableChange : function(node, state){ this.disabled = state; - if (this.checkbox) { - this.checkbox.disabled = state; - } + if (this.checkbox) { + this.checkbox.disabled = state; + } if(state){ this.addClass("x-tree-node-disabled"); }else{ this.removeClass("x-tree-node-disabled"); - } + } }, // private @@ -48266,7 +50526,7 @@ Ext.tree.TreeNodeUI.prototype = { */ removeClass : function(cls){ if(this.elNode){ - Ext.fly(this.elNode).removeClass(cls); + Ext.fly(this.elNode).removeClass(cls); } }, @@ -48275,12 +50535,12 @@ Ext.tree.TreeNodeUI.prototype = { if(this.rendered){ this.holder = document.createElement("div"); this.holder.appendChild(this.wrap); - } + } }, // private fireEvent : function(){ - return this.node.fireEvent.apply(this.node, arguments); + return this.node.fireEvent.apply(this.node, arguments); }, // private @@ -48288,7 +50548,7 @@ Ext.tree.TreeNodeUI.prototype = { this.node.on("move", this.onMove, this); if(this.node.disabled){ - this.onDisableChange(this.node, true); + this.onDisableChange(this.node, true); } if(this.node.hidden){ this.hide(); @@ -48326,7 +50586,7 @@ Ext.tree.TreeNodeUI.prototype = { this.node.hidden = false; if(this.wrap){ this.wrap.style.display = ""; - } + } }, // private @@ -48395,8 +50655,8 @@ Ext.tree.TreeNodeUI.prototype = { // private onCheckChange : function(){ var checked = this.checkbox.checked; - // fix for IE6 - this.checkbox.defaultChecked = checked; + // fix for IE6 + this.checkbox.defaultChecked = checked; this.node.attributes.checked = checked; this.fireEvent('checkchange', this.node, checked); }, @@ -48412,12 +50672,12 @@ Ext.tree.TreeNodeUI.prototype = { startDrop : function(){ this.dropping = true; }, - + // delayed drop so the click event doesn't get fired on a drop - endDrop : function(){ + endDrop : function(){ setTimeout(function(){ this.dropping = false; - }.createDelegate(this), 50); + }.createDelegate(this), 50); }, // private @@ -48458,7 +50718,7 @@ Ext.tree.TreeNodeUI.prototype = { blur : function(){ try{ this.anchor.blur(); - }catch(e){} + }catch(e){} }, // private @@ -48473,7 +50733,7 @@ Ext.tree.TreeNodeUI.prototype = { } this.animating = true; this.updateExpandIcon(); - + ct.slideIn('t', { callback : function(){ this.animating = false; @@ -48520,12 +50780,15 @@ Ext.tree.TreeNodeUI.prototype = { // private getContainer : function(){ - return this.ctNode; + return this.ctNode; }, - // private +/** + * Returns the element which encapsulates this node. + * @return {HtmlElement} The DOM element. The default implementation uses a <li>. + */ getEl : function(){ - return this.wrap; + return this.wrap; }, // private @@ -48540,15 +50803,15 @@ Ext.tree.TreeNodeUI.prototype = { // private onRender : function(){ - this.render(); + this.render(); }, // private render : function(bulkRender){ var n = this.node, a = n.attributes; - var targetNode = n.parentNode ? + var targetNode = n.parentNode ? n.parentNode.ui.getContainer() : n.ownerTree.innerCt.dom; - + if(!this.rendered){ this.rendered = true; @@ -48565,7 +50828,7 @@ Ext.tree.TreeNodeUI.prototype = { if(a.qtipTitle){ this.textNode.setAttribute("ext:qtitle", a.qtipTitle); } - } + } }else if(a.qtipCfg){ a.qtipCfg.target = Ext.id(this.textNode); Ext.QuickTips.register(a.qtipCfg); @@ -48586,10 +50849,10 @@ Ext.tree.TreeNodeUI.prototype = { // add some indent caching, this helps performance when rendering a large tree this.indentMarkup = n.parentNode ? n.parentNode.ui.getChildIndent() : ''; - var cb = typeof a.checked == 'boolean'; - - var href = a.href ? a.href : Ext.isGecko ? "" : "#"; - var buf = ['
  • ', + var cb = Ext.isBoolean(a.checked), + nel, + href = a.href ? a.href : Ext.isGecko ? "" : "#", + buf = ['
  • ', '',this.indentMarkup,"", '', '', @@ -48599,13 +50862,12 @@ Ext.tree.TreeNodeUI.prototype = { '', "
  • "].join(''); - var nel; if(bulkRender !== true && n.nextSibling && (nel = n.nextSibling.ui.getEl())){ this.wrap = Ext.DomHelper.insertHtml("beforeBegin", nel, buf); }else{ this.wrap = Ext.DomHelper.insertHtml("beforeEnd", targetNode, buf); } - + this.elNode = this.wrap.childNodes[0]; this.ctNode = this.wrap.childNodes[1]; var cs = this.elNode.childNodes; @@ -48615,8 +50877,8 @@ Ext.tree.TreeNodeUI.prototype = { var index = 3; if(cb){ this.checkbox = cs[3]; - // fix for IE6 - this.checkbox.defaultChecked = this.checkbox.checked; + // fix for IE6 + this.checkbox.defaultChecked = this.checkbox.checked; index++; } this.anchor = cs[index]; @@ -48630,7 +50892,7 @@ Ext.tree.TreeNodeUI.prototype = { getAnchor : function(){ return this.anchor; }, - + /** * Returns the text node. * @return {HtmlNode} The DOM text node. @@ -48638,7 +50900,7 @@ Ext.tree.TreeNodeUI.prototype = { getTextEl : function(){ return this.textNode; }, - + /** * Returns the icon <img> element. * @return {HtmlElement} The DOM image element. @@ -48653,15 +50915,17 @@ Ext.tree.TreeNodeUI.prototype = { * @return {Boolean} The checked flag. */ isChecked : function(){ - return this.checkbox ? this.checkbox.checked : false; + return this.checkbox ? this.checkbox.checked : false; }, // private updateExpandIcon : function(){ if(this.rendered){ - var n = this.node, c1, c2; - var cls = n.isLast() ? "x-tree-elbow-end" : "x-tree-elbow"; - var hasChild = n.hasChildNodes(); + var n = this.node, + c1, + c2, + cls = n.isLast() ? "x-tree-elbow-end" : "x-tree-elbow", + hasChild = n.hasChildNodes(); if(hasChild || n.attributes.expandable){ if(n.expanded){ cls += "-minus"; @@ -48682,7 +50946,7 @@ Ext.tree.TreeNodeUI.prototype = { } }else{ if(!this.wasLeaf){ - Ext.fly(this.elNode).replaceClass("x-tree-node-expanded", "x-tree-node-leaf"); + Ext.fly(this.elNode).replaceClass("x-tree-node-expanded", "x-tree-node-collapsed"); delete this.c1; delete this.c2; this.wasLeaf = true; @@ -48695,7 +50959,7 @@ Ext.tree.TreeNodeUI.prototype = { } } }, - + // private onIdChange: function(id){ if(this.rendered){ @@ -48706,8 +50970,8 @@ Ext.tree.TreeNodeUI.prototype = { // private getChildIndent : function(){ if(!this.childIndent){ - var buf = []; - var p = this.node; + var buf = [], + p = this.node; while(p){ if(!p.isRoot || (p.isRoot && p.ownerTree.rootVisible)){ if(!p.isLast()) { @@ -48726,8 +50990,8 @@ Ext.tree.TreeNodeUI.prototype = { // private renderIndent : function(){ if(this.rendered){ - var indent = ""; - var p = this.node.parentNode; + var indent = "", + p = this.node.parentNode; if(p){ indent = p.ui.getChildIndent(); } @@ -48743,23 +51007,14 @@ Ext.tree.TreeNodeUI.prototype = { if(this.elNode){ Ext.dd.Registry.unregister(this.elNode.id); } - delete this.elNode; - delete this.ctNode; - delete this.indentNode; - delete this.ecNode; - delete this.iconNode; - delete this.checkbox; - delete this.anchor; - delete this.textNode; - - if (this.holder){ - delete this.wrap; - Ext.removeNode(this.holder); - delete this.holder; - }else{ - Ext.removeNode(this.wrap); - delete this.wrap; - } + + Ext.each(['textnode', 'anchor', 'checkbox', 'indentNode', 'ecNode', 'iconNode', 'elNode', 'ctNode', 'wrap', 'holder'], function(el){ + if(this[el]){ + Ext.fly(this[el]).remove(); + delete this[el]; + } + }, this); + delete this.node; } }; @@ -48853,7 +51108,7 @@ Ext.tree.TreeLoader = function(config){ "loadexception" ); Ext.tree.TreeLoader.superclass.constructor.call(this); - if(typeof this.paramOrder == 'string'){ + if(Ext.isString(this.paramOrder)){ this.paramOrder = this.paramOrder.split(/[\s,|]/); } }; @@ -48899,15 +51154,15 @@ Ext.extend(Ext.tree.TreeLoader, Ext.util.Observable, { /** * @cfg {Array/String} paramOrder Defaults to undefined. Only used when using directFn. - * A list of params to be executed - * server side. Specify the params in the order in which they must be executed on the server-side + * Specifies the params in the order in which they must be passed to the server-side Direct method * as either (1) an Array of String values, or (2) a String of params delimited by either whitespace, * comma, or pipe. For example, * any of the following would be acceptable:
    
    +nodeParameter: 'node',
     paramOrder: ['param1','param2','param3']
    -paramOrder: 'param1 param2 param3'
    -paramOrder: 'param1,param2,param3'
    -paramOrder: 'param1|param2|param'
    +paramOrder: 'node param1 param2 param3'
    +paramOrder: 'param1,node,param2,param3'
    +paramOrder: 'param1|param2|param|node'
          
    */ paramOrder: undefined, @@ -48919,6 +51174,12 @@ paramOrder: 'param1|param2|param' */ paramsAsHash: false, + /** + * @cfg {String} nodeParameter The name of the parameter sent to the server which contains + * the identifier of the node. Defaults to 'node'. + */ + nodeParameter: 'node', + /** * @cfg {Function} directFn * Function to call when executing a request. @@ -48930,8 +51191,10 @@ paramOrder: 'param1|param2|param' * This is called automatically when a node is expanded, but may be used to reload * a node (or append new children if the {@link #clearOnLoad} option is false.) * @param {Ext.tree.TreeNode} node - * @param {Function} callback - * @param (Object) scope + * @param {Function} callback Function to call after the node has been loaded. The + * function is passed the TreeNode which was requested to be loaded. + * @param (Object) scope The cope (this reference) in which the callback is executed. + * defaults to the loaded TreeNode. */ load : function(node, callback, scope){ if(this.clearOnLoad){ @@ -48965,27 +51228,29 @@ paramOrder: 'param1|param2|param' }, getParams: function(node){ - var buf = [], bp = this.baseParams; + var bp = Ext.apply({}, this.baseParams), + np = this.nodeParameter, + po = this.paramOrder; + + np && (bp[ np ] = node.id); + if(this.directFn){ - buf.push(node.id); - if(bp){ - if(this.paramOrder){ - for(var i = 0, len = this.paramOrder.length; i < len; i++){ - buf.push(bp[this.paramOrder[i]]); - } - }else if(this.paramsAsHash){ - buf.push(bp); + var buf = [node.id]; + if(po){ + // reset 'buf' if the nodeParameter was included in paramOrder + if(np && po.indexOf(np) > -1){ + buf = []; + } + + for(var i = 0, len = po.length; i < len; i++){ + buf.push(bp[ po[i] ]); } + }else if(this.paramsAsHash){ + buf = [bp]; } return buf; }else{ - for(var key in bp){ - if(!Ext.isFunction(bp[key])){ - buf.push(encodeURIComponent(key), "=", encodeURIComponent(bp[key]), "&"); - } - } - buf.push("node=", encodeURIComponent(node.id)); - return buf.join(""); + return bp; } }, @@ -49075,7 +51340,7 @@ new Ext.tree.TreePanel({ if(this.applyLoader !== false && !attr.loader){ attr.loader = this; } - if(typeof attr.uiProvider == 'string'){ + if(Ext.isString(attr.uiProvider)){ attr.uiProvider = this.uiProviders[attr.uiProvider] || eval(attr.uiProvider); } if(attr.nodeType){ @@ -49117,6 +51382,11 @@ new Ext.tree.TreePanel({ var a = response.argument; this.fireEvent("loadexception", this, a.node, response); this.runCallback(a.callback, a.scope || a.node, [a.node]); + }, + + destroy : function(){ + this.abort(); + this.purgeListeners(); } });/** * @class Ext.tree.TreeFilter @@ -49172,7 +51442,7 @@ Ext.tree.TreeFilter.prototype = { * node in the tree (or from the startNode). If the function returns true, the node is kept * otherwise it is filtered. If a node is filtered, its children are also filtered. * @param {Function} fn The filter function - * @param {Object} scope (optional) The scope of the function (defaults to the current node) + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node. */ filterBy : function(fn, scope, startNode){ startNode = startNode || this.tree.root; @@ -49228,7 +51498,7 @@ Ext.tree.TreeFilter.prototype = { }; /** * @class Ext.tree.TreeSorter - * Provides sorting of nodes in a {@link Ext.tree.TreePanel}. The TreeSorter automatically monitors events on the + * Provides sorting of nodes in a {@link Ext.tree.TreePanel}. The TreeSorter automatically monitors events on the * associated TreePanel that might affect the tree's sort order (beforechildrenrendered, append, insert and textchange). * Example usage:
    *
    
    @@ -49249,33 +51519,33 @@ Ext.tree.TreeSorter = function(tree, config){
         /**
          * @cfg {Boolean} folderSort True to sort leaf nodes under non-leaf nodes (defaults to false)
          */
    -    /** 
    -     * @cfg {String} property The named attribute on the node to sort by (defaults to "text").  Note that this 
    +    /**
    +     * @cfg {String} property The named attribute on the node to sort by (defaults to "text").  Note that this
          * property is only used if no {@link #sortType} function is specified, otherwise it is ignored.
          */
    -    /** 
    +    /**
          * @cfg {String} dir The direction to sort ("asc" or "desc," case-insensitive, defaults to "asc")
          */
    -    /** 
    +    /**
          * @cfg {String} leafAttr The attribute used to determine leaf nodes when {@link #folderSort} = true (defaults to "leaf")
          */
    -    /** 
    +    /**
          * @cfg {Boolean} caseSensitive true for case-sensitive sort (defaults to false)
          */
    -    /** 
    +    /**
          * @cfg {Function} sortType A custom "casting" function used to convert node values before sorting.  The function
          * will be called with a single parameter (the {@link Ext.tree.TreeNode} being evaluated) and is expected to return
          * the node's sort value cast to the specific data type required for sorting.  This could be used, for example, when
    -     * a node's text (or other attribute) should be sorted as a date or numeric value.  See the class description for 
    +     * a node's text (or other attribute) should be sorted as a date or numeric value.  See the class description for
          * example usage.  Note that if a sortType is specified, any {@link #property} config will be ignored.
          */
    -    
    +
         Ext.apply(this, config);
         tree.on("beforechildrenrendered", this.doSort, this);
         tree.on("append", this.updateSort, this);
         tree.on("insert", this.updateSort, this);
         tree.on("textchange", this.updateSortParent, this);
    -    
    +
         var dsc = this.dir && this.dir.toLowerCase() == "desc";
         var p = this.property || "text";
         var sortType = this.sortType;
    @@ -49292,14 +51562,14 @@ Ext.tree.TreeSorter = function(tree, config){
                     return -1;
                 }
             }
    -    	var v1 = sortType ? sortType(n1) : (cs ? n1.attributes[p] : n1.attributes[p].toUpperCase());
    -    	var v2 = sortType ? sortType(n2) : (cs ? n2.attributes[p] : n2.attributes[p].toUpperCase());
    -    	if(v1 < v2){
    -			return dsc ? +1 : -1;
    -		}else if(v1 > v2){
    -			return dsc ? -1 : +1;
    +        var v1 = sortType ? sortType(n1) : (cs ? n1.attributes[p] : n1.attributes[p].toUpperCase());
    +        var v2 = sortType ? sortType(n2) : (cs ? n2.attributes[p] : n2.attributes[p].toUpperCase());
    +        if(v1 < v2){
    +            return dsc ? +1 : -1;
    +        }else if(v1 > v2){
    +            return dsc ? -1 : +1;
             }else{
    -	    	return 0;
    +            return 0;
             }
         };
     };
    @@ -49308,20 +51578,20 @@ Ext.tree.TreeSorter.prototype = {
         doSort : function(node){
             node.sort(this.sortFn);
         },
    -    
    +
         compareNodes : function(n1, n2){
             return (n1.text.toUpperCase() > n2.text.toUpperCase() ? 1 : -1);
         },
    -    
    +
         updateSort : function(tree, node){
             if(node.childrenRendered){
                 this.doSort.defer(1, this, [node]);
             }
         },
    -    
    +
         updateSortParent : function(node){
    -		var p = node.parentNode;
    -		if(p && p.childrenRendered){
    +        var p = node.parentNode;
    +        if(p && p.childrenRendered){
                 this.doSort.defer(1, this, [p]);
             }
         }
    @@ -49728,6 +51998,7 @@ Ext.extend(Ext.tree.TreeDragZone, Ext.dd.DragZone, {
     Ext.tree.TreeEditor = function(tree, fc, config){
         fc = fc || {};
         var field = fc.events ? fc : new Ext.form.TextField(fc);
    +    
         Ext.tree.TreeEditor.superclass.constructor.call(this, field, config);
     
         this.tree = tree;
    @@ -49779,12 +52050,20 @@ Ext.extend(Ext.tree.TreeEditor, Ext.Editor, {
         editDelay : 350,
     
         initEditor : function(tree){
    -        tree.on('beforeclick', this.beforeNodeClick, this);
    -        tree.on('dblclick', this.onNodeDblClick, this);
    -        this.on('complete', this.updateNode, this);
    -        this.on('beforestartedit', this.fitToTree, this);
    +        tree.on({
    +            scope      : this,
    +            beforeclick: this.beforeNodeClick,
    +            dblclick   : this.onNodeDblClick
    +        });
    +        
    +        this.on({
    +            scope          : this,
    +            complete       : this.updateNode,
    +            beforestartedit: this.fitToTree,
    +            specialkey     : this.onSpecialKey
    +        });
    +        
             this.on('startedit', this.bindScroll, this, {delay:10});
    -        this.on('specialkey', this.onSpecialKey, this);
         },
     
         // private
    @@ -49866,6 +52145,14 @@ Ext.extend(Ext.tree.TreeEditor, Ext.Editor, {
                 e.stopEvent();
                 this.completeEdit();
             }
    +    },
    +    
    +    onDestroy : function(){
    +        clearTimeout(this.autoEditTimer);
    +        Ext.tree.TreeEditor.superclass.onDestroy.call(this);
    +        var tree = this.tree;
    +        tree.un('beforeclick', this.beforeNodeClick, this);
    +        tree.un('dblclick', this.onNodeDblClick, this);
         }
     });/*! SWFObject v2.2  
         is released under the MIT License  
    @@ -50655,7 +52942,7 @@ Ext.FlashComponent = Ext.extend(Ext.BoxComponent, {
          * @cfg {String} flashVersion
          * Indicates the version the flash content was published for. Defaults to '9.0.45'.
          */
    -    flashVersion : '9.0.45',
    +    flashVersion : '9.0.115',
     
         /**
          * @cfg {String} backgroundColor
    @@ -50701,7 +52988,14 @@ Ext.FlashComponent = Ext.extend(Ext.BoxComponent, {
         initComponent : function(){
             Ext.FlashComponent.superclass.initComponent.call(this);
     
    -        this.addEvents('initialize');
    +        this.addEvents(
    +            /**
    +             * @event initialize
    +             * 
    +             * @param {Chart} this
    +             */
    +            'initialize'
    +        );
         },
     
         onRender : function(){
    @@ -50785,17 +53079,23 @@ Ext.FlashEventProxy = {
      * @extends Ext.FlashComponent
      * The Ext.chart package provides the capability to visualize data with flash based charting.
      * Each chart binds directly to an Ext.data.Store enabling automatic updates of the chart.
    + * To change the look and feel of a chart, see the {@link #chartStyle} and {@link #extraStyle} config options.
      * @constructor
      * @xtype chart
      */
      
      Ext.chart.Chart = Ext.extend(Ext.FlashComponent, {
         refreshBuffer: 100,
    +    
    +    /**
    +     * @cfg {String} backgroundColor
    +     * @hide
    +     */
     
         /**
          * @cfg {Object} chartStyle
    -     * Sets styles for this chart. Contains a number of default values. Modifying this property will override
    -     * the base styles on the chart.
    +     * Sets styles for this chart. This contains default styling, so modifying this property will override
    +     * the built in styles of the chart. Use {@link #extraStyle} to add customizations to the default styling. 
          */
         chartStyle: {
             padding: 10,
    @@ -50833,9 +53133,75 @@ Ext.FlashEventProxy = {
         /**
          * @cfg {Object} extraStyle
          * Contains extra styles that will be added or overwritten to the default chartStyle. Defaults to null.
    +     * For a detailed list of the options available, visit the YUI Charts site 
    +     * at http://developer.yahoo.com/yui/charts/#basicstyles
    + * Some of the options availabe:
    + *
      + *
    • padding - The space around the edge of the chart's contents. Padding does not increase the size of the chart.
    • + *
    • animationEnabled - A Boolean value that specifies whether marker animations are enabled or not. Enabled by default.
    • + *
    • font - An Object defining the font style to be used in the chart. Defaults to { name: 'Tahoma', color: 0x444444, size: 11 }
      + *
        + *
      • name - font name
      • + *
      • color - font color (hex code, ie: "#ff0000", "ff0000" or 0xff0000)
      • + *
      • size - font size in points (numeric portion only, ie: 11)
      • + *
      • bold - boolean
      • + *
      • italic - boolean
      • + *
      • underline - boolean
      • + *
      + *
    • + *
    • border - An object defining the border style around the chart. The chart itself will decrease in dimensions to accomodate the border.
      + *
        + *
      • color - border color (hex code, ie: "#ff0000", "ff0000" or 0xff0000)
      • + *
      • size - border size in pixels (numeric portion only, ie: 1)
      • + *
      + *
    • + *
    • background - An object defining the background style of the chart.
      + *
        + *
      • color - border color (hex code, ie: "#ff0000", "ff0000" or 0xff0000)
      • + *
      • image - an image URL. May be relative to the current document or absolute.
      • + *
      + *
    • + *
    • legend - An object defining the legend style
      + *
        + *
      • display - location of the legend. Possible values are "none", "left", "right", "top", and "bottom".
      • + *
      • spacing - an image URL. May be relative to the current document or absolute.
      • + *
      • padding, border, background, font - same options as described above.
      • + *
    • + *
    • dataTip - An object defining the style of the data tip (tooltip).
      + *
        + *
      • padding, border, background, font - same options as described above.
      • + *
    • + *
    • xAxis and yAxis - An object defining the style of the style of either axis.
      + *
        + *
      • color - same option as described above.
      • + *
      • size - same option as described above.
      • + *
      • showLabels - boolean
      • + *
      • labelRotation - a value in degrees from -90 through 90. Default is zero.
      • + *
    • + *
    • majorGridLines and minorGridLines - An object defining the style of the style of the grid lines.
      + *
        + *
      • color, size - same options as described above.
      • + *
    • + *
    • zeroGridLine - An object defining the style of the style of the zero grid line.
      + *
        + *
      • color, size - same options as described above.
      • + *
    • + *
    • majorTicks and minorTicks - An object defining the style of the style of ticks in the chart.
      + *
        + *
      • color, size - same options as described above.
      • + *
      • length - the length of each tick in pixels extending from the axis.
      • + *
      • display - how the ticks are drawn. Possible values are "none", "inside", "outside", and "cross".
      • + *
    • + *
    */ extraStyle: null, + /** + * @cfg {Object} seriesStyles + * Contains styles to apply to the series after a refresh. Defaults to null. + */ + seriesStyles: null, + /** * @cfg {Boolean} disableCaching * True to add a "cache buster" to the end of the chart url. Defaults to true for Opera and IE. @@ -50858,7 +53224,20 @@ Ext.FlashEventProxy = { 'itemdoubleclick', 'itemdragstart', 'itemdrag', - 'itemdragend' + 'itemdragend', + /** + * @event beforerefresh + * Fires before a refresh to the chart data is called. If the beforerefresh handler returns + * false the {@link #refresh} action will be cancelled. + * @param {Chart} this + */ + 'beforerefresh', + /** + * @event refresh + * Fires after the chart data has been refreshed. + * @param {Chart} this + */ + 'refresh' ); this.store = Ext.StoreMgr.lookup(this.store); }, @@ -50888,6 +53267,7 @@ Ext.FlashEventProxy = { * @param styles {Array} Initializer for all Chart series styles. */ setSeriesStyles: function(styles){ + this.seriesStyles = styles; var s = []; Ext.each(styles, function(style){ s.push(Ext.encode(style)); @@ -50975,51 +53355,57 @@ Ext.FlashEventProxy = { }, refresh : function(){ - var styleChanged = false; - // convert the store data into something YUI charts can understand - var data = [], rs = this.store.data.items; - for(var j = 0, len = rs.length; j < len; j++){ - data[j] = rs[j].data; - } - //make a copy of the series definitions so that we aren't - //editing them directly. - var dataProvider = []; - var seriesCount = 0; - var currentSeries = null; - var i = 0; - if(this.series){ - seriesCount = this.series.length; - for(i = 0; i < seriesCount; i++){ - currentSeries = this.series[i]; - var clonedSeries = {}; - for(var prop in currentSeries){ - if(prop == "style" && currentSeries.style !== null){ - clonedSeries.style = Ext.encode(currentSeries.style); - styleChanged = true; - //we don't want to modify the styles again next time - //so null out the style property. - // this causes issues - // currentSeries.style = null; - } else{ - clonedSeries[prop] = currentSeries[prop]; - } - } - dataProvider.push(clonedSeries); - } - } - - if(seriesCount > 0){ - for(i = 0; i < seriesCount; i++){ - currentSeries = dataProvider[i]; - if(!currentSeries.type){ - currentSeries.type = this.type; - } - currentSeries.dataProvider = data; - } - } else{ - dataProvider.push({type: this.type, dataProvider: data}); + if(this.fireEvent('beforerefresh', this) !== false){ + var styleChanged = false; + // convert the store data into something YUI charts can understand + var data = [], rs = this.store.data.items; + for(var j = 0, len = rs.length; j < len; j++){ + data[j] = rs[j].data; + } + //make a copy of the series definitions so that we aren't + //editing them directly. + var dataProvider = []; + var seriesCount = 0; + var currentSeries = null; + var i = 0; + if(this.series){ + seriesCount = this.series.length; + for(i = 0; i < seriesCount; i++){ + currentSeries = this.series[i]; + var clonedSeries = {}; + for(var prop in currentSeries){ + if(prop == "style" && currentSeries.style !== null){ + clonedSeries.style = Ext.encode(currentSeries.style); + styleChanged = true; + //we don't want to modify the styles again next time + //so null out the style property. + // this causes issues + // currentSeries.style = null; + } else{ + clonedSeries[prop] = currentSeries[prop]; + } + } + dataProvider.push(clonedSeries); + } + } + + if(seriesCount > 0){ + for(i = 0; i < seriesCount; i++){ + currentSeries = dataProvider[i]; + if(!currentSeries.type){ + currentSeries.type = this.type; + } + currentSeries.dataProvider = data; + } + } else{ + dataProvider.push({type: this.type, dataProvider: data}); + } + this.swf.setDataProvider(dataProvider); + if(this.seriesStyles){ + this.setSeriesStyles(this.seriesStyles); + } + this.fireEvent('refresh', this); } - this.swf.setDataProvider(dataProvider); }, createFnProxy : function(fn, old){ @@ -51499,833 +53885,776 @@ Ext.chart.PieSeries = Ext.extend(Ext.chart.Series, { type: "pie", dataField: null, categoryField: null -});/** - * @class Ext.layout.MenuLayout - * @extends Ext.layout.ContainerLayout - *

    Layout manager used by {@link Ext.menu.Menu}. Generally this class should not need to be used directly.

    - */ - Ext.layout.MenuLayout = Ext.extend(Ext.layout.ContainerLayout, { - monitorResize : true, - - setContainer : function(ct){ - this.monitorResize = !ct.floating; - // This event is only fired by the menu in IE, used so we don't couple - // the menu with the layout. - ct.on('autosize', this.doAutoSize, this); - Ext.layout.MenuLayout.superclass.setContainer.call(this, ct); - }, - - renderItem : function(c, position, target){ - if (!this.itemTpl) { - this.itemTpl = Ext.layout.MenuLayout.prototype.itemTpl = new Ext.XTemplate( - '
  • ', - '', - '', - '', - '
  • ' - ); - } - - if(c && !c.rendered){ - if(Ext.isNumber(position)){ - position = target.dom.childNodes[position]; - } - var a = this.getItemArgs(c); - -// The Component's positionEl is the
  • it is rendered into - c.render(c.positionEl = position ? - this.itemTpl.insertBefore(position, a, true) : - this.itemTpl.append(target, a, true)); - -// Link the containing
  • to the item. - c.positionEl.menuItemId = c.getItemId(); - -// If rendering a regular Component, and it needs an icon, -// move the Component rightwards. - if (!a.isMenuItem && a.needsIcon) { - c.positionEl.addClass('x-menu-list-item-indent'); - } - this.configureItem(c, position); - }else if(c && !this.isValidParent(c, target)){ - if(Ext.isNumber(position)){ - position = target.dom.childNodes[position]; - } - target.dom.insertBefore(c.getActionEl().dom, position || null); - } - }, - - getItemArgs : function(c) { - var isMenuItem = c instanceof Ext.menu.Item; - return { - isMenuItem: isMenuItem, - needsIcon: !isMenuItem && (c.icon || c.iconCls), - icon: c.icon || Ext.BLANK_IMAGE_URL, - iconCls: 'x-menu-item-icon ' + (c.iconCls || ''), - itemId: 'x-menu-el-' + c.id, - itemCls: 'x-menu-list-item ' - }; - }, - -// Valid if the Component is in a
  • which is part of our target
      - isValidParent : function(c, target) { - return c.el.up('li.x-menu-list-item', 5).dom.parentNode === (target.dom || target); - }, - - onLayout : function(ct, target){ - this.renderAll(ct, target); - this.doAutoSize(); - }, - - doAutoSize : function(){ - var ct = this.container, w = ct.width; - if(ct.floating){ - if(w){ - ct.setWidth(w); - }else if(Ext.isIE){ - ct.setWidth(Ext.isStrict && (Ext.isIE7 || Ext.isIE8) ? 'auto' : ct.minWidth); - var el = ct.getEl(), t = el.dom.offsetWidth; // force recalc - ct.setWidth(ct.getLayoutTarget().getWidth() + el.getFrameWidth('lr')); - } - } - } -}); -Ext.Container.LAYOUTS['menu'] = Ext.layout.MenuLayout; - -/** - * @class Ext.menu.Menu - * @extends Ext.Container - *

      A menu object. This is the container to which you may add menu items. Menu can also serve as a base class - * when you want a specialized menu based off of another component (like {@link Ext.menu.DateMenu} for example).

      - *

      Menus may contain either {@link Ext.menu.Item menu items}, or general {@link Ext.Component Component}s.

      - *

      To make a contained general {@link Ext.Component Component} line up with other {@link Ext.menu.Item menu items} - * specify iconCls: 'no-icon'. This reserves a space for an icon, and indents the Component in line - * with the other menu items. See {@link Ext.form.ComboBox}.{@link Ext.form.ComboBox#getListParent getListParent} - * for an example.

      - *

      By default, Menus are absolutely positioned, floating Components. By configuring a Menu with - * {@link #floating}:false, a Menu may be used as child of a Container.

      - * - * @xtype menu - */ -Ext.menu.Menu = Ext.extend(Ext.Container, { - /** - * @cfg {Object} defaults - * A config object that will be applied to all items added to this container either via the {@link #items} - * config or via the {@link #add} method. The defaults config can contain any number of - * name/value property pairs to be added to each item, and should be valid for the types of items - * being added to the menu. - */ - /** - * @cfg {Mixed} items - * An array of items to be added to this menu. Menus may contain either {@link Ext.menu.Item menu items}, - * or general {@link Ext.Component Component}s. - */ - /** - * @cfg {Number} minWidth The minimum width of the menu in pixels (defaults to 120) - */ - minWidth : 120, - /** - * @cfg {Boolean/String} shadow True or 'sides' for the default effect, 'frame' for 4-way shadow, and 'drop' - * for bottom-right shadow (defaults to 'sides') - */ - shadow : 'sides', - /** - * @cfg {String} subMenuAlign The {@link Ext.Element#alignTo} anchor position value to use for submenus of - * this menu (defaults to 'tl-tr?') - */ - subMenuAlign : 'tl-tr?', - /** - * @cfg {String} defaultAlign The default {@link Ext.Element#alignTo} anchor position value for this menu - * relative to its element of origin (defaults to 'tl-bl?') - */ - defaultAlign : 'tl-bl?', - /** - * @cfg {Boolean} allowOtherMenus True to allow multiple menus to be displayed at the same time (defaults to false) - */ - allowOtherMenus : false, - /** - * @cfg {Boolean} ignoreParentClicks True to ignore clicks on any item in this menu that is a parent item (displays - * a submenu) so that the submenu is not dismissed when clicking the parent item (defaults to false). - */ - ignoreParentClicks : false, - /** - * @cfg {Boolean} enableScrolling True to allow the menu container to have scroller controls if the menu is too long (defaults to true). - */ - enableScrolling : true, - /** - * @cfg {Number} maxHeight The maximum height of the menu. Only applies when enableScrolling is set to True (defaults to null). - */ - maxHeight : null, - /** - * @cfg {Number} scrollIncrement The amount to scroll the menu. Only applies when enableScrolling is set to True (defaults to 24). - */ - scrollIncrement : 24, - /** - * @cfg {Boolean} showSeparator True to show the icon separator. (defaults to true). - */ - showSeparator : true, - /** - * @cfg {Array} defaultOffsets An array specifying the [x, y] offset in pixels by which to - * change the default Menu popup position after aligning according to the {@link #defaultAlign} - * configuration. Defaults to [0, 0]. - */ - defaultOffsets : [0, 0], - - /** - * @cfg {Boolean} plain - * True to remove the incised line down the left side of the menu. Defaults to false. - */ - plain : false, - - /** - * @cfg {Boolean} floating - *

      By default, a Menu configured as floating:true - * will be rendered as an {@link Ext.Layer} (an absolutely positioned, - * floating Component with zindex=15000). - * If configured as floating:false, the Menu may be - * used as child item of another Container instead of a free-floating - * {@link Ext.Layer Layer}. - */ - floating : true, - - // private - hidden : true, - - /** - * @cfg {String/Object} layout - * This class assigns a default layout (layout:'menu'). - * Developers may override this configuration option if another layout is required. - * See {@link Ext.Container#layout} for additional information. - */ - layout : 'menu', - hideMode : 'offsets', // Important for laying out Components - scrollerHeight : 8, - autoLayout : true, // Provided for backwards compat - defaultType : 'menuitem', - - initComponent : function(){ - if(Ext.isArray(this.initialConfig)){ - Ext.apply(this, {items:this.initialConfig}); - } - this.addEvents( - /** - * @event click - * Fires when this menu is clicked (or when the enter key is pressed while it is active) - * @param {Ext.menu.Menu} this - * @param {Ext.menu.Item} menuItem The menu item that was clicked - * @param {Ext.EventObject} e - */ - 'click', - /** - * @event mouseover - * Fires when the mouse is hovering over this menu - * @param {Ext.menu.Menu} this - * @param {Ext.EventObject} e - * @param {Ext.menu.Item} menuItem The menu item that was clicked - */ - 'mouseover', - /** - * @event mouseout - * Fires when the mouse exits this menu - * @param {Ext.menu.Menu} this - * @param {Ext.EventObject} e - * @param {Ext.menu.Item} menuItem The menu item that was clicked - */ - 'mouseout', - /** - * @event itemclick - * Fires when a menu item contained in this menu is clicked - * @param {Ext.menu.BaseItem} baseItem The BaseItem that was clicked - * @param {Ext.EventObject} e - */ - 'itemclick' - ); - Ext.menu.MenuMgr.register(this); - if(this.floating){ - Ext.EventManager.onWindowResize(this.hide, this); - }else{ - if(this.initialConfig.hidden !== false){ - this.hidden = false; - } - this.internalDefaults = {hideOnClick: false}; - } - Ext.menu.Menu.superclass.initComponent.call(this); - if(this.autoLayout){ - this.on({ - add: this.doLayout, - remove: this.doLayout, - scope: this - }); - } - }, - - //private - getLayoutTarget : function() { - return this.ul; - }, - - // private - onRender : function(ct, position){ - if(!ct){ - ct = Ext.getBody(); - } - - var dh = { - id: this.getId(), - cls: 'x-menu ' + ((this.floating) ? 'x-menu-floating x-layer ' : '') + (this.cls || '') + (this.plain ? ' x-menu-plain' : '') + (this.showSeparator ? '' : ' x-menu-nosep'), - style: this.style, - cn: [ - {tag: 'a', cls: 'x-menu-focus', href: '#', onclick: 'return false;', tabIndex: '-1'}, - {tag: 'ul', cls: 'x-menu-list'} - ] - }; - if(this.floating){ - this.el = new Ext.Layer({ - shadow: this.shadow, - dh: dh, - constrain: false, - parentEl: ct, - zindex:15000 - }); - }else{ - this.el = ct.createChild(dh); - } - Ext.menu.Menu.superclass.onRender.call(this, ct, position); - - if(!this.keyNav){ - this.keyNav = new Ext.menu.MenuNav(this); - } - // generic focus element - this.focusEl = this.el.child('a.x-menu-focus'); - this.ul = this.el.child('ul.x-menu-list'); - this.mon(this.ul, { - scope: this, - click: this.onClick, - mouseover: this.onMouseOver, - mouseout: this.onMouseOut - }); - if(this.enableScrolling){ - this.mon(this.el, { - scope: this, - delegate: '.x-menu-scroller', - click: this.onScroll, - mouseover: this.deactivateActive - }); - } - }, - - // private - findTargetItem : function(e){ - var t = e.getTarget('.x-menu-list-item', this.ul, true); - if(t && t.menuItemId){ - return this.items.get(t.menuItemId); - } - }, - - // private - onClick : function(e){ - var t = this.findTargetItem(e); - if(t){ - if(t.isFormField){ - this.setActiveItem(t); - }else if(t instanceof Ext.menu.BaseItem){ - if(t.menu && this.ignoreParentClicks){ - t.expandMenu(); - e.preventDefault(); - }else if(t.onClick){ - t.onClick(e); - this.fireEvent('click', this, t, e); - } - } - } - }, - - // private - setActiveItem : function(item, autoExpand){ - if(item != this.activeItem){ - this.deactivateActive(); - if((this.activeItem = item).isFormField){ - item.focus(); - }else{ - item.activate(autoExpand); - } - }else if(autoExpand){ - item.expandMenu(); - } - }, - - deactivateActive : function(){ - var a = this.activeItem; - if(a){ - if(a.isFormField){ - //Fields cannot deactivate, but Combos must collapse - if(a.collapse){ - a.collapse(); - } - }else{ - a.deactivate(); - } - delete this.activeItem; - } - }, - - // private - tryActivate : function(start, step){ - var items = this.items; - for(var i = start, len = items.length; i >= 0 && i < len; i+= step){ - var item = items.get(i); - if(!item.disabled && (item.canActivate || item.isFormField)){ - this.setActiveItem(item, false); - return item; - } - } - return false; - }, - - // private - onMouseOver : function(e){ - var t = this.findTargetItem(e); - if(t){ - if(t.canActivate && !t.disabled){ - this.setActiveItem(t, true); - } - } - this.over = true; - this.fireEvent('mouseover', this, e, t); - }, - - // private - onMouseOut : function(e){ - var t = this.findTargetItem(e); - if(t){ - if(t == this.activeItem && t.shouldDeactivate && t.shouldDeactivate(e)){ - this.activeItem.deactivate(); - delete this.activeItem; - } - } - this.over = false; - this.fireEvent('mouseout', this, e, t); - }, - - // private - onScroll : function(e, t){ - if(e){ - e.stopEvent(); - } - var ul = this.ul.dom, top = Ext.fly(t).is('.x-menu-scroller-top'); - ul.scrollTop += this.scrollIncrement * (top ? -1 : 1); - if(top ? ul.scrollTop <= 0 : ul.scrollTop + this.activeMax >= ul.scrollHeight){ - this.onScrollerOut(null, t); - } - }, - - // private - onScrollerIn : function(e, t){ - var ul = this.ul.dom, top = Ext.fly(t).is('.x-menu-scroller-top'); - if(top ? ul.scrollTop > 0 : ul.scrollTop + this.activeMax < ul.scrollHeight){ - Ext.fly(t).addClass(['x-menu-item-active', 'x-menu-scroller-active']); - } - }, - - // private - onScrollerOut : function(e, t){ - Ext.fly(t).removeClass(['x-menu-item-active', 'x-menu-scroller-active']); - }, - - /** - * If {@link #floating}=true, shows this menu relative to - * another element using {@link #showat}, otherwise uses {@link Ext.Component#show}. - * @param {Mixed} element The element to align to - * @param {String} position (optional) The {@link Ext.Element#alignTo} anchor position to use in aligning to - * the element (defaults to this.defaultAlign) - * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined) - */ - show : function(el, pos, parentMenu){ - if(this.floating){ - this.parentMenu = parentMenu; - if(!this.el){ - this.render(); - this.doLayout(false, true); - } - this.showAt(this.el.getAlignToXY(el, pos || this.defaultAlign, this.defaultOffsets), parentMenu); - }else{ - Ext.menu.Menu.superclass.show.call(this); - } - }, - - /** - * Displays this menu at a specific xy position and fires the 'show' event if a - * handler for the 'beforeshow' event does not return false cancelling the operation. - * @param {Array} xyPosition Contains X & Y [x, y] values for the position at which to show the menu (coordinates are page-based) - * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined) - */ - showAt : function(xy, parentMenu){ - if(this.fireEvent('beforeshow', this) !== false){ - this.parentMenu = parentMenu; - if(!this.el){ - this.render(); - } - if(this.enableScrolling){ - // set the position so we can figure out the constrain value. - this.el.setXY(xy); - //constrain the value, keep the y coordinate the same - this.constrainScroll(xy[1]); - xy = [this.el.adjustForConstraints(xy)[0], xy[1]]; - }else{ - //constrain to the viewport. - xy = this.el.adjustForConstraints(xy); - } - this.el.setXY(xy); - this.el.show(); - Ext.menu.Menu.superclass.onShow.call(this); - if(Ext.isIE){ - // internal event, used so we don't couple the layout to the menu - this.fireEvent('autosize', this); - if(!Ext.isIE8){ - this.el.repaint(); - } - } - this.hidden = false; - this.focus(); - this.fireEvent('show', this); - } - }, - - constrainScroll : function(y){ - var max, full = this.ul.setHeight('auto').getHeight(); - if(this.floating){ - max = this.maxHeight ? this.maxHeight : Ext.fly(this.el.dom.parentNode).getViewSize().height - y; - }else{ - max = this.getHeight(); - } - if(full > max && max > 0){ - this.activeMax = max - this.scrollerHeight * 2 - this.el.getFrameWidth('tb') - Ext.num(this.el.shadowOffset, 0); - this.ul.setHeight(this.activeMax); - this.createScrollers(); - this.el.select('.x-menu-scroller').setDisplayed(''); - }else{ - this.ul.setHeight(full); - this.el.select('.x-menu-scroller').setDisplayed('none'); - } - this.ul.dom.scrollTop = 0; - }, - - createScrollers : function(){ - if(!this.scroller){ - this.scroller = { - pos: 0, - top: this.el.insertFirst({ - tag: 'div', - cls: 'x-menu-scroller x-menu-scroller-top', - html: ' ' - }), - bottom: this.el.createChild({ - tag: 'div', - cls: 'x-menu-scroller x-menu-scroller-bottom', - html: ' ' - }) - }; - this.scroller.top.hover(this.onScrollerIn, this.onScrollerOut, this); - this.scroller.topRepeater = new Ext.util.ClickRepeater(this.scroller.top, { - listeners: { - click: this.onScroll.createDelegate(this, [null, this.scroller.top], false) - } - }); - this.scroller.bottom.hover(this.onScrollerIn, this.onScrollerOut, this); - this.scroller.bottomRepeater = new Ext.util.ClickRepeater(this.scroller.bottom, { - listeners: { - click: this.onScroll.createDelegate(this, [null, this.scroller.bottom], false) - } - }); - } - }, - - onLayout : function(){ - if(this.isVisible()){ - if(this.enableScrolling){ - this.constrainScroll(this.el.getTop()); - } - if(this.floating){ - this.el.sync(); - } - } - }, - - focus : function(){ - if(!this.hidden){ - this.doFocus.defer(50, this); - } - }, - - doFocus : function(){ - if(!this.hidden){ - this.focusEl.focus(); - } - }, - - /** - * Hides this menu and optionally all parent menus - * @param {Boolean} deep (optional) True to hide all parent menus recursively, if any (defaults to false) - */ - hide : function(deep){ - this.deepHide = deep; - Ext.menu.Menu.superclass.hide.call(this); - delete this.deepHide; - }, - - // private - onHide : function(){ - Ext.menu.Menu.superclass.onHide.call(this); - this.deactivateActive(); - if(this.el && this.floating){ - this.el.hide(); - } - var pm = this.parentMenu; - if(this.deepHide === true && pm){ - if(pm.floating){ - pm.hide(true); - }else{ - pm.deactivateActive(); - } - } - }, - - // private - lookupComponent : function(c){ - if(Ext.isString(c)){ - c = (c == 'separator' || c == '-') ? new Ext.menu.Separator() : new Ext.menu.TextItem(c); - this.applyDefaults(c); - }else{ - if(Ext.isObject(c)){ - c = this.getMenuItem(c); - }else if(c.tagName || c.el){ // element. Wrap it. - c = new Ext.BoxComponent({ - el: c - }); - } - } - return c; - }, - - applyDefaults : function(c){ - if(!Ext.isString(c)){ - c = Ext.menu.Menu.superclass.applyDefaults.call(this, c); - var d = this.internalDefaults; - if(d){ - if(c.events){ - Ext.applyIf(c.initialConfig, d); - Ext.apply(c, d); - }else{ - Ext.applyIf(c, d); - } - } - } - return c; - }, - - // private - getMenuItem : function(config){ - if(!config.isXType){ - if(!config.xtype && Ext.isBoolean(config.checked)){ - return new Ext.menu.CheckItem(config) - } - return Ext.create(config, this.defaultType); - } - return config; - }, - - /** - * Adds a separator bar to the menu - * @return {Ext.menu.Item} The menu item that was added - */ - addSeparator : function(){ - return this.add(new Ext.menu.Separator()); - }, - - /** - * Adds an {@link Ext.Element} object to the menu - * @param {Mixed} el The element or DOM node to add, or its id - * @return {Ext.menu.Item} The menu item that was added - */ - addElement : function(el){ - return this.add(new Ext.menu.BaseItem(el)); - }, - - /** - * Adds an existing object based on {@link Ext.menu.BaseItem} to the menu - * @param {Ext.menu.Item} item The menu item to add - * @return {Ext.menu.Item} The menu item that was added - */ - addItem : function(item){ - return this.add(item); - }, - - /** - * Creates a new {@link Ext.menu.Item} based an the supplied config object and adds it to the menu - * @param {Object} config A MenuItem config object - * @return {Ext.menu.Item} The menu item that was added - */ - addMenuItem : function(config){ - return this.add(this.getMenuItem(config)); - }, - - /** - * Creates a new {@link Ext.menu.TextItem} with the supplied text and adds it to the menu - * @param {String} text The text to display in the menu item - * @return {Ext.menu.Item} The menu item that was added - */ - addText : function(text){ - return this.add(new Ext.menu.TextItem(text)); - }, - - //private - onDestroy : function(){ - Ext.menu.Menu.superclass.onDestroy.call(this); - Ext.menu.MenuMgr.unregister(this); - Ext.EventManager.removeResizeListener(this.hide, this); - if(this.keyNav) { - this.keyNav.disable(); - } - var s = this.scroller; - if(s){ - Ext.destroy(s.topRepeater, s.bottomRepeater, s.top, s.bottom); - } - Ext.destroy( - this.el, - this.focusEl, - this.ul - ); - } -}); - -Ext.reg('menu', Ext.menu.Menu); - -// MenuNav is a private utility class used internally by the Menu -Ext.menu.MenuNav = Ext.extend(Ext.KeyNav, function(){ - function up(e, m){ - if(!m.tryActivate(m.items.indexOf(m.activeItem)-1, -1)){ - m.tryActivate(m.items.length-1, -1); - } - } - function down(e, m){ - if(!m.tryActivate(m.items.indexOf(m.activeItem)+1, 1)){ - m.tryActivate(0, 1); - } - } - return { - constructor : function(menu){ - Ext.menu.MenuNav.superclass.constructor.call(this, menu.el); - this.scope = this.menu = menu; - }, - - doRelay : function(e, h){ - var k = e.getKey(); -// Keystrokes within a form Field (e.g.: down in a Combo) do not navigate. Allow only TAB - if (this.menu.activeItem && this.menu.activeItem.isFormField && k != e.TAB) { - return false; - } - if(!this.menu.activeItem && e.isNavKeyPress() && k != e.SPACE && k != e.RETURN){ - this.menu.tryActivate(0, 1); - return false; - } - return h.call(this.scope || this, e, this.menu); - }, - - tab: function(e, m) { - e.stopEvent(); - if (e.shiftKey) { - up(e, m); - } else { - down(e, m); - } - }, - - up : up, - - down : down, - - right : function(e, m){ - if(m.activeItem){ - m.activeItem.expandMenu(true); - } - }, - - left : function(e, m){ - m.hide(); - if(m.parentMenu && m.parentMenu.activeItem){ - m.parentMenu.activeItem.activate(); - } - }, - - enter : function(e, m){ - if(m.activeItem){ - e.stopPropagation(); - m.activeItem.onClick(e); - m.fireEvent('click', this, m.activeItem); - return true; - } - } - }; -}()); -/** - * @class Ext.menu.MenuMgr - * Provides a common registry of all menu items on a page so that they can be easily accessed by id. - * @singleton +});/** + * @class Ext.menu.Menu + * @extends Ext.Container + *

      A menu object. This is the container to which you may add menu items. Menu can also serve as a base class + * when you want a specialized menu based off of another component (like {@link Ext.menu.DateMenu} for example).

      + *

      Menus may contain either {@link Ext.menu.Item menu items}, or general {@link Ext.Component Component}s.

      + *

      To make a contained general {@link Ext.Component Component} line up with other {@link Ext.menu.Item menu items} + * specify iconCls: 'no-icon'. This reserves a space for an icon, and indents the Component in line + * with the other menu items. See {@link Ext.form.ComboBox}.{@link Ext.form.ComboBox#getListParent getListParent} + * for an example.

      + *

      By default, Menus are absolutely positioned, floating Components. By configuring a Menu with + * {@link #floating}:false, a Menu may be used as child of a Container.

      + * + * @xtype menu */ -Ext.menu.MenuMgr = function(){ - var menus, active, groups = {}, attached = false, lastShow = new Date(); +Ext.menu.Menu = Ext.extend(Ext.Container, { + /** + * @cfg {Object} defaults + * A config object that will be applied to all items added to this container either via the {@link #items} + * config or via the {@link #add} method. The defaults config can contain any number of + * name/value property pairs to be added to each item, and should be valid for the types of items + * being added to the menu. + */ + /** + * @cfg {Mixed} items + * An array of items to be added to this menu. Menus may contain either {@link Ext.menu.Item menu items}, + * or general {@link Ext.Component Component}s. + */ + /** + * @cfg {Number} minWidth The minimum width of the menu in pixels (defaults to 120) + */ + minWidth : 120, + /** + * @cfg {Boolean/String} shadow True or 'sides' for the default effect, 'frame' for 4-way shadow, and 'drop' + * for bottom-right shadow (defaults to 'sides') + */ + shadow : 'sides', + /** + * @cfg {String} subMenuAlign The {@link Ext.Element#alignTo} anchor position value to use for submenus of + * this menu (defaults to 'tl-tr?') + */ + subMenuAlign : 'tl-tr?', + /** + * @cfg {String} defaultAlign The default {@link Ext.Element#alignTo} anchor position value for this menu + * relative to its element of origin (defaults to 'tl-bl?') + */ + defaultAlign : 'tl-bl?', + /** + * @cfg {Boolean} allowOtherMenus True to allow multiple menus to be displayed at the same time (defaults to false) + */ + allowOtherMenus : false, + /** + * @cfg {Boolean} ignoreParentClicks True to ignore clicks on any item in this menu that is a parent item (displays + * a submenu) so that the submenu is not dismissed when clicking the parent item (defaults to false). + */ + ignoreParentClicks : false, + /** + * @cfg {Boolean} enableScrolling True to allow the menu container to have scroller controls if the menu is too long (defaults to true). + */ + enableScrolling : true, + /** + * @cfg {Number} maxHeight The maximum height of the menu. Only applies when enableScrolling is set to True (defaults to null). + */ + maxHeight : null, + /** + * @cfg {Number} scrollIncrement The amount to scroll the menu. Only applies when enableScrolling is set to True (defaults to 24). + */ + scrollIncrement : 24, + /** + * @cfg {Boolean} showSeparator True to show the icon separator. (defaults to true). + */ + showSeparator : true, + /** + * @cfg {Array} defaultOffsets An array specifying the [x, y] offset in pixels by which to + * change the default Menu popup position after aligning according to the {@link #defaultAlign} + * configuration. Defaults to [0, 0]. + */ + defaultOffsets : [0, 0], - // private - called when first menu is created - function init(){ - menus = {}; - active = new Ext.util.MixedCollection(); - Ext.getDoc().addKeyListener(27, function(){ - if(active.length > 0){ - hideAll(); - } - }); - } + /** + * @cfg {Boolean} plain + * True to remove the incised line down the left side of the menu. Defaults to false. + */ + plain : false, - // private - function hideAll(){ - if(active && active.length > 0){ - var c = active.clone(); - c.each(function(m){ - m.hide(); - }); - } - } + /** + * @cfg {Boolean} floating + *

      By default, a Menu configured as floating:true + * will be rendered as an {@link Ext.Layer} (an absolutely positioned, + * floating Component with zindex=15000). + * If configured as floating:false, the Menu may be + * used as child item of another Container instead of a free-floating + * {@link Ext.Layer Layer}. + */ + floating : true, - // private - function onHide(m){ - active.remove(m); - if(active.length < 1){ - Ext.getDoc().un("mousedown", onMouseDown); - attached = false; - } - } - // private - function onShow(m){ - var last = active.last(); - lastShow = new Date(); - active.add(m); - if(!attached){ - Ext.getDoc().on("mousedown", onMouseDown); - attached = true; - } - if(m.parentMenu){ - m.getEl().setZIndex(parseInt(m.parentMenu.getEl().getStyle("z-index"), 10) + 3); - m.parentMenu.activeChild = m; - }else if(last && last.isVisible()){ - m.getEl().setZIndex(parseInt(last.getEl().getStyle("z-index"), 10) + 3); - } - } + /** + * @cfg {Number} zIndex + * zIndex to use when the menu is floating. + */ + zIndex: 15000, - // private - function onBeforeHide(m){ - if(m.activeChild){ - m.activeChild.hide(); - } + // private + hidden : true, + + /** + * @cfg {String/Object} layout + * This class assigns a default layout (layout:'menu'). + * Developers may override this configuration option if another layout is required. + * See {@link Ext.Container#layout} for additional information. + */ + layout : 'menu', + hideMode : 'offsets', // Important for laying out Components + scrollerHeight : 8, + autoLayout : true, // Provided for backwards compat + defaultType : 'menuitem', + bufferResize : false, + + initComponent : function(){ + if(Ext.isArray(this.initialConfig)){ + Ext.apply(this, {items:this.initialConfig}); + } + this.addEvents( + /** + * @event click + * Fires when this menu is clicked (or when the enter key is pressed while it is active) + * @param {Ext.menu.Menu} this + * @param {Ext.menu.Item} menuItem The menu item that was clicked + * @param {Ext.EventObject} e + */ + 'click', + /** + * @event mouseover + * Fires when the mouse is hovering over this menu + * @param {Ext.menu.Menu} this + * @param {Ext.EventObject} e + * @param {Ext.menu.Item} menuItem The menu item that was clicked + */ + 'mouseover', + /** + * @event mouseout + * Fires when the mouse exits this menu + * @param {Ext.menu.Menu} this + * @param {Ext.EventObject} e + * @param {Ext.menu.Item} menuItem The menu item that was clicked + */ + 'mouseout', + /** + * @event itemclick + * Fires when a menu item contained in this menu is clicked + * @param {Ext.menu.BaseItem} baseItem The BaseItem that was clicked + * @param {Ext.EventObject} e + */ + 'itemclick' + ); + Ext.menu.MenuMgr.register(this); + if(this.floating){ + Ext.EventManager.onWindowResize(this.hide, this); + }else{ + if(this.initialConfig.hidden !== false){ + this.hidden = false; + } + this.internalDefaults = {hideOnClick: false}; + } + Ext.menu.Menu.superclass.initComponent.call(this); + if(this.autoLayout){ + var fn = this.doLayout.createDelegate(this, []); + this.on({ + add: fn, + remove: fn + }); + } + }, + + //private + getLayoutTarget : function() { + return this.ul; + }, + + // private + onRender : function(ct, position){ + if(!ct){ + ct = Ext.getBody(); + } + + var dh = { + id: this.getId(), + cls: 'x-menu ' + ((this.floating) ? 'x-menu-floating x-layer ' : '') + (this.cls || '') + (this.plain ? ' x-menu-plain' : '') + (this.showSeparator ? '' : ' x-menu-nosep'), + style: this.style, + cn: [ + {tag: 'a', cls: 'x-menu-focus', href: '#', onclick: 'return false;', tabIndex: '-1'}, + {tag: 'ul', cls: 'x-menu-list'} + ] + }; + if(this.floating){ + this.el = new Ext.Layer({ + shadow: this.shadow, + dh: dh, + constrain: false, + parentEl: ct, + zindex: this.zIndex + }); + }else{ + this.el = ct.createChild(dh); + } + Ext.menu.Menu.superclass.onRender.call(this, ct, position); + + if(!this.keyNav){ + this.keyNav = new Ext.menu.MenuNav(this); + } + // generic focus element + this.focusEl = this.el.child('a.x-menu-focus'); + this.ul = this.el.child('ul.x-menu-list'); + this.mon(this.ul, { + scope: this, + click: this.onClick, + mouseover: this.onMouseOver, + mouseout: this.onMouseOut + }); + if(this.enableScrolling){ + this.mon(this.el, { + scope: this, + delegate: '.x-menu-scroller', + click: this.onScroll, + mouseover: this.deactivateActive + }); + } + }, + + // private + findTargetItem : function(e){ + var t = e.getTarget('.x-menu-list-item', this.ul, true); + if(t && t.menuItemId){ + return this.items.get(t.menuItemId); + } + }, + + // private + onClick : function(e){ + var t = this.findTargetItem(e); + if(t){ + if(t.isFormField){ + this.setActiveItem(t); + }else if(t instanceof Ext.menu.BaseItem){ + if(t.menu && this.ignoreParentClicks){ + t.expandMenu(); + e.preventDefault(); + }else if(t.onClick){ + t.onClick(e); + this.fireEvent('click', this, t, e); + } + } + } + }, + + // private + setActiveItem : function(item, autoExpand){ + if(item != this.activeItem){ + this.deactivateActive(); + if((this.activeItem = item).isFormField){ + item.focus(); + }else{ + item.activate(autoExpand); + } + }else if(autoExpand){ + item.expandMenu(); + } + }, + + deactivateActive : function(){ + var a = this.activeItem; + if(a){ + if(a.isFormField){ + //Fields cannot deactivate, but Combos must collapse + if(a.collapse){ + a.collapse(); + } + }else{ + a.deactivate(); + } + delete this.activeItem; + } + }, + + // private + tryActivate : function(start, step){ + var items = this.items; + for(var i = start, len = items.length; i >= 0 && i < len; i+= step){ + var item = items.get(i); + if(!item.disabled && (item.canActivate || item.isFormField)){ + this.setActiveItem(item, false); + return item; + } + } + return false; + }, + + // private + onMouseOver : function(e){ + var t = this.findTargetItem(e); + if(t){ + if(t.canActivate && !t.disabled){ + this.setActiveItem(t, true); + } + } + this.over = true; + this.fireEvent('mouseover', this, e, t); + }, + + // private + onMouseOut : function(e){ + var t = this.findTargetItem(e); + if(t){ + if(t == this.activeItem && t.shouldDeactivate && t.shouldDeactivate(e)){ + this.activeItem.deactivate(); + delete this.activeItem; + } + } + this.over = false; + this.fireEvent('mouseout', this, e, t); + }, + + // private + onScroll : function(e, t){ + if(e){ + e.stopEvent(); + } + var ul = this.ul.dom, top = Ext.fly(t).is('.x-menu-scroller-top'); + ul.scrollTop += this.scrollIncrement * (top ? -1 : 1); + if(top ? ul.scrollTop <= 0 : ul.scrollTop + this.activeMax >= ul.scrollHeight){ + this.onScrollerOut(null, t); + } + }, + + // private + onScrollerIn : function(e, t){ + var ul = this.ul.dom, top = Ext.fly(t).is('.x-menu-scroller-top'); + if(top ? ul.scrollTop > 0 : ul.scrollTop + this.activeMax < ul.scrollHeight){ + Ext.fly(t).addClass(['x-menu-item-active', 'x-menu-scroller-active']); + } + }, + + // private + onScrollerOut : function(e, t){ + Ext.fly(t).removeClass(['x-menu-item-active', 'x-menu-scroller-active']); + }, + + /** + * If {@link #floating}=true, shows this menu relative to + * another element using {@link #showat}, otherwise uses {@link Ext.Component#show}. + * @param {Mixed} element The element to align to + * @param {String} position (optional) The {@link Ext.Element#alignTo} anchor position to use in aligning to + * the element (defaults to this.defaultAlign) + * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined) + */ + show : function(el, pos, parentMenu){ + if(this.floating){ + this.parentMenu = parentMenu; + if(!this.el){ + this.render(); + this.doLayout(false, true); + } + this.showAt(this.el.getAlignToXY(el, pos || this.defaultAlign, this.defaultOffsets), parentMenu); + }else{ + Ext.menu.Menu.superclass.show.call(this); + } + }, + + /** + * Displays this menu at a specific xy position and fires the 'show' event if a + * handler for the 'beforeshow' event does not return false cancelling the operation. + * @param {Array} xyPosition Contains X & Y [x, y] values for the position at which to show the menu (coordinates are page-based) + * @param {Ext.menu.Menu} parentMenu (optional) This menu's parent menu, if applicable (defaults to undefined) + */ + showAt : function(xy, parentMenu){ + if(this.fireEvent('beforeshow', this) !== false){ + this.parentMenu = parentMenu; + if(!this.el){ + this.render(); + } + if(this.enableScrolling){ + // set the position so we can figure out the constrain value. + this.el.setXY(xy); + //constrain the value, keep the y coordinate the same + xy[1] = this.constrainScroll(xy[1]); + xy = [this.el.adjustForConstraints(xy)[0], xy[1]]; + }else{ + //constrain to the viewport. + xy = this.el.adjustForConstraints(xy); + } + this.el.setXY(xy); + this.el.show(); + Ext.menu.Menu.superclass.onShow.call(this); + if(Ext.isIE){ + // internal event, used so we don't couple the layout to the menu + this.fireEvent('autosize', this); + if(!Ext.isIE8){ + this.el.repaint(); + } + } + this.hidden = false; + this.focus(); + this.fireEvent('show', this); + } + }, + + constrainScroll : function(y){ + var max, full = this.ul.setHeight('auto').getHeight(), + returnY = y, normalY, parentEl, scrollTop, viewHeight; + if(this.floating){ + parentEl = Ext.fly(this.el.dom.parentNode); + scrollTop = parentEl.getScroll().top; + viewHeight = parentEl.getViewSize().height; + //Normalize y by the scroll position for the parent element. Need to move it into the coordinate space + //of the view. + normalY = y - scrollTop; + max = this.maxHeight ? this.maxHeight : viewHeight - normalY; + if(full > viewHeight) { + max = viewHeight; + //Set returnY equal to (0,0) in view space by reducing y by the value of normalY + returnY = y - normalY; + } else if(max < full) { + returnY = y - (full - max); + max = full; + } + }else{ + max = this.getHeight(); + } + if(full > max && max > 0){ + this.activeMax = max - this.scrollerHeight * 2 - this.el.getFrameWidth('tb') - Ext.num(this.el.shadowOffset, 0); + this.ul.setHeight(this.activeMax); + this.createScrollers(); + this.el.select('.x-menu-scroller').setDisplayed(''); + }else{ + this.ul.setHeight(full); + this.el.select('.x-menu-scroller').setDisplayed('none'); + } + this.ul.dom.scrollTop = 0; + return returnY; + }, + + createScrollers : function(){ + if(!this.scroller){ + this.scroller = { + pos: 0, + top: this.el.insertFirst({ + tag: 'div', + cls: 'x-menu-scroller x-menu-scroller-top', + html: ' ' + }), + bottom: this.el.createChild({ + tag: 'div', + cls: 'x-menu-scroller x-menu-scroller-bottom', + html: ' ' + }) + }; + this.scroller.top.hover(this.onScrollerIn, this.onScrollerOut, this); + this.scroller.topRepeater = new Ext.util.ClickRepeater(this.scroller.top, { + listeners: { + click: this.onScroll.createDelegate(this, [null, this.scroller.top], false) + } + }); + this.scroller.bottom.hover(this.onScrollerIn, this.onScrollerOut, this); + this.scroller.bottomRepeater = new Ext.util.ClickRepeater(this.scroller.bottom, { + listeners: { + click: this.onScroll.createDelegate(this, [null, this.scroller.bottom], false) + } + }); + } + }, + + onLayout : function(){ + if(this.isVisible()){ + if(this.enableScrolling){ + this.constrainScroll(this.el.getTop()); + } + if(this.floating){ + this.el.sync(); + } + } + }, + + focus : function(){ + if(!this.hidden){ + this.doFocus.defer(50, this); + } + }, + + doFocus : function(){ + if(!this.hidden){ + this.focusEl.focus(); + } + }, + + /** + * Hides this menu and optionally all parent menus + * @param {Boolean} deep (optional) True to hide all parent menus recursively, if any (defaults to false) + */ + hide : function(deep){ + if (!this.isDestroyed) { + this.deepHide = deep; + Ext.menu.Menu.superclass.hide.call(this); + delete this.deepHide; + } + }, + + // private + onHide : function(){ + Ext.menu.Menu.superclass.onHide.call(this); + this.deactivateActive(); + if(this.el && this.floating){ + this.el.hide(); + } + var pm = this.parentMenu; + if(this.deepHide === true && pm){ + if(pm.floating){ + pm.hide(true); + }else{ + pm.deactivateActive(); + } + } + }, + + // private + lookupComponent : function(c){ + if(Ext.isString(c)){ + c = (c == 'separator' || c == '-') ? new Ext.menu.Separator() : new Ext.menu.TextItem(c); + this.applyDefaults(c); + }else{ + if(Ext.isObject(c)){ + c = this.getMenuItem(c); + }else if(c.tagName || c.el){ // element. Wrap it. + c = new Ext.BoxComponent({ + el: c + }); + } + } + return c; + }, + + applyDefaults : function(c){ + if(!Ext.isString(c)){ + c = Ext.menu.Menu.superclass.applyDefaults.call(this, c); + var d = this.internalDefaults; + if(d){ + if(c.events){ + Ext.applyIf(c.initialConfig, d); + Ext.apply(c, d); + }else{ + Ext.applyIf(c, d); + } + } + } + return c; + }, + + // private + getMenuItem : function(config){ + if(!config.isXType){ + if(!config.xtype && Ext.isBoolean(config.checked)){ + return new Ext.menu.CheckItem(config) + } + return Ext.create(config, this.defaultType); + } + return config; + }, + + /** + * Adds a separator bar to the menu + * @return {Ext.menu.Item} The menu item that was added + */ + addSeparator : function(){ + return this.add(new Ext.menu.Separator()); + }, + + /** + * Adds an {@link Ext.Element} object to the menu + * @param {Mixed} el The element or DOM node to add, or its id + * @return {Ext.menu.Item} The menu item that was added + */ + addElement : function(el){ + return this.add(new Ext.menu.BaseItem({ + el: el + })); + }, + + /** + * Adds an existing object based on {@link Ext.menu.BaseItem} to the menu + * @param {Ext.menu.Item} item The menu item to add + * @return {Ext.menu.Item} The menu item that was added + */ + addItem : function(item){ + return this.add(item); + }, + + /** + * Creates a new {@link Ext.menu.Item} based an the supplied config object and adds it to the menu + * @param {Object} config A MenuItem config object + * @return {Ext.menu.Item} The menu item that was added + */ + addMenuItem : function(config){ + return this.add(this.getMenuItem(config)); + }, + + /** + * Creates a new {@link Ext.menu.TextItem} with the supplied text and adds it to the menu + * @param {String} text The text to display in the menu item + * @return {Ext.menu.Item} The menu item that was added + */ + addText : function(text){ + return this.add(new Ext.menu.TextItem(text)); + }, + + //private + onDestroy : function(){ + Ext.EventManager.removeResizeListener(this.hide, this); + var pm = this.parentMenu; + if(pm && pm.activeChild == this){ + delete pm.activeChild; + } + delete this.parentMenu; + Ext.menu.Menu.superclass.onDestroy.call(this); + Ext.menu.MenuMgr.unregister(this); + if(this.keyNav) { + this.keyNav.disable(); + } + var s = this.scroller; + if(s){ + Ext.destroy(s.topRepeater, s.bottomRepeater, s.top, s.bottom); + } + Ext.destroy( + this.el, + this.focusEl, + this.ul + ); + } +}); + +Ext.reg('menu', Ext.menu.Menu); + +// MenuNav is a private utility class used internally by the Menu +Ext.menu.MenuNav = Ext.extend(Ext.KeyNav, function(){ + function up(e, m){ + if(!m.tryActivate(m.items.indexOf(m.activeItem)-1, -1)){ + m.tryActivate(m.items.length-1, -1); + } + } + function down(e, m){ + if(!m.tryActivate(m.items.indexOf(m.activeItem)+1, 1)){ + m.tryActivate(0, 1); + } + } + return { + constructor : function(menu){ + Ext.menu.MenuNav.superclass.constructor.call(this, menu.el); + this.scope = this.menu = menu; + }, + + doRelay : function(e, h){ + var k = e.getKey(); +// Keystrokes within a form Field (e.g.: down in a Combo) do not navigate. Allow only TAB + if (this.menu.activeItem && this.menu.activeItem.isFormField && k != e.TAB) { + return false; + } + if(!this.menu.activeItem && e.isNavKeyPress() && k != e.SPACE && k != e.RETURN){ + this.menu.tryActivate(0, 1); + return false; + } + return h.call(this.scope || this, e, this.menu); + }, + + tab: function(e, m) { + e.stopEvent(); + if (e.shiftKey) { + up(e, m); + } else { + down(e, m); + } + }, + + up : up, + + down : down, + + right : function(e, m){ + if(m.activeItem){ + m.activeItem.expandMenu(true); + } + }, + + left : function(e, m){ + m.hide(); + if(m.parentMenu && m.parentMenu.activeItem){ + m.parentMenu.activeItem.activate(); + } + }, + + enter : function(e, m){ + if(m.activeItem){ + e.stopPropagation(); + m.activeItem.onClick(e); + m.fireEvent('click', this, m.activeItem); + return true; + } + } + }; +}()); +/** + * @class Ext.menu.MenuMgr + * Provides a common registry of all menu items on a page so that they can be easily accessed by id. + * @singleton + */ +Ext.menu.MenuMgr = function(){ + var menus, active, groups = {}, attached = false, lastShow = new Date(); + + // private - called when first menu is created + function init(){ + menus = {}; + active = new Ext.util.MixedCollection(); + Ext.getDoc().addKeyListener(27, function(){ + if(active.length > 0){ + hideAll(); + } + }); + } + + // private + function hideAll(){ + if(active && active.length > 0){ + var c = active.clone(); + c.each(function(m){ + m.hide(); + }); + return true; + } + return false; + } + + // private + function onHide(m){ + active.remove(m); + if(active.length < 1){ + Ext.getDoc().un("mousedown", onMouseDown); + attached = false; + } + } + + // private + function onShow(m){ + var last = active.last(); + lastShow = new Date(); + active.add(m); + if(!attached){ + Ext.getDoc().on("mousedown", onMouseDown); + attached = true; + } + if(m.parentMenu){ + m.getEl().setZIndex(parseInt(m.parentMenu.getEl().getStyle("z-index"), 10) + 3); + m.parentMenu.activeChild = m; + }else if(last && !last.isDestroyed && last.isVisible()){ + m.getEl().setZIndex(parseInt(last.getEl().getStyle("z-index"), 10) + 3); + } + } + + // private + function onBeforeHide(m){ + if(m.activeChild){ + m.activeChild.hide(); + } if(m.autoHideTimer){ clearTimeout(m.autoHideTimer); delete m.autoHideTimer; @@ -52365,9 +54694,10 @@ Ext.menu.MenuMgr = function(){ /** * Hides all menus that are currently visible + * @return {Boolean} success True if any active menus were hidden. */ hideAll : function(){ - hideAll(); + return hideAll(); }, // private @@ -52376,18 +54706,12 @@ Ext.menu.MenuMgr = function(){ init(); } menus[menu.id] = menu; - menu.on("beforehide", onBeforeHide); - menu.on("hide", onHide); - menu.on("beforeshow", onBeforeShow); - menu.on("show", onShow); - var g = menu.group; - if(g && menu.events["checkchange"]){ - if(!groups[g]){ - groups[g] = []; - } - groups[g].push(menu); - menu.on("checkchange", onCheck); - } + menu.on({ + beforehide: onBeforeHide, + hide: onHide, + beforeshow: onBeforeShow, + show: onShow + }); }, /** @@ -52418,11 +54742,6 @@ Ext.menu.MenuMgr = function(){ menu.un("hide", onHide); menu.un("beforeshow", onBeforeShow); menu.un("show", onShow); - var g = menu.group; - if(g && menu.events["checkchange"]){ - groups[g].remove(menu); - menu.un("checkchange", onCheck); - } }, // private @@ -52481,37 +54800,7 @@ Ext.menu.MenuMgr = function(){ * @param {Object} config Configuration options * @xtype menubaseitem */ -Ext.menu.BaseItem = function(config){ - Ext.menu.BaseItem.superclass.constructor.call(this, config); - - this.addEvents( - /** - * @event click - * Fires when this item is clicked - * @param {Ext.menu.BaseItem} this - * @param {Ext.EventObject} e - */ - 'click', - /** - * @event activate - * Fires when this item is activated - * @param {Ext.menu.BaseItem} this - */ - 'activate', - /** - * @event deactivate - * Fires when this item is deactivated - * @param {Ext.menu.BaseItem} this - */ - 'deactivate' - ); - - if(this.handler){ - this.on("click", this.handler, this.scope); - } -}; - -Ext.extend(Ext.menu.BaseItem, Ext.Component, { +Ext.menu.BaseItem = Ext.extend(Ext.Component, { /** * @property parentMenu * @type Ext.menu.Menu @@ -52551,6 +54840,34 @@ Ext.extend(Ext.menu.BaseItem, Ext.Component, { // private actionMode : "container", + + initComponent : function(){ + Ext.menu.BaseItem.superclass.initComponent.call(this); + this.addEvents( + /** + * @event click + * Fires when this item is clicked + * @param {Ext.menu.BaseItem} this + * @param {Ext.EventObject} e + */ + 'click', + /** + * @event activate + * Fires when this item is activated + * @param {Ext.menu.BaseItem} this + */ + 'activate', + /** + * @event deactivate + * Fires when this item is deactivated + * @param {Ext.menu.BaseItem} this + */ + 'deactivate' + ); + if(this.handler){ + this.on("click", this.handler, this.scope); + } + }, // private onRender : function(container, position){ @@ -52559,9 +54876,12 @@ Ext.extend(Ext.menu.BaseItem, Ext.Component, { this.parentMenu = this.ownerCt; }else{ this.container.addClass('x-menu-list-item'); - this.mon(this.el, 'click', this.onClick, this); - this.mon(this.el, 'mouseenter', this.activate, this); - this.mon(this.el, 'mouseleave', this.deactivate, this); + this.mon(this.el, { + scope: this, + click: this.onClick, + mouseenter: this.activate, + mouseleave: this.deactivate + }); } }, @@ -52569,7 +54889,7 @@ Ext.extend(Ext.menu.BaseItem, Ext.Component, { * Sets the function that will handle click events for this item (equivalent to passing in the {@link #handler} * config property). If an existing handler is already registered, it will be unregistered for you. * @param {Function} handler The function that should be called on click - * @param {Object} scope The scope that should be passed to the handler + * @param {Object} scope The scope (this reference) in which the handler function is executed. Defaults to this menu item. */ setHandler : function(handler, scope){ if(this.handler){ @@ -52639,14 +54959,7 @@ Ext.reg('menubaseitem', Ext.menu.BaseItem);/** * is applied as a config object (and should contain a text property). * @xtype menutextitem */ -Ext.menu.TextItem = function(cfg){ - if(typeof cfg == 'string'){ - cfg = {text: cfg} - } - Ext.menu.TextItem.superclass.constructor.call(this, cfg); -}; - -Ext.extend(Ext.menu.TextItem, Ext.menu.BaseItem, { +Ext.menu.TextItem = Ext.extend(Ext.menu.BaseItem, { /** * @cfg {String} text The text to display for this item (defaults to '') */ @@ -52658,6 +54971,13 @@ Ext.extend(Ext.menu.TextItem, Ext.menu.BaseItem, { * @cfg {String} itemCls The default CSS class to use for text items (defaults to "x-menu-text") */ itemCls : "x-menu-text", + + constructor : function(config){ + if(typeof config == 'string'){ + config = {text: config} + } + Ext.menu.TextItem.superclass.constructor.call(this, config); + }, // private onRender : function(){ @@ -52677,11 +54997,7 @@ Ext.reg('menutextitem', Ext.menu.TextItem);/** * @param {Object} config Configuration options * @xtype menuseparator */ -Ext.menu.Separator = function(config){ - Ext.menu.Separator.superclass.constructor.call(this, config); -}; - -Ext.extend(Ext.menu.Separator, Ext.menu.BaseItem, { +Ext.menu.Separator = Ext.extend(Ext.menu.BaseItem, { /** * @cfg {String} itemCls The default CSS class to use for separators (defaults to "x-menu-sep") */ @@ -52718,13 +55034,7 @@ Ext.reg('menuseparator', Ext.menu.Separator);/** * @param {Object} config Configuration options * @xtype menuitem */ -Ext.menu.Item = function(config){ - Ext.menu.Item.superclass.constructor.call(this, config); - if(this.menu){ - this.menu = Ext.menu.MenuMgr.get(this.menu); - } -}; -Ext.extend(Ext.menu.Item, Ext.menu.BaseItem, { +Ext.menu.Item = Ext.extend(Ext.menu.BaseItem, { /** * @property menu * @type Ext.menu.Menu @@ -52769,6 +55079,14 @@ Ext.extend(Ext.menu.Item, Ext.menu.BaseItem, { // private ctype: 'Ext.menu.Item', + initComponent : function(){ + Ext.menu.Item.superclass.initComponent.call(this); + if(this.menu){ + this.menu = Ext.menu.MenuMgr.get(this.menu); + this.menu.ownerCt = this; + } + }, + // private onRender : function(container, position){ if (!this.itemTpl) { @@ -52787,6 +55105,9 @@ Ext.extend(Ext.menu.Item, Ext.menu.BaseItem, { this.el = position ? this.itemTpl.insertBefore(position, a, true) : this.itemTpl.append(container, a, true); this.iconEl = this.el.child('img.x-menu-item-icon'); this.textEl = this.el.child('.x-menu-item-text'); + if(!this.href) { // if no link defined, prevent the default anchor event + this.mon(this.el, 'click', Ext.emptyFn, null, { preventDefault: true }); + } Ext.menu.Item.superclass.onRender.call(this, container, position); }, @@ -52825,10 +55146,11 @@ Ext.extend(Ext.menu.Item, Ext.menu.BaseItem, { this.iconEl.replaceClass(oldCls, this.iconCls); } }, - + //private beforeDestroy: function(){ if (this.menu){ + delete this.menu.ownerCt; this.menu.destroy(); } Ext.menu.Item.superclass.beforeDestroy.call(this); @@ -52920,37 +55242,7 @@ Ext.reg('menuitem', Ext.menu.Item);/** * @param {Object} config Configuration options * @xtype menucheckitem */ -Ext.menu.CheckItem = function(config){ - Ext.menu.CheckItem.superclass.constructor.call(this, config); - this.addEvents( - /** - * @event beforecheckchange - * Fires before the checked value is set, providing an opportunity to cancel if needed - * @param {Ext.menu.CheckItem} this - * @param {Boolean} checked The new checked value that will be set - */ - "beforecheckchange" , - /** - * @event checkchange - * Fires after the checked value has been set - * @param {Ext.menu.CheckItem} this - * @param {Boolean} checked The checked value that was set - */ - "checkchange" - ); - /** - * A function that handles the checkchange event. The function is undefined by default, but if an implementation - * is provided, it will be called automatically when the checkchange event fires. - * @param {Ext.menu.CheckItem} this - * @param {Boolean} checked The checked value that was set - * @method checkHandler - */ - if(this.checkHandler){ - this.on('checkchange', this.checkHandler, this.scope); - } - Ext.menu.MenuMgr.registerCheckable(this); -}; -Ext.extend(Ext.menu.CheckItem, Ext.menu.Item, { +Ext.menu.CheckItem = Ext.extend(Ext.menu.Item, { /** * @cfg {String} group * All check items with the same group name will automatically be grouped into a single-select @@ -52974,6 +55266,37 @@ Ext.extend(Ext.menu.CheckItem, Ext.menu.Item, { // private ctype: "Ext.menu.CheckItem", + + initComponent : function(){ + Ext.menu.CheckItem.superclass.initComponent.call(this); + this.addEvents( + /** + * @event beforecheckchange + * Fires before the checked value is set, providing an opportunity to cancel if needed + * @param {Ext.menu.CheckItem} this + * @param {Boolean} checked The new checked value that will be set + */ + "beforecheckchange" , + /** + * @event checkchange + * Fires after the checked value has been set + * @param {Ext.menu.CheckItem} this + * @param {Boolean} checked The checked value that was set + */ + "checkchange" + ); + /** + * A function that handles the checkchange event. The function is undefined by default, but if an implementation + * is provided, it will be called automatically when the checkchange event fires. + * @param {Ext.menu.CheckItem} this + * @param {Boolean} checked The checked value that was set + * @method checkHandler + */ + if(this.checkHandler){ + this.on('checkchange', this.checkHandler, this.scope); + } + Ext.menu.MenuMgr.registerCheckable(this); + }, // private onRender : function(c){ @@ -52999,12 +55322,13 @@ Ext.extend(Ext.menu.CheckItem, Ext.menu.Item, { * @param {Boolean} suppressEvent (optional) True to prevent the checkchange event from firing (defaults to false) */ setChecked : function(state, suppressEvent){ - if(this.checked != state && this.fireEvent("beforecheckchange", this, state) !== false){ + var suppress = suppressEvent === true; + if(this.checked != state && (suppress || this.fireEvent("beforecheckchange", this, state) !== false)){ if(this.container){ this.container[state ? "addClass" : "removeClass"]("x-menu-item-checked"); } this.checked = state; - if(suppressEvent !== true){ + if(!suppress){ this.fireEvent("checkchange", this, state); } } @@ -53111,6 +55435,7 @@ Ext.reg('menucheckitem', Ext.menu.CheckItem);/** * @param {Date} date The selected date */ this.relayEvents(this.picker, ['select']); + this.on('show', this.picker.focus, this.picker); this.on('select', this.menuHide, this); if(this.handler){ this.on('select', this.handler, this.scope || this); @@ -53222,7 +55547,7 @@ Ext.reg('menucheckitem', Ext.menu.CheckItem);/** * @event select * Fires when a color is selected from the {@link #palette Ext.ColorPalette} * @param {Ext.ColorPalette} palette The {@link #palette Ext.ColorPalette} - * @param {String} color The 6-digit color hex code (without the # symbol) + * @param {String} color The 6-digit color hex code (without the # symbol) */ this.relayEvents(this.palette, ['select']); this.on('select', this.menuHide, this); @@ -53248,6 +55573,12 @@ Ext.reg('colormenu', Ext.menu.ColorMenu); * @xtype field */ Ext.form.Field = Ext.extend(Ext.BoxComponent, { + /** + *

      The label Element associated with this Field. Only available after this Field has been rendered by a + * {@link form Ext.layout.FormLayout} layout manager.

      + * @type Ext.Element + * @property label + */ /** * @cfg {String} inputType The type attribute for input fields -- e.g. radio, text, password, file (defaults * to 'text'). The types 'file' and 'password' must be used to render those field types currently -- there are @@ -53314,17 +55645,16 @@ Ext.form.Field = Ext.extend(Ext.BoxComponent, { */ fieldClass : 'x-form-field', /** - * @cfg {String} msgTarget The location where error text should display. Should be one of the following values - * (defaults to 'qtip'): - *
      -Value         Description
      ------------   ----------------------------------------------------------------------
      -qtip          Display a quick tip when the user hovers over the field
      -title         Display a default browser title attribute popup
      -under         Add a block div beneath the field containing the error text
      -side          Add an error icon to the right of the field with a popup on hover
      -[element id]  Add the error text directly to the innerHTML of the specified element
      -
      + * @cfg {String} msgTarget

      The location where the message text set through {@link #markInvalid} should display. + * Must be one of the following values:

      + *
        + *
      • qtip Display a quick tip containing the message when the user hovers over the field. This is the default. + *
        {@link Ext.QuickTips#init Ext.QuickTips.init} must have been called for this setting to work. + *
      • title Display the message in a default browser title attribute popup.
      • + *
      • under Add a block div beneath the field containing the error message.
      • + *
      • side Add an error icon to the right of the field, displaying the message in a popup on hover.
      • + *
      • [element id] Add the error message directly to the innerHTML of the specified element.
      • + *
      */ msgTarget : 'qtip', /** @@ -53348,6 +55678,11 @@ side Add an error icon to the right of the field with a popup on hover * disabled Fields will not be {@link Ext.form.BasicForm#submit submitted}.

      */ disabled : false, + /** + * @cfg {Boolean} submitValue False to clear the name attribute on the field so that it is not submitted during a form post. + * Defaults to true. + */ + submitValue: true, // private isFormField : true, @@ -53455,7 +55790,9 @@ var form = new Ext.form.FormPanel({ this.autoEl = cfg; } Ext.form.Field.superclass.onRender.call(this, ct, position); - + if(this.submitValue === false){ + this.el.dom.removeAttribute('name'); + } var type = this.el.dom.type; if(type){ if(type == 'password'){ @@ -53464,7 +55801,7 @@ var form = new Ext.form.FormPanel({ this.el.addClass('x-form-'+type); } if(this.readOnly){ - this.el.dom.readOnly = true; + this.setReadOnly(true); } if(this.tabIndex !== undefined){ this.el.dom.setAttribute('tabIndex', this.tabIndex); @@ -53512,6 +55849,17 @@ var form = new Ext.form.FormPanel({ return String(this.getValue()) !== String(this.originalValue); }, + /** + * Sets the read only state of this field. + * @param {Boolean} readOnly Whether the field should be read only. + */ + setReadOnly : function(readOnly){ + if(this.rendered){ + this.el.dom.readOnly = readOnly; + } + this.readOnly = readOnly; + }, + // private afterRender : function(){ Ext.form.Field.superclass.afterRender.call(this); @@ -53556,6 +55904,13 @@ var form = new Ext.form.FormPanel({ } if(!this.hasFocus){ this.hasFocus = true; + /** + *

      The value that the Field had at the time it was last focused. This is the value that is passed + * to the {@link #change} event which is fired if the value has been changed when the Field is blurred.

      + *

      This will be undefined until the Field has been visited. Compare {@link #originalValue}.

      + * @type mixed + * @property startValue + */ this.startValue = this.getValue(); this.fireEvent('focus', this); } @@ -53571,7 +55926,7 @@ var form = new Ext.form.FormPanel({ this.el.removeClass(this.focusClass); } this.hasFocus = false; - if(this.validationEvent !== false && (this.validateOnBlur || this.validationEvent != 'blur')){ + if(this.validationEvent !== false && (this.validateOnBlur || this.validationEvent == 'blur')){ this.validate(); } var v = this.getValue(); @@ -53636,9 +55991,19 @@ var form = new Ext.form.FormPanel({ }, /** - * Mark this field as invalid, using {@link #msgTarget} to determine how to - * display the error and applying {@link #invalidClass} to the field's element. - * Note: this method does not actually make the field + * Gets the active error message for this field. + * @return {String} Returns the active error message on the field, if there is no error, an empty string is returned. + */ + getActiveError : function(){ + return this.activeError || ''; + }, + + /** + *

      Display an error message associated with this field, using {@link #msgTarget} to determine how to + * display the message and applying {@link #invalidClass} to the field's UI element.

      + *

      Note: this method does not cause the Field's {@link #validate} method to return false + * if the value does pass validation. So simply marking a Field as invalid will not prevent + * submission of forms submitted with the {@link Ext.form.Action.Submit#clientValidation} option set.

      * {@link #isValid invalid}. * @param {String} msg (optional) The validation message (defaults to {@link #invalidText}) */ @@ -53659,6 +56024,7 @@ var form = new Ext.form.FormPanel({ t.style.display = this.msgDisplay; } } + this.activeError = msg; this.fireEvent('invalid', this, msg); }, @@ -53681,6 +56047,7 @@ var form = new Ext.form.FormPanel({ t.style.display = 'none'; } } + delete this.activeError; this.fireEvent('valid', this); }, @@ -53695,7 +56062,12 @@ var form = new Ext.form.FormPanel({ this.el.findParent('.x-form-field-wrap', 5, true); // else direct field wrap }, - // private + // Alignment for 'under' target + alignErrorEl : function(){ + this.errorEl.setWidth(this.getErrorCt().getWidth(true) - 20); + }, + + // Alignment for 'side' target alignErrorIcon : function(){ this.errorIcon.alignTo(this.el, 'tl-tr', [2, 0]); }, @@ -53802,8 +56174,12 @@ Ext.form.MessageTargets = { return; } field.errorEl = elp.createChild({cls:'x-form-invalid-msg'}); - field.errorEl.setWidth(elp.getWidth(true)-20); + field.on('resize', field.alignErrorEl, field); + field.on('destroy', function(){ + Ext.destroy(this.errorEl); + }, field); } + field.alignErrorEl(); field.errorEl.update(msg); Ext.form.Field.msgFx[field.msgFx].show(field.errorEl, field); }, @@ -53826,19 +56202,21 @@ Ext.form.MessageTargets = { return; } field.errorIcon = elp.createChild({cls:'x-form-invalid-icon'}); + field.on('resize', field.alignErrorIcon, field); + field.on('destroy', function(){ + Ext.destroy(this.errorIcon); + }, field); } field.alignErrorIcon(); field.errorIcon.dom.qtip = msg; field.errorIcon.dom.qclass = 'x-form-invalid-tip'; field.errorIcon.show(); - field.on('resize', field.alignErrorIcon, field); }, clear: function(field){ field.el.removeClass(field.invalidClass); if(field.errorIcon){ field.errorIcon.dom.qtip = ''; field.errorIcon.hide(); - field.un('resize', field.alignErrorIcon, field); }else{ field.el.dom.title = ''; } @@ -54155,10 +56533,15 @@ var myField = new Ext.form.NumberField({ // private onKeyUpBuffered : function(e){ - if(!e.isNavKeyPress()){ + if(this.doAutoSize(e)){ this.autoSize(); } }, + + // private + doAutoSize : function(e){ + return !e.isNavKeyPress(); + }, // private onKeyUp : function(e){ @@ -54213,12 +56596,18 @@ var myField = new Ext.form.NumberField({ // private filterKeys : function(e){ - // special keys don't generate charCodes, so leave them alone - if(e.ctrlKey || e.isSpecialKey()){ + if(e.ctrlKey){ return; } - - if(!this.maskRe.test(String.fromCharCode(e.getCharCode()))){ + var k = e.getKey(); + if(Ext.isGecko && (e.isNavKeyPress() || k == e.BACKSPACE || (k == e.DELETE && e.button == -1))){ + return; + } + var cc = String.fromCharCode(e.getCharCode()); + if(!Ext.isGecko && e.isSpecialKey() && !cc){ + return; + } + if(!this.maskRe.test(cc)){ e.stopEvent(); } }, @@ -54385,8 +56774,8 @@ var myField = new Ext.form.NumberField({ var d = document.createElement('div'); d.appendChild(document.createTextNode(v)); v = d.innerHTML; - d = null; Ext.removeNode(d); + d = null; v += ' '; var w = Math.min(this.growMax, Math.max(this.metrics.getWidth(v) + /* add extra padding */ 10, this.growMin)); this.el.setWidth(w); @@ -54402,348 +56791,390 @@ var myField = new Ext.form.NumberField({ } }); Ext.reg('textfield', Ext.form.TextField); -/** - * @class Ext.form.TriggerField - * @extends Ext.form.TextField - * Provides a convenient wrapper for TextFields that adds a clickable trigger button (looks like a combobox by default). - * The trigger has no default action, so you must assign a function to implement the trigger click handler by - * overriding {@link #onTriggerClick}. You can create a TriggerField directly, as it renders exactly like a combobox - * for which you can provide a custom implementation. For example: - *
      
      -var trigger = new Ext.form.TriggerField();
      -trigger.onTriggerClick = myTriggerFn;
      -trigger.applyToMarkup('my-field');
      -
      - * - * However, in general you will most likely want to use TriggerField as the base class for a reusable component. - * {@link Ext.form.DateField} and {@link Ext.form.ComboBox} are perfect examples of this. - * - * @constructor - * Create a new TriggerField. - * @param {Object} config Configuration options (valid {@Ext.form.TextField} config options will also be applied - * to the base TextField) - * @xtype trigger - */ -Ext.form.TriggerField = Ext.extend(Ext.form.TextField, { - /** - * @cfg {String} triggerClass - * An additional CSS class used to style the trigger button. The trigger will always get the - * class 'x-form-trigger' by default and triggerClass will be appended if specified. - */ - /** - * @cfg {Mixed} triggerConfig - *

      A {@link Ext.DomHelper DomHelper} config object specifying the structure of the - * trigger element for this Field. (Optional).

      - *

      Specify this when you need a customized element to act as the trigger button for a TriggerField.

      - *

      Note that when using this option, it is the developer's responsibility to ensure correct sizing, positioning - * and appearance of the trigger. Defaults to:

      - *
      {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " + this.triggerClass}
      - */ - /** - * @cfg {String/Object} autoCreate

      A {@link Ext.DomHelper DomHelper} element spec, or true for a default - * element spec. Used to create the {@link Ext.Component#getEl Element} which will encapsulate this Component. - * See {@link Ext.Component#autoEl autoEl} for details. Defaults to:

      - *
      {tag: "input", type: "text", size: "16", autocomplete: "off"}
      - */ - defaultAutoCreate : {tag: "input", type: "text", size: "16", autocomplete: "off"}, - /** - * @cfg {Boolean} hideTrigger true to hide the trigger element and display only the base - * text field (defaults to false) - */ - hideTrigger:false, - /** - * @cfg {Boolean} editable false to prevent the user from typing text directly into the field, - * the field will only respond to a click on the trigger to set the value. (defaults to true) - */ - editable: true, - /** - * @cfg {String} wrapFocusClass The class added to the to the wrap of the trigger element. Defaults to - * x-trigger-wrap-focus. - */ - wrapFocusClass: 'x-trigger-wrap-focus', - /** - * @hide - * @method autoSize - */ - autoSize: Ext.emptyFn, - // private - monitorTab : true, - // private - deferHeight : true, - // private - mimicing : false, - - actionMode: 'wrap', - - defaultTriggerWidth: 17, - - // private - onResize : function(w, h){ - Ext.form.TriggerField.superclass.onResize.call(this, w, h); - var tw = this.getTriggerWidth(); - if(Ext.isNumber(w)){ - this.el.setWidth(w - tw); - } - this.wrap.setWidth(this.el.getWidth() + tw); - }, - - getTriggerWidth: function(){ - var tw = this.trigger.getWidth(); - if(!this.hideTrigger && tw === 0){ - tw = this.defaultTriggerWidth; - } - return tw; - }, - - // private - alignErrorIcon : function(){ - if(this.wrap){ - this.errorIcon.alignTo(this.wrap, 'tl-tr', [2, 0]); - } - }, - - // private - onRender : function(ct, position){ - this.doc = Ext.isIE ? Ext.getBody() : Ext.getDoc(); - Ext.form.TriggerField.superclass.onRender.call(this, ct, position); - - this.wrap = this.el.wrap({cls: 'x-form-field-wrap x-form-field-trigger-wrap'}); - this.trigger = this.wrap.createChild(this.triggerConfig || - {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " + this.triggerClass}); - if(this.hideTrigger){ - this.trigger.setDisplayed(false); - } - this.initTrigger(); - if(!this.width){ - this.wrap.setWidth(this.el.getWidth()+this.trigger.getWidth()); - } - if(!this.editable){ - this.editable = true; - this.setEditable(false); - } - this.resizeEl = this.positionEl = this.wrap; - }, - - afterRender : function(){ - Ext.form.TriggerField.superclass.afterRender.call(this); - }, - - // private - initTrigger : function(){ - this.mon(this.trigger, 'click', this.onTriggerClick, this, {preventDefault:true}); - this.trigger.addClassOnOver('x-form-trigger-over'); - this.trigger.addClassOnClick('x-form-trigger-click'); - }, - - // private - onDestroy : function(){ - Ext.destroy(this.trigger, this.wrap); - if (this.mimicing){ - this.doc.un('mousedown', this.mimicBlur, this); - } - Ext.form.TriggerField.superclass.onDestroy.call(this); - }, - - // private - onFocus : function(){ - Ext.form.TriggerField.superclass.onFocus.call(this); - if(!this.mimicing){ - this.wrap.addClass(this.wrapFocusClass); - this.mimicing = true; - this.doc.on('mousedown', this.mimicBlur, this, {delay: 10}); - if(this.monitorTab){ - this.on('specialkey', this.checkTab, this); - } - } - }, - - // private - checkTab : function(me, e){ - if(e.getKey() == e.TAB){ - this.triggerBlur(); - } - }, - - // private - onBlur : Ext.emptyFn, - - // private - mimicBlur : function(e){ - if(!this.isDestroyed && !this.wrap.contains(e.target) && this.validateBlur(e)){ - this.triggerBlur(); - } - }, - - // private - triggerBlur : function(){ - this.mimicing = false; - this.doc.un('mousedown', this.mimicBlur, this); - if(this.monitorTab && this.el){ - this.un('specialkey', this.checkTab, this); - } - Ext.form.TriggerField.superclass.onBlur.call(this); - if(this.wrap){ - this.wrap.removeClass(this.wrapFocusClass); - } - }, - - beforeBlur : Ext.emptyFn, - - /** - * Allow or prevent the user from directly editing the field text. If false is passed, - * the user will only be able to modify the field using the trigger. This method - * is the runtime equivalent of setting the 'editable' config option at config time. - * @param {Boolean} value True to allow the user to directly edit the field text - */ - setEditable : function(value){ - if(value == this.editable){ - return; - } - this.editable = value; - if(!value){ - this.el.addClass('x-trigger-noedit').on('click', this.onTriggerClick, this).dom.setAttribute('readOnly', true); - }else{ - this.el.removeClass('x-trigger-noedit').un('click', this.onTriggerClick, this).dom.removeAttribute('readOnly'); - } - }, - - // private - // This should be overriden by any subclass that needs to check whether or not the field can be blurred. - validateBlur : function(e){ - return true; - }, - - /** - * The function that should handle the trigger's click event. This method does nothing by default - * until overridden by an implementing function. See Ext.form.ComboBox and Ext.form.DateField for - * sample implementations. - * @method - * @param {EventObject} e - */ - onTriggerClick : Ext.emptyFn - - /** - * @cfg {Boolean} grow @hide - */ - /** - * @cfg {Number} growMin @hide - */ - /** - * @cfg {Number} growMax @hide - */ -}); - -/** - * @class Ext.form.TwinTriggerField - * @extends Ext.form.TriggerField - * TwinTriggerField is not a public class to be used directly. It is meant as an abstract base class - * to be extended by an implementing class. For an example of implementing this class, see the custom - * SearchField implementation here: - * http://extjs.com/deploy/ext/examples/form/custom.html - */ -Ext.form.TwinTriggerField = Ext.extend(Ext.form.TriggerField, { - /** - * @cfg {Mixed} triggerConfig - *

      A {@link Ext.DomHelper DomHelper} config object specifying the structure of the trigger elements - * for this Field. (Optional).

      - *

      Specify this when you need a customized element to contain the two trigger elements for this Field. - * Each trigger element must be marked by the CSS class x-form-trigger (also see - * {@link #trigger1Class} and {@link #trigger2Class}).

      - *

      Note that when using this option, it is the developer's responsibility to ensure correct sizing, - * positioning and appearance of the triggers.

      - */ - /** - * @cfg {String} trigger1Class - * An additional CSS class used to style the trigger button. The trigger will always get the - * class 'x-form-trigger' by default and triggerClass will be appended if specified. - */ - /** - * @cfg {String} trigger2Class - * An additional CSS class used to style the trigger button. The trigger will always get the - * class 'x-form-trigger' by default and triggerClass will be appended if specified. - */ - - initComponent : function(){ - Ext.form.TwinTriggerField.superclass.initComponent.call(this); - - this.triggerConfig = { - tag:'span', cls:'x-form-twin-triggers', cn:[ - {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " + this.trigger1Class}, - {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " + this.trigger2Class} - ]}; - }, - - getTrigger : function(index){ - return this.triggers[index]; - }, - - initTrigger : function(){ - var ts = this.trigger.select('.x-form-trigger', true); - var triggerField = this; - ts.each(function(t, all, index){ - var triggerIndex = 'Trigger'+(index+1); - t.hide = function(){ - var w = triggerField.wrap.getWidth(); - this.dom.style.display = 'none'; - triggerField.el.setWidth(w-triggerField.trigger.getWidth()); - this['hidden' + triggerIndex] = true; - }; - t.show = function(){ - var w = triggerField.wrap.getWidth(); - this.dom.style.display = ''; - triggerField.el.setWidth(w-triggerField.trigger.getWidth()); - this['hidden' + triggerIndex] = false; - }; - - if(this['hide'+triggerIndex]){ - t.dom.style.display = 'none'; - this['hidden' + triggerIndex] = true; - } - this.mon(t, 'click', this['on'+triggerIndex+'Click'], this, {preventDefault:true}); - t.addClassOnOver('x-form-trigger-over'); - t.addClassOnClick('x-form-trigger-click'); - }, this); - this.triggers = ts.elements; - }, - - getTriggerWidth: function(){ - var tw = 0; - Ext.each(this.triggers, function(t, index){ - var triggerIndex = 'Trigger' + (index + 1), - w = t.getWidth(); - if(w === 0 && !this['hidden' + triggerIndex]){ - tw += this.defaultTriggerWidth; - }else{ - tw += w; - } - }, this); - return tw; - }, - - // private - onDestroy : function() { - Ext.destroy(this.triggers); - Ext.form.TwinTriggerField.superclass.onDestroy.call(this); - }, - - /** - * The function that should handle the trigger's click event. This method does nothing by default - * until overridden by an implementing function. See {@link Ext.form.TriggerField#onTriggerClick} - * for additional information. - * @method - * @param {EventObject} e - */ - onTrigger1Click : Ext.emptyFn, - /** - * The function that should handle the trigger's click event. This method does nothing by default - * until overridden by an implementing function. See {@link Ext.form.TriggerField#onTriggerClick} - * for additional information. - * @method - * @param {EventObject} e - */ - onTrigger2Click : Ext.emptyFn -}); -Ext.reg('trigger', Ext.form.TriggerField);/** +/** + * @class Ext.form.TriggerField + * @extends Ext.form.TextField + * Provides a convenient wrapper for TextFields that adds a clickable trigger button (looks like a combobox by default). + * The trigger has no default action, so you must assign a function to implement the trigger click handler by + * overriding {@link #onTriggerClick}. You can create a TriggerField directly, as it renders exactly like a combobox + * for which you can provide a custom implementation. For example: + *
      
      +var trigger = new Ext.form.TriggerField();
      +trigger.onTriggerClick = myTriggerFn;
      +trigger.applyToMarkup('my-field');
      +
      + * + * However, in general you will most likely want to use TriggerField as the base class for a reusable component. + * {@link Ext.form.DateField} and {@link Ext.form.ComboBox} are perfect examples of this. + * + * @constructor + * Create a new TriggerField. + * @param {Object} config Configuration options (valid {@Ext.form.TextField} config options will also be applied + * to the base TextField) + * @xtype trigger + */ +Ext.form.TriggerField = Ext.extend(Ext.form.TextField, { + /** + * @cfg {String} triggerClass + * An additional CSS class used to style the trigger button. The trigger will always get the + * class 'x-form-trigger' by default and triggerClass will be appended if specified. + */ + /** + * @cfg {Mixed} triggerConfig + *

      A {@link Ext.DomHelper DomHelper} config object specifying the structure of the + * trigger element for this Field. (Optional).

      + *

      Specify this when you need a customized element to act as the trigger button for a TriggerField.

      + *

      Note that when using this option, it is the developer's responsibility to ensure correct sizing, positioning + * and appearance of the trigger. Defaults to:

      + *
      {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " + this.triggerClass}
      + */ + /** + * @cfg {String/Object} autoCreate

      A {@link Ext.DomHelper DomHelper} element spec, or true for a default + * element spec. Used to create the {@link Ext.Component#getEl Element} which will encapsulate this Component. + * See {@link Ext.Component#autoEl autoEl} for details. Defaults to:

      + *
      {tag: "input", type: "text", size: "16", autocomplete: "off"}
      + */ + defaultAutoCreate : {tag: "input", type: "text", size: "16", autocomplete: "off"}, + /** + * @cfg {Boolean} hideTrigger true to hide the trigger element and display only the base + * text field (defaults to false) + */ + hideTrigger:false, + /** + * @cfg {Boolean} editable false to prevent the user from typing text directly into the field, + * the field will only respond to a click on the trigger to set the value. (defaults to true). + */ + editable: true, + /** + * @cfg {Boolean} readOnly true to prevent the user from changing the field, and + * hides the trigger. Superceeds the editable and hideTrigger options if the value is true. + * (defaults to false) + */ + readOnly: false, + /** + * @cfg {String} wrapFocusClass The class added to the to the wrap of the trigger element. Defaults to + * x-trigger-wrap-focus. + */ + wrapFocusClass: 'x-trigger-wrap-focus', + /** + * @hide + * @method autoSize + */ + autoSize: Ext.emptyFn, + // private + monitorTab : true, + // private + deferHeight : true, + // private + mimicing : false, + + actionMode: 'wrap', + + defaultTriggerWidth: 17, + + // private + onResize : function(w, h){ + Ext.form.TriggerField.superclass.onResize.call(this, w, h); + var tw = this.getTriggerWidth(); + if(Ext.isNumber(w)){ + this.el.setWidth(w - tw); + } + this.wrap.setWidth(this.el.getWidth() + tw); + }, + + getTriggerWidth: function(){ + var tw = this.trigger.getWidth(); + if(!this.hideTrigger && tw === 0){ + tw = this.defaultTriggerWidth; + } + return tw; + }, + + // private + alignErrorIcon : function(){ + if(this.wrap){ + this.errorIcon.alignTo(this.wrap, 'tl-tr', [2, 0]); + } + }, + + // private + onRender : function(ct, position){ + this.doc = Ext.isIE ? Ext.getBody() : Ext.getDoc(); + Ext.form.TriggerField.superclass.onRender.call(this, ct, position); + + this.wrap = this.el.wrap({cls: 'x-form-field-wrap x-form-field-trigger-wrap'}); + this.trigger = this.wrap.createChild(this.triggerConfig || + {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " + this.triggerClass}); + this.initTrigger(); + if(!this.width){ + this.wrap.setWidth(this.el.getWidth()+this.trigger.getWidth()); + } + this.resizeEl = this.positionEl = this.wrap; + }, + + updateEditState: function(){ + if(this.rendered){ + if (this.readOnly) { + this.el.dom.readOnly = true; + this.el.addClass('x-trigger-noedit'); + this.mun(this.el, 'click', this.onTriggerClick, this); + this.trigger.setDisplayed(false); + } else { + if (!this.editable) { + this.el.dom.readOnly = true; + this.el.addClass('x-trigger-noedit'); + this.mon(this.el, 'click', this.onTriggerClick, this); + } else { + this.el.dom.readOnly = false; + this.el.removeClass('x-trigger-noedit'); + this.mun(this.el, 'click', this.onTriggerClick, this); + } + this.trigger.setDisplayed(!this.hideTrigger); + } + this.onResize(this.width || this.wrap.getWidth()); + } + }, + + setHideTrigger: function(hideTrigger){ + if(hideTrigger != this.hideTrigger){ + this.hideTrigger = hideTrigger; + this.updateEditState(); + } + }, + + /** + * @param {Boolean} value True to allow the user to directly edit the field text + * Allow or prevent the user from directly editing the field text. If false is passed, + * the user will only be able to modify the field using the trigger. Will also add + * a click event to the text field which will call the trigger. This method + * is the runtime equivalent of setting the 'editable' config option at config time. + */ + setEditable: function(editable){ + if(editable != this.editable){ + this.editable = editable; + this.updateEditState(); + } + }, + + /** + * @param {Boolean} value True to prevent the user changing the field and explicitly + * hide the trigger. + * Setting this to true will superceed settings editable and hideTrigger. + * Setting this to false will defer back to editable and hideTrigger. This method + * is the runtime equivalent of setting the 'readOnly' config option at config time. + */ + setReadOnly: function(readOnly){ + if(readOnly != this.readOnly){ + this.readOnly = readOnly; + this.updateEditState(); + } + }, + + afterRender : function(){ + Ext.form.TriggerField.superclass.afterRender.call(this); + this.updateEditState(); + }, + + // private + initTrigger : function(){ + this.mon(this.trigger, 'click', this.onTriggerClick, this, {preventDefault:true}); + this.trigger.addClassOnOver('x-form-trigger-over'); + this.trigger.addClassOnClick('x-form-trigger-click'); + }, + + // private + onDestroy : function(){ + Ext.destroy(this.trigger, this.wrap); + if (this.mimicing){ + this.doc.un('mousedown', this.mimicBlur, this); + } + delete this.doc; + Ext.form.TriggerField.superclass.onDestroy.call(this); + }, + + // private + onFocus : function(){ + Ext.form.TriggerField.superclass.onFocus.call(this); + if(!this.mimicing){ + this.wrap.addClass(this.wrapFocusClass); + this.mimicing = true; + this.doc.on('mousedown', this.mimicBlur, this, {delay: 10}); + if(this.monitorTab){ + this.on('specialkey', this.checkTab, this); + } + } + }, + + // private + checkTab : function(me, e){ + if(e.getKey() == e.TAB){ + this.triggerBlur(); + } + }, + + // private + onBlur : Ext.emptyFn, + + // private + mimicBlur : function(e){ + if(!this.isDestroyed && !this.wrap.contains(e.target) && this.validateBlur(e)){ + this.triggerBlur(); + } + }, + + // private + triggerBlur : function(){ + this.mimicing = false; + this.doc.un('mousedown', this.mimicBlur, this); + if(this.monitorTab && this.el){ + this.un('specialkey', this.checkTab, this); + } + Ext.form.TriggerField.superclass.onBlur.call(this); + if(this.wrap){ + this.wrap.removeClass(this.wrapFocusClass); + } + }, + + beforeBlur : Ext.emptyFn, + + // private + // This should be overriden by any subclass that needs to check whether or not the field can be blurred. + validateBlur : function(e){ + return true; + }, + + /** + * The function that should handle the trigger's click event. This method does nothing by default + * until overridden by an implementing function. See Ext.form.ComboBox and Ext.form.DateField for + * sample implementations. + * @method + * @param {EventObject} e + */ + onTriggerClick : Ext.emptyFn + + /** + * @cfg {Boolean} grow @hide + */ + /** + * @cfg {Number} growMin @hide + */ + /** + * @cfg {Number} growMax @hide + */ +}); + +/** + * @class Ext.form.TwinTriggerField + * @extends Ext.form.TriggerField + * TwinTriggerField is not a public class to be used directly. It is meant as an abstract base class + * to be extended by an implementing class. For an example of implementing this class, see the custom + * SearchField implementation here: + * http://extjs.com/deploy/ext/examples/form/custom.html + */ +Ext.form.TwinTriggerField = Ext.extend(Ext.form.TriggerField, { + /** + * @cfg {Mixed} triggerConfig + *

      A {@link Ext.DomHelper DomHelper} config object specifying the structure of the trigger elements + * for this Field. (Optional).

      + *

      Specify this when you need a customized element to contain the two trigger elements for this Field. + * Each trigger element must be marked by the CSS class x-form-trigger (also see + * {@link #trigger1Class} and {@link #trigger2Class}).

      + *

      Note that when using this option, it is the developer's responsibility to ensure correct sizing, + * positioning and appearance of the triggers.

      + */ + /** + * @cfg {String} trigger1Class + * An additional CSS class used to style the trigger button. The trigger will always get the + * class 'x-form-trigger' by default and triggerClass will be appended if specified. + */ + /** + * @cfg {String} trigger2Class + * An additional CSS class used to style the trigger button. The trigger will always get the + * class 'x-form-trigger' by default and triggerClass will be appended if specified. + */ + + initComponent : function(){ + Ext.form.TwinTriggerField.superclass.initComponent.call(this); + + this.triggerConfig = { + tag:'span', cls:'x-form-twin-triggers', cn:[ + {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " + this.trigger1Class}, + {tag: "img", src: Ext.BLANK_IMAGE_URL, cls: "x-form-trigger " + this.trigger2Class} + ]}; + }, + + getTrigger : function(index){ + return this.triggers[index]; + }, + + initTrigger : function(){ + var ts = this.trigger.select('.x-form-trigger', true); + var triggerField = this; + ts.each(function(t, all, index){ + var triggerIndex = 'Trigger'+(index+1); + t.hide = function(){ + var w = triggerField.wrap.getWidth(); + this.dom.style.display = 'none'; + triggerField.el.setWidth(w-triggerField.trigger.getWidth()); + this['hidden' + triggerIndex] = true; + }; + t.show = function(){ + var w = triggerField.wrap.getWidth(); + this.dom.style.display = ''; + triggerField.el.setWidth(w-triggerField.trigger.getWidth()); + this['hidden' + triggerIndex] = false; + }; + + if(this['hide'+triggerIndex]){ + t.dom.style.display = 'none'; + this['hidden' + triggerIndex] = true; + } + this.mon(t, 'click', this['on'+triggerIndex+'Click'], this, {preventDefault:true}); + t.addClassOnOver('x-form-trigger-over'); + t.addClassOnClick('x-form-trigger-click'); + }, this); + this.triggers = ts.elements; + }, + + getTriggerWidth: function(){ + var tw = 0; + Ext.each(this.triggers, function(t, index){ + var triggerIndex = 'Trigger' + (index + 1), + w = t.getWidth(); + if(w === 0 && !this['hidden' + triggerIndex]){ + tw += this.defaultTriggerWidth; + }else{ + tw += w; + } + }, this); + return tw; + }, + + // private + onDestroy : function() { + Ext.destroy(this.triggers); + Ext.form.TwinTriggerField.superclass.onDestroy.call(this); + }, + + /** + * The function that should handle the trigger's click event. This method does nothing by default + * until overridden by an implementing function. See {@link Ext.form.TriggerField#onTriggerClick} + * for additional information. + * @method + * @param {EventObject} e + */ + onTrigger1Click : Ext.emptyFn, + /** + * The function that should handle the trigger's click event. This method does nothing by default + * until overridden by an implementing function. See {@link Ext.form.TriggerField#onTriggerClick} + * for additional information. + * @method + * @param {EventObject} e + */ + onTrigger2Click : Ext.emptyFn +}); +Ext.reg('trigger', Ext.form.TriggerField); +/** * @class Ext.form.TextArea * @extends Ext.form.TextField * Multiline text field. Can be used as a direct replacement for traditional textarea fields, plus adds @@ -54765,7 +57196,6 @@ Ext.form.TextArea = Ext.extend(Ext.form.TextField, { */ growMax: 1000, growAppend : ' \n ', - growPad : Ext.isWebKit ? -6 : 0, enterIsSpecial : false, @@ -54804,7 +57234,7 @@ Ext.form.TextArea = Ext.extend(Ext.form.TextField, { }, onDestroy : function(){ - Ext.destroy(this.textSizeEl); + Ext.removeNode(this.textSizeEl); Ext.form.TextArea.superclass.onDestroy.call(this); }, @@ -54813,13 +57243,10 @@ Ext.form.TextArea = Ext.extend(Ext.form.TextField, { this.fireEvent("specialkey", this, e); } }, - + // private - onKeyUp : function(e){ - if(!e.isNavKeyPress() || e.getKey() == e.ENTER){ - this.autoSize(); - } - Ext.form.TextArea.superclass.onKeyUp.call(this, e); + doAutoSize : function(e){ + return !e.isNavKeyPress() || e.getKey() == e.ENTER; }, /** @@ -54830,23 +57257,22 @@ Ext.form.TextArea = Ext.extend(Ext.form.TextField, { if(!this.grow || !this.textSizeEl){ return; } - var el = this.el; - var v = el.dom.value; - var ts = this.textSizeEl; - ts.innerHTML = ''; - ts.appendChild(document.createTextNode(v)); - v = ts.innerHTML; + var el = this.el, + v = Ext.util.Format.htmlEncode(el.dom.value), + ts = this.textSizeEl, + h; + Ext.fly(ts).setWidth(this.el.getWidth()); if(v.length < 1){ v = "  "; }else{ v += this.growAppend; if(Ext.isIE){ - v = v.replace(/\n/g, '
      '); + v = v.replace(/\n/g, ' 
      '); } } ts.innerHTML = v; - var h = Math.min(this.growMax, Math.max(ts.offsetHeight, this.growMin) + this.growPad); + h = Math.min(this.growMax, Math.max(ts.offsetHeight, this.growMin)); if(h != this.lastHeight){ this.lastHeight = h; this.el.setHeight(h); @@ -54959,10 +57385,26 @@ Ext.form.NumberField = Ext.extend(Ext.form.TextField, { }, setValue : function(v){ - v = typeof v == 'number' ? v : parseFloat(String(v).replace(this.decimalSeparator, ".")); + v = Ext.isNumber(v) ? v : parseFloat(String(v).replace(this.decimalSeparator, ".")); v = isNaN(v) ? '' : String(v).replace(".", this.decimalSeparator); return Ext.form.NumberField.superclass.setValue.call(this, v); }, + + /** + * Replaces any existing {@link #minValue} with the new value. + * @param {Number} value The minimum value + */ + setMinValue : function(value){ + this.minValue = Ext.num(value, Number.NEGATIVE_INFINITY); + }, + + /** + * Replaces any existing {@link #maxValue} with the new value. + * @param {Number} value The maximum value + */ + setMaxValue : function(value){ + this.maxValue = Ext.num(value, Number.MAX_VALUE); + }, // private parseValue : function(value){ @@ -55121,6 +57563,18 @@ disabledDates: ["^03"] this.disabledDatesRE = null; this.initDisabledDays(); }, + + initEvents: function() { + Ext.form.DateField.superclass.initEvents.call(this); + this.keyNav = new Ext.KeyNav(this.el, { + "down": function(e) { + this.onTriggerClick(); + }, + scope: this, + forceKeyDown: true + }); + }, + // private initDisabledDays : function(){ @@ -55286,7 +57740,7 @@ dateField.setValue('2006-05-04'); // private onDestroy : function(){ - Ext.destroy(this.menu); + Ext.destroy(this.menu, this.keyNav); Ext.form.DateField.superclass.onDestroy.call(this); }, @@ -55307,7 +57761,8 @@ dateField.setValue('2006-05-04'); } if(this.menu == null){ this.menu = new Ext.menu.DateMenu({ - hideOnClick: false + hideOnClick: false, + focusOnSelect: false }); } this.onFocus(); @@ -55460,4612 +57915,4789 @@ Ext.form.DisplayField = Ext.extend(Ext.form.Field, { }); Ext.reg('displayfield', Ext.form.DisplayField); -/** - * @class Ext.form.ComboBox - * @extends Ext.form.TriggerField - *

      A combobox control with support for autocomplete, remote-loading, paging and many other features.

      - *

      A ComboBox works in a similar manner to a traditional HTML <select> field. The difference is - * that to submit the {@link #valueField}, you must specify a {@link #hiddenName} to create a hidden input - * field to hold the value of the valueField. The {@link #displayField} is shown in the text field - * which is named according to the {@link #name}.

      - *

      Events

      - *

      To do something when something in ComboBox is selected, configure the select event:

      
      -var cb = new Ext.form.ComboBox({
      -    // all of your config options
      -    listeners:{
      -         scope: yourScope,
      -         'select': yourFunction
      -    }
      -});
      -
      -// Alternatively, you can assign events after the object is created:
      -var cb = new Ext.form.ComboBox(yourOptions);
      -cb.on('select', yourFunction, yourScope);
      - * 

      - * - *

      ComboBox in Grid

      - *

      If using a ComboBox in an {@link Ext.grid.EditorGridPanel Editor Grid} a {@link Ext.grid.Column#renderer renderer} - * will be needed to show the displayField when the editor is not active. Set up the renderer manually, or implement - * a reusable render, for example:

      
      -// create reusable renderer
      -Ext.util.Format.comboRenderer = function(combo){
      -    return function(value){
      -        var record = combo.findRecord(combo.{@link #valueField}, value);
      -        return record ? record.get(combo.{@link #displayField}) : combo.{@link #valueNotFoundText};
      -    }
      -}
      -
      -// create the combo instance
      -var combo = new Ext.form.ComboBox({
      -    {@link #typeAhead}: true,
      -    {@link #triggerAction}: 'all',
      -    {@link #lazyRender}:true,
      -    {@link #mode}: 'local',
      -    {@link #store}: new Ext.data.ArrayStore({
      -        id: 0,
      -        fields: [
      -            'myId',
      -            'displayText'
      -        ],
      -        data: [[1, 'item1'], [2, 'item2']]
      -    }),
      -    {@link #valueField}: 'myId',
      -    {@link #displayField}: 'displayText'
      -});
      -
      -// snippet of column model used within grid
      -var cm = new Ext.grid.ColumnModel([{
      -       ...
      -    },{
      -       header: "Some Header",
      -       dataIndex: 'whatever',
      -       width: 130,
      -       editor: combo, // specify reference to combo instance
      -       renderer: Ext.util.Format.comboRenderer(combo) // pass combo instance to reusable renderer
      -    },
      -    ...
      -]);
      - * 

      - * - *

      Filtering

      - *

      A ComboBox {@link #doQuery uses filtering itself}, for information about filtering the ComboBox - * store manually see {@link #lastQuery}.

      - * @constructor - * Create a new ComboBox. - * @param {Object} config Configuration options - * @xtype combo - */ -Ext.form.ComboBox = Ext.extend(Ext.form.TriggerField, { - /** - * @cfg {Mixed} transform The id, DOM node or element of an existing HTML SELECT to convert to a ComboBox. - * Note that if you specify this and the combo is going to be in an {@link Ext.form.BasicForm} or - * {@link Ext.form.FormPanel}, you must also set {@link #lazyRender} = true. - */ - /** - * @cfg {Boolean} lazyRender true to prevent the ComboBox from rendering until requested - * (should always be used when rendering into an {@link Ext.Editor} (e.g. {@link Ext.grid.EditorGridPanel Grids}), - * defaults to false). - */ - /** - * @cfg {String/Object} autoCreate

      A {@link Ext.DomHelper DomHelper} element spec, or true for a default - * element spec. Used to create the {@link Ext.Component#getEl Element} which will encapsulate this Component. - * See {@link Ext.Component#autoEl autoEl} for details. Defaults to:

      - *
      {tag: "input", type: "text", size: "24", autocomplete: "off"}
      - */ - /** - * @cfg {Ext.data.Store/Array} store The data source to which this combo is bound (defaults to undefined). - * Acceptable values for this property are: - *
        - *
      • any {@link Ext.data.Store Store} subclass
      • - *
      • an Array : Arrays will be converted to a {@link Ext.data.ArrayStore} internally, - * automatically generating {@link Ext.data.Field#name field names} to work with all data components. - *
          - *
        • 1-dimensional array : (e.g., ['Foo','Bar'])
          - * A 1-dimensional array will automatically be expanded (each array item will be used for both the combo - * {@link #valueField} and {@link #displayField})
        • - *
        • 2-dimensional array : (e.g., [['f','Foo'],['b','Bar']])
          - * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo - * {@link #valueField}, while the value at index 1 is assumed to be the combo {@link #displayField}. - *
      - *

      See also {@link #mode}.

      - */ - /** - * @cfg {String} title If supplied, a header element is created containing this text and added into the top of - * the dropdown list (defaults to undefined, with no header element) - */ - - // private - defaultAutoCreate : {tag: "input", type: "text", size: "24", autocomplete: "off"}, - /** - * @cfg {Number} listWidth The width (used as a parameter to {@link Ext.Element#setWidth}) of the dropdown - * list (defaults to the width of the ComboBox field). See also {@link #minListWidth} - */ - /** - * @cfg {String} displayField The underlying {@link Ext.data.Field#name data field name} to bind to this - * ComboBox (defaults to undefined if {@link #mode} = 'remote' or 'field1' if - * {@link #transform transforming a select} or if the {@link #store field name is autogenerated based on - * the store configuration}). - *

      See also {@link #valueField}.

      - *

      Note: if using a ComboBox in an {@link Ext.grid.EditorGridPanel Editor Grid} a - * {@link Ext.grid.Column#renderer renderer} will be needed to show the displayField when the editor is not - * active.

      - */ - /** - * @cfg {String} valueField The underlying {@link Ext.data.Field#name data value name} to bind to this - * ComboBox (defaults to undefined if {@link #mode} = 'remote' or 'field2' if - * {@link #transform transforming a select} or if the {@link #store field name is autogenerated based on - * the store configuration}). - *

      Note: use of a valueField requires the user to make a selection in order for a value to be - * mapped. See also {@link #hiddenName}, {@link #hiddenValue}, and {@link #displayField}.

      - */ - /** - * @cfg {String} hiddenName If specified, a hidden form field with this name is dynamically generated to store the - * field's data value (defaults to the underlying DOM element's name). Required for the combo's value to automatically - * post during a form submission. See also {@link #valueField}. - *

      Note: the hidden field's id will also default to this name if {@link #hiddenId} is not specified. - * The ComboBox {@link Ext.Component#id id} and the {@link #hiddenId} should be different, since - * no two DOM nodes should share the same id. So, if the ComboBox {@link Ext.form.Field#name name} and - * hiddenName are the same, you should specify a unique {@link #hiddenId}.

      - */ - /** - * @cfg {String} hiddenId If {@link #hiddenName} is specified, hiddenId can also be provided - * to give the hidden field a unique id (defaults to the {@link #hiddenName}). The hiddenId - * and combo {@link Ext.Component#id id} should be different, since no two DOM - * nodes should share the same id. - */ - /** - * @cfg {String} hiddenValue Sets the initial value of the hidden field if {@link #hiddenName} is - * specified to contain the selected {@link #valueField}, from the Store. Defaults to the configured - * {@link Ext.form.Field#value value}. - */ - /** - * @cfg {String} listClass The CSS class to add to the predefined 'x-combo-list' class - * applied the dropdown list element (defaults to ''). - */ - listClass : '', - /** - * @cfg {String} selectedClass CSS class to apply to the selected item in the dropdown list - * (defaults to 'x-combo-selected') - */ - selectedClass : 'x-combo-selected', - /** - * @cfg {String} listEmptyText The empty text to display in the data view if no items are found. - * (defaults to '') - */ - listEmptyText: '', - /** - * @cfg {String} triggerClass An additional CSS class used to style the trigger button. The trigger will always - * get the class 'x-form-trigger' and triggerClass will be appended if specified - * (defaults to 'x-form-arrow-trigger' which displays a downward arrow icon). - */ - triggerClass : 'x-form-arrow-trigger', - /** - * @cfg {Boolean/String} shadow true or "sides" for the default effect, "frame" for - * 4-way shadow, and "drop" for bottom-right - */ - shadow : 'sides', - /** - * @cfg {String} listAlign A valid anchor position value. See {@link Ext.Element#alignTo} for details - * on supported anchor positions (defaults to 'tl-bl?') - */ - listAlign : 'tl-bl?', - /** - * @cfg {Number} maxHeight The maximum height in pixels of the dropdown list before scrollbars are shown - * (defaults to 300) - */ - maxHeight : 300, - /** - * @cfg {Number} minHeight The minimum height in pixels of the dropdown list when the list is constrained by its - * distance to the viewport edges (defaults to 90) - */ - minHeight : 90, - /** - * @cfg {String} triggerAction The action to execute when the trigger is clicked. - *
        - *
      • 'query' : Default - *

        {@link #doQuery run the query} using the {@link Ext.form.Field#getRawValue raw value}.

      • - *
      • 'all' : - *

        {@link #doQuery run the query} specified by the {@link #allQuery} config option

      • - *
      - *

      See also {@link #queryParam}.

      - */ - triggerAction : 'query', - /** - * @cfg {Number} minChars The minimum number of characters the user must type before autocomplete and - * {@link #typeAhead} activate (defaults to 4 if {@link #mode} = 'remote' or 0 if - * {@link #mode} = 'local', does not apply if - * {@link Ext.form.TriggerField#editable editable} = false). - */ - minChars : 4, - /** - * @cfg {Boolean} typeAhead true to populate and autoselect the remainder of the text being - * typed after a configurable delay ({@link #typeAheadDelay}) if it matches a known value (defaults - * to false) - */ - typeAhead : false, - /** - * @cfg {Number} queryDelay The length of time in milliseconds to delay between the start of typing and - * sending the query to filter the dropdown list (defaults to 500 if {@link #mode} = 'remote' - * or 10 if {@link #mode} = 'local') - */ - queryDelay : 500, - /** - * @cfg {Number} pageSize If greater than 0, a {@link Ext.PagingToolbar} is displayed in the - * footer of the dropdown list and the {@link #doQuery filter queries} will execute with page start and - * {@link Ext.PagingToolbar#pageSize limit} parameters. Only applies when {@link #mode} = 'remote' - * (defaults to 0). - */ - pageSize : 0, - /** - * @cfg {Boolean} selectOnFocus true to select any existing text in the field immediately on focus. - * Only applies when {@link Ext.form.TriggerField#editable editable} = true (defaults to - * false). - */ - selectOnFocus : false, - /** - * @cfg {String} queryParam Name of the query ({@link Ext.data.Store#baseParam baseParam} name for the store) - * as it will be passed on the querystring (defaults to 'query') - */ - queryParam : 'query', - /** - * @cfg {String} loadingText The text to display in the dropdown list while data is loading. Only applies - * when {@link #mode} = 'remote' (defaults to 'Loading...') - */ - loadingText : 'Loading...', - /** - * @cfg {Boolean} resizable true to add a resize handle to the bottom of the dropdown list - * (creates an {@link Ext.Resizable} with 'se' {@link Ext.Resizable#pinned pinned} handles). - * Defaults to false. - */ - resizable : false, - /** - * @cfg {Number} handleHeight The height in pixels of the dropdown list resize handle if - * {@link #resizable} = true (defaults to 8) - */ - handleHeight : 8, - /** - * @cfg {String} allQuery The text query to send to the server to return all records for the list - * with no filtering (defaults to '') - */ - allQuery: '', - /** - * @cfg {String} mode Acceptable values are: - *
        - *
      • 'remote' : Default - *

        Automatically loads the {@link #store} the first time the trigger - * is clicked. If you do not want the store to be automatically loaded the first time the trigger is - * clicked, set to 'local' and manually load the store. To force a requery of the store - * every time the trigger is clicked see {@link #lastQuery}.

      • - *
      • 'local' : - *

        ComboBox loads local data

        - *
        
        -var combo = new Ext.form.ComboBox({
        -    renderTo: document.body,
        -    mode: 'local',
        -    store: new Ext.data.ArrayStore({
        -        id: 0,
        -        fields: [
        -            'myId',  // numeric value is the key
        -            'displayText'
        -        ],
        -        data: [[1, 'item1'], [2, 'item2']]  // data is local
        -    }),
        -    valueField: 'myId',
        -    displayField: 'displayText',
        -    triggerAction: 'all'
        -});
        -     * 
      • - *
      - */ - mode: 'remote', - /** - * @cfg {Number} minListWidth The minimum width of the dropdown list in pixels (defaults to 70, will - * be ignored if {@link #listWidth} has a higher value) - */ - minListWidth : 70, - /** - * @cfg {Boolean} forceSelection true to restrict the selected value to one of the values in the list, - * false to allow the user to set arbitrary text into the field (defaults to false) - */ - forceSelection : false, - /** - * @cfg {Number} typeAheadDelay The length of time in milliseconds to wait until the typeahead text is displayed - * if {@link #typeAhead} = true (defaults to 250) - */ - typeAheadDelay : 250, - /** - * @cfg {String} valueNotFoundText When using a name/value combo, if the value passed to setValue is not found in - * the store, valueNotFoundText will be displayed as the field text if defined (defaults to undefined). If this - * default text is used, it means there is no value set and no validation will occur on this field. - */ - - /** - * @cfg {Boolean} lazyInit true to not initialize the list for this combo until the field is focused - * (defaults to true) - */ - lazyInit : true, - - /** - * The value of the match string used to filter the store. Delete this property to force a requery. - * Example use: - *
      
      -var combo = new Ext.form.ComboBox({
      -    ...
      -    mode: 'remote',
      -    ...
      -    listeners: {
      -        // delete the previous query in the beforequery event or set
      -        // combo.lastQuery = null (this will reload the store the next time it expands)
      -        beforequery: function(qe){
      -            delete qe.combo.lastQuery;
      -        }
      -    }
      -});
      -     * 
      - * To make sure the filter in the store is not cleared the first time the ComboBox trigger is used - * configure the combo with lastQuery=''. Example use: - *
      
      -var combo = new Ext.form.ComboBox({
      -    ...
      -    mode: 'local',
      -    triggerAction: 'all',
      -    lastQuery: ''
      -});
      -     * 
      - * @property lastQuery - * @type String - */ - - // private - initComponent : function(){ - Ext.form.ComboBox.superclass.initComponent.call(this); - this.addEvents( - /** - * @event expand - * Fires when the dropdown list is expanded - * @param {Ext.form.ComboBox} combo This combo box - */ - 'expand', - /** - * @event collapse - * Fires when the dropdown list is collapsed - * @param {Ext.form.ComboBox} combo This combo box - */ - 'collapse', - /** - * @event beforeselect - * Fires before a list item is selected. Return false to cancel the selection. - * @param {Ext.form.ComboBox} combo This combo box - * @param {Ext.data.Record} record The data record returned from the underlying store - * @param {Number} index The index of the selected item in the dropdown list - */ - 'beforeselect', - /** - * @event select - * Fires when a list item is selected - * @param {Ext.form.ComboBox} combo This combo box - * @param {Ext.data.Record} record The data record returned from the underlying store - * @param {Number} index The index of the selected item in the dropdown list - */ - 'select', - /** - * @event beforequery - * Fires before all queries are processed. Return false to cancel the query or set the queryEvent's - * cancel property to true. - * @param {Object} queryEvent An object that has these properties:
        - *
      • combo : Ext.form.ComboBox
        This combo box
      • - *
      • query : String
        The query
      • - *
      • forceAll : Boolean
        True to force "all" query
      • - *
      • cancel : Boolean
        Set to true to cancel the query
      • - *
      - */ - 'beforequery' - ); - if(this.transform){ - var s = Ext.getDom(this.transform); - if(!this.hiddenName){ - this.hiddenName = s.name; - } - if(!this.store){ - this.mode = 'local'; - var d = [], opts = s.options; - for(var i = 0, len = opts.length;i < len; i++){ - var o = opts[i], - value = (o.hasAttribute ? o.hasAttribute('value') : o.getAttributeNode('value').specified) ? o.value : o.text; - if(o.selected && Ext.isEmpty(this.value, true)) { - this.value = value; - } - d.push([value, o.text]); - } - this.store = new Ext.data.ArrayStore({ - 'id': 0, - fields: ['value', 'text'], - data : d, - autoDestroy: true - }); - this.valueField = 'value'; - this.displayField = 'text'; - } - s.name = Ext.id(); // wipe out the name in case somewhere else they have a reference - if(!this.lazyRender){ - this.target = true; - this.el = Ext.DomHelper.insertBefore(s, this.autoCreate || this.defaultAutoCreate); - this.render(this.el.parentNode, s); - Ext.removeNode(s); // remove it - }else{ - Ext.removeNode(s); // remove it - } - } - //auto-configure store from local array data - else if(this.store){ - this.store = Ext.StoreMgr.lookup(this.store); - if(this.store.autoCreated){ - this.displayField = this.valueField = 'field1'; - if(!this.store.expandData){ - this.displayField = 'field2'; - } - this.mode = 'local'; - } - } - - this.selectedIndex = -1; - if(this.mode == 'local'){ - if(!Ext.isDefined(this.initialConfig.queryDelay)){ - this.queryDelay = 10; - } - if(!Ext.isDefined(this.initialConfig.minChars)){ - this.minChars = 0; - } - } - }, - - // private - onRender : function(ct, position){ - Ext.form.ComboBox.superclass.onRender.call(this, ct, position); - if(this.hiddenName){ - this.hiddenField = this.el.insertSibling({tag:'input', type:'hidden', name: this.hiddenName, - id: (this.hiddenId||this.hiddenName)}, 'before', true); - - // prevent input submission - this.el.dom.removeAttribute('name'); - } - if(Ext.isGecko){ - this.el.dom.setAttribute('autocomplete', 'off'); - } - - if(!this.lazyInit){ - this.initList(); - }else{ - this.on('focus', this.initList, this, {single: true}); - } - }, - - // private - initValue : function(){ - Ext.form.ComboBox.superclass.initValue.call(this); - if(this.hiddenField){ - this.hiddenField.value = - Ext.isDefined(this.hiddenValue) ? this.hiddenValue : - Ext.isDefined(this.value) ? this.value : ''; - } - }, - - // private - initList : function(){ - if(!this.list){ - var cls = 'x-combo-list'; - - this.list = new Ext.Layer({ - parentEl: this.getListParent(), - shadow: this.shadow, - cls: [cls, this.listClass].join(' '), - constrain:false - }); - - var lw = this.listWidth || Math.max(this.wrap.getWidth(), this.minListWidth); - this.list.setSize(lw, 0); - this.list.swallowEvent('mousewheel'); - this.assetHeight = 0; - if(this.syncFont !== false){ - this.list.setStyle('font-size', this.el.getStyle('font-size')); - } - if(this.title){ - this.header = this.list.createChild({cls:cls+'-hd', html: this.title}); - this.assetHeight += this.header.getHeight(); - } - - this.innerList = this.list.createChild({cls:cls+'-inner'}); - this.mon(this.innerList, 'mouseover', this.onViewOver, this); - this.mon(this.innerList, 'mousemove', this.onViewMove, this); - this.innerList.setWidth(lw - this.list.getFrameWidth('lr')); - - if(this.pageSize){ - this.footer = this.list.createChild({cls:cls+'-ft'}); - this.pageTb = new Ext.PagingToolbar({ - store: this.store, - pageSize: this.pageSize, - renderTo:this.footer - }); - this.assetHeight += this.footer.getHeight(); - } - - if(!this.tpl){ - /** - * @cfg {String/Ext.XTemplate} tpl

      The template string, or {@link Ext.XTemplate} instance to - * use to display each item in the dropdown list. The dropdown list is displayed in a - * DataView. See {@link #view}.

      - *

      The default template string is:

      
      -                  '<tpl for="."><div class="x-combo-list-item">{' + this.displayField + '}</div></tpl>'
      -                * 
      - *

      Override the default value to create custom UI layouts for items in the list. - * For example:

      
      -                  '<tpl for="."><div ext:qtip="{state}. {nick}" class="x-combo-list-item">{state}</div></tpl>'
      -                * 
      - *

      The template must contain one or more substitution parameters using field - * names from the Combo's {@link #store Store}. In the example above an - *

      ext:qtip
      attribute is added to display other fields from the Store.

      - *

      To preserve the default visual look of list items, add the CSS class name - *

      x-combo-list-item
      to the template's container element.

      - *

      Also see {@link #itemSelector} for additional details.

      - */ - this.tpl = '
      {' + this.displayField + '}
      '; - /** - * @cfg {String} itemSelector - *

      A simple CSS selector (e.g. div.some-class or span:first-child) that will be - * used to determine what nodes the {@link #view Ext.DataView} which handles the dropdown - * display will be working with.

      - *

      Note: this setting is required if a custom XTemplate has been - * specified in {@link #tpl} which assigns a class other than

      'x-combo-list-item'
      - * to dropdown list items - */ - } - - /** - * The {@link Ext.DataView DataView} used to display the ComboBox's options. - * @type Ext.DataView - */ - this.view = new Ext.DataView({ - applyTo: this.innerList, - tpl: this.tpl, - singleSelect: true, - selectedClass: this.selectedClass, - itemSelector: this.itemSelector || '.' + cls + '-item', - emptyText: this.listEmptyText - }); - - this.mon(this.view, 'click', this.onViewClick, this); - - this.bindStore(this.store, true); - - if(this.resizable){ - this.resizer = new Ext.Resizable(this.list, { - pinned:true, handles:'se' - }); - this.mon(this.resizer, 'resize', function(r, w, h){ - this.maxHeight = h-this.handleHeight-this.list.getFrameWidth('tb')-this.assetHeight; - this.listWidth = w; - this.innerList.setWidth(w - this.list.getFrameWidth('lr')); - this.restrictHeight(); - }, this); - - this[this.pageSize?'footer':'innerList'].setStyle('margin-bottom', this.handleHeight+'px'); - } - } - }, - - /** - *

      Returns the element used to house this ComboBox's pop-up list. Defaults to the document body.

      - * A custom implementation may be provided as a configuration option if the floating list needs to be rendered - * to a different Element. An example might be rendering the list inside a Menu so that clicking - * the list does not hide the Menu:
      
      -var store = new Ext.data.ArrayStore({
      -    autoDestroy: true,
      -    fields: ['initials', 'fullname'],
      -    data : [
      -        ['FF', 'Fred Flintstone'],
      -        ['BR', 'Barney Rubble']
      -    ]
      -});
      -
      -var combo = new Ext.form.ComboBox({
      -    store: store,
      -    displayField: 'fullname',
      -    emptyText: 'Select a name...',
      -    forceSelection: true,
      -    getListParent: function() {
      -        return this.el.up('.x-menu');
      -    },
      -    iconCls: 'no-icon', //use iconCls if placing within menu to shift to right side of menu
      -    mode: 'local',
      -    selectOnFocus: true,
      -    triggerAction: 'all',
      -    typeAhead: true,
      -    width: 135
      -});
      -
      -var menu = new Ext.menu.Menu({
      -    id: 'mainMenu',
      -    items: [
      -        combo // A Field in a Menu
      -    ]
      -});
      -
      - */ - getListParent : function() { - return document.body; - }, - - /** - * Returns the store associated with this combo. - * @return {Ext.data.Store} The store - */ - getStore : function(){ - return this.store; - }, - - // private - bindStore : function(store, initial){ - if(this.store && !initial){ - if(this.store !== store && this.store.autoDestroy){ - this.store.destroy(); - }else{ - this.store.un('beforeload', this.onBeforeLoad, this); - this.store.un('load', this.onLoad, this); - this.store.un('exception', this.collapse, this); - } - if(!store){ - this.store = null; - if(this.view){ - this.view.bindStore(null); - } - if(this.pageTb){ - this.pageTb.bindStore(null); - } - } - } - if(store){ - if(!initial) { - this.lastQuery = null; - if(this.pageTb) { - this.pageTb.bindStore(store); - } - } - - this.store = Ext.StoreMgr.lookup(store); - this.store.on({ - scope: this, - beforeload: this.onBeforeLoad, - load: this.onLoad, - exception: this.collapse - }); - - if(this.view){ - this.view.bindStore(store); - } - } - }, - - // private - initEvents : function(){ - Ext.form.ComboBox.superclass.initEvents.call(this); - - this.keyNav = new Ext.KeyNav(this.el, { - "up" : function(e){ - this.inKeyMode = true; - this.selectPrev(); - }, - - "down" : function(e){ - if(!this.isExpanded()){ - this.onTriggerClick(); - }else{ - this.inKeyMode = true; - this.selectNext(); - } - }, - - "enter" : function(e){ - this.onViewClick(); - }, - - "esc" : function(e){ - this.collapse(); - }, - - "tab" : function(e){ - this.onViewClick(false); - return true; - }, - - scope : this, - - doRelay : function(e, h, hname){ - if(hname == 'down' || this.scope.isExpanded()){ - // this MUST be called before ComboBox#fireKey() - var relay = Ext.KeyNav.prototype.doRelay.apply(this, arguments); - if(!Ext.isIE && Ext.EventManager.useKeydown){ - // call Combo#fireKey() for browsers which use keydown event (except IE) - this.scope.fireKey(e); - } - return relay; - } - return true; - }, - - forceKeyDown : true, - defaultEventAction: 'stopEvent' - }); - this.queryDelay = Math.max(this.queryDelay || 10, - this.mode == 'local' ? 10 : 250); - this.dqTask = new Ext.util.DelayedTask(this.initQuery, this); - if(this.typeAhead){ - this.taTask = new Ext.util.DelayedTask(this.onTypeAhead, this); - } - if(this.editable !== false && !this.enableKeyEvents){ - this.mon(this.el, 'keyup', this.onKeyUp, this); - } - }, - - // private - onDestroy : function(){ - if (this.dqTask){ - this.dqTask.cancel(); - this.dqTask = null; - } - this.bindStore(null); - Ext.destroy( - this.resizer, - this.view, - this.pageTb, - this.list - ); - Ext.form.ComboBox.superclass.onDestroy.call(this); - }, - - // private - fireKey : function(e){ - if (!this.isExpanded()) { - Ext.form.ComboBox.superclass.fireKey.call(this, e); - } - }, - - // private - onResize : function(w, h){ - Ext.form.ComboBox.superclass.onResize.apply(this, arguments); - if(this.isVisible() && this.list){ - this.doResize(w); - }else{ - this.bufferSize = w; - } - }, - - doResize: function(w){ - if(!Ext.isDefined(this.listWidth)){ - var lw = Math.max(w, this.minListWidth); - this.list.setWidth(lw); - this.innerList.setWidth(lw - this.list.getFrameWidth('lr')); - } - }, - - // private - onEnable : function(){ - Ext.form.ComboBox.superclass.onEnable.apply(this, arguments); - if(this.hiddenField){ - this.hiddenField.disabled = false; - } - }, - - // private - onDisable : function(){ - Ext.form.ComboBox.superclass.onDisable.apply(this, arguments); - if(this.hiddenField){ - this.hiddenField.disabled = true; - } - }, - - // private - onBeforeLoad : function(){ - if(!this.hasFocus){ - return; - } - this.innerList.update(this.loadingText ? - '
      '+this.loadingText+'
      ' : ''); - this.restrictHeight(); - this.selectedIndex = -1; - }, - - // private - onLoad : function(){ - if(!this.hasFocus){ - return; - } - if(this.store.getCount() > 0 || this.listEmptyText){ - this.expand(); - this.restrictHeight(); - if(this.lastQuery == this.allQuery){ - if(this.editable){ - this.el.dom.select(); - } - if(!this.selectByValue(this.value, true)){ - this.select(0, true); - } - }else{ - this.selectNext(); - if(this.typeAhead && this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE){ - this.taTask.delay(this.typeAheadDelay); - } - } - }else{ - this.onEmptyResults(); - } - //this.el.focus(); - }, - - // private - onTypeAhead : function(){ - if(this.store.getCount() > 0){ - var r = this.store.getAt(0); - var newValue = r.data[this.displayField]; - var len = newValue.length; - var selStart = this.getRawValue().length; - if(selStart != len){ - this.setRawValue(newValue); - this.selectText(selStart, newValue.length); - } - } - }, - - // private - onSelect : function(record, index){ - if(this.fireEvent('beforeselect', this, record, index) !== false){ - this.setValue(record.data[this.valueField || this.displayField]); - this.collapse(); - this.fireEvent('select', this, record, index); - } - }, - - // inherit docs - getName: function(){ - var hf = this.hiddenField; - return hf && hf.name ? hf.name : this.hiddenName || Ext.form.ComboBox.superclass.getName.call(this); - }, - - /** - * Returns the currently selected field value or empty string if no value is set. - * @return {String} value The selected value - */ - getValue : function(){ - if(this.valueField){ - return Ext.isDefined(this.value) ? this.value : ''; - }else{ - return Ext.form.ComboBox.superclass.getValue.call(this); - } - }, - - /** - * Clears any text/value currently set in the field - */ - clearValue : function(){ - if(this.hiddenField){ - this.hiddenField.value = ''; - } - this.setRawValue(''); - this.lastSelectionText = ''; - this.applyEmptyText(); - this.value = ''; - }, - - /** - * Sets the specified value into the field. If the value finds a match, the corresponding record text - * will be displayed in the field. If the value does not match the data value of an existing item, - * and the valueNotFoundText config option is defined, it will be displayed as the default field text. - * Otherwise the field will be blank (although the value will still be set). - * @param {String} value The value to match - * @return {Ext.form.Field} this - */ - setValue : function(v){ - var text = v; - if(this.valueField){ - var r = this.findRecord(this.valueField, v); - if(r){ - text = r.data[this.displayField]; - }else if(Ext.isDefined(this.valueNotFoundText)){ - text = this.valueNotFoundText; - } - } - this.lastSelectionText = text; - if(this.hiddenField){ - this.hiddenField.value = v; - } - Ext.form.ComboBox.superclass.setValue.call(this, text); - this.value = v; - return this; - }, - - // private - findRecord : function(prop, value){ - var record; - if(this.store.getCount() > 0){ - this.store.each(function(r){ - if(r.data[prop] == value){ - record = r; - return false; - } - }); - } - return record; - }, - - // private - onViewMove : function(e, t){ - this.inKeyMode = false; - }, - - // private - onViewOver : function(e, t){ - if(this.inKeyMode){ // prevent key nav and mouse over conflicts - return; - } - var item = this.view.findItemFromChild(t); - if(item){ - var index = this.view.indexOf(item); - this.select(index, false); - } - }, - - // private - onViewClick : function(doFocus){ - var index = this.view.getSelectedIndexes()[0], - s = this.store, - r = s.getAt(index); - if(r){ - this.onSelect(r, index); - }else if(s.getCount() === 0){ - this.onEmptyResults(); - } - if(doFocus !== false){ - this.el.focus(); - } - }, - - // private - restrictHeight : function(){ - this.innerList.dom.style.height = ''; - var inner = this.innerList.dom, - pad = this.list.getFrameWidth('tb') + (this.resizable ? this.handleHeight : 0) + this.assetHeight, - h = Math.max(inner.clientHeight, inner.offsetHeight, inner.scrollHeight), - ha = this.getPosition()[1]-Ext.getBody().getScroll().top, - hb = Ext.lib.Dom.getViewHeight()-ha-this.getSize().height, - space = Math.max(ha, hb, this.minHeight || 0)-this.list.shadowOffset-pad-5; - - h = Math.min(h, space, this.maxHeight); - - this.innerList.setHeight(h); - this.list.beginUpdate(); - this.list.setHeight(h+pad); - this.list.alignTo(this.wrap, this.listAlign); - this.list.endUpdate(); - }, - - // private - onEmptyResults : function(){ - this.collapse(); - }, - - /** - * Returns true if the dropdown list is expanded, else false. - */ - isExpanded : function(){ - return this.list && this.list.isVisible(); - }, - - /** - * Select an item in the dropdown list by its data value. This function does NOT cause the select event to fire. - * The store must be loaded and the list expanded for this function to work, otherwise use setValue. - * @param {String} value The data value of the item to select - * @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the - * selected item if it is not currently in view (defaults to true) - * @return {Boolean} True if the value matched an item in the list, else false - */ - selectByValue : function(v, scrollIntoView){ - if(!Ext.isEmpty(v, true)){ - var r = this.findRecord(this.valueField || this.displayField, v); - if(r){ - this.select(this.store.indexOf(r), scrollIntoView); - return true; - } - } - return false; - }, - - /** - * Select an item in the dropdown list by its numeric index in the list. This function does NOT cause the select event to fire. - * The store must be loaded and the list expanded for this function to work, otherwise use setValue. - * @param {Number} index The zero-based index of the list item to select - * @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the - * selected item if it is not currently in view (defaults to true) - */ - select : function(index, scrollIntoView){ - this.selectedIndex = index; - this.view.select(index); - if(scrollIntoView !== false){ - var el = this.view.getNode(index); - if(el){ - this.innerList.scrollChildIntoView(el, false); - } - } - }, - - // private - selectNext : function(){ - var ct = this.store.getCount(); - if(ct > 0){ - if(this.selectedIndex == -1){ - this.select(0); - }else if(this.selectedIndex < ct-1){ - this.select(this.selectedIndex+1); - } - } - }, - - // private - selectPrev : function(){ - var ct = this.store.getCount(); - if(ct > 0){ - if(this.selectedIndex == -1){ - this.select(0); - }else if(this.selectedIndex !== 0){ - this.select(this.selectedIndex-1); - } - } - }, - - // private - onKeyUp : function(e){ - var k = e.getKey(); - if(this.editable !== false && (k == e.BACKSPACE || !e.isSpecialKey())){ - this.lastKey = k; - this.dqTask.delay(this.queryDelay); - } - Ext.form.ComboBox.superclass.onKeyUp.call(this, e); - }, - - // private - validateBlur : function(){ - return !this.list || !this.list.isVisible(); - }, - - // private - initQuery : function(){ - this.doQuery(this.getRawValue()); - }, - - // private - beforeBlur : function(){ - var val = this.getRawValue(), - rec = this.findRecord(this.displayField, val); - if(!rec && this.forceSelection){ - if(val.length > 0 && val != this.emptyText){ - this.el.dom.value = Ext.isDefined(this.lastSelectionText) ? this.lastSelectionText : ''; - this.applyEmptyText(); - }else{ - this.clearValue(); - } - }else{ - if(rec){ - val = rec.get(this.valueField || this.displayField); - } - this.setValue(val); - } - }, - - /** - * Execute a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the - * query allowing the query action to be canceled if needed. - * @param {String} query The SQL query to execute - * @param {Boolean} forceAll true to force the query to execute even if there are currently fewer - * characters in the field than the minimum specified by the {@link #minChars} config option. It - * also clears any filter previously saved in the current store (defaults to false) - */ - doQuery : function(q, forceAll){ - q = Ext.isEmpty(q) ? '' : q; - var qe = { - query: q, - forceAll: forceAll, - combo: this, - cancel:false - }; - if(this.fireEvent('beforequery', qe)===false || qe.cancel){ - return false; - } - q = qe.query; - forceAll = qe.forceAll; - if(forceAll === true || (q.length >= this.minChars)){ - if(this.lastQuery !== q){ - this.lastQuery = q; - if(this.mode == 'local'){ - this.selectedIndex = -1; - if(forceAll){ - this.store.clearFilter(); - }else{ - this.store.filter(this.displayField, q); - } - this.onLoad(); - }else{ - this.store.baseParams[this.queryParam] = q; - this.store.load({ - params: this.getParams(q) - }); - this.expand(); - } - }else{ - this.selectedIndex = -1; - this.onLoad(); - } - } - }, - - // private - getParams : function(q){ - var p = {}; - //p[this.queryParam] = q; - if(this.pageSize){ - p.start = 0; - p.limit = this.pageSize; - } - return p; - }, - - /** - * Hides the dropdown list if it is currently expanded. Fires the {@link #collapse} event on completion. - */ - collapse : function(){ - if(!this.isExpanded()){ - return; - } - this.list.hide(); - Ext.getDoc().un('mousewheel', this.collapseIf, this); - Ext.getDoc().un('mousedown', this.collapseIf, this); - this.fireEvent('collapse', this); - }, - - // private - collapseIf : function(e){ - if(!e.within(this.wrap) && !e.within(this.list)){ - this.collapse(); - } - }, - - /** - * Expands the dropdown list if it is currently hidden. Fires the {@link #expand} event on completion. - */ - expand : function(){ - if(this.isExpanded() || !this.hasFocus){ - return; - } - if(this.bufferSize){ - this.doResize(this.bufferSize); - delete this.bufferSize; - } - this.list.alignTo(this.wrap, this.listAlign); - this.list.show(); - if(Ext.isGecko2){ - this.innerList.setOverflow('auto'); // necessary for FF 2.0/Mac - } - Ext.getDoc().on({ - scope: this, - mousewheel: this.collapseIf, - mousedown: this.collapseIf - }); - this.fireEvent('expand', this); - }, - - /** - * @method onTriggerClick - * @hide - */ - // private - // Implements the default empty TriggerField.onTriggerClick function - onTriggerClick : function(){ - if(this.disabled){ - return; - } - if(this.isExpanded()){ - this.collapse(); - this.el.focus(); - }else { - this.onFocus({}); - if(this.triggerAction == 'all') { - this.doQuery(this.allQuery, true); - } else { - this.doQuery(this.getRawValue()); - } - this.el.focus(); - } - } - - /** - * @hide - * @method autoSize - */ - /** - * @cfg {Boolean} grow @hide - */ - /** - * @cfg {Number} growMin @hide - */ - /** - * @cfg {Number} growMax @hide - */ - -}); -Ext.reg('combo', Ext.form.ComboBox);/** - * @class Ext.form.Checkbox - * @extends Ext.form.Field - * Single checkbox field. Can be used as a direct replacement for traditional checkbox fields. +/** + * @class Ext.form.ComboBox + * @extends Ext.form.TriggerField + *

      A combobox control with support for autocomplete, remote-loading, paging and many other features.

      + *

      A ComboBox works in a similar manner to a traditional HTML <select> field. The difference is + * that to submit the {@link #valueField}, you must specify a {@link #hiddenName} to create a hidden input + * field to hold the value of the valueField. The {@link #displayField} is shown in the text field + * which is named according to the {@link #name}.

      + *

      Events

      + *

      To do something when something in ComboBox is selected, configure the select event:

      
      +var cb = new Ext.form.ComboBox({
      +    // all of your config options
      +    listeners:{
      +         scope: yourScope,
      +         'select': yourFunction
      +    }
      +});
      +
      +// Alternatively, you can assign events after the object is created:
      +var cb = new Ext.form.ComboBox(yourOptions);
      +cb.on('select', yourFunction, yourScope);
      + * 

      + * + *

      ComboBox in Grid

      + *

      If using a ComboBox in an {@link Ext.grid.EditorGridPanel Editor Grid} a {@link Ext.grid.Column#renderer renderer} + * will be needed to show the displayField when the editor is not active. Set up the renderer manually, or implement + * a reusable render, for example:

      
      +// create reusable renderer
      +Ext.util.Format.comboRenderer = function(combo){
      +    return function(value){
      +        var record = combo.findRecord(combo.{@link #valueField}, value);
      +        return record ? record.get(combo.{@link #displayField}) : combo.{@link #valueNotFoundText};
      +    }
      +}
      +
      +// create the combo instance
      +var combo = new Ext.form.ComboBox({
      +    {@link #typeAhead}: true,
      +    {@link #triggerAction}: 'all',
      +    {@link #lazyRender}:true,
      +    {@link #mode}: 'local',
      +    {@link #store}: new Ext.data.ArrayStore({
      +        id: 0,
      +        fields: [
      +            'myId',
      +            'displayText'
      +        ],
      +        data: [[1, 'item1'], [2, 'item2']]
      +    }),
      +    {@link #valueField}: 'myId',
      +    {@link #displayField}: 'displayText'
      +});
      +
      +// snippet of column model used within grid
      +var cm = new Ext.grid.ColumnModel([{
      +       ...
      +    },{
      +       header: "Some Header",
      +       dataIndex: 'whatever',
      +       width: 130,
      +       editor: combo, // specify reference to combo instance
      +       renderer: Ext.util.Format.comboRenderer(combo) // pass combo instance to reusable renderer
      +    },
      +    ...
      +]);
      + * 

      + * + *

      Filtering

      + *

      A ComboBox {@link #doQuery uses filtering itself}, for information about filtering the ComboBox + * store manually see {@link #lastQuery}.

      * @constructor - * Creates a new Checkbox + * Create a new ComboBox. * @param {Object} config Configuration options - * @xtype checkbox + * @xtype combo */ -Ext.form.Checkbox = Ext.extend(Ext.form.Field, { +Ext.form.ComboBox = Ext.extend(Ext.form.TriggerField, { /** - * @cfg {String} focusClass The CSS class to use when the checkbox receives focus (defaults to undefined) + * @cfg {Mixed} transform The id, DOM node or element of an existing HTML SELECT to convert to a ComboBox. + * Note that if you specify this and the combo is going to be in an {@link Ext.form.BasicForm} or + * {@link Ext.form.FormPanel}, you must also set {@link #lazyRender} = true. */ - focusClass : undefined, /** - * @cfg {String} fieldClass The default CSS class for the checkbox (defaults to 'x-form-field') + * @cfg {Boolean} lazyRender true to prevent the ComboBox from rendering until requested + * (should always be used when rendering into an {@link Ext.Editor} (e.g. {@link Ext.grid.EditorGridPanel Grids}), + * defaults to false). */ - fieldClass : 'x-form-field', /** - * @cfg {Boolean} checked true if the checkbox should render initially checked (defaults to false) + * @cfg {String/Object} autoCreate

      A {@link Ext.DomHelper DomHelper} element spec, or true for a default + * element spec. Used to create the {@link Ext.Component#getEl Element} which will encapsulate this Component. + * See {@link Ext.Component#autoEl autoEl} for details. Defaults to:

      + *
      {tag: "input", type: "text", size: "24", autocomplete: "off"}
      */ - checked : false, /** - * @cfg {String/Object} autoCreate A DomHelper element spec, or true for a default element spec (defaults to - * {tag: 'input', type: 'checkbox', autocomplete: 'off'}) + * @cfg {Ext.data.Store/Array} store The data source to which this combo is bound (defaults to undefined). + * Acceptable values for this property are: + *
        + *
      • any {@link Ext.data.Store Store} subclass
      • + *
      • an Array : Arrays will be converted to a {@link Ext.data.ArrayStore} internally, + * automatically generating {@link Ext.data.Field#name field names} to work with all data components. + *
          + *
        • 1-dimensional array : (e.g., ['Foo','Bar'])
          + * A 1-dimensional array will automatically be expanded (each array item will be used for both the combo + * {@link #valueField} and {@link #displayField})
        • + *
        • 2-dimensional array : (e.g., [['f','Foo'],['b','Bar']])
          + * For a multi-dimensional array, the value in index 0 of each item will be assumed to be the combo + * {@link #valueField}, while the value at index 1 is assumed to be the combo {@link #displayField}. + *
      + *

      See also {@link #mode}.

      */ - defaultAutoCreate : { tag: 'input', type: 'checkbox', autocomplete: 'off'}, /** - * @cfg {String} boxLabel The text that appears beside the checkbox + * @cfg {String} title If supplied, a header element is created containing this text and added into the top of + * the dropdown list (defaults to undefined, with no header element) */ + + // private + defaultAutoCreate : {tag: "input", type: "text", size: "24", autocomplete: "off"}, /** - * @cfg {String} inputValue The value that should go into the generated input element's value attribute + * @cfg {Number} listWidth The width (used as a parameter to {@link Ext.Element#setWidth}) of the dropdown + * list (defaults to the width of the ComboBox field). See also {@link #minListWidth} */ /** - * @cfg {Function} handler A function called when the {@link #checked} value changes (can be used instead of - * handling the check event). The handler is passed the following parameters: + * @cfg {String} displayField The underlying {@link Ext.data.Field#name data field name} to bind to this + * ComboBox (defaults to undefined if {@link #mode} = 'remote' or 'field1' if + * {@link #transform transforming a select} or if the {@link #store field name is autogenerated based on + * the store configuration}). + *

      See also {@link #valueField}.

      + *

      Note: if using a ComboBox in an {@link Ext.grid.EditorGridPanel Editor Grid} a + * {@link Ext.grid.Column#renderer renderer} will be needed to show the displayField when the editor is not + * active.

      + */ + /** + * @cfg {String} valueField The underlying {@link Ext.data.Field#name data value name} to bind to this + * ComboBox (defaults to undefined if {@link #mode} = 'remote' or 'field2' if + * {@link #transform transforming a select} or if the {@link #store field name is autogenerated based on + * the store configuration}). + *

      Note: use of a valueField requires the user to make a selection in order for a value to be + * mapped. See also {@link #hiddenName}, {@link #hiddenValue}, and {@link #displayField}.

      + */ + /** + * @cfg {String} hiddenName If specified, a hidden form field with this name is dynamically generated to store the + * field's data value (defaults to the underlying DOM element's name). Required for the combo's value to automatically + * post during a form submission. See also {@link #valueField}. + *

      Note: the hidden field's id will also default to this name if {@link #hiddenId} is not specified. + * The ComboBox {@link Ext.Component#id id} and the {@link #hiddenId} should be different, since + * no two DOM nodes should share the same id. So, if the ComboBox {@link Ext.form.Field#name name} and + * hiddenName are the same, you should specify a unique {@link #hiddenId}.

      + */ + /** + * @cfg {String} hiddenId If {@link #hiddenName} is specified, hiddenId can also be provided + * to give the hidden field a unique id (defaults to the {@link #hiddenName}). The hiddenId + * and combo {@link Ext.Component#id id} should be different, since no two DOM + * nodes should share the same id. + */ + /** + * @cfg {String} hiddenValue Sets the initial value of the hidden field if {@link #hiddenName} is + * specified to contain the selected {@link #valueField}, from the Store. Defaults to the configured + * {@link Ext.form.Field#value value}. + */ + /** + * @cfg {String} listClass The CSS class to add to the predefined 'x-combo-list' class + * applied the dropdown list element (defaults to ''). + */ + listClass : '', + /** + * @cfg {String} selectedClass CSS class to apply to the selected item in the dropdown list + * (defaults to 'x-combo-selected') + */ + selectedClass : 'x-combo-selected', + /** + * @cfg {String} listEmptyText The empty text to display in the data view if no items are found. + * (defaults to '') + */ + listEmptyText: '', + /** + * @cfg {String} triggerClass An additional CSS class used to style the trigger button. The trigger will always + * get the class 'x-form-trigger' and triggerClass will be appended if specified + * (defaults to 'x-form-arrow-trigger' which displays a downward arrow icon). + */ + triggerClass : 'x-form-arrow-trigger', + /** + * @cfg {Boolean/String} shadow true or "sides" for the default effect, "frame" for + * 4-way shadow, and "drop" for bottom-right + */ + shadow : 'sides', + /** + * @cfg {String/Array} listAlign A valid anchor position value. See {@link Ext.Element#alignTo} for details + * on supported anchor positions and offsets. To specify x/y offsets as well, this value + * may be specified as an Array of {@link Ext.Element#alignTo} method arguments.

      + *
      [ 'tl-bl?', [6,0] ]
      (defaults to 'tl-bl?') + */ + listAlign : 'tl-bl?', + /** + * @cfg {Number} maxHeight The maximum height in pixels of the dropdown list before scrollbars are shown + * (defaults to 300) + */ + maxHeight : 300, + /** + * @cfg {Number} minHeight The minimum height in pixels of the dropdown list when the list is constrained by its + * distance to the viewport edges (defaults to 90) + */ + minHeight : 90, + /** + * @cfg {String} triggerAction The action to execute when the trigger is clicked. *
        - *
      • checkbox : Ext.form.Checkbox
        The Checkbox being toggled.
      • - *
      • checked : Boolean
        The new checked state of the checkbox.
      • + *
      • 'query' : Default + *

        {@link #doQuery run the query} using the {@link Ext.form.Field#getRawValue raw value}.

      • + *
      • 'all' : + *

        {@link #doQuery run the query} specified by the {@link #allQuery} config option

      • *
      + *

      See also {@link #queryParam}.

      */ + triggerAction : 'query', /** - * @cfg {Object} scope An object to use as the scope ('this' reference) of the {@link #handler} function - * (defaults to this Checkbox). + * @cfg {Number} minChars The minimum number of characters the user must type before autocomplete and + * {@link #typeAhead} activate (defaults to 4 if {@link #mode} = 'remote' or 0 if + * {@link #mode} = 'local', does not apply if + * {@link Ext.form.TriggerField#editable editable} = false). + */ + minChars : 4, + /** + * @cfg {Boolean} autoSelect true to select the first result gathered by the data store (defaults + * to true). A false value would require a manual selection from the dropdown list to set the components value + * unless the value of ({@link #typeAheadDelay}) were true. + */ + autoSelect : true, + /** + * @cfg {Boolean} typeAhead true to populate and autoselect the remainder of the text being + * typed after a configurable delay ({@link #typeAheadDelay}) if it matches a known value (defaults + * to false) + */ + typeAhead : false, + /** + * @cfg {Number} queryDelay The length of time in milliseconds to delay between the start of typing and + * sending the query to filter the dropdown list (defaults to 500 if {@link #mode} = 'remote' + * or 10 if {@link #mode} = 'local') + */ + queryDelay : 500, + /** + * @cfg {Number} pageSize If greater than 0, a {@link Ext.PagingToolbar} is displayed in the + * footer of the dropdown list and the {@link #doQuery filter queries} will execute with page start and + * {@link Ext.PagingToolbar#pageSize limit} parameters. Only applies when {@link #mode} = 'remote' + * (defaults to 0). + */ + pageSize : 0, + /** + * @cfg {Boolean} selectOnFocus true to select any existing text in the field immediately on focus. + * Only applies when {@link Ext.form.TriggerField#editable editable} = true (defaults to + * false). + */ + selectOnFocus : false, + /** + * @cfg {String} queryParam Name of the query ({@link Ext.data.Store#baseParam baseParam} name for the store) + * as it will be passed on the querystring (defaults to 'query') + */ + queryParam : 'query', + /** + * @cfg {String} loadingText The text to display in the dropdown list while data is loading. Only applies + * when {@link #mode} = 'remote' (defaults to 'Loading...') + */ + loadingText : 'Loading...', + /** + * @cfg {Boolean} resizable true to add a resize handle to the bottom of the dropdown list + * (creates an {@link Ext.Resizable} with 'se' {@link Ext.Resizable#pinned pinned} handles). + * Defaults to false. + */ + resizable : false, + /** + * @cfg {Number} handleHeight The height in pixels of the dropdown list resize handle if + * {@link #resizable} = true (defaults to 8) + */ + handleHeight : 8, + /** + * @cfg {String} allQuery The text query to send to the server to return all records for the list + * with no filtering (defaults to '') + */ + allQuery: '', + /** + * @cfg {String} mode Acceptable values are: + *
        + *
      • 'remote' : Default + *

        Automatically loads the {@link #store} the first time the trigger + * is clicked. If you do not want the store to be automatically loaded the first time the trigger is + * clicked, set to 'local' and manually load the store. To force a requery of the store + * every time the trigger is clicked see {@link #lastQuery}.

      • + *
      • 'local' : + *

        ComboBox loads local data

        + *
        
        +var combo = new Ext.form.ComboBox({
        +    renderTo: document.body,
        +    mode: 'local',
        +    store: new Ext.data.ArrayStore({
        +        id: 0,
        +        fields: [
        +            'myId',  // numeric value is the key
        +            'displayText'
        +        ],
        +        data: [[1, 'item1'], [2, 'item2']]  // data is local
        +    }),
        +    valueField: 'myId',
        +    displayField: 'displayText',
        +    triggerAction: 'all'
        +});
        +     * 
      • + *
      + */ + mode: 'remote', + /** + * @cfg {Number} minListWidth The minimum width of the dropdown list in pixels (defaults to 70, will + * be ignored if {@link #listWidth} has a higher value) + */ + minListWidth : 70, + /** + * @cfg {Boolean} forceSelection true to restrict the selected value to one of the values in the list, + * false to allow the user to set arbitrary text into the field (defaults to false) + */ + forceSelection : false, + /** + * @cfg {Number} typeAheadDelay The length of time in milliseconds to wait until the typeahead text is displayed + * if {@link #typeAhead} = true (defaults to 250) + */ + typeAheadDelay : 250, + /** + * @cfg {String} valueNotFoundText When using a name/value combo, if the value passed to setValue is not found in + * the store, valueNotFoundText will be displayed as the field text if defined (defaults to undefined). If this + * default text is used, it means there is no value set and no validation will occur on this field. + */ + + /** + * @cfg {Boolean} lazyInit true to not initialize the list for this combo until the field is focused + * (defaults to true) + */ + lazyInit : true, + + /** + * @cfg {Boolean} clearFilterOnReset true to clear any filters on the store (when in local mode) when reset is called + * (defaults to true) + */ + clearFilterOnReset : true, + + /** + * @cfg {Boolean} submitValue False to clear the name attribute on the field so that it is not submitted during a form post. + * If a hiddenName is specified, setting this to true will cause both the hidden field and the element to be submitted. + * Defaults to undefined. + */ + submitValue: undefined, + + /** + * The value of the match string used to filter the store. Delete this property to force a requery. + * Example use: + *
      
      +var combo = new Ext.form.ComboBox({
      +    ...
      +    mode: 'remote',
      +    ...
      +    listeners: {
      +        // delete the previous query in the beforequery event or set
      +        // combo.lastQuery = null (this will reload the store the next time it expands)
      +        beforequery: function(qe){
      +            delete qe.combo.lastQuery;
      +        }
      +    }
      +});
      +     * 
      + * To make sure the filter in the store is not cleared the first time the ComboBox trigger is used + * configure the combo with lastQuery=''. Example use: + *
      
      +var combo = new Ext.form.ComboBox({
      +    ...
      +    mode: 'local',
      +    triggerAction: 'all',
      +    lastQuery: ''
      +});
      +     * 
      + * @property lastQuery + * @type String */ // private - actionMode : 'wrap', - - // private initComponent : function(){ - Ext.form.Checkbox.superclass.initComponent.call(this); + Ext.form.ComboBox.superclass.initComponent.call(this); this.addEvents( /** - * @event check - * Fires when the checkbox is checked or unchecked. - * @param {Ext.form.Checkbox} this This checkbox - * @param {Boolean} checked The new checked value + * @event expand + * Fires when the dropdown list is expanded + * @param {Ext.form.ComboBox} combo This combo box */ - 'check' + 'expand', + /** + * @event collapse + * Fires when the dropdown list is collapsed + * @param {Ext.form.ComboBox} combo This combo box + */ + 'collapse', + + /** + * @event beforeselect + * Fires before a list item is selected. Return false to cancel the selection. + * @param {Ext.form.ComboBox} combo This combo box + * @param {Ext.data.Record} record The data record returned from the underlying store + * @param {Number} index The index of the selected item in the dropdown list + */ + 'beforeselect', + /** + * @event select + * Fires when a list item is selected + * @param {Ext.form.ComboBox} combo This combo box + * @param {Ext.data.Record} record The data record returned from the underlying store + * @param {Number} index The index of the selected item in the dropdown list + */ + 'select', + /** + * @event beforequery + * Fires before all queries are processed. Return false to cancel the query or set the queryEvent's + * cancel property to true. + * @param {Object} queryEvent An object that has these properties:
        + *
      • combo : Ext.form.ComboBox
        This combo box
      • + *
      • query : String
        The query
      • + *
      • forceAll : Boolean
        True to force "all" query
      • + *
      • cancel : Boolean
        Set to true to cancel the query
      • + *
      + */ + 'beforequery' ); + if(this.transform){ + var s = Ext.getDom(this.transform); + if(!this.hiddenName){ + this.hiddenName = s.name; + } + if(!this.store){ + this.mode = 'local'; + var d = [], opts = s.options; + for(var i = 0, len = opts.length;i < len; i++){ + var o = opts[i], + value = (o.hasAttribute ? o.hasAttribute('value') : o.getAttributeNode('value').specified) ? o.value : o.text; + if(o.selected && Ext.isEmpty(this.value, true)) { + this.value = value; + } + d.push([value, o.text]); + } + this.store = new Ext.data.ArrayStore({ + 'id': 0, + fields: ['value', 'text'], + data : d, + autoDestroy: true + }); + this.valueField = 'value'; + this.displayField = 'text'; + } + s.name = Ext.id(); // wipe out the name in case somewhere else they have a reference + if(!this.lazyRender){ + this.target = true; + this.el = Ext.DomHelper.insertBefore(s, this.autoCreate || this.defaultAutoCreate); + this.render(this.el.parentNode, s); + } + Ext.removeNode(s); + } + //auto-configure store from local array data + else if(this.store){ + this.store = Ext.StoreMgr.lookup(this.store); + if(this.store.autoCreated){ + this.displayField = this.valueField = 'field1'; + if(!this.store.expandData){ + this.displayField = 'field2'; + } + this.mode = 'local'; + } + } + + this.selectedIndex = -1; + if(this.mode == 'local'){ + if(!Ext.isDefined(this.initialConfig.queryDelay)){ + this.queryDelay = 10; + } + if(!Ext.isDefined(this.initialConfig.minChars)){ + this.minChars = 0; + } + } }, // private - onResize : function(){ - Ext.form.Checkbox.superclass.onResize.apply(this, arguments); - if(!this.boxLabel && !this.fieldLabel){ - this.el.alignTo(this.wrap, 'c-c'); + onRender : function(ct, position){ + if(this.hiddenName && !Ext.isDefined(this.submitValue)){ + this.submitValue = false; + } + Ext.form.ComboBox.superclass.onRender.call(this, ct, position); + if(this.hiddenName){ + this.hiddenField = this.el.insertSibling({tag:'input', type:'hidden', name: this.hiddenName, + id: (this.hiddenId||this.hiddenName)}, 'before', true); + + } + if(Ext.isGecko){ + this.el.dom.setAttribute('autocomplete', 'off'); + } + + if(!this.lazyInit){ + this.initList(); + }else{ + this.on('focus', this.initList, this, {single: true}); } }, // private - initEvents : function(){ - Ext.form.Checkbox.superclass.initEvents.call(this); - this.mon(this.el, { - scope: this, - click: this.onClick, - change: this.onClick - }); + initValue : function(){ + Ext.form.ComboBox.superclass.initValue.call(this); + if(this.hiddenField){ + this.hiddenField.value = + Ext.value(Ext.isDefined(this.hiddenValue) ? this.hiddenValue : this.value, ''); + } + }, + + // private + initList : function(){ + if(!this.list){ + var cls = 'x-combo-list', + listParent = Ext.getDom(this.getListParent() || Ext.getBody()), + zindex = parseInt(Ext.fly(listParent).getStyle('z-index') ,10); + + if (this.ownerCt && !zindex){ + this.findParentBy(function(ct){ + zindex = parseInt(ct.getPositionEl().getStyle('z-index'), 10); + return !!zindex; + }); + } + + this.list = new Ext.Layer({ + parentEl: listParent, + shadow: this.shadow, + cls: [cls, this.listClass].join(' '), + constrain:false, + zindex: (zindex || 12000) + 5 + }); + + var lw = this.listWidth || Math.max(this.wrap.getWidth(), this.minListWidth); + this.list.setSize(lw, 0); + this.list.swallowEvent('mousewheel'); + this.assetHeight = 0; + if(this.syncFont !== false){ + this.list.setStyle('font-size', this.el.getStyle('font-size')); + } + if(this.title){ + this.header = this.list.createChild({cls:cls+'-hd', html: this.title}); + this.assetHeight += this.header.getHeight(); + } + + this.innerList = this.list.createChild({cls:cls+'-inner'}); + this.mon(this.innerList, 'mouseover', this.onViewOver, this); + this.mon(this.innerList, 'mousemove', this.onViewMove, this); + this.innerList.setWidth(lw - this.list.getFrameWidth('lr')); + + if(this.pageSize){ + this.footer = this.list.createChild({cls:cls+'-ft'}); + this.pageTb = new Ext.PagingToolbar({ + store: this.store, + pageSize: this.pageSize, + renderTo:this.footer + }); + this.assetHeight += this.footer.getHeight(); + } + + if(!this.tpl){ + /** + * @cfg {String/Ext.XTemplate} tpl

      The template string, or {@link Ext.XTemplate} instance to + * use to display each item in the dropdown list. The dropdown list is displayed in a + * DataView. See {@link #view}.

      + *

      The default template string is:

      
      +                  '<tpl for="."><div class="x-combo-list-item">{' + this.displayField + '}</div></tpl>'
      +                * 
      + *

      Override the default value to create custom UI layouts for items in the list. + * For example:

      
      +                  '<tpl for="."><div ext:qtip="{state}. {nick}" class="x-combo-list-item">{state}</div></tpl>'
      +                * 
      + *

      The template must contain one or more substitution parameters using field + * names from the Combo's {@link #store Store}. In the example above an + *

      ext:qtip
      attribute is added to display other fields from the Store.

      + *

      To preserve the default visual look of list items, add the CSS class name + *

      x-combo-list-item
      to the template's container element.

      + *

      Also see {@link #itemSelector} for additional details.

      + */ + this.tpl = '
      {' + this.displayField + '}
      '; + /** + * @cfg {String} itemSelector + *

      A simple CSS selector (e.g. div.some-class or span:first-child) that will be + * used to determine what nodes the {@link #view Ext.DataView} which handles the dropdown + * display will be working with.

      + *

      Note: this setting is required if a custom XTemplate has been + * specified in {@link #tpl} which assigns a class other than

      'x-combo-list-item'
      + * to dropdown list items + */ + } + + /** + * The {@link Ext.DataView DataView} used to display the ComboBox's options. + * @type Ext.DataView + */ + this.view = new Ext.DataView({ + applyTo: this.innerList, + tpl: this.tpl, + singleSelect: true, + selectedClass: this.selectedClass, + itemSelector: this.itemSelector || '.' + cls + '-item', + emptyText: this.listEmptyText, + deferEmptyText: false + }); + + this.mon(this.view, { + containerclick : this.onViewClick, + click : this.onViewClick, + scope :this + }); + + this.bindStore(this.store, true); + + if(this.resizable){ + this.resizer = new Ext.Resizable(this.list, { + pinned:true, handles:'se' + }); + this.mon(this.resizer, 'resize', function(r, w, h){ + this.maxHeight = h-this.handleHeight-this.list.getFrameWidth('tb')-this.assetHeight; + this.listWidth = w; + this.innerList.setWidth(w - this.list.getFrameWidth('lr')); + this.restrictHeight(); + }, this); + + this[this.pageSize?'footer':'innerList'].setStyle('margin-bottom', this.handleHeight+'px'); + } + } }, /** - * @hide - * Overridden and disabled. The editor element does not support standard valid/invalid marking. - * @method + *

      Returns the element used to house this ComboBox's pop-up list. Defaults to the document body.

      + * A custom implementation may be provided as a configuration option if the floating list needs to be rendered + * to a different Element. An example might be rendering the list inside a Menu so that clicking + * the list does not hide the Menu:
      
      +var store = new Ext.data.ArrayStore({
      +    autoDestroy: true,
      +    fields: ['initials', 'fullname'],
      +    data : [
      +        ['FF', 'Fred Flintstone'],
      +        ['BR', 'Barney Rubble']
      +    ]
      +});
      +
      +var combo = new Ext.form.ComboBox({
      +    store: store,
      +    displayField: 'fullname',
      +    emptyText: 'Select a name...',
      +    forceSelection: true,
      +    getListParent: function() {
      +        return this.el.up('.x-menu');
      +    },
      +    iconCls: 'no-icon', //use iconCls if placing within menu to shift to right side of menu
      +    mode: 'local',
      +    selectOnFocus: true,
      +    triggerAction: 'all',
      +    typeAhead: true,
      +    width: 135
      +});
      +
      +var menu = new Ext.menu.Menu({
      +    id: 'mainMenu',
      +    items: [
      +        combo // A Field in a Menu
      +    ]
      +});
      +
      */ - markInvalid : Ext.emptyFn, + getListParent : function() { + return document.body; + }, + /** - * @hide - * Overridden and disabled. The editor element does not support standard valid/invalid marking. - * @method + * Returns the store associated with this combo. + * @return {Ext.data.Store} The store */ - clearInvalid : Ext.emptyFn, + getStore : function(){ + return this.store; + }, // private - onRender : function(ct, position){ - Ext.form.Checkbox.superclass.onRender.call(this, ct, position); - if(this.inputValue !== undefined){ - this.el.dom.value = this.inputValue; + bindStore : function(store, initial){ + if(this.store && !initial){ + if(this.store !== store && this.store.autoDestroy){ + this.store.destroy(); + }else{ + this.store.un('beforeload', this.onBeforeLoad, this); + this.store.un('load', this.onLoad, this); + this.store.un('exception', this.collapse, this); + } + if(!store){ + this.store = null; + if(this.view){ + this.view.bindStore(null); + } + if(this.pageTb){ + this.pageTb.bindStore(null); + } + } } - this.wrap = this.el.wrap({cls: 'x-form-check-wrap'}); - if(this.boxLabel){ - this.wrap.createChild({tag: 'label', htmlFor: this.el.id, cls: 'x-form-cb-label', html: this.boxLabel}); + if(store){ + if(!initial) { + this.lastQuery = null; + if(this.pageTb) { + this.pageTb.bindStore(store); + } + } + + this.store = Ext.StoreMgr.lookup(store); + this.store.on({ + scope: this, + beforeload: this.onBeforeLoad, + load: this.onLoad, + exception: this.collapse + }); + + if(this.view){ + this.view.bindStore(store); + } } - if(this.checked){ - this.setValue(true); - }else{ - this.checked = this.el.dom.checked; + }, + + reset : function(){ + Ext.form.ComboBox.superclass.reset.call(this); + if(this.clearFilterOnReset && this.mode == 'local'){ + this.store.clearFilter(); } - // Need to repaint for IE, otherwise positioning is broken - if(Ext.isIE){ - this.wrap.repaint(); + }, + + // private + initEvents : function(){ + Ext.form.ComboBox.superclass.initEvents.call(this); + + + this.keyNav = new Ext.KeyNav(this.el, { + "up" : function(e){ + this.inKeyMode = true; + this.selectPrev(); + }, + + "down" : function(e){ + if(!this.isExpanded()){ + this.onTriggerClick(); + }else{ + this.inKeyMode = true; + this.selectNext(); + } + }, + + "enter" : function(e){ + this.onViewClick(); + }, + + "esc" : function(e){ + this.collapse(); + }, + + "tab" : function(e){ + this.collapse(); + return true; + }, + + scope : this, + + doRelay : function(e, h, hname){ + if(hname == 'down' || this.scope.isExpanded()){ + // this MUST be called before ComboBox#fireKey() + var relay = Ext.KeyNav.prototype.doRelay.apply(this, arguments); + if(!Ext.isIE && Ext.EventManager.useKeydown){ + // call Combo#fireKey() for browsers which use keydown event (except IE) + this.scope.fireKey(e); + } + return relay; + } + return true; + }, + + forceKeyDown : true, + defaultEventAction: 'stopEvent' + }); + this.queryDelay = Math.max(this.queryDelay || 10, + this.mode == 'local' ? 10 : 250); + this.dqTask = new Ext.util.DelayedTask(this.initQuery, this); + if(this.typeAhead){ + this.taTask = new Ext.util.DelayedTask(this.onTypeAhead, this); + } + if(!this.enableKeyEvents){ + this.mon(this.el, 'keyup', this.onKeyUp, this); } - this.resizeEl = this.positionEl = this.wrap; }, + // private onDestroy : function(){ - Ext.destroy(this.wrap); - Ext.form.Checkbox.superclass.onDestroy.call(this); + if (this.dqTask){ + this.dqTask.cancel(); + this.dqTask = null; + } + this.bindStore(null); + Ext.destroy( + this.resizer, + this.view, + this.pageTb, + this.list + ); + Ext.destroyMembers(this, 'hiddenField'); + Ext.form.ComboBox.superclass.onDestroy.call(this); }, // private - initValue : function() { - this.originalValue = this.getValue(); + fireKey : function(e){ + if (!this.isExpanded()) { + Ext.form.ComboBox.superclass.fireKey.call(this, e); + } + }, + + // private + onResize : function(w, h){ + Ext.form.ComboBox.superclass.onResize.apply(this, arguments); + if(this.isVisible() && this.list){ + this.doResize(w); + }else{ + this.bufferSize = w; + } + }, + + doResize: function(w){ + if(!Ext.isDefined(this.listWidth)){ + var lw = Math.max(w, this.minListWidth); + this.list.setWidth(lw); + this.innerList.setWidth(lw - this.list.getFrameWidth('lr')); + } + }, + + // private + onEnable : function(){ + Ext.form.ComboBox.superclass.onEnable.apply(this, arguments); + if(this.hiddenField){ + this.hiddenField.disabled = false; + } + }, + + // private + onDisable : function(){ + Ext.form.ComboBox.superclass.onDisable.apply(this, arguments); + if(this.hiddenField){ + this.hiddenField.disabled = true; + } + }, + + // private + onBeforeLoad : function(){ + if(!this.hasFocus){ + return; + } + this.innerList.update(this.loadingText ? + '
      '+this.loadingText+'
      ' : ''); + this.restrictHeight(); + this.selectedIndex = -1; + }, + + // private + onLoad : function(){ + if(!this.hasFocus){ + return; + } + if(this.store.getCount() > 0 || this.listEmptyText){ + this.expand(); + this.restrictHeight(); + if(this.lastQuery == this.allQuery){ + if(this.editable){ + this.el.dom.select(); + } + + if(this.autoSelect !== false && !this.selectByValue(this.value, true)){ + this.select(0, true); + } + }else{ + if(this.autoSelect !== false){ + this.selectNext(); + } + if(this.typeAhead && this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE){ + this.taTask.delay(this.typeAheadDelay); + } + } + }else{ + this.collapse(); + } + + }, + + // private + onTypeAhead : function(){ + if(this.store.getCount() > 0){ + var r = this.store.getAt(0); + var newValue = r.data[this.displayField]; + var len = newValue.length; + var selStart = this.getRawValue().length; + if(selStart != len){ + this.setRawValue(newValue); + this.selectText(selStart, newValue.length); + } + } + }, + + // private + assertValue : function(){ + + var val = this.getRawValue(), + rec = this.findRecord(this.displayField, val); + + if(!rec && this.forceSelection){ + if(val.length > 0 && val != this.emptyText){ + this.el.dom.value = Ext.value(this.lastSelectionText, ''); + this.applyEmptyText(); + }else{ + this.clearValue(); + } + }else{ + if(rec){ + val = rec.get(this.valueField || this.displayField); + } + this.setValue(val); + } + + }, + + // private + onSelect : function(record, index){ + if(this.fireEvent('beforeselect', this, record, index) !== false){ + this.setValue(record.data[this.valueField || this.displayField]); + this.collapse(); + this.fireEvent('select', this, record, index); + } + }, + + // inherit docs + getName: function(){ + var hf = this.hiddenField; + return hf && hf.name ? hf.name : this.hiddenName || Ext.form.ComboBox.superclass.getName.call(this); }, /** - * Returns the checked state of the checkbox. - * @return {Boolean} True if checked, else false + * Returns the currently selected field value or empty string if no value is set. + * @return {String} value The selected value */ getValue : function(){ - if(this.rendered){ - return this.el.dom.checked; + if(this.valueField){ + return Ext.isDefined(this.value) ? this.value : ''; + }else{ + return Ext.form.ComboBox.superclass.getValue.call(this); } - return this.checked; }, - // private - onClick : function(){ - if(this.el.dom.checked != this.checked){ - this.setValue(this.el.dom.checked); + /** + * Clears any text/value currently set in the field + */ + clearValue : function(){ + if(this.hiddenField){ + this.hiddenField.value = ''; } + this.setRawValue(''); + this.lastSelectionText = ''; + this.applyEmptyText(); + this.value = ''; }, /** - * Sets the checked state of the checkbox, fires the 'check' event, and calls a - * {@link #handler} (if configured). - * @param {Boolean/String} checked The following values will check the checkbox: - * true, 'true', '1', or 'on'. Any other value will uncheck the checkbox. + * Sets the specified value into the field. If the value finds a match, the corresponding record text + * will be displayed in the field. If the value does not match the data value of an existing item, + * and the valueNotFoundText config option is defined, it will be displayed as the default field text. + * Otherwise the field will be blank (although the value will still be set). + * @param {String} value The value to match * @return {Ext.form.Field} this */ setValue : function(v){ - var checked = this.checked ; - this.checked = (v === true || v === 'true' || v == '1' || String(v).toLowerCase() == 'on'); - if(this.rendered){ - this.el.dom.checked = this.checked; - this.el.dom.defaultChecked = this.checked; - } - if(checked != this.checked){ - this.fireEvent('check', this, this.checked); - if(this.handler){ - this.handler.call(this.scope || this, this, this.checked); + var text = v; + if(this.valueField){ + var r = this.findRecord(this.valueField, v); + if(r){ + text = r.data[this.displayField]; + }else if(Ext.isDefined(this.valueNotFoundText)){ + text = this.valueNotFoundText; } } + this.lastSelectionText = text; + if(this.hiddenField){ + this.hiddenField.value = Ext.value(v, ''); + } + Ext.form.ComboBox.superclass.setValue.call(this, text); + this.value = v; return this; - } -}); -Ext.reg('checkbox', Ext.form.Checkbox); -/** - * @class Ext.form.CheckboxGroup - * @extends Ext.form.Field - *

      A grouping container for {@link Ext.form.Checkbox} controls.

      - *

      Sample usage:

      - *
      
      -var myCheckboxGroup = new Ext.form.CheckboxGroup({
      -    id:'myGroup',
      -    xtype: 'checkboxgroup',
      -    fieldLabel: 'Single Column',
      -    itemCls: 'x-check-group-alt',
      -    // Put all controls in a single column with width 100%
      -    columns: 1,
      -    items: [
      -        {boxLabel: 'Item 1', name: 'cb-col-1'},
      -        {boxLabel: 'Item 2', name: 'cb-col-2', checked: true},
      -        {boxLabel: 'Item 3', name: 'cb-col-3'}
      -    ]
      -});
      - * 
      - * @constructor - * Creates a new CheckboxGroup - * @param {Object} config Configuration options - * @xtype checkboxgroup - */ -Ext.form.CheckboxGroup = Ext.extend(Ext.form.Field, { - /** - * @cfg {Array} items An Array of {@link Ext.form.Checkbox Checkbox}es or Checkbox config objects - * to arrange in the group. - */ - /** - * @cfg {String/Number/Array} columns Specifies the number of columns to use when displaying grouped - * checkbox/radio controls using automatic layout. This config can take several types of values: - *
      • 'auto' :

        The controls will be rendered one per column on one row and the width - * of each column will be evenly distributed based on the width of the overall field container. This is the default.

      • - *
      • Number :

        If you specific a number (e.g., 3) that number of columns will be - * created and the contained controls will be automatically distributed based on the value of {@link #vertical}.

      • - *
      • Array : Object

        You can also specify an array of column widths, mixing integer - * (fixed width) and float (percentage width) values as needed (e.g., [100, .25, .75]). Any integer values will - * be rendered first, then any float values will be calculated as a percentage of the remaining space. Float - * values do not have to add up to 1 (100%) although if you want the controls to take up the entire field - * container you should do so.

      - */ - columns : 'auto', - /** - * @cfg {Boolean} vertical True to distribute contained controls across columns, completely filling each column - * top to bottom before starting on the next column. The number of controls in each column will be automatically - * calculated to keep columns as even as possible. The default value is false, so that controls will be added - * to columns one at a time, completely filling each row left to right before starting on the next row. - */ - vertical : false, - /** - * @cfg {Boolean} allowBlank False to validate that at least one item in the group is checked (defaults to true). - * If no items are selected at validation time, {@link @blankText} will be used as the error text. - */ - allowBlank : true, - /** - * @cfg {String} blankText Error text to display if the {@link #allowBlank} validation fails (defaults to "You must - * select at least one item in this group") - */ - blankText : "You must select at least one item in this group", - - // private - defaultType : 'checkbox', - - // private - groupCls : 'x-form-check-group', - - // private - initComponent: function(){ - this.addEvents( - /** - * @event change - * Fires when the state of a child checkbox changes. - * @param {Ext.form.CheckboxGroup} this - * @param {Array} checked An array containing the checked boxes. - */ - 'change' - ); - Ext.form.CheckboxGroup.superclass.initComponent.call(this); - }, - - // private - onRender : function(ct, position){ - if(!this.el){ - var panelCfg = { - id: this.id, - cls: this.groupCls, - layout: 'column', - border: false, - renderTo: ct, - bufferResize: false // Default this to false, since it doesn't really have a proper ownerCt. - }; - var colCfg = { - defaultType: this.defaultType, - layout: 'form', - border: false, - defaults: { - hideLabel: true, - anchor: '100%' - } - }; - - if(this.items[0].items){ - - // The container has standard ColumnLayout configs, so pass them in directly - - Ext.apply(panelCfg, { - layoutConfig: {columns: this.items.length}, - defaults: this.defaults, - items: this.items - }); - for(var i=0, len=this.items.length; i0 && i%rows==0){ - ri++; - } - if(this.items[i].fieldLabel){ - this.items[i].hideLabel = false; - } - cols[ri].items.push(this.items[i]); - }; - }else{ - for(var i=0, len=this.items.length; i
  • - * See {@link Ext.form.Checkbox#setValue} for additional information. - * @param {Mixed} id The checkbox to check, or as described by example shown. - * @param {Boolean} value (optional) The value to set the item. - * @return {Ext.form.CheckboxGroup} this - */ - setValue: function(){ - if(this.rendered){ - this.onSetValue.apply(this, arguments); - }else{ - this.buffered = true; - this.value = arguments; - } - return this; - }, - - onSetValue: function(id, value){ - if(arguments.length == 1){ - if(Ext.isArray(id)){ - // an array of boolean values - Ext.each(id, function(val, idx){ - var item = this.items.itemAt(idx); - if(item){ - item.setValue(val); - } - }, this); - }else if(Ext.isObject(id)){ - // set of name/value pairs - for(var i in id){ - var f = this.getBox(i); - if(f){ - f.setValue(id[i]); - } - } - }else{ - this.setValueForItem(id); - } - }else{ - var f = this.getBox(id); - if(f){ - f.setValue(value); - } - } - }, - - // private - onDestroy: function(){ - Ext.destroy(this.panel); - Ext.form.CheckboxGroup.superclass.onDestroy.call(this); - - }, - - setValueForItem : function(val){ - val = String(val).split(','); - this.eachItem(function(item){ - if(val.indexOf(item.inputValue)> -1){ - item.setValue(true); - } - }); - }, - - // private - getBox : function(id){ - var box = null; - this.eachItem(function(f){ - if(id == f || f.dataIndex == id || f.id == id || f.getName() == id){ - box = f; - return false; - } - }); - return box; - }, - - /** - * Gets an array of the selected {@link Ext.form.Checkbox} in the group. - * @return {Array} An array of the selected checkboxes. - */ - getValue : function(){ - var out = []; - this.eachItem(function(item){ - if(item.checked){ - out.push(item); - } - }); - return out; - }, - - // private - eachItem: function(fn){ - if(this.items && this.items.each){ - this.items.each(fn, this); - } - }, - - /** - * @cfg {String} name - * @hide - */ - - /** - * @method getRawValue - * @hide - */ - getRawValue : Ext.emptyFn, - - /** - * @method setRawValue - * @hide - */ - setRawValue : Ext.emptyFn - -}); - -Ext.reg('checkboxgroup', Ext.form.CheckboxGroup); -/** - * @class Ext.form.Radio - * @extends Ext.form.Checkbox - * Single radio field. Same as Checkbox, but provided as a convenience for automatically setting the input type. - * Radio grouping is handled automatically by the browser if you give each radio in a group the same name. - * @constructor - * Creates a new Radio - * @param {Object} config Configuration options - * @xtype radio - */ -Ext.form.Radio = Ext.extend(Ext.form.Checkbox, { - inputType: 'radio', + }, + + // private + findRecord : function(prop, value){ + var record; + if(this.store.getCount() > 0){ + this.store.each(function(r){ + if(r.data[prop] == value){ + record = r; + return false; + } + }); + } + return record; + }, + + // private + onViewMove : function(e, t){ + this.inKeyMode = false; + }, + + // private + onViewOver : function(e, t){ + if(this.inKeyMode){ // prevent key nav and mouse over conflicts + return; + } + var item = this.view.findItemFromChild(t); + if(item){ + var index = this.view.indexOf(item); + this.select(index, false); + } + }, + + // private + onViewClick : function(doFocus){ + var index = this.view.getSelectedIndexes()[0], + s = this.store, + r = s.getAt(index); + if(r){ + this.onSelect(r, index); + }else { + this.collapse(); + } + if(doFocus !== false){ + this.el.focus(); + } + }, + + + // private + restrictHeight : function(){ + this.innerList.dom.style.height = ''; + var inner = this.innerList.dom, + pad = this.list.getFrameWidth('tb') + (this.resizable ? this.handleHeight : 0) + this.assetHeight, + h = Math.max(inner.clientHeight, inner.offsetHeight, inner.scrollHeight), + ha = this.getPosition()[1]-Ext.getBody().getScroll().top, + hb = Ext.lib.Dom.getViewHeight()-ha-this.getSize().height, + space = Math.max(ha, hb, this.minHeight || 0)-this.list.shadowOffset-pad-5; + + h = Math.min(h, space, this.maxHeight); + + this.innerList.setHeight(h); + this.list.beginUpdate(); + this.list.setHeight(h+pad); + this.list.alignTo.apply(this.list, [this.el].concat(this.listAlign)); + this.list.endUpdate(); + }, /** - * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide - * @method + * Returns true if the dropdown list is expanded, else false. */ - markInvalid : Ext.emptyFn, + isExpanded : function(){ + return this.list && this.list.isVisible(); + }, + /** - * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide - * @method + * Select an item in the dropdown list by its data value. This function does NOT cause the select event to fire. + * The store must be loaded and the list expanded for this function to work, otherwise use setValue. + * @param {String} value The data value of the item to select + * @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the + * selected item if it is not currently in view (defaults to true) + * @return {Boolean} True if the value matched an item in the list, else false */ - clearInvalid : Ext.emptyFn, + selectByValue : function(v, scrollIntoView){ + if(!Ext.isEmpty(v, true)){ + var r = this.findRecord(this.valueField || this.displayField, v); + if(r){ + this.select(this.store.indexOf(r), scrollIntoView); + return true; + } + } + return false; + }, /** - * If this radio is part of a group, it will return the selected value - * @return {String} + * Select an item in the dropdown list by its numeric index in the list. This function does NOT cause the select event to fire. + * The store must be loaded and the list expanded for this function to work, otherwise use setValue. + * @param {Number} index The zero-based index of the list item to select + * @param {Boolean} scrollIntoView False to prevent the dropdown list from autoscrolling to display the + * selected item if it is not currently in view (defaults to true) */ - getGroupValue : function(){ - var p = this.el.up('form') || Ext.getBody(); - var c = p.child('input[name='+this.el.dom.name+']:checked', true); - return c ? c.value : null; + select : function(index, scrollIntoView){ + this.selectedIndex = index; + this.view.select(index); + if(scrollIntoView !== false){ + var el = this.view.getNode(index); + if(el){ + this.innerList.scrollChildIntoView(el, false); + } + } + }, // private - onClick : function(){ - if(this.el.dom.checked != this.checked){ - var els = this.getCheckEl().select('input[name=' + this.el.dom.name + ']'); - els.each(function(el){ - if(el.dom.id == this.id){ - this.setValue(true); - }else{ - Ext.getCmp(el.dom.id).setValue(false); - } - }, this); - } + selectNext : function(){ + var ct = this.store.getCount(); + if(ct > 0){ + if(this.selectedIndex == -1){ + this.select(0); + }else if(this.selectedIndex < ct-1){ + this.select(this.selectedIndex+1); + } + } }, - /** - * Sets either the checked/unchecked status of this Radio, or, if a string value - * is passed, checks a sibling Radio of the same name whose value is the value specified. - * @param value {String/Boolean} Checked value, or the value of the sibling radio button to check. - * @return {Ext.form.Field} this - */ - setValue : function(v){ - if (typeof v == 'boolean') { - Ext.form.Radio.superclass.setValue.call(this, v); - } else { - var r = this.getCheckEl().child('input[name=' + this.el.dom.name + '][value=' + v + ']', true); - if(r){ - Ext.getCmp(r.id).setValue(true); + // private + selectPrev : function(){ + var ct = this.store.getCount(); + if(ct > 0){ + if(this.selectedIndex == -1){ + this.select(0); + }else if(this.selectedIndex !== 0){ + this.select(this.selectedIndex-1); } } - return this; }, - + // private - getCheckEl: function(){ - if(this.inGroup){ - return this.el.up('.x-form-radio-group') + onKeyUp : function(e){ + var k = e.getKey(); + if(this.editable !== false && this.readOnly !== true && (k == e.BACKSPACE || !e.isSpecialKey())){ + + this.lastKey = k; + this.dqTask.delay(this.queryDelay); } - return this.el.up('form') || Ext.getBody(); - } -}); -Ext.reg('radio', Ext.form.Radio); -/** - * @class Ext.form.RadioGroup - * @extends Ext.form.CheckboxGroup - * A grouping container for {@link Ext.form.Radio} controls. - * @constructor - * Creates a new RadioGroup - * @param {Object} config Configuration options - * @xtype radiogroup - */ -Ext.form.RadioGroup = Ext.extend(Ext.form.CheckboxGroup, { - /** - * @cfg {Boolean} allowBlank True to allow every item in the group to be blank (defaults to true). - * If allowBlank = false and no items are selected at validation time, {@link @blankText} will - * be used as the error text. - */ - allowBlank : true, - /** - * @cfg {String} blankText Error text to display if the {@link #allowBlank} validation fails - * (defaults to 'You must select one item in this group') - */ - blankText : 'You must select one item in this group', - - // private - defaultType : 'radio', - - // private - groupCls : 'x-form-radio-group', - - /** - * @event change - * Fires when the state of a child radio changes. - * @param {Ext.form.RadioGroup} this - * @param {Ext.form.Radio} checked The checked radio - */ - - /** - * Gets the selected {@link Ext.form.Radio} in the group, if it exists. - * @return {Ext.form.Radio} The selected radio. - */ - getValue : function(){ - var out = null; - this.eachItem(function(item){ - if(item.checked){ - out = item; - return false; - } - }); - return out; - }, - - /** - * Sets the checked radio in the group. - * @param {String/Ext.form.Radio} id The radio to check. - * @param {Boolean} value The value to set the radio. - * @return {Ext.form.RadioGroup} this - */ - onSetValue : function(id, value){ - if(arguments.length > 1){ - var f = this.getBox(id); - if(f){ - f.setValue(value); - if(f.checked){ - this.eachItem(function(item){ - if (item !== f){ - item.setValue(false); - } - }); - } - } - }else{ - this.setValueForItem(id); - } - }, - - setValueForItem : function(val){ - val = String(val).split(',')[0]; - this.eachItem(function(item){ - item.setValue(val == item.inputValue); - }); - }, - - // private - fireChecked : function(){ - if(!this.checkTask){ - this.checkTask = new Ext.util.DelayedTask(this.bufferChecked, this); - } - this.checkTask.delay(10); - }, - - // private - bufferChecked : function(){ - var out = null; - this.eachItem(function(item){ - if(item.checked){ - out = item; - return false; - } - }); - this.fireEvent('change', this, out); - }, - - onDestroy : function(){ - if(this.checkTask){ - this.checkTask.cancel(); - this.checkTask = null; - } - Ext.form.RadioGroup.superclass.onDestroy.call(this); - } - -}); - -Ext.reg('radiogroup', Ext.form.RadioGroup); -/** - * @class Ext.form.Hidden - * @extends Ext.form.Field - * A basic hidden field for storing hidden values in forms that need to be passed in the form submit. - * @constructor - * Create a new Hidden field. - * @param {Object} config Configuration options - * @xtype hidden - */ -Ext.form.Hidden = Ext.extend(Ext.form.Field, { - // private - inputType : 'hidden', - - // private - onRender : function(){ - Ext.form.Hidden.superclass.onRender.apply(this, arguments); - }, - - // private - initEvents : function(){ - this.originalValue = this.getValue(); - }, - - // These are all private overrides - setSize : Ext.emptyFn, - setWidth : Ext.emptyFn, - setHeight : Ext.emptyFn, - setPosition : Ext.emptyFn, - setPagePosition : Ext.emptyFn, - markInvalid : Ext.emptyFn, - clearInvalid : Ext.emptyFn -}); -Ext.reg('hidden', Ext.form.Hidden);/** - * @class Ext.form.BasicForm - * @extends Ext.util.Observable - *

    Encapsulates the DOM <form> element at the heart of the {@link Ext.form.FormPanel FormPanel} class, and provides - * input field management, validation, submission, and form loading services.

    - *

    By default, Ext Forms are submitted through Ajax, using an instance of {@link Ext.form.Action.Submit}. - * To enable normal browser submission of an Ext Form, use the {@link #standardSubmit} config option.

    - *

    File Uploads

    - *

    {@link #fileUpload File uploads} are not performed using Ajax submission, that - * is they are not performed using XMLHttpRequests. Instead the form is submitted in the standard - * manner with the DOM <form> element temporarily modified to have its - * target set to refer - * to a dynamically generated, hidden <iframe> which is inserted into the document - * but removed after the return data has been gathered.

    - *

    The server response is parsed by the browser to create the document for the IFRAME. If the - * server is using JSON to send the return object, then the - * Content-Type header - * must be set to "text/html" in order to tell the browser to insert the text unchanged into the document body.

    - *

    Characters which are significant to an HTML parser must be sent as HTML entities, so encode - * "<" as "&lt;", "&" as "&amp;" etc.

    - *

    The response text is retrieved from the document, and a fake XMLHttpRequest object - * is created containing a responseText property in order to conform to the - * requirements of event handlers and callbacks.

    - *

    Be aware that file upload packets are sent with the content type multipart/form - * and some server technologies (notably JEE) may require some custom processing in order to - * retrieve parameter names and parameter values from the packet content.

    - * @constructor - * @param {Mixed} el The form element or its id - * @param {Object} config Configuration options - */ -Ext.form.BasicForm = function(el, config){ - Ext.apply(this, config); - if(Ext.isString(this.paramOrder)){ - this.paramOrder = this.paramOrder.split(/[\s,|]/); - } - /** - * @property items - * A {@link Ext.util.MixedCollection MixedCollection) containing all the Ext.form.Fields in this form. - * @type MixedCollection - */ - this.items = new Ext.util.MixedCollection(false, function(o){ - return o.getItemId(); - }); - this.addEvents( - /** - * @event beforeaction - * Fires before any action is performed. Return false to cancel the action. - * @param {Form} this - * @param {Action} action The {@link Ext.form.Action} to be performed - */ - 'beforeaction', - /** - * @event actionfailed - * Fires when an action fails. - * @param {Form} this - * @param {Action} action The {@link Ext.form.Action} that failed - */ - 'actionfailed', - /** - * @event actioncomplete - * Fires when an action is completed. - * @param {Form} this - * @param {Action} action The {@link Ext.form.Action} that completed - */ - 'actioncomplete' - ); + Ext.form.ComboBox.superclass.onKeyUp.call(this, e); + }, - if(el){ - this.initEl(el); - } - Ext.form.BasicForm.superclass.constructor.call(this); -}; + // private + validateBlur : function(){ + return !this.list || !this.list.isVisible(); + }, + + // private + initQuery : function(){ + this.doQuery(this.getRawValue()); + }, + + // private + beforeBlur : function(){ + this.assertValue(); + }, + + // private + postBlur : function(){ + Ext.form.ComboBox.superclass.postBlur.call(this); + this.collapse(); + this.inKeyMode = false; + }, + + /** + * Execute a query to filter the dropdown list. Fires the {@link #beforequery} event prior to performing the + * query allowing the query action to be canceled if needed. + * @param {String} query The SQL query to execute + * @param {Boolean} forceAll true to force the query to execute even if there are currently fewer + * characters in the field than the minimum specified by the {@link #minChars} config option. It + * also clears any filter previously saved in the current store (defaults to false) + */ + doQuery : function(q, forceAll){ + q = Ext.isEmpty(q) ? '' : q; + var qe = { + query: q, + forceAll: forceAll, + combo: this, + cancel:false + }; + if(this.fireEvent('beforequery', qe)===false || qe.cancel){ + return false; + } + q = qe.query; + forceAll = qe.forceAll; + if(forceAll === true || (q.length >= this.minChars)){ + if(this.lastQuery !== q){ + this.lastQuery = q; + if(this.mode == 'local'){ + this.selectedIndex = -1; + if(forceAll){ + this.store.clearFilter(); + }else{ + this.store.filter(this.displayField, q); + } + this.onLoad(); + }else{ + this.store.baseParams[this.queryParam] = q; + this.store.load({ + params: this.getParams(q) + }); + this.expand(); + } + }else{ + this.selectedIndex = -1; + this.onLoad(); + } + } + }, + + // private + getParams : function(q){ + var p = {}; + //p[this.queryParam] = q; + if(this.pageSize){ + p.start = 0; + p.limit = this.pageSize; + } + return p; + }, -Ext.extend(Ext.form.BasicForm, Ext.util.Observable, { /** - * @cfg {String} method - * The request method to use (GET or POST) for form actions if one isn't supplied in the action options. + * Hides the dropdown list if it is currently expanded. Fires the {@link #collapse} event on completion. */ + collapse : function(){ + if(!this.isExpanded()){ + return; + } + this.list.hide(); + Ext.getDoc().un('mousewheel', this.collapseIf, this); + Ext.getDoc().un('mousedown', this.collapseIf, this); + this.fireEvent('collapse', this); + }, + + // private + collapseIf : function(e){ + if(!e.within(this.wrap) && !e.within(this.list)){ + this.collapse(); + } + }, + /** - * @cfg {DataReader} reader - * An Ext.data.DataReader (e.g. {@link Ext.data.XmlReader}) to be used to read - * data when executing 'load' actions. This is optional as there is built-in - * support for processing JSON. For additional information on using an XMLReader - * see the example provided in examples/form/xml-form.html. + * Expands the dropdown list if it is currently hidden. Fires the {@link #expand} event on completion. */ + expand : function(){ + if(this.isExpanded() || !this.hasFocus){ + return; + } + if(this.bufferSize){ + this.doResize(this.bufferSize); + delete this.bufferSize; + } + this.list.alignTo.apply(this.list, [this.el].concat(this.listAlign)); + this.list.show(); + if(Ext.isGecko2){ + this.innerList.setOverflow('auto'); // necessary for FF 2.0/Mac + } + this.mon(Ext.getDoc(), { + scope: this, + mousewheel: this.collapseIf, + mousedown: this.collapseIf + }); + this.fireEvent('expand', this); + }, + /** - * @cfg {DataReader} errorReader - *

    An Ext.data.DataReader (e.g. {@link Ext.data.XmlReader}) to be used to - * read field error messages returned from 'submit' actions. This is optional - * as there is built-in support for processing JSON.

    - *

    The Records which provide messages for the invalid Fields must use the - * Field name (or id) as the Record ID, and must contain a field called 'msg' - * which contains the error message.

    - *

    The errorReader does not have to be a full-blown implementation of a - * DataReader. It simply needs to implement a read(xhr) function - * which returns an Array of Records in an object with the following - * structure:

    
    -{
    -    records: recordArray
    -}
    -
    + * @method onTriggerClick + * @hide */ + // private + // Implements the default empty TriggerField.onTriggerClick function + onTriggerClick : function(){ + if(this.readOnly || this.disabled){ + return; + } + if(this.isExpanded()){ + this.collapse(); + this.el.focus(); + }else { + this.onFocus({}); + if(this.triggerAction == 'all') { + this.doQuery(this.allQuery, true); + } else { + this.doQuery(this.getRawValue()); + } + this.el.focus(); + } + } + /** - * @cfg {String} url - * The URL to use for form actions if one isn't supplied in the - * {@link #doAction doAction} options. + * @hide + * @method autoSize */ /** - * @cfg {Boolean} fileUpload - * Set to true if this form is a file upload. - *

    File uploads are not performed using normal 'Ajax' techniques, that is they are not - * performed using XMLHttpRequests. Instead the form is submitted in the standard manner with the - * DOM <form> element temporarily modified to have its - * target set to refer - * to a dynamically generated, hidden <iframe> which is inserted into the document - * but removed after the return data has been gathered.

    - *

    The server response is parsed by the browser to create the document for the IFRAME. If the - * server is using JSON to send the return object, then the - * Content-Type header - * must be set to "text/html" in order to tell the browser to insert the text unchanged into the document body.

    - *

    Characters which are significant to an HTML parser must be sent as HTML entities, so encode - * "<" as "&lt;", "&" as "&amp;" etc.

    - *

    The response text is retrieved from the document, and a fake XMLHttpRequest object - * is created containing a responseText property in order to conform to the - * requirements of event handlers and callbacks.

    - *

    Be aware that file upload packets are sent with the content type multipart/form - * and some server technologies (notably JEE) may require some custom processing in order to - * retrieve parameter names and parameter values from the packet content.

    + * @cfg {Boolean} grow @hide */ /** - * @cfg {Object} baseParams - *

    Parameters to pass with all requests. e.g. baseParams: {id: '123', foo: 'bar'}.

    - *

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

    + * @cfg {Number} growMin @hide */ /** - * @cfg {Number} timeout Timeout for form actions in seconds (default is 30 seconds). + * @cfg {Number} growMax @hide */ - timeout: 30, - /** - * @cfg {Object} api (Optional) If specified load and submit actions will be handled - * with {@link Ext.form.Action.DirectLoad} and {@link Ext.form.Action.DirectSubmit}. - * Methods which have been imported by Ext.Direct can be specified here to load and submit - * forms. - * Such as the following:
    
    -api: {
    -    load: App.ss.MyProfile.load,
    -    submit: App.ss.MyProfile.submit
    -}
    -
    - *

    Load actions can use {@link #paramOrder} or {@link #paramsAsHash} - * to customize how the load method is invoked. - * Submit actions will always use a standard form submit. The formHandler configuration must - * be set on the associated server-side method which has been imported by Ext.Direct

    +}); +Ext.reg('combo', Ext.form.ComboBox); +/** + * @class Ext.form.Checkbox + * @extends Ext.form.Field + * Single checkbox field. Can be used as a direct replacement for traditional checkbox fields. + * @constructor + * Creates a new Checkbox + * @param {Object} config Configuration options + * @xtype checkbox + */ +Ext.form.Checkbox = Ext.extend(Ext.form.Field, { + /** + * @cfg {String} focusClass The CSS class to use when the checkbox receives focus (defaults to undefined) */ - + focusClass : undefined, /** - * @cfg {Array/String} paramOrder

    A list of params to be executed server side. - * Defaults to undefined. Only used for the {@link #api} - * load configuration.

    - *

    Specify the params in the order in which they must be executed on the - * server-side as either (1) an Array of String values, or (2) a String of params - * delimited by either whitespace, comma, or pipe. For example, - * any of the following would be acceptable:

    
    -paramOrder: ['param1','param2','param3']
    -paramOrder: 'param1 param2 param3'
    -paramOrder: 'param1,param2,param3'
    -paramOrder: 'param1|param2|param'
    -     
    + * @cfg {String} fieldClass The default CSS class for the checkbox (defaults to 'x-form-field') */ - paramOrder: undefined, - + fieldClass : 'x-form-field', /** - * @cfg {Boolean} paramsAsHash Only used for the {@link #api} - * load configuration. Send parameters as a collection of named - * arguments (defaults to false). Providing a - * {@link #paramOrder} nullifies this configuration. + * @cfg {Boolean} checked true if the checkbox should render initially checked (defaults to false) */ - paramsAsHash: false, - + checked : false, /** - * @cfg {String} waitTitle - * The default title to show for the waiting message box (defaults to 'Please Wait...') + * @cfg {String/Object} autoCreate A DomHelper element spec, or true for a default element spec (defaults to + * {tag: 'input', type: 'checkbox', autocomplete: 'off'}) */ - waitTitle: 'Please Wait...', - - // private - activeAction : null, - + defaultAutoCreate : { tag: 'input', type: 'checkbox', autocomplete: 'off'}, /** - * @cfg {Boolean} trackResetOnLoad If set to true, {@link #reset}() resets to the last loaded - * or {@link #setValues}() data instead of when the form was first created. Defaults to false. + * @cfg {String} boxLabel The text that appears beside the checkbox */ - trackResetOnLoad : false, - /** - * @cfg {Boolean} standardSubmit - *

    If set to true, standard HTML form submits are used instead - * of XHR (Ajax) style form submissions. Defaults to false.

    - *

    Note: When using standardSubmit, the - * options to {@link #submit} are ignored because - * Ext's Ajax infrastracture is bypassed. To pass extra parameters (e.g. - * baseParams and params), utilize hidden fields - * to submit extra data, for example:

    - *
    
    -new Ext.FormPanel({
    -    standardSubmit: true,
    -    baseParams: {
    -        foo: 'bar'
    -    },
    -    {@link url}: 'myProcess.php',
    -    items: [{
    -        xtype: 'textfield',
    -        name: 'userName'
    -    }],
    -    buttons: [{
    -        text: 'Save',
    -        handler: function(){
    -            var fp = this.ownerCt.ownerCt,
    -                form = fp.getForm();
    -            if (form.isValid()) {
    -                // check if there are baseParams and if
    -                // hiddent items have been added already
    -                if (fp.baseParams && !fp.paramsAdded) {
    -                    // add hidden items for all baseParams
    -                    for (i in fp.baseParams) {
    -                        fp.add({
    -                            xtype: 'hidden',
    -                            name: i,
    -                            value: fp.baseParams[i]
    -                        });
    -                    }
    -                    fp.doLayout();
    -                    // set a custom flag to prevent re-adding
    -                    fp.paramsAdded = true;
    -                }
    -                form.{@link #submit}();
    -            }
    -        }
    -    }]
    -});
    -     * 
    + * @cfg {String} inputValue The value that should go into the generated input element's value attribute */ /** - * By default wait messages are displayed with Ext.MessageBox.wait. You can target a specific - * element by passing it or its id or mask the form itself by passing in true. - * @type Mixed - * @property waitMsgTarget + * @cfg {Function} handler A function called when the {@link #checked} value changes (can be used instead of + * handling the check event). The handler is passed the following parameters: + *
      + *
    • checkbox : Ext.form.Checkbox
      The Checkbox being toggled.
    • + *
    • checked : Boolean
      The new checked state of the checkbox.
    • + *
    */ - - // private - initEl : function(el){ - this.el = Ext.get(el); - this.id = this.el.id || Ext.id(); - if(!this.standardSubmit){ - this.el.on('submit', this.onSubmit, this); - } - this.el.addClass('x-form'); - }, - /** - * Get the HTML form Element - * @return Ext.Element + * @cfg {Object} scope An object to use as the scope ('this' reference) of the {@link #handler} function + * (defaults to this Checkbox). */ - getEl: function(){ - return this.el; - }, // private - onSubmit : function(e){ - e.stopEvent(); + actionMode : 'wrap', + + // private + initComponent : function(){ + Ext.form.Checkbox.superclass.initComponent.call(this); + this.addEvents( + /** + * @event check + * Fires when the checkbox is checked or unchecked. + * @param {Ext.form.Checkbox} this This checkbox + * @param {Boolean} checked The new checked value + */ + 'check' + ); }, // private - destroy: function() { - this.items.each(function(f){ - Ext.destroy(f); - }); - if(this.el){ - this.el.removeAllListeners(); - this.el.remove(); + onResize : function(){ + Ext.form.Checkbox.superclass.onResize.apply(this, arguments); + if(!this.boxLabel && !this.fieldLabel){ + this.el.alignTo(this.wrap, 'c-c'); } - this.purgeListeners(); }, - /** - * Returns true if client-side validation on the form is successful. - * @return Boolean - */ - isValid : function(){ - var valid = true; - this.items.each(function(f){ - if(!f.validate()){ - valid = false; - } + // private + initEvents : function(){ + Ext.form.Checkbox.superclass.initEvents.call(this); + this.mon(this.el, { + scope: this, + click: this.onClick, + change: this.onClick }); - return valid; }, /** - *

    Returns true if any fields in this form have changed from their original values.

    - *

    Note that if this BasicForm was configured with {@link #trackResetOnLoad} then the - * Fields' original values are updated when the values are loaded by {@link #setValues} - * or {@link #loadRecord}.

    - * @return Boolean + * @hide + * Overridden and disabled. The editor element does not support standard valid/invalid marking. + * @method */ - isDirty : function(){ - var dirty = false; - this.items.each(function(f){ - if(f.isDirty()){ - dirty = true; - return false; - } - }); - return dirty; - }, - + markInvalid : Ext.emptyFn, /** - * Performs a predefined action ({@link Ext.form.Action.Submit} or - * {@link Ext.form.Action.Load}) or a custom extension of {@link Ext.form.Action} - * to perform application-specific processing. - * @param {String/Object} actionName The name of the predefined action type, - * or instance of {@link Ext.form.Action} to perform. - * @param {Object} options (optional) The options to pass to the {@link Ext.form.Action}. - * All of the config options listed below are supported by both the - * {@link Ext.form.Action.Submit submit} and {@link Ext.form.Action.Load load} - * actions unless otherwise noted (custom actions could also accept - * other config options):
      - * - *
    • url : String
      The url for the action (defaults - * to the form's {@link #url}.)
    • - * - *
    • method : String
      The form method to use (defaults - * to the form's method, or POST if not defined)
    • - * - *
    • params : String/Object

      The params to pass - * (defaults to the form's baseParams, or none if not defined)

      - *

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

    • - * - *
    • headers : Object
      Request headers to set for the action - * (defaults to the form's default headers)
    • - * - *
    • success : Function
      The callback that will - * be invoked after a successful response (see top of - * {@link Ext.form.Action.Submit submit} and {@link Ext.form.Action.Load load} - * for a description of what constitutes a successful response). - * The function is passed the following parameters:
        - *
      • form : Ext.form.BasicForm
        The form that requested the action
      • - *
      • action : The {@link Ext.form.Action Action} object which performed the operation. - *
        The action object contains these properties of interest:
          - *
        • {@link Ext.form.Action#response response}
        • - *
        • {@link Ext.form.Action#result result} : interrogate for custom postprocessing
        • - *
        • {@link Ext.form.Action#type type}
        • - *
    • - * - *
    • failure : Function
      The callback that will be invoked after a - * failed transaction attempt. The function is passed the following parameters:
        - *
      • form : The {@link Ext.form.BasicForm} that requested the action.
      • - *
      • action : The {@link Ext.form.Action Action} object which performed the operation. - *
        The action object contains these properties of interest:
          - *
        • {@link Ext.form.Action#failureType failureType}
        • - *
        • {@link Ext.form.Action#response response}
        • - *
        • {@link Ext.form.Action#result result} : interrogate for custom postprocessing
        • - *
        • {@link Ext.form.Action#type type}
        • - *
    • - * - *
    • scope : Object
      The scope in which to call the - * callback functions (The this reference for the callback functions).
    • - * - *
    • clientValidation : Boolean
      Submit Action only. - * Determines whether a Form's fields are validated in a final call to - * {@link Ext.form.BasicForm#isValid isValid} prior to submission. Set to false - * to prevent this. If undefined, pre-submission field validation is performed.
    - * - * @return {BasicForm} this + * @hide + * Overridden and disabled. The editor element does not support standard valid/invalid marking. + * @method */ - doAction : function(action, options){ - if(Ext.isString(action)){ - action = new Ext.form.Action.ACTION_TYPES[action](this, options); + clearInvalid : Ext.emptyFn, + + // private + onRender : function(ct, position){ + Ext.form.Checkbox.superclass.onRender.call(this, ct, position); + if(this.inputValue !== undefined){ + this.el.dom.value = this.inputValue; } - if(this.fireEvent('beforeaction', this, action) !== false){ - this.beforeAction(action); - action.run.defer(100, action); + this.wrap = this.el.wrap({cls: 'x-form-check-wrap'}); + if(this.boxLabel){ + this.wrap.createChild({tag: 'label', htmlFor: this.el.id, cls: 'x-form-cb-label', html: this.boxLabel}); } - return this; + if(this.checked){ + this.setValue(true); + }else{ + this.checked = this.el.dom.checked; + } + // Need to repaint for IE, otherwise positioning is broken + if(Ext.isIE){ + this.wrap.repaint(); + } + this.resizeEl = this.positionEl = this.wrap; }, - /** - * Shortcut to {@link #doAction do} a {@link Ext.form.Action.Submit submit action}. - * @param {Object} options The options to pass to the action (see {@link #doAction} for details).
    - *

    Note: this is ignored when using the {@link #standardSubmit} option.

    - *

    The following code:

    
    -myFormPanel.getForm().submit({
    -    clientValidation: true,
    -    url: 'updateConsignment.php',
    -    params: {
    -        newStatus: 'delivered'
    +    // private
    +    onDestroy : function(){
    +        Ext.destroy(this.wrap);
    +        Ext.form.Checkbox.superclass.onDestroy.call(this);
         },
    -    success: function(form, action) {
    -       Ext.Msg.alert('Success', action.result.msg);
    +
    +    // private
    +    initValue : function() {
    +        this.originalValue = this.getValue();
         },
    -    failure: function(form, action) {
    -        switch (action.failureType) {
    -            case Ext.form.Action.CLIENT_INVALID:
    -                Ext.Msg.alert('Failure', 'Form fields may not be submitted with invalid values');
    -                break;
    -            case Ext.form.Action.CONNECT_FAILURE:
    -                Ext.Msg.alert('Failure', 'Ajax communication failed');
    -                break;
    -            case Ext.form.Action.SERVER_INVALID:
    -               Ext.Msg.alert('Failure', action.result.msg);
    -       }
    -    }
    -});
    -
    - * would process the following server response for a successful submission:
    
    -{
    -    "success":true, // note this is Boolean, not string
    -    "msg":"Consignment updated"
    -}
    -
    - * and the following server response for a failed submission:
    
    -{
    -    "success":false, // note this is Boolean, not string
    -    "msg":"You do not have permission to perform this operation"
    -}
    -
    - * @return {BasicForm} this + + /** + * Returns the checked state of the checkbox. + * @return {Boolean} True if checked, else false */ - submit : function(options){ - if(this.standardSubmit){ - var v = this.isValid(); - if(v){ - var el = this.el.dom; - if(this.url && Ext.isEmpty(el.action)){ - el.action = this.url; - } - el.submit(); - } - return v; + getValue : function(){ + if(this.rendered){ + return this.el.dom.checked; } - var submitAction = String.format('{0}submit', this.api ? 'direct' : ''); - this.doAction(submitAction, options); - return this; + return this.checked; }, - /** - * Shortcut to {@link #doAction do} a {@link Ext.form.Action.Load load action}. - * @param {Object} options The options to pass to the action (see {@link #doAction} for details) - * @return {BasicForm} this - */ - load : function(options){ - var loadAction = String.format('{0}load', this.api ? 'direct' : ''); - this.doAction(loadAction, options); - return this; + // private + onClick : function(){ + if(this.el.dom.checked != this.checked){ + this.setValue(this.el.dom.checked); + } }, /** - * Persists the values in this form into the passed {@link Ext.data.Record} object in a beginEdit/endEdit block. - * @param {Record} record The record to edit - * @return {BasicForm} this + * Sets the checked state of the checkbox, fires the 'check' event, and calls a + * {@link #handler} (if configured). + * @param {Boolean/String} checked The following values will check the checkbox: + * true, 'true', '1', or 'on'. Any other value will uncheck the checkbox. + * @return {Ext.form.Field} this */ - updateRecord : function(record){ - record.beginEdit(); - var fs = record.fields; - fs.each(function(f){ - var field = this.findField(f.name); - if(field){ - record.set(f.name, field.getValue()); + setValue : function(v){ + var checked = this.checked ; + this.checked = (v === true || v === 'true' || v == '1' || String(v).toLowerCase() == 'on'); + if(this.rendered){ + this.el.dom.checked = this.checked; + this.el.dom.defaultChecked = this.checked; + } + if(checked != this.checked){ + this.fireEvent('check', this, this.checked); + if(this.handler){ + this.handler.call(this.scope || this, this, this.checked); } - }, this); - record.endEdit(); + } return this; - }, - + } +}); +Ext.reg('checkbox', Ext.form.Checkbox); +/** + * @class Ext.form.CheckboxGroup + * @extends Ext.form.Field + *

    A grouping container for {@link Ext.form.Checkbox} controls.

    + *

    Sample usage:

    + *
    
    +var myCheckboxGroup = new Ext.form.CheckboxGroup({
    +    id:'myGroup',
    +    xtype: 'checkboxgroup',
    +    fieldLabel: 'Single Column',
    +    itemCls: 'x-check-group-alt',
    +    // Put all controls in a single column with width 100%
    +    columns: 1,
    +    items: [
    +        {boxLabel: 'Item 1', name: 'cb-col-1'},
    +        {boxLabel: 'Item 2', name: 'cb-col-2', checked: true},
    +        {boxLabel: 'Item 3', name: 'cb-col-3'}
    +    ]
    +});
    + * 
    + * @constructor + * Creates a new CheckboxGroup + * @param {Object} config Configuration options + * @xtype checkboxgroup + */ +Ext.form.CheckboxGroup = Ext.extend(Ext.form.Field, { /** - * Loads an {@link Ext.data.Record} into this form by calling {@link #setValues} with the - * {@link Ext.data.Record#data record data}. - * See also {@link #trackResetOnLoad}. - * @param {Record} record The record to load - * @return {BasicForm} this + * @cfg {Array} items An Array of {@link Ext.form.Checkbox Checkbox}es or Checkbox config objects + * to arrange in the group. */ - loadRecord : function(record){ - this.setValues(record.data); - return this; - }, + /** + * @cfg {String/Number/Array} columns Specifies the number of columns to use when displaying grouped + * checkbox/radio controls using automatic layout. This config can take several types of values: + *
    • 'auto' :

      The controls will be rendered one per column on one row and the width + * of each column will be evenly distributed based on the width of the overall field container. This is the default.

    • + *
    • Number :

      If you specific a number (e.g., 3) that number of columns will be + * created and the contained controls will be automatically distributed based on the value of {@link #vertical}.

    • + *
    • Array : Object

      You can also specify an array of column widths, mixing integer + * (fixed width) and float (percentage width) values as needed (e.g., [100, .25, .75]). Any integer values will + * be rendered first, then any float values will be calculated as a percentage of the remaining space. Float + * values do not have to add up to 1 (100%) although if you want the controls to take up the entire field + * container you should do so.

    + */ + columns : 'auto', + /** + * @cfg {Boolean} vertical True to distribute contained controls across columns, completely filling each column + * top to bottom before starting on the next column. The number of controls in each column will be automatically + * calculated to keep columns as even as possible. The default value is false, so that controls will be added + * to columns one at a time, completely filling each row left to right before starting on the next row. + */ + vertical : false, + /** + * @cfg {Boolean} allowBlank False to validate that at least one item in the group is checked (defaults to true). + * If no items are selected at validation time, {@link @blankText} will be used as the error text. + */ + allowBlank : true, + /** + * @cfg {String} blankText Error text to display if the {@link #allowBlank} validation fails (defaults to "You must + * select at least one item in this group") + */ + blankText : "You must select at least one item in this group", // private - beforeAction : function(action){ - var o = action.options; - if(o.waitMsg){ - if(this.waitMsgTarget === true){ - this.el.mask(o.waitMsg, 'x-mask-loading'); - }else if(this.waitMsgTarget){ - this.waitMsgTarget = Ext.get(this.waitMsgTarget); - this.waitMsgTarget.mask(o.waitMsg, 'x-mask-loading'); - }else{ - Ext.MessageBox.wait(o.waitMsg, o.waitTitle || this.waitTitle); - } - } - }, + defaultType : 'checkbox', // private - afterAction : function(action, success){ - this.activeAction = null; - var o = action.options; - if(o.waitMsg){ - if(this.waitMsgTarget === true){ - this.el.unmask(); - }else if(this.waitMsgTarget){ - this.waitMsgTarget.unmask(); - }else{ - Ext.MessageBox.updateProgress(1); - Ext.MessageBox.hide(); - } - } - if(success){ - if(o.reset){ - this.reset(); - } - Ext.callback(o.success, o.scope, [this, action]); - this.fireEvent('actioncomplete', this, action); - }else{ - Ext.callback(o.failure, o.scope, [this, action]); - this.fireEvent('actionfailed', this, action); - } + groupCls : 'x-form-check-group', + + // private + initComponent: function(){ + this.addEvents( + /** + * @event change + * Fires when the state of a child checkbox changes. + * @param {Ext.form.CheckboxGroup} this + * @param {Array} checked An array containing the checked boxes. + */ + 'change' + ); + this.on('change', this.validate, this); + Ext.form.CheckboxGroup.superclass.initComponent.call(this); }, - /** - * Find a {@link Ext.form.Field} in this form. - * @param {String} id The value to search for (specify either a {@link Ext.Component#id id}, - * {@link Ext.grid.Column#dataIndex dataIndex}, {@link Ext.form.Field#getName name or hiddenName}). - * @return Field - */ - findField : function(id){ - var field = this.items.get(id); - if(!Ext.isObject(field)){ - this.items.each(function(f){ - if(f.isFormField && (f.dataIndex == id || f.id == id || f.getName() == id)){ - field = f; - return false; + // private + onRender : function(ct, position){ + if(!this.el){ + var panelCfg = { + autoEl: { + id: this.id + }, + cls: this.groupCls, + layout: 'column', + renderTo: ct, + bufferResize: false // Default this to false, since it doesn't really have a proper ownerCt. + }; + var colCfg = { + xtype: 'container', + defaultType: this.defaultType, + layout: 'form', + defaults: { + hideLabel: true, + anchor: '100%' } - }); - } - return field || null; - }, + }; + if(this.items[0].items){ - /** - * Mark fields in this form invalid in bulk. - * @param {Array/Object} errors Either an array in the form [{id:'fieldId', msg:'The message'},...] or an object hash of {id: msg, id2: msg2} - * @return {BasicForm} this - */ - markInvalid : function(errors){ - if(Ext.isArray(errors)){ - for(var i = 0, len = errors.length; i < len; i++){ - var fieldError = errors[i]; - var f = this.findField(fieldError.id); - if(f){ - f.markInvalid(fieldError.msg); + // The container has standard ColumnLayout configs, so pass them in directly + + Ext.apply(panelCfg, { + layoutConfig: {columns: this.items.length}, + defaults: this.defaults, + items: this.items + }); + for(var i=0, len=this.items.length; i0 && i%rows==0){ + ri++; + } + if(this.items[i].fieldLabel){ + this.items[i].hideLabel = false; + } + cols[ri].items.push(this.items[i]); + }; + }else{ + for(var i=0, len=this.items.length; i -[{id:'clientName', value:'Fred. Olsen Lines'}, - {id:'portOfLoading', value:'FXT'}, - {id:'portOfDischarge', value:'OSL'} ] - * or an object hash of the form:
    
    -{
    -    clientName: 'Fred. Olsen Lines',
    -    portOfLoading: 'FXT',
    -    portOfDischarge: 'OSL'
    -}
    - * @return {BasicForm} this - */ - setValues : function(values){ - if(Ext.isArray(values)){ // array of objects - for(var i = 0, len = values.length; i < len; i++){ - var v = values[i]; - var f = this.findField(v.id); - if(f){ - f.setValue(v.value); - if(this.trackResetOnLoad){ - f.originalValue = f.getValue(); - } - } + initValue : function(){ + if(this.value){ + this.setValue.apply(this, this.buffered ? this.value : [this.value]); + delete this.buffered; + delete this.value; + } + }, + + afterRender : function(){ + Ext.form.CheckboxGroup.superclass.afterRender.call(this); + this.eachItem(function(item){ + item.on('check', this.fireChecked, this); + item.inGroup = true; + }); + }, + + // private + doLayout: function(){ + //ugly method required to layout hidden items + if(this.rendered){ + this.panel.forceLayout = this.ownerCt.forceLayout; + this.panel.doLayout(); + } + }, + + // private + fireChecked: function(){ + var arr = []; + this.eachItem(function(item){ + if(item.checked){ + arr.push(item); } - }else{ // object hash - var field, id; - for(id in values){ - if(!Ext.isFunction(values[id]) && (field = this.findField(id))){ - field.setValue(values[id]); - if(this.trackResetOnLoad){ - field.originalValue = field.getValue(); - } + }); + this.fireEvent('change', this, arr); + }, + + // private + validateValue : function(value){ + if(!this.allowBlank){ + var blank = true; + this.eachItem(function(f){ + if(f.checked){ + return (blank = false); } + }); + if(blank){ + this.markInvalid(this.blankText); + return false; } } - return this; + return true; }, - /** - *

    Returns the fields in this form as an object with key/value pairs as they would be submitted using a standard form submit. - * If multiple fields exist with the same name they are returned as an array.

    - *

    Note: The values are collected from all enabled HTML input elements within the form, not from - * the Ext Field objects. This means that all returned values are Strings (or Arrays of Strings) and that the - * value can potentially be the emptyText of a field.

    - * @param {Boolean} asString (optional) Pass true to return the values as a string. (defaults to false, returning an Object) - * @return {String/Object} - */ - getValues : function(asString){ - var fs = Ext.lib.Ajax.serializeForm(this.el.dom); - if(asString === true){ - return fs; + // private + isDirty: function(){ + //override the behaviour to check sub items. + if (this.disabled || !this.rendered) { + return false; } - return Ext.urlDecode(fs); + + var dirty = false; + this.eachItem(function(item){ + if(item.isDirty()){ + dirty = true; + return false; + } + }); + return dirty; }, - getFieldValues : function(){ - var o = {}; - this.items.each(function(f){ - o[f.getName()] = f.getValue(); + // private + onDisable : function(){ + this.eachItem(function(item){ + item.disable(); }); - return o; }, - /** - * Clears all invalid messages in this form. - * @return {BasicForm} this - */ - clearInvalid : function(){ - this.items.each(function(f){ - f.clearInvalid(); + // private + onEnable : function(){ + this.eachItem(function(item){ + item.enable(); }); - return this; }, - /** - * Resets this form. - * @return {BasicForm} this - */ + // private + doLayout: function(){ + if(this.rendered){ + this.panel.forceLayout = this.ownerCt.forceLayout; + this.panel.doLayout(); + } + }, + + // private + onResize : function(w, h){ + this.panel.setSize(w, h); + this.panel.doLayout(); + }, + + // inherit docs from Field reset : function(){ - this.items.each(function(f){ - f.reset(); + this.eachItem(function(c){ + if(c.reset){ + c.reset(); + } }); - return this; + // Defer the clearInvalid so if BaseForm's collection is being iterated it will be called AFTER it is complete. + // Important because reset is being called on both the group and the individual items. + (function() { + this.clearInvalid(); + }).defer(50, this); }, /** - * Add Ext.form Components to this form's Collection. This does not result in rendering of - * the passed Component, it just enables the form to validate Fields, and distribute values to - * Fields. - *

    You will not usually call this function. In order to be rendered, a Field must be added - * to a {@link Ext.Container Container}, usually an {@link Ext.form.FormPanel FormPanel}. - * The FormPanel to which the field is added takes care of adding the Field to the BasicForm's - * collection.

    - * @param {Field} field1 - * @param {Field} field2 (optional) - * @param {Field} etc (optional) - * @return {BasicForm} this + * {@link Ext.form.Checkbox#setValue Set the value(s)} of an item or items + * in the group. Examples illustrating how this method may be called: + *
    
    +// call with name and value
    +myCheckboxGroup.setValue('cb-col-1', true);
    +// call with an array of boolean values
    +myCheckboxGroup.setValue([true, false, false]);
    +// call with an object literal specifying item:value pairs
    +myCheckboxGroup.setValue({
    +    'cb-col-2': false,
    +    'cb-col-3': true
    +});
    +// use comma separated string to set items with name to true (checked)
    +myCheckboxGroup.setValue('cb-col-1,cb-col-3');
    +     * 
    + * See {@link Ext.form.Checkbox#setValue} for additional information. + * @param {Mixed} id The checkbox to check, or as described by example shown. + * @param {Boolean} value (optional) The value to set the item. + * @return {Ext.form.CheckboxGroup} this */ - add : function(){ - this.items.addAll(Array.prototype.slice.call(arguments, 0)); + setValue: function(){ + if(this.rendered){ + this.onSetValue.apply(this, arguments); + }else{ + this.buffered = true; + this.value = arguments; + } return this; }, + onSetValue: function(id, value){ + if(arguments.length == 1){ + if(Ext.isArray(id)){ + // an array of boolean values + Ext.each(id, function(val, idx){ + var item = this.items.itemAt(idx); + if(item){ + item.setValue(val); + } + }, this); + }else if(Ext.isObject(id)){ + // set of name/value pairs + for(var i in id){ + var f = this.getBox(i); + if(f){ + f.setValue(id[i]); + } + } + }else{ + this.setValueForItem(id); + } + }else{ + var f = this.getBox(id); + if(f){ + f.setValue(value); + } + } + }, + + // private + beforeDestroy: function(){ + Ext.destroy(this.panel); + Ext.form.CheckboxGroup.superclass.beforeDestroy.call(this); - /** - * Removes a field from the items collection (does NOT remove its markup). - * @param {Field} field - * @return {BasicForm} this - */ - remove : function(field){ - this.items.remove(field); - return this; }, - /** - * Iterates through the {@link Ext.form.Field Field}s which have been {@link #add add}ed to this BasicForm, - * checks them for an id attribute, and calls {@link Ext.form.Field#applyToMarkup} on the existing dom element with that id. - * @return {BasicForm} this - */ - render : function(){ - this.items.each(function(f){ - if(f.isFormField && !f.rendered && document.getElementById(f.id)){ // if the element exists - f.applyToMarkup(f.id); + setValueForItem : function(val){ + val = String(val).split(','); + this.eachItem(function(item){ + if(val.indexOf(item.inputValue)> -1){ + item.setValue(true); } }); - return this; }, - /** - * Calls {@link Ext#apply} for all fields in this form with the passed object. - * @param {Object} values - * @return {BasicForm} this - */ - applyToFields : function(o){ - this.items.each(function(f){ - Ext.apply(f, o); + // private + getBox : function(id){ + var box = null; + this.eachItem(function(f){ + if(id == f || f.dataIndex == id || f.id == id || f.getName() == id){ + box = f; + return false; + } }); - return this; + return box; }, /** - * Calls {@link Ext#applyIf} for all field in this form with the passed object. - * @param {Object} values - * @return {BasicForm} this + * Gets an array of the selected {@link Ext.form.Checkbox} in the group. + * @return {Array} An array of the selected checkboxes. */ - applyIfToFields : function(o){ - this.items.each(function(f){ - Ext.applyIf(f, o); + getValue : function(){ + var out = []; + this.eachItem(function(item){ + if(item.checked){ + out.push(item); + } }); - return this; + return out; }, - callFieldMethod : function(fnName, args){ - args = args || []; - this.items.each(function(f){ - if(Ext.isFunction(f[fnName])){ - f[fnName].apply(f, args); - } - }); - return this; - } -}); + // private + eachItem: function(fn){ + if(this.items && this.items.each){ + this.items.each(fn, this); + } + }, -// back compat -Ext.BasicForm = Ext.form.BasicForm;/** - * @class Ext.form.FormPanel - * @extends Ext.Panel - *

    Standard form container.

    - * - *

    Layout

    - *

    By default, FormPanel is configured with layout:'form' to use an {@link Ext.layout.FormLayout} - * layout manager, which styles and renders fields and labels correctly. When nesting additional Containers - * within a FormPanel, you should ensure that any descendant Containers which host input Fields use the - * {@link Ext.layout.FormLayout} layout manager.

    - * - *

    BasicForm

    - *

    Although not listed as configuration options of FormPanel, the FormPanel class accepts all - * of the config options required to configure its internal {@link Ext.form.BasicForm} for: - *

      - *
    • {@link Ext.form.BasicForm#fileUpload file uploads}
    • - *
    • functionality for {@link Ext.form.BasicForm#doAction loading, validating and submitting} the form
    • - *
    - * - *

    Note: If subclassing FormPanel, any configuration options for the BasicForm must be applied to - * the initialConfig property of the FormPanel. Applying {@link Ext.form.BasicForm BasicForm} - * configuration settings to this will not affect the BasicForm's configuration.

    - * - *

    Form Validation

    - *

    For information on form validation see the following:

    - *
      - *
    • {@link Ext.form.TextField}
    • - *
    • {@link Ext.form.VTypes}
    • - *
    • {@link Ext.form.BasicForm#doAction BasicForm.doAction clientValidation notes}
    • - *
    • {@link Ext.form.FormPanel#monitorValid monitorValid}
    • - *
    - * - *

    Form Submission

    - *

    By default, Ext Forms are submitted through Ajax, using {@link Ext.form.Action}. To enable normal browser - * submission of the {@link Ext.form.BasicForm BasicForm} contained in this FormPanel, see the - * {@link Ext.form.BasicForm#standardSubmit standardSubmit} option.

    - * - * @constructor - * @param {Object} config Configuration options - * @xtype form - */ -Ext.FormPanel = Ext.extend(Ext.Panel, { - /** - * @cfg {String} formId (optional) The id of the FORM tag (defaults to an auto-generated id). - */ - /** - * @cfg {Boolean} hideLabels - *

    true to hide field labels by default (sets display:none). Defaults to - * false.

    - *

    Also see {@link Ext.Component}.{@link Ext.Component#hideLabel hideLabel}. - */ /** - * @cfg {Number} labelPad - * The default padding in pixels for field labels (defaults to 5). labelPad only - * applies if {@link #labelWidth} is also specified, otherwise it will be ignored. + * @cfg {String} name + * @hide */ + /** - * @cfg {String} labelSeparator - * See {@link Ext.Component}.{@link Ext.Component#labelSeparator labelSeparator} + * @method getRawValue + * @hide */ + getRawValue : Ext.emptyFn, + /** - * @cfg {Number} labelWidth The width of labels in pixels. This property cascades to child containers - * and can be overridden on any child container (e.g., a fieldset can specify a different labelWidth - * for its fields) (defaults to 100). + * @method setRawValue + * @hide */ + setRawValue : Ext.emptyFn + +}); + +Ext.reg('checkboxgroup', Ext.form.CheckboxGroup); +/** + * @class Ext.form.Radio + * @extends Ext.form.Checkbox + * Single radio field. Same as Checkbox, but provided as a convenience for automatically setting the input type. + * Radio grouping is handled automatically by the browser if you give each radio in a group the same name. + * @constructor + * Creates a new Radio + * @param {Object} config Configuration options + * @xtype radio + */ +Ext.form.Radio = Ext.extend(Ext.form.Checkbox, { + inputType: 'radio', + /** - * @cfg {String} itemCls A css class to apply to the x-form-item of fields. This property cascades to child containers. + * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide + * @method */ + markInvalid : Ext.emptyFn, /** - * @cfg {Array} buttons - * An array of {@link Ext.Button}s or {@link Ext.Button} configs used to add buttons to the footer of this FormPanel.
    - *

    Buttons in the footer of a FormPanel may be configured with the option formBind: true. This causes - * the form's {@link #monitorValid valid state monitor task} to enable/disable those Buttons depending on - * the form's valid/invalid state.

    + * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide + * @method */ - + clearInvalid : Ext.emptyFn, /** - * @cfg {Number} minButtonWidth Minimum width of all buttons in pixels (defaults to 75). + * If this radio is part of a group, it will return the selected value + * @return {String} */ - minButtonWidth : 75, + getGroupValue : function(){ + var p = this.el.up('form') || Ext.getBody(); + var c = p.child('input[name='+this.el.dom.name+']:checked', true); + return c ? c.value : null; + }, + + // private + onClick : function(){ + if(this.el.dom.checked != this.checked){ + var els = this.getCheckEl().select('input[name=' + this.el.dom.name + ']'); + els.each(function(el){ + if(el.dom.id == this.id){ + this.setValue(true); + }else{ + Ext.getCmp(el.dom.id).setValue(false); + } + }, this); + } + }, /** - * @cfg {String} labelAlign The label alignment value used for the text-align specification - * for the container. Valid values are "left", "top" or "right" - * (defaults to "left"). This property cascades to child containers and can be - * overridden on any child container (e.g., a fieldset can specify a different labelAlign - * for its fields). + * Sets either the checked/unchecked status of this Radio, or, if a string value + * is passed, checks a sibling Radio of the same name whose value is the value specified. + * @param value {String/Boolean} Checked value, or the value of the sibling radio button to check. + * @return {Ext.form.Field} this */ - labelAlign : 'left', + setValue : function(v){ + if (typeof v == 'boolean') { + Ext.form.Radio.superclass.setValue.call(this, v); + } else if (this.rendered) { + var r = this.getCheckEl().child('input[name=' + this.el.dom.name + '][value=' + v + ']', true); + if(r){ + Ext.getCmp(r.id).setValue(true); + } + } + return this; + }, + // private + getCheckEl: function(){ + if(this.inGroup){ + return this.el.up('.x-form-radio-group') + } + return this.el.up('form') || Ext.getBody(); + } +}); +Ext.reg('radio', Ext.form.Radio); +/** + * @class Ext.form.RadioGroup + * @extends Ext.form.CheckboxGroup + * A grouping container for {@link Ext.form.Radio} controls. + * @constructor + * Creates a new RadioGroup + * @param {Object} config Configuration options + * @xtype radiogroup + */ +Ext.form.RadioGroup = Ext.extend(Ext.form.CheckboxGroup, { /** - * @cfg {Boolean} monitorValid If true, the form monitors its valid state client-side and - * regularly fires the {@link #clientvalidation} event passing that state.
    - *

    When monitoring valid state, the FormPanel enables/disables any of its configured - * {@link #buttons} which have been configured with formBind: true depending - * on whether the {@link Ext.form.BasicForm#isValid form is valid} or not. Defaults to false

    + * @cfg {Array} items An Array of {@link Ext.form.Radio Radio}s or Radio config objects + * to arrange in the group. */ - monitorValid : false, - /** - * @cfg {Number} monitorPoll The milliseconds to poll valid state, ignored if monitorValid is not true (defaults to 200) + * @cfg {Boolean} allowBlank True to allow every item in the group to be blank (defaults to true). + * If allowBlank = false and no items are selected at validation time, {@link @blankText} will + * be used as the error text. */ - monitorPoll : 200, - + allowBlank : true, /** - * @cfg {String} layout Defaults to 'form'. Normally this configuration property should not be altered. - * For additional details see {@link Ext.layout.FormLayout} and {@link Ext.Container#layout Ext.Container.layout}. + * @cfg {String} blankText Error text to display if the {@link #allowBlank} validation fails + * (defaults to 'You must select one item in this group') */ - layout : 'form', - + blankText : 'You must select one item in this group', + // private - initComponent : function(){ - this.form = this.createForm(); - Ext.FormPanel.superclass.initComponent.call(this); - - this.bodyCfg = { - tag: 'form', - cls: this.baseCls + '-body', - method : this.method || 'POST', - id : this.formId || Ext.id() - }; - if(this.fileUpload) { - this.bodyCfg.enctype = 'multipart/form-data'; - } - this.initItems(); - - this.addEvents( - /** - * @event clientvalidation - * If the monitorValid config option is true, this event fires repetitively to notify of valid state - * @param {Ext.form.FormPanel} this - * @param {Boolean} valid true if the form has passed client-side validation - */ - 'clientvalidation' - ); - - this.relayEvents(this.form, ['beforeaction', 'actionfailed', 'actioncomplete']); - }, - + defaultType : 'radio', + // private - createForm : function(){ - var config = Ext.applyIf({listeners: {}}, this.initialConfig); - return new Ext.form.BasicForm(null, config); + groupCls : 'x-form-radio-group', + + /** + * @event change + * Fires when the state of a child radio changes. + * @param {Ext.form.RadioGroup} this + * @param {Ext.form.Radio} checked The checked radio + */ + + /** + * Gets the selected {@link Ext.form.Radio} in the group, if it exists. + * @return {Ext.form.Radio} The selected radio. + */ + getValue : function(){ + var out = null; + this.eachItem(function(item){ + if(item.checked){ + out = item; + return false; + } + }); + return out; }, - - // private - initFields : function(){ - var f = this.form; - var formPanel = this; - var fn = function(c){ - if(formPanel.isField(c)){ - f.add(c); - }else if(c.findBy && c != formPanel){ - formPanel.applySettings(c); - //each check required for check/radio groups. - if(c.items && c.items.each){ - c.items.each(fn, this); + + /** + * Sets the checked radio in the group. + * @param {String/Ext.form.Radio} id The radio to check. + * @param {Boolean} value The value to set the radio. + * @return {Ext.form.RadioGroup} this + */ + onSetValue : function(id, value){ + if(arguments.length > 1){ + var f = this.getBox(id); + if(f){ + f.setValue(value); + if(f.checked){ + this.eachItem(function(item){ + if (item !== f){ + item.setValue(false); + } + }); } } - }; - this.items.each(fn, this); + }else{ + this.setValueForItem(id); + } }, - // private - applySettings: function(c){ - var ct = c.ownerCt; - Ext.applyIf(c, { - labelAlign: ct.labelAlign, - labelWidth: ct.labelWidth, - itemCls: ct.itemCls + setValueForItem : function(val){ + val = String(val).split(',')[0]; + this.eachItem(function(item){ + item.setValue(val == item.inputValue); }); }, - - // private - getLayoutTarget : function(){ - return this.form.el; - }, - - /** - * Provides access to the {@link Ext.form.BasicForm Form} which this Panel contains. - * @return {Ext.form.BasicForm} The {@link Ext.form.BasicForm Form} which this Panel contains. - */ - getForm : function(){ - return this.form; - }, - + // private - onRender : function(ct, position){ - this.initFields(); - Ext.FormPanel.superclass.onRender.call(this, ct, position); - this.form.initEl(this.body); + fireChecked : function(){ + if(!this.checkTask){ + this.checkTask = new Ext.util.DelayedTask(this.bufferChecked, this); + } + this.checkTask.delay(10); }, // private - beforeDestroy : function(){ - this.stopMonitoring(); - Ext.FormPanel.superclass.beforeDestroy.call(this); - /* - * Clear the items here to prevent them being destroyed again. - * Don't move this behaviour to BasicForm because it can be used - * on it's own. - */ - this.form.items.clear(); - Ext.destroy(this.form); + bufferChecked : function(){ + var out = null; + this.eachItem(function(item){ + if(item.checked){ + out = item; + return false; + } + }); + this.fireEvent('change', this, out); }, + + onDestroy : function(){ + if(this.checkTask){ + this.checkTask.cancel(); + this.checkTask = null; + } + Ext.form.RadioGroup.superclass.onDestroy.call(this); + } - // Determine if a Component is usable as a form Field. - isField : function(c) { - return !!c.setValue && !!c.getValue && !!c.markInvalid && !!c.clearInvalid; - }, +}); - // private - initEvents : function(){ - Ext.FormPanel.superclass.initEvents.call(this); - // Listeners are required here to catch bubbling events from children. - this.on({ - scope: this, - add: this.onAddEvent, - remove: this.onRemoveEvent +Ext.reg('radiogroup', Ext.form.RadioGroup); +/** + * @class Ext.form.Hidden + * @extends Ext.form.Field + * A basic hidden field for storing hidden values in forms that need to be passed in the form submit. + * @constructor + * Create a new Hidden field. + * @param {Object} config Configuration options + * @xtype hidden + */ +Ext.form.Hidden = Ext.extend(Ext.form.Field, { + // private + inputType : 'hidden', + + // private + onRender : function(){ + Ext.form.Hidden.superclass.onRender.apply(this, arguments); + }, + + // private + initEvents : function(){ + this.originalValue = this.getValue(); + }, + + // These are all private overrides + setSize : Ext.emptyFn, + setWidth : Ext.emptyFn, + setHeight : Ext.emptyFn, + setPosition : Ext.emptyFn, + setPagePosition : Ext.emptyFn, + markInvalid : Ext.emptyFn, + clearInvalid : Ext.emptyFn +}); +Ext.reg('hidden', Ext.form.Hidden);/** + * @class Ext.form.BasicForm + * @extends Ext.util.Observable + *

    Encapsulates the DOM <form> element at the heart of the {@link Ext.form.FormPanel FormPanel} class, and provides + * input field management, validation, submission, and form loading services.

    + *

    By default, Ext Forms are submitted through Ajax, using an instance of {@link Ext.form.Action.Submit}. + * To enable normal browser submission of an Ext Form, use the {@link #standardSubmit} config option.

    + *

    File Uploads

    + *

    {@link #fileUpload File uploads} are not performed using Ajax submission, that + * is they are not performed using XMLHttpRequests. Instead the form is submitted in the standard + * manner with the DOM <form> element temporarily modified to have its + * target set to refer + * to a dynamically generated, hidden <iframe> which is inserted into the document + * but removed after the return data has been gathered.

    + *

    The server response is parsed by the browser to create the document for the IFRAME. If the + * server is using JSON to send the return object, then the + * Content-Type header + * must be set to "text/html" in order to tell the browser to insert the text unchanged into the document body.

    + *

    Characters which are significant to an HTML parser must be sent as HTML entities, so encode + * "<" as "&lt;", "&" as "&amp;" etc.

    + *

    The response text is retrieved from the document, and a fake XMLHttpRequest object + * is created containing a responseText property in order to conform to the + * requirements of event handlers and callbacks.

    + *

    Be aware that file upload packets are sent with the content type multipart/form + * and some server technologies (notably JEE) may require some custom processing in order to + * retrieve parameter names and parameter values from the packet content.

    + * @constructor + * @param {Mixed} el The form element or its id + * @param {Object} config Configuration options + */ +Ext.form.BasicForm = Ext.extend(Ext.util.Observable, { + + constructor: function(el, config){ + Ext.apply(this, config); + if(Ext.isString(this.paramOrder)){ + this.paramOrder = this.paramOrder.split(/[\s,|]/); + } + /** + * A {@link Ext.util.MixedCollection MixedCollection} containing all the Ext.form.Fields in this form. + * @type MixedCollection + * @property items + */ + this.items = new Ext.util.MixedCollection(false, function(o){ + return o.getItemId(); }); - if(this.monitorValid){ // initialize after render - this.startMonitoring(); + this.addEvents( + /** + * @event beforeaction + * Fires before any action is performed. Return false to cancel the action. + * @param {Form} this + * @param {Action} action The {@link Ext.form.Action} to be performed + */ + 'beforeaction', + /** + * @event actionfailed + * Fires when an action fails. + * @param {Form} this + * @param {Action} action The {@link Ext.form.Action} that failed + */ + 'actionfailed', + /** + * @event actioncomplete + * Fires when an action is completed. + * @param {Form} this + * @param {Action} action The {@link Ext.form.Action} that completed + */ + 'actioncomplete' + ); + + if(el){ + this.initEl(el); } + Ext.form.BasicForm.superclass.constructor.call(this); }, - // private - onAdd: function(c){ - Ext.FormPanel.superclass.onAdd.call(this, c); - this.processAdd(c); - }, + /** + * @cfg {String} method + * The request method to use (GET or POST) for form actions if one isn't supplied in the action options. + */ + /** + * @cfg {DataReader} reader + * An Ext.data.DataReader (e.g. {@link Ext.data.XmlReader}) to be used to read + * data when executing 'load' actions. This is optional as there is built-in + * support for processing JSON. For additional information on using an XMLReader + * see the example provided in examples/form/xml-form.html. + */ + /** + * @cfg {DataReader} errorReader + *

    An Ext.data.DataReader (e.g. {@link Ext.data.XmlReader}) to be used to + * read field error messages returned from 'submit' actions. This is optional + * as there is built-in support for processing JSON.

    + *

    The Records which provide messages for the invalid Fields must use the + * Field name (or id) as the Record ID, and must contain a field called 'msg' + * which contains the error message.

    + *

    The errorReader does not have to be a full-blown implementation of a + * DataReader. It simply needs to implement a read(xhr) function + * which returns an Array of Records in an object with the following + * structure:

    
    +{
    +    records: recordArray
    +}
    +
    + */ + /** + * @cfg {String} url + * The URL to use for form actions if one isn't supplied in the + * {@link #doAction doAction} options. + */ + /** + * @cfg {Boolean} fileUpload + * Set to true if this form is a file upload. + *

    File uploads are not performed using normal 'Ajax' techniques, that is they are not + * performed using XMLHttpRequests. Instead the form is submitted in the standard manner with the + * DOM <form> element temporarily modified to have its + * target set to refer + * to a dynamically generated, hidden <iframe> which is inserted into the document + * but removed after the return data has been gathered.

    + *

    The server response is parsed by the browser to create the document for the IFRAME. If the + * server is using JSON to send the return object, then the + * Content-Type header + * must be set to "text/html" in order to tell the browser to insert the text unchanged into the document body.

    + *

    Characters which are significant to an HTML parser must be sent as HTML entities, so encode + * "<" as "&lt;", "&" as "&amp;" etc.

    + *

    The response text is retrieved from the document, and a fake XMLHttpRequest object + * is created containing a responseText property in order to conform to the + * requirements of event handlers and callbacks.

    + *

    Be aware that file upload packets are sent with the content type multipart/form + * and some server technologies (notably JEE) may require some custom processing in order to + * retrieve parameter names and parameter values from the packet content.

    + */ + /** + * @cfg {Object} baseParams + *

    Parameters to pass with all requests. e.g. baseParams: {id: '123', foo: 'bar'}.

    + *

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

    + */ + /** + * @cfg {Number} timeout Timeout for form actions in seconds (default is 30 seconds). + */ + timeout: 30, + + /** + * @cfg {Object} api (Optional) If specified load and submit actions will be handled + * with {@link Ext.form.Action.DirectLoad} and {@link Ext.form.Action.DirectSubmit}. + * Methods which have been imported by Ext.Direct can be specified here to load and submit + * forms. + * Such as the following:
    
    +api: {
    +    load: App.ss.MyProfile.load,
    +    submit: App.ss.MyProfile.submit
    +}
    +
    + *

    Load actions can use {@link #paramOrder} or {@link #paramsAsHash} + * to customize how the load method is invoked. + * Submit actions will always use a standard form submit. The formHandler configuration must + * be set on the associated server-side method which has been imported by Ext.Direct

    + */ + + /** + * @cfg {Array/String} paramOrder

    A list of params to be executed server side. + * Defaults to undefined. Only used for the {@link #api} + * load configuration.

    + *

    Specify the params in the order in which they must be executed on the + * server-side as either (1) an Array of String values, or (2) a String of params + * delimited by either whitespace, comma, or pipe. For example, + * any of the following would be acceptable:

    
    +paramOrder: ['param1','param2','param3']
    +paramOrder: 'param1 param2 param3'
    +paramOrder: 'param1,param2,param3'
    +paramOrder: 'param1|param2|param'
    +     
    + */ + paramOrder: undefined, + + /** + * @cfg {Boolean} paramsAsHash Only used for the {@link #api} + * load configuration. Send parameters as a collection of named + * arguments (defaults to false). Providing a + * {@link #paramOrder} nullifies this configuration. + */ + paramsAsHash: false, + /** + * @cfg {String} waitTitle + * The default title to show for the waiting message box (defaults to 'Please Wait...') + */ + waitTitle: 'Please Wait...', + // private - onAddEvent: function(ct, c){ - if(ct !== this){ - this.processAdd(c); - } + activeAction : null, + + /** + * @cfg {Boolean} trackResetOnLoad If set to true, {@link #reset}() resets to the last loaded + * or {@link #setValues}() data instead of when the form was first created. Defaults to false. + */ + trackResetOnLoad : false, + + /** + * @cfg {Boolean} standardSubmit + *

    If set to true, standard HTML form submits are used instead + * of XHR (Ajax) style form submissions. Defaults to false.

    + *

    Note: When using standardSubmit, the + * options to {@link #submit} are ignored because + * Ext's Ajax infrastracture is bypassed. To pass extra parameters (e.g. + * baseParams and params), utilize hidden fields + * to submit extra data, for example:

    + *
    
    +new Ext.FormPanel({
    +    standardSubmit: true,
    +    baseParams: {
    +        foo: 'bar'
         },
    -    
    +    {@link url}: 'myProcess.php',
    +    items: [{
    +        xtype: 'textfield',
    +        name: 'userName'
    +    }],
    +    buttons: [{
    +        text: 'Save',
    +        handler: function(){
    +            var fp = this.ownerCt.ownerCt,
    +                form = fp.getForm();
    +            if (form.isValid()) {
    +                // check if there are baseParams and if
    +                // hiddent items have been added already
    +                if (fp.baseParams && !fp.paramsAdded) {
    +                    // add hidden items for all baseParams
    +                    for (i in fp.baseParams) {
    +                        fp.add({
    +                            xtype: 'hidden',
    +                            name: i,
    +                            value: fp.baseParams[i]
    +                        });
    +                    }
    +                    fp.doLayout();
    +                    // set a custom flag to prevent re-adding
    +                    fp.paramsAdded = true;
    +                }
    +                form.{@link #submit}();
    +            }
    +        }
    +    }]
    +});
    +     * 
    + */ + /** + * By default wait messages are displayed with Ext.MessageBox.wait. You can target a specific + * element by passing it or its id or mask the form itself by passing in true. + * @type Mixed + * @property waitMsgTarget + */ + // private - processAdd : function(c){ - // If a single form Field, add it - if(this.isField(c)){ - this.form.add(c); - // If a Container, add any Fields it might contain - }else if(c.findBy){ - this.applySettings(c); - this.form.add.apply(this.form, c.findBy(this.isField)); + initEl : function(el){ + this.el = Ext.get(el); + this.id = this.el.id || Ext.id(); + if(!this.standardSubmit){ + this.el.on('submit', this.onSubmit, this); } + this.el.addClass('x-form'); }, - - // private - onRemove: function(c){ - Ext.FormPanel.superclass.onRemove.call(this, c); - this.processRemove(c); + + /** + * Get the HTML form Element + * @return Ext.Element + */ + getEl: function(){ + return this.el; }, - - onRemoveEvent: function(ct, c){ - if(ct !== this){ - this.processRemove(c); - } + + // private + onSubmit : function(e){ + e.stopEvent(); }, - + // private - processRemove : function(c){ - // If a single form Field, remove it - if(this.isField(c)){ - this.form.remove(c); - // If a Container, remove any Fields it might contain - }else if(c.findBy){ - Ext.each(c.findBy(this.isField), this.form.remove, this.form); + destroy: function() { + this.items.each(function(f){ + Ext.destroy(f); + }); + if(this.el){ + this.el.removeAllListeners(); + this.el.remove(); } + this.purgeListeners(); }, /** - * Starts monitoring of the valid state of this form. Usually this is done by passing the config - * option "monitorValid" + * Returns true if client-side validation on the form is successful. + * @return Boolean */ - startMonitoring : function(){ - if(!this.validTask){ - this.validTask = new Ext.util.TaskRunner(); - this.validTask.start({ - run : this.bindHandler, - interval : this.monitorPoll || 200, - scope: this - }); - } + isValid : function(){ + var valid = true; + this.items.each(function(f){ + if(!f.validate()){ + valid = false; + } + }); + return valid; }, /** - * Stops monitoring of the valid state of this form + *

    Returns true if any fields in this form have changed from their original values.

    + *

    Note that if this BasicForm was configured with {@link #trackResetOnLoad} then the + * Fields' original values are updated when the values are loaded by {@link #setValues} + * or {@link #loadRecord}.

    + * @return Boolean */ - stopMonitoring : function(){ - if(this.validTask){ - this.validTask.stopAll(); - this.validTask = null; - } + isDirty : function(){ + var dirty = false; + this.items.each(function(f){ + if(f.isDirty()){ + dirty = true; + return false; + } + }); + return dirty; }, /** - * This is a proxy for the underlying BasicForm's {@link Ext.form.BasicForm#load} call. - * @param {Object} options The options to pass to the action (see {@link Ext.form.BasicForm#doAction} for details) + * Performs a predefined action ({@link Ext.form.Action.Submit} or + * {@link Ext.form.Action.Load}) or a custom extension of {@link Ext.form.Action} + * to perform application-specific processing. + * @param {String/Object} actionName The name of the predefined action type, + * or instance of {@link Ext.form.Action} to perform. + * @param {Object} options (optional) The options to pass to the {@link Ext.form.Action}. + * All of the config options listed below are supported by both the + * {@link Ext.form.Action.Submit submit} and {@link Ext.form.Action.Load load} + * actions unless otherwise noted (custom actions could also accept + * other config options):
      + * + *
    • url : String
      The url for the action (defaults + * to the form's {@link #url}.)
    • + * + *
    • method : String
      The form method to use (defaults + * to the form's method, or POST if not defined)
    • + * + *
    • params : String/Object

      The params to pass + * (defaults to the form's baseParams, or none if not defined)

      + *

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

    • + * + *
    • headers : Object
      Request headers to set for the action + * (defaults to the form's default headers)
    • + * + *
    • success : Function
      The callback that will + * be invoked after a successful response (see top of + * {@link Ext.form.Action.Submit submit} and {@link Ext.form.Action.Load load} + * for a description of what constitutes a successful response). + * The function is passed the following parameters:
        + *
      • form : Ext.form.BasicForm
        The form that requested the action
      • + *
      • action : The {@link Ext.form.Action Action} object which performed the operation. + *
        The action object contains these properties of interest:
          + *
        • {@link Ext.form.Action#response response}
        • + *
        • {@link Ext.form.Action#result result} : interrogate for custom postprocessing
        • + *
        • {@link Ext.form.Action#type type}
        • + *
    • + * + *
    • failure : Function
      The callback that will be invoked after a + * failed transaction attempt. The function is passed the following parameters:
        + *
      • form : The {@link Ext.form.BasicForm} that requested the action.
      • + *
      • action : The {@link Ext.form.Action Action} object which performed the operation. + *
        The action object contains these properties of interest:
          + *
        • {@link Ext.form.Action#failureType failureType}
        • + *
        • {@link Ext.form.Action#response response}
        • + *
        • {@link Ext.form.Action#result result} : interrogate for custom postprocessing
        • + *
        • {@link Ext.form.Action#type type}
        • + *
    • + * + *
    • scope : Object
      The scope in which to call the + * callback functions (The this reference for the callback functions).
    • + * + *
    • clientValidation : Boolean
      Submit Action only. + * Determines whether a Form's fields are validated in a final call to + * {@link Ext.form.BasicForm#isValid isValid} prior to submission. Set to false + * to prevent this. If undefined, pre-submission field validation is performed.
    + * + * @return {BasicForm} this */ - load : function(){ - this.form.load.apply(this.form, arguments); - }, - - // private - onDisable : function(){ - Ext.FormPanel.superclass.onDisable.call(this); - if(this.form){ - this.form.items.each(function(){ - this.disable(); - }); + doAction : function(action, options){ + if(Ext.isString(action)){ + action = new Ext.form.Action.ACTION_TYPES[action](this, options); } - }, - - // private - onEnable : function(){ - Ext.FormPanel.superclass.onEnable.call(this); - if(this.form){ - this.form.items.each(function(){ - this.enable(); - }); + if(this.fireEvent('beforeaction', this, action) !== false){ + this.beforeAction(action); + action.run.defer(100, action); } + return this; }, - // private - bindHandler : function(){ - var valid = true; - this.form.items.each(function(f){ - if(!f.isValid(true)){ - valid = false; - return false; - } - }); - if(this.fbar){ - var fitems = this.fbar.items.items; - for(var i = 0, len = fitems.length; i < len; i++){ - var btn = fitems[i]; - if(btn.formBind === true && btn.disabled === valid){ - btn.setDisabled(!valid); + /** + * Shortcut to {@link #doAction do} a {@link Ext.form.Action.Submit submit action}. + * @param {Object} options The options to pass to the action (see {@link #doAction} for details).
    + *

    Note: this is ignored when using the {@link #standardSubmit} option.

    + *

    The following code:

    
    +myFormPanel.getForm().submit({
    +    clientValidation: true,
    +    url: 'updateConsignment.php',
    +    params: {
    +        newStatus: 'delivered'
    +    },
    +    success: function(form, action) {
    +       Ext.Msg.alert('Success', action.result.msg);
    +    },
    +    failure: function(form, action) {
    +        switch (action.failureType) {
    +            case Ext.form.Action.CLIENT_INVALID:
    +                Ext.Msg.alert('Failure', 'Form fields may not be submitted with invalid values');
    +                break;
    +            case Ext.form.Action.CONNECT_FAILURE:
    +                Ext.Msg.alert('Failure', 'Ajax communication failed');
    +                break;
    +            case Ext.form.Action.SERVER_INVALID:
    +               Ext.Msg.alert('Failure', action.result.msg);
    +       }
    +    }
    +});
    +
    + * would process the following server response for a successful submission:
    
    +{
    +    "success":true, // note this is Boolean, not string
    +    "msg":"Consignment updated"
    +}
    +
    + * and the following server response for a failed submission:
    
    +{
    +    "success":false, // note this is Boolean, not string
    +    "msg":"You do not have permission to perform this operation"
    +}
    +
    + * @return {BasicForm} this + */ + submit : function(options){ + if(this.standardSubmit){ + var v = this.isValid(); + if(v){ + var el = this.el.dom; + if(this.url && Ext.isEmpty(el.action)){ + el.action = this.url; } + el.submit(); } + return v; } - this.fireEvent('clientvalidation', this, valid); - } -}); -Ext.reg('form', Ext.FormPanel); - -Ext.form.FormPanel = Ext.FormPanel; + var submitAction = String.format('{0}submit', this.api ? 'direct' : ''); + this.doAction(submitAction, options); + return this; + }, + /** + * Shortcut to {@link #doAction do} a {@link Ext.form.Action.Load load action}. + * @param {Object} options The options to pass to the action (see {@link #doAction} for details) + * @return {BasicForm} this + */ + load : function(options){ + var loadAction = String.format('{0}load', this.api ? 'direct' : ''); + this.doAction(loadAction, options); + return this; + }, + + /** + * Persists the values in this form into the passed {@link Ext.data.Record} object in a beginEdit/endEdit block. + * @param {Record} record The record to edit + * @return {BasicForm} this + */ + updateRecord : function(record){ + record.beginEdit(); + var fs = record.fields; + fs.each(function(f){ + var field = this.findField(f.name); + if(field){ + record.set(f.name, field.getValue()); + } + }, this); + record.endEdit(); + return this; + }, + + /** + * Loads an {@link Ext.data.Record} into this form by calling {@link #setValues} with the + * {@link Ext.data.Record#data record data}. + * See also {@link #trackResetOnLoad}. + * @param {Record} record The record to load + * @return {BasicForm} this + */ + loadRecord : function(record){ + this.setValues(record.data); + return this; + }, + + // private + beforeAction : function(action){ + var o = action.options; + if(o.waitMsg){ + if(this.waitMsgTarget === true){ + this.el.mask(o.waitMsg, 'x-mask-loading'); + }else if(this.waitMsgTarget){ + this.waitMsgTarget = Ext.get(this.waitMsgTarget); + this.waitMsgTarget.mask(o.waitMsg, 'x-mask-loading'); + }else{ + Ext.MessageBox.wait(o.waitMsg, o.waitTitle || this.waitTitle); + } + } + }, + + // private + afterAction : function(action, success){ + this.activeAction = null; + var o = action.options; + if(o.waitMsg){ + if(this.waitMsgTarget === true){ + this.el.unmask(); + }else if(this.waitMsgTarget){ + this.waitMsgTarget.unmask(); + }else{ + Ext.MessageBox.updateProgress(1); + Ext.MessageBox.hide(); + } + } + if(success){ + if(o.reset){ + this.reset(); + } + Ext.callback(o.success, o.scope, [this, action]); + this.fireEvent('actioncomplete', this, action); + }else{ + Ext.callback(o.failure, o.scope, [this, action]); + this.fireEvent('actionfailed', this, action); + } + }, + + /** + * Find a {@link Ext.form.Field} in this form. + * @param {String} id The value to search for (specify either a {@link Ext.Component#id id}, + * {@link Ext.grid.Column#dataIndex dataIndex}, {@link Ext.form.Field#getName name or hiddenName}). + * @return Field + */ + findField : function(id){ + var field = this.items.get(id); + if(!Ext.isObject(field)){ + this.items.each(function(f){ + if(f.isFormField && (f.dataIndex == id || f.id == id || f.getName() == id)){ + field = f; + return false; + } + }); + } + return field || null; + }, + + + /** + * Mark fields in this form invalid in bulk. + * @param {Array/Object} errors Either an array in the form [{id:'fieldId', msg:'The message'},...] or an object hash of {id: msg, id2: msg2} + * @return {BasicForm} this + */ + markInvalid : function(errors){ + if(Ext.isArray(errors)){ + for(var i = 0, len = errors.length; i < len; i++){ + var fieldError = errors[i]; + var f = this.findField(fieldError.id); + if(f){ + f.markInvalid(fieldError.msg); + } + } + }else{ + var field, id; + for(id in errors){ + if(!Ext.isFunction(errors[id]) && (field = this.findField(id))){ + field.markInvalid(errors[id]); + } + } + } + return this; + }, + + /** + * Set values for fields in this form in bulk. + * @param {Array/Object} values Either an array in the form:
    
    +[{id:'clientName', value:'Fred. Olsen Lines'},
    + {id:'portOfLoading', value:'FXT'},
    + {id:'portOfDischarge', value:'OSL'} ]
    + * or an object hash of the form:
    
    +{
    +    clientName: 'Fred. Olsen Lines',
    +    portOfLoading: 'FXT',
    +    portOfDischarge: 'OSL'
    +}
    + * @return {BasicForm} this + */ + setValues : function(values){ + if(Ext.isArray(values)){ // array of objects + for(var i = 0, len = values.length; i < len; i++){ + var v = values[i]; + var f = this.findField(v.id); + if(f){ + f.setValue(v.value); + if(this.trackResetOnLoad){ + f.originalValue = f.getValue(); + } + } + } + }else{ // object hash + var field, id; + for(id in values){ + if(!Ext.isFunction(values[id]) && (field = this.findField(id))){ + field.setValue(values[id]); + if(this.trackResetOnLoad){ + field.originalValue = field.getValue(); + } + } + } + } + return this; + }, + + /** + *

    Returns the fields in this form as an object with key/value pairs as they would be submitted using a standard form submit. + * If multiple fields exist with the same name they are returned as an array.

    + *

    Note: The values are collected from all enabled HTML input elements within the form, not from + * the Ext Field objects. This means that all returned values are Strings (or Arrays of Strings) and that the + * value can potentially be the emptyText of a field.

    + * @param {Boolean} asString (optional) Pass true to return the values as a string. (defaults to false, returning an Object) + * @return {String/Object} + */ + getValues : function(asString){ + var fs = Ext.lib.Ajax.serializeForm(this.el.dom); + if(asString === true){ + return fs; + } + return Ext.urlDecode(fs); + }, + + /** + * Retrieves the fields in the form as a set of key/value pairs, using the {@link Ext.form.Field#getValue getValue()} method. + * If multiple fields exist with the same name they are returned as an array. + * @param {Boolean} dirtyOnly (optional) True to return only fields that are dirty. + * @return {Object} The values in the form + */ + getFieldValues : function(dirtyOnly){ + var o = {}, + n, + key, + val; + this.items.each(function(f){ + if(dirtyOnly !== true || f.isDirty()){ + n = f.getName(); + key = o[n]; + val = f.getValue(); + + if(Ext.isDefined(key)){ + if(Ext.isArray(key)){ + o[n].push(val); + }else{ + o[n] = [key, val]; + } + }else{ + o[n] = val; + } + } + }); + return o; + }, + + /** + * Clears all invalid messages in this form. + * @return {BasicForm} this + */ + clearInvalid : function(){ + this.items.each(function(f){ + f.clearInvalid(); + }); + return this; + }, + + /** + * Resets this form. + * @return {BasicForm} this + */ + reset : function(){ + this.items.each(function(f){ + f.reset(); + }); + return this; + }, + + /** + * Add Ext.form Components to this form's Collection. This does not result in rendering of + * the passed Component, it just enables the form to validate Fields, and distribute values to + * Fields. + *

    You will not usually call this function. In order to be rendered, a Field must be added + * to a {@link Ext.Container Container}, usually an {@link Ext.form.FormPanel FormPanel}. + * The FormPanel to which the field is added takes care of adding the Field to the BasicForm's + * collection.

    + * @param {Field} field1 + * @param {Field} field2 (optional) + * @param {Field} etc (optional) + * @return {BasicForm} this + */ + add : function(){ + this.items.addAll(Array.prototype.slice.call(arguments, 0)); + return this; + }, + + + /** + * Removes a field from the items collection (does NOT remove its markup). + * @param {Field} field + * @return {BasicForm} this + */ + remove : function(field){ + this.items.remove(field); + return this; + }, + + /** + * Iterates through the {@link Ext.form.Field Field}s which have been {@link #add add}ed to this BasicForm, + * checks them for an id attribute, and calls {@link Ext.form.Field#applyToMarkup} on the existing dom element with that id. + * @return {BasicForm} this + */ + render : function(){ + this.items.each(function(f){ + if(f.isFormField && !f.rendered && document.getElementById(f.id)){ // if the element exists + f.applyToMarkup(f.id); + } + }); + return this; + }, + + /** + * Calls {@link Ext#apply} for all fields in this form with the passed object. + * @param {Object} values + * @return {BasicForm} this + */ + applyToFields : function(o){ + this.items.each(function(f){ + Ext.apply(f, o); + }); + return this; + }, + + /** + * Calls {@link Ext#applyIf} for all field in this form with the passed object. + * @param {Object} values + * @return {BasicForm} this + */ + applyIfToFields : function(o){ + this.items.each(function(f){ + Ext.applyIf(f, o); + }); + return this; + }, + + callFieldMethod : function(fnName, args){ + args = args || []; + this.items.each(function(f){ + if(Ext.isFunction(f[fnName])){ + f[fnName].apply(f, args); + } + }); + return this; + } +}); + +// back compat +Ext.BasicForm = Ext.form.BasicForm;/** + * @class Ext.form.FormPanel + * @extends Ext.Panel + *

    Standard form container.

    + * + *

    Layout

    + *

    By default, FormPanel is configured with layout:'form' to use an {@link Ext.layout.FormLayout} + * layout manager, which styles and renders fields and labels correctly. When nesting additional Containers + * within a FormPanel, you should ensure that any descendant Containers which host input Fields use the + * {@link Ext.layout.FormLayout} layout manager.

    + * + *

    BasicForm

    + *

    Although not listed as configuration options of FormPanel, the FormPanel class accepts all + * of the config options required to configure its internal {@link Ext.form.BasicForm} for: + *

      + *
    • {@link Ext.form.BasicForm#fileUpload file uploads}
    • + *
    • functionality for {@link Ext.form.BasicForm#doAction loading, validating and submitting} the form
    • + *
    + * + *

    Note: If subclassing FormPanel, any configuration options for the BasicForm must be applied to + * the initialConfig property of the FormPanel. Applying {@link Ext.form.BasicForm BasicForm} + * configuration settings to this will not affect the BasicForm's configuration.

    + * + *

    Form Validation

    + *

    For information on form validation see the following:

    + *
      + *
    • {@link Ext.form.TextField}
    • + *
    • {@link Ext.form.VTypes}
    • + *
    • {@link Ext.form.BasicForm#doAction BasicForm.doAction clientValidation notes}
    • + *
    • {@link Ext.form.FormPanel#monitorValid monitorValid}
    • + *
    + * + *

    Form Submission

    + *

    By default, Ext Forms are submitted through Ajax, using {@link Ext.form.Action}. To enable normal browser + * submission of the {@link Ext.form.BasicForm BasicForm} contained in this FormPanel, see the + * {@link Ext.form.BasicForm#standardSubmit standardSubmit} option.

    + * + * @constructor + * @param {Object} config Configuration options + * @xtype form + */ +Ext.FormPanel = Ext.extend(Ext.Panel, { + /** + * @cfg {String} formId (optional) The id of the FORM tag (defaults to an auto-generated id). + */ + /** + * @cfg {Boolean} hideLabels + *

    true to hide field labels by default (sets display:none). Defaults to + * false.

    + *

    Also see {@link Ext.Component}.{@link Ext.Component#hideLabel hideLabel}. + */ + /** + * @cfg {Number} labelPad + * The default padding in pixels for field labels (defaults to 5). labelPad only + * applies if {@link #labelWidth} is also specified, otherwise it will be ignored. + */ + /** + * @cfg {String} labelSeparator + * See {@link Ext.Component}.{@link Ext.Component#labelSeparator labelSeparator} + */ + /** + * @cfg {Number} labelWidth The width of labels in pixels. This property cascades to child containers + * and can be overridden on any child container (e.g., a fieldset can specify a different labelWidth + * for its fields) (defaults to 100). + */ + /** + * @cfg {String} itemCls A css class to apply to the x-form-item of fields. This property cascades to child containers. + */ + /** + * @cfg {Array} buttons + * An array of {@link Ext.Button}s or {@link Ext.Button} configs used to add buttons to the footer of this FormPanel.
    + *

    Buttons in the footer of a FormPanel may be configured with the option formBind: true. This causes + * the form's {@link #monitorValid valid state monitor task} to enable/disable those Buttons depending on + * the form's valid/invalid state.

    + */ + + + /** + * @cfg {Number} minButtonWidth Minimum width of all buttons in pixels (defaults to 75). + */ + minButtonWidth : 75, + + /** + * @cfg {String} labelAlign The label alignment value used for the text-align specification + * for the container. Valid values are "left", "top" or "right" + * (defaults to "left"). This property cascades to child containers and can be + * overridden on any child container (e.g., a fieldset can specify a different labelAlign + * for its fields). + */ + labelAlign : 'left', + + /** + * @cfg {Boolean} monitorValid If true, the form monitors its valid state client-side and + * regularly fires the {@link #clientvalidation} event passing that state.
    + *

    When monitoring valid state, the FormPanel enables/disables any of its configured + * {@link #buttons} which have been configured with formBind: true depending + * on whether the {@link Ext.form.BasicForm#isValid form is valid} or not. Defaults to false

    + */ + monitorValid : false, + + /** + * @cfg {Number} monitorPoll The milliseconds to poll valid state, ignored if monitorValid is not true (defaults to 200) + */ + monitorPoll : 200, + + /** + * @cfg {String} layout Defaults to 'form'. Normally this configuration property should not be altered. + * For additional details see {@link Ext.layout.FormLayout} and {@link Ext.Container#layout Ext.Container.layout}. + */ + layout : 'form', + + // private + initComponent : function(){ + this.form = this.createForm(); + Ext.FormPanel.superclass.initComponent.call(this); + + this.bodyCfg = { + tag: 'form', + cls: this.baseCls + '-body', + method : this.method || 'POST', + id : this.formId || Ext.id() + }; + if(this.fileUpload) { + this.bodyCfg.enctype = 'multipart/form-data'; + } + this.initItems(); + + this.addEvents( + /** + * @event clientvalidation + * If the monitorValid config option is true, this event fires repetitively to notify of valid state + * @param {Ext.form.FormPanel} this + * @param {Boolean} valid true if the form has passed client-side validation + */ + 'clientvalidation' + ); + + this.relayEvents(this.form, ['beforeaction', 'actionfailed', 'actioncomplete']); + }, + + // private + createForm : function(){ + var config = Ext.applyIf({listeners: {}}, this.initialConfig); + return new Ext.form.BasicForm(null, config); + }, + + // private + initFields : function(){ + var f = this.form; + var formPanel = this; + var fn = function(c){ + if(formPanel.isField(c)){ + f.add(c); + }else if(c.findBy && c != formPanel){ + formPanel.applySettings(c); + //each check required for check/radio groups. + if(c.items && c.items.each){ + c.items.each(fn, this); + } + } + }; + this.items.each(fn, this); + }, + + // private + applySettings: function(c){ + var ct = c.ownerCt; + Ext.applyIf(c, { + labelAlign: ct.labelAlign, + labelWidth: ct.labelWidth, + itemCls: ct.itemCls + }); + }, + + // private + getLayoutTarget : function(){ + return this.form.el; + }, + + /** + * Provides access to the {@link Ext.form.BasicForm Form} which this Panel contains. + * @return {Ext.form.BasicForm} The {@link Ext.form.BasicForm Form} which this Panel contains. + */ + getForm : function(){ + return this.form; + }, + + // private + onRender : function(ct, position){ + this.initFields(); + Ext.FormPanel.superclass.onRender.call(this, ct, position); + this.form.initEl(this.body); + }, + + // private + beforeDestroy : function(){ + this.stopMonitoring(); + /* + * Don't move this behaviour to BasicForm because it can be used + * on it's own. + */ + Ext.destroy(this.form); + this.form.items.clear(); + Ext.FormPanel.superclass.beforeDestroy.call(this); + }, + + // Determine if a Component is usable as a form Field. + isField : function(c) { + return !!c.setValue && !!c.getValue && !!c.markInvalid && !!c.clearInvalid; + }, + + // private + initEvents : function(){ + Ext.FormPanel.superclass.initEvents.call(this); + // Listeners are required here to catch bubbling events from children. + this.on({ + scope: this, + add: this.onAddEvent, + remove: this.onRemoveEvent + }); + if(this.monitorValid){ // initialize after render + this.startMonitoring(); + } + }, + + // private + onAdd: function(c){ + Ext.FormPanel.superclass.onAdd.call(this, c); + this.processAdd(c); + }, + + // private + onAddEvent: function(ct, c){ + if(ct !== this){ + this.processAdd(c); + } + }, + + // private + processAdd : function(c){ + // If a single form Field, add it + if(this.isField(c)){ + this.form.add(c); + // If a Container, add any Fields it might contain + }else if(c.findBy){ + this.applySettings(c); + this.form.add.apply(this.form, c.findBy(this.isField)); + } + }, + + // private + onRemove: function(c){ + Ext.FormPanel.superclass.onRemove.call(this, c); + this.processRemove(c); + }, + + onRemoveEvent: function(ct, c){ + if(ct !== this){ + this.processRemove(c); + } + }, + + // private + processRemove : function(c){ + // If a single form Field, remove it + if(this.isField(c)){ + this.form.remove(c); + // If a Container, its already destroyed by the time it gets here. Remove any references to destroyed fields. + }else if(c.findBy){ + var isDestroyed = function(o) { + return !!o.isDestroyed; + } + this.form.items.filterBy(isDestroyed, this.form).each(this.form.remove, this.form); + } + }, + + /** + * Starts monitoring of the valid state of this form. Usually this is done by passing the config + * option "monitorValid" + */ + startMonitoring : function(){ + if(!this.validTask){ + this.validTask = new Ext.util.TaskRunner(); + this.validTask.start({ + run : this.bindHandler, + interval : this.monitorPoll || 200, + scope: this + }); + } + }, + + /** + * Stops monitoring of the valid state of this form + */ + stopMonitoring : function(){ + if(this.validTask){ + this.validTask.stopAll(); + this.validTask = null; + } + }, + + /** + * This is a proxy for the underlying BasicForm's {@link Ext.form.BasicForm#load} call. + * @param {Object} options The options to pass to the action (see {@link Ext.form.BasicForm#doAction} for details) + */ + load : function(){ + this.form.load.apply(this.form, arguments); + }, + + // private + onDisable : function(){ + Ext.FormPanel.superclass.onDisable.call(this); + if(this.form){ + this.form.items.each(function(){ + this.disable(); + }); + } + }, + + // private + onEnable : function(){ + Ext.FormPanel.superclass.onEnable.call(this); + if(this.form){ + this.form.items.each(function(){ + this.enable(); + }); + } + }, + + // private + bindHandler : function(){ + var valid = true; + this.form.items.each(function(f){ + if(!f.isValid(true)){ + valid = false; + return false; + } + }); + if(this.fbar){ + var fitems = this.fbar.items.items; + for(var i = 0, len = fitems.length; i < len; i++){ + var btn = fitems[i]; + if(btn.formBind === true && btn.disabled === valid){ + btn.setDisabled(!valid); + } + } + } + this.fireEvent('clientvalidation', this, valid); + } +}); +Ext.reg('form', Ext.FormPanel); + +Ext.form.FormPanel = Ext.FormPanel; /** - * @class Ext.form.FieldSet - * @extends Ext.Panel - * Standard container used for grouping items within a {@link Ext.form.FormPanel form}. - *
    
    -var form = new Ext.FormPanel({
    -    title: 'Simple Form with FieldSets',
    -    labelWidth: 75, // label settings here cascade unless overridden
    -    url: 'save-form.php',
    -    frame:true,
    -    bodyStyle:'padding:5px 5px 0',
    -    width: 700,
    -    renderTo: document.body,
    -    layout:'column', // arrange items in columns
    -    defaults: {      // defaults applied to items
    -        layout: 'form',
    -        border: false,
    -        bodyStyle: 'padding:4px'
    -    },
    -    items: [{
    -        // Fieldset in Column 1
    -        xtype:'fieldset',
    -        columnWidth: 0.5,
    -        title: 'Fieldset 1',
    -        collapsible: true,
    -        autoHeight:true,
    -        defaults: {
    -            anchor: '-20' // leave room for error icon
    -        },
    -        defaultType: 'textfield',
    -        items :[{
    -                fieldLabel: 'Field 1'
    -            }, {
    -                fieldLabel: 'Field 2'
    -            }, {
    -                fieldLabel: 'Field 3'
    -            }
    -        ]
    -    },{
    -        // Fieldset in Column 2 - Panel inside
    -        xtype:'fieldset',
    -        title: 'Show Panel', // title, header, or checkboxToggle creates fieldset header
    -        autoHeight:true,
    -        columnWidth: 0.5,
    -        checkboxToggle: true,
    -        collapsed: true, // fieldset initially collapsed
    -        layout:'anchor',
    -        items :[{
    -            xtype: 'panel',
    -            anchor: '100%',
    -            title: 'Panel inside a fieldset',
    -            frame: true,
    -            height: 100
    -        }]
    -    }]
    -});
    - * 
    - * @constructor - * @param {Object} config Configuration options - * @xtype fieldset - */ -Ext.form.FieldSet = Ext.extend(Ext.Panel, { - /** - * @cfg {Mixed} checkboxToggle true to render a checkbox into the fieldset frame just - * in front of the legend to expand/collapse the fieldset when the checkbox is toggled. (defaults - * to false). - *

    A {@link Ext.DomHelper DomHelper} element spec may also be specified to create the checkbox. - * If true is specified, the default DomHelper config object used to create the element - * is:

    
    -     * {tag: 'input', type: 'checkbox', name: this.checkboxName || this.id+'-checkbox'}
    -     * 
    - */ - /** - * @cfg {String} checkboxName The name to assign to the fieldset's checkbox if {@link #checkboxToggle} = true - * (defaults to '[checkbox id]-checkbox'). - */ - /** - * @cfg {Boolean} collapsible - * true to make the fieldset collapsible and have the expand/collapse toggle button automatically - * rendered into the legend element, false to keep the fieldset statically sized with no collapse - * button (defaults to false). Another option is to configure {@link #checkboxToggle}. - */ - /** - * @cfg {Number} labelWidth The width of labels. This property cascades to child containers. - */ - /** - * @cfg {String} itemCls A css class to apply to the x-form-item of fields (see - * {@link Ext.layout.FormLayout}.{@link Ext.layout.FormLayout#fieldTpl fieldTpl} for details). - * This property cascades to child containers. - */ - /** - * @cfg {String} baseCls The base CSS class applied to the fieldset (defaults to 'x-fieldset'). - */ - baseCls : 'x-fieldset', - /** - * @cfg {String} layout The {@link Ext.Container#layout} to use inside the fieldset (defaults to 'form'). - */ - layout : 'form', - /** - * @cfg {Boolean} animCollapse - * true to animate the transition when the panel is collapsed, false to skip the - * animation (defaults to false). - */ - animCollapse : false, - - // private - onRender : function(ct, position){ - if(!this.el){ - this.el = document.createElement('fieldset'); - this.el.id = this.id; - if (this.title || this.header || this.checkboxToggle) { - this.el.appendChild(document.createElement('legend')).className = 'x-fieldset-header'; - } - } - - Ext.form.FieldSet.superclass.onRender.call(this, ct, position); - - if(this.checkboxToggle){ - var o = typeof this.checkboxToggle == 'object' ? - this.checkboxToggle : - {tag: 'input', type: 'checkbox', name: this.checkboxName || this.id+'-checkbox'}; - this.checkbox = this.header.insertFirst(o); - this.checkbox.dom.checked = !this.collapsed; - this.mon(this.checkbox, 'click', this.onCheckClick, this); - } - }, - - // private - onCollapse : function(doAnim, animArg){ - if(this.checkbox){ - this.checkbox.dom.checked = false; - } - Ext.form.FieldSet.superclass.onCollapse.call(this, doAnim, animArg); - - }, - - // private - onExpand : function(doAnim, animArg){ - if(this.checkbox){ - this.checkbox.dom.checked = true; - } - Ext.form.FieldSet.superclass.onExpand.call(this, doAnim, animArg); - }, - - /** - * This function is called by the fieldset's checkbox when it is toggled (only applies when - * checkboxToggle = true). This method should never be called externally, but can be - * overridden to provide custom behavior when the checkbox is toggled if needed. - */ - onCheckClick : function(){ - this[this.checkbox.dom.checked ? 'expand' : 'collapse'](); - } - - /** - * @cfg {String/Number} activeItem - * @hide - */ - /** - * @cfg {Mixed} applyTo - * @hide - */ - /** - * @cfg {Boolean} bodyBorder - * @hide - */ - /** - * @cfg {Boolean} border - * @hide - */ - /** - * @cfg {Boolean/Number} bufferResize - * @hide - */ - /** - * @cfg {Boolean} collapseFirst - * @hide - */ - /** - * @cfg {String} defaultType - * @hide - */ - /** - * @cfg {String} disabledClass - * @hide - */ - /** - * @cfg {String} elements - * @hide - */ - /** - * @cfg {Boolean} floating - * @hide - */ - /** - * @cfg {Boolean} footer - * @hide - */ - /** - * @cfg {Boolean} frame - * @hide - */ - /** - * @cfg {Boolean} header - * @hide - */ - /** - * @cfg {Boolean} headerAsText - * @hide - */ - /** - * @cfg {Boolean} hideCollapseTool - * @hide - */ - /** - * @cfg {String} iconCls - * @hide - */ - /** - * @cfg {Boolean/String} shadow - * @hide - */ - /** - * @cfg {Number} shadowOffset - * @hide - */ - /** - * @cfg {Boolean} shim - * @hide - */ - /** - * @cfg {Object/Array} tbar - * @hide - */ - /** - * @cfg {Array} tools - * @hide - */ - /** - * @cfg {Ext.Template/Ext.XTemplate} toolTemplate - * @hide - */ - /** - * @cfg {String} xtype - * @hide - */ - /** - * @property header - * @hide - */ - /** - * @property footer - * @hide - */ - /** - * @method focus - * @hide - */ - /** - * @method getBottomToolbar - * @hide - */ - /** - * @method getTopToolbar - * @hide - */ - /** - * @method setIconClass - * @hide - */ - /** - * @event activate - * @hide - */ - /** - * @event beforeclose - * @hide - */ - /** - * @event bodyresize - * @hide - */ - /** - * @event close - * @hide - */ - /** - * @event deactivate - * @hide - */ -}); -Ext.reg('fieldset', Ext.form.FieldSet); -/** - * @class Ext.form.HtmlEditor - * @extends Ext.form.Field - * Provides a lightweight HTML Editor component. Some toolbar features are not supported by Safari and will be - * automatically hidden when needed. These are noted in the config options where appropriate. - *

    The editor's toolbar buttons have tooltips defined in the {@link #buttonTips} property, but they are not - * enabled by default unless the global {@link Ext.QuickTips} singleton is {@link Ext.QuickTips#init initialized}. - *

    Note: The focus/blur and validation marking functionality inherited from Ext.form.Field is NOT - * supported by this editor. - *

    An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an Editor within - * any element that has display set to 'none' can cause problems in Safari and Firefox due to their default iframe reloading bugs. - *

    Example usage: + * @class Ext.form.FieldSet + * @extends Ext.Panel + * Standard container used for grouping items within a {@link Ext.form.FormPanel form}. *
    
    -// Simple example rendered with default options:
    -Ext.QuickTips.init();  // enable tooltips
    -new Ext.form.HtmlEditor({
    -    renderTo: Ext.getBody(),
    -    width: 800,
    -    height: 300
    -});
    -
    -// Passed via xtype into a container and with custom options:
    -Ext.QuickTips.init();  // enable tooltips
    -new Ext.Panel({
    -    title: 'HTML Editor',
    -    renderTo: Ext.getBody(),
    -    width: 600,
    -    height: 300,
    -    frame: true,
    -    layout: 'fit',
    -    items: {
    -        xtype: 'htmleditor',
    -        enableColors: false,
    -        enableAlignments: false
    -    }
    -});
    -
    - * @constructor - * Create a new HtmlEditor - * @param {Object} config - * @xtype htmleditor - */ - -Ext.form.HtmlEditor = Ext.extend(Ext.form.Field, { - /** - * @cfg {Boolean} enableFormat Enable the bold, italic and underline buttons (defaults to true) - */ - enableFormat : true, - /** - * @cfg {Boolean} enableFontSize Enable the increase/decrease font size buttons (defaults to true) - */ - enableFontSize : true, - /** - * @cfg {Boolean} enableColors Enable the fore/highlight color buttons (defaults to true) - */ - enableColors : true, - /** - * @cfg {Boolean} enableAlignments Enable the left, center, right alignment buttons (defaults to true) - */ - enableAlignments : true, - /** - * @cfg {Boolean} enableLists Enable the bullet and numbered list buttons. Not available in Safari. (defaults to true) - */ - enableLists : true, - /** - * @cfg {Boolean} enableSourceEdit Enable the switch to source edit button. Not available in Safari. (defaults to true) - */ - enableSourceEdit : true, - /** - * @cfg {Boolean} enableLinks Enable the create link button. Not available in Safari. (defaults to true) - */ - enableLinks : true, - /** - * @cfg {Boolean} enableFont Enable font selection. Not available in Safari. (defaults to true) - */ - enableFont : true, - /** - * @cfg {String} createLinkText The default text for the create link prompt - */ - createLinkText : 'Please enter the URL for the link:', - /** - * @cfg {String} defaultLinkValue The default value for the create link prompt (defaults to http:/ /) - */ - defaultLinkValue : 'http:/'+'/', - /** - * @cfg {Array} fontFamilies An array of available font families - */ - fontFamilies : [ - 'Arial', - 'Courier New', - 'Tahoma', - 'Times New Roman', - 'Verdana' - ], - defaultFont: 'tahoma', - /** - * @cfg {String} defaultValue A default value to be put into the editor to resolve focus issues (defaults to ​ (Zero-width space),   (Non-breaking space) in Opera and IE6). - */ - defaultValue: (Ext.isOpera || Ext.isIE6) ? ' ' : '​', - - // private properties - actionMode: 'wrap', - validationEvent : false, - deferHeight: true, - initialized : false, - activated : false, - sourceEditMode : false, - onFocus : Ext.emptyFn, - iframePad:3, - hideMode:'offsets', - defaultAutoCreate : { - tag: "textarea", - style:"width:500px;height:300px;", - autocomplete: "off" - }, - - // private - initComponent : function(){ - this.addEvents( - /** - * @event initialize - * Fires when the editor is fully initialized (including the iframe) - * @param {HtmlEditor} this - */ - 'initialize', - /** - * @event activate - * Fires when the editor is first receives the focus. Any insertion must wait - * until after this event. - * @param {HtmlEditor} this - */ - 'activate', - /** - * @event beforesync - * Fires before the textarea is updated with content from the editor iframe. Return false - * to cancel the sync. - * @param {HtmlEditor} this - * @param {String} html - */ - 'beforesync', - /** - * @event beforepush - * Fires before the iframe editor is updated with content from the textarea. Return false - * to cancel the push. - * @param {HtmlEditor} this - * @param {String} html - */ - 'beforepush', - /** - * @event sync - * Fires when the textarea is updated with content from the editor iframe. - * @param {HtmlEditor} this - * @param {String} html - */ - 'sync', - /** - * @event push - * Fires when the iframe editor is updated with content from the textarea. - * @param {HtmlEditor} this - * @param {String} html - */ - 'push', - /** - * @event editmodechange - * Fires when the editor switches edit modes - * @param {HtmlEditor} this - * @param {Boolean} sourceEdit True if source edit, false if standard editing. - */ - 'editmodechange' - ) - }, - - // private - createFontOptions : function(){ - var buf = [], fs = this.fontFamilies, ff, lc; - for(var i = 0, len = fs.length; i< len; i++){ - ff = fs[i]; - lc = ff.toLowerCase(); - buf.push( - '' - ); - } - return buf.join(''); - }, - - /* - * Protected method that will not generally be called directly. It - * is called when the editor creates its toolbar. Override this method if you need to - * add custom toolbar buttons. - * @param {HtmlEditor} editor - */ - createToolbar : function(editor){ - - var tipsEnabled = Ext.QuickTips && Ext.QuickTips.isEnabled(); - - function btn(id, toggle, handler){ - return { - itemId : id, - cls : 'x-btn-icon', - iconCls: 'x-edit-'+id, - enableToggle:toggle !== false, - scope: editor, - handler:handler||editor.relayBtnCmd, - clickEvent:'mousedown', - tooltip: tipsEnabled ? editor.buttonTips[id] || undefined : undefined, - overflowText: editor.buttonTips[id].title || undefined, - tabIndex:-1 - }; - } - - // build the toolbar - var tb = new Ext.Toolbar({ - renderTo:this.wrap.dom.firstChild - }); - - // stop form submits - this.mon(tb.el, 'click', function(e){ - e.preventDefault(); - }); - - if(this.enableFont && !Ext.isSafari2){ - this.fontSelect = tb.el.createChild({ - tag:'select', - cls:'x-font-select', - html: this.createFontOptions() - }); - this.mon(this.fontSelect, 'change', function(){ - var font = this.fontSelect.dom.value; - this.relayCmd('fontname', font); - this.deferFocus(); - }, this); - - tb.add( - this.fontSelect.dom, - '-' - ); - } - - if(this.enableFormat){ - tb.add( - btn('bold'), - btn('italic'), - btn('underline') - ); - } - - if(this.enableFontSize){ - tb.add( - '-', - btn('increasefontsize', false, this.adjustFont), - btn('decreasefontsize', false, this.adjustFont) - ); - } - - if(this.enableColors){ - tb.add( - '-', { - itemId:'forecolor', - cls:'x-btn-icon', - iconCls: 'x-edit-forecolor', - clickEvent:'mousedown', - tooltip: tipsEnabled ? editor.buttonTips.forecolor || undefined : undefined, - tabIndex:-1, - menu : new Ext.menu.ColorMenu({ - allowReselect: true, - focus: Ext.emptyFn, - value:'000000', - plain:true, - listeners: { - scope: this, - select: function(cp, color){ - this.execCmd('forecolor', Ext.isWebKit || Ext.isIE ? '#'+color : color); - this.deferFocus(); - } - }, - clickEvent:'mousedown' - }) - }, { - itemId:'backcolor', - cls:'x-btn-icon', - iconCls: 'x-edit-backcolor', - clickEvent:'mousedown', - tooltip: tipsEnabled ? editor.buttonTips.backcolor || undefined : undefined, - tabIndex:-1, - menu : new Ext.menu.ColorMenu({ - focus: Ext.emptyFn, - value:'FFFFFF', - plain:true, - allowReselect: true, - listeners: { - scope: this, - select: function(cp, color){ - if(Ext.isGecko){ - this.execCmd('useCSS', false); - this.execCmd('hilitecolor', color); - this.execCmd('useCSS', true); - this.deferFocus(); - }else{ - this.execCmd(Ext.isOpera ? 'hilitecolor' : 'backcolor', Ext.isWebKit || Ext.isIE ? '#'+color : color); - this.deferFocus(); - } - } - }, - clickEvent:'mousedown' - }) - } - ); - } - - if(this.enableAlignments){ - tb.add( - '-', - btn('justifyleft'), - btn('justifycenter'), - btn('justifyright') - ); - } - - if(!Ext.isSafari2){ - if(this.enableLinks){ - tb.add( - '-', - btn('createlink', false, this.createLink) - ); - } - - if(this.enableLists){ - tb.add( - '-', - btn('insertorderedlist'), - btn('insertunorderedlist') - ); - } - if(this.enableSourceEdit){ - tb.add( - '-', - btn('sourceedit', true, function(btn){ - this.toggleSourceEdit(!this.sourceEditMode); - }) - ); - } - } - - this.tb = tb; - }, - - /** - * Protected method that will not generally be called directly. It - * is called when the editor initializes the iframe with HTML contents. Override this method if you - * want to change the initialization markup of the iframe (e.g. to add stylesheets). - */ - getDocMarkup : function(){ - return ''; - }, - - // private - getEditorBody : function(){ - return this.doc.body || this.doc.documentElement; - }, - - // private - getDoc : function(){ - return Ext.isIE ? this.getWin().document : (this.iframe.contentDocument || this.getWin().document); - }, - - // private - getWin : function(){ - return Ext.isIE ? this.iframe.contentWindow : window.frames[this.iframe.name]; - }, - - // private - onRender : function(ct, position){ - Ext.form.HtmlEditor.superclass.onRender.call(this, ct, position); - this.el.dom.style.border = '0 none'; - this.el.dom.setAttribute('tabIndex', -1); - this.el.addClass('x-hidden'); - if(Ext.isIE){ // fix IE 1px bogus margin - this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;') - } - this.wrap = this.el.wrap({ - cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'} - }); - - this.createToolbar(this); - - this.disableItems(true); - // is this needed? - // this.tb.doLayout(); - - this.createIFrame(); - - if(!this.width){ - var sz = this.el.getSize(); - this.setSize(sz.width, this.height || sz.height); - } - this.resizeEl = this.positionEl = this.wrap; - }, - - createIFrame: function(){ - var iframe = document.createElement('iframe'); - iframe.name = Ext.id(); - iframe.frameBorder = '0'; - iframe.src = Ext.SSL_SECURE_URL; - this.wrap.dom.appendChild(iframe); - - this.iframe = iframe; - - this.monitorTask = Ext.TaskMgr.start({ - run: this.checkDesignMode, - scope: this, - interval:100 - }); - }, - - initFrame : function(){ - Ext.TaskMgr.stop(this.monitorTask); - this.doc = this.getDoc(); - this.win = this.getWin(); - - this.doc.open(); - this.doc.write(this.getDocMarkup()); - this.doc.close(); - - var task = { // must defer to wait for browser to be ready - run : function(){ - if(this.doc.body || this.doc.readyState == 'complete'){ - Ext.TaskMgr.stop(task); - this.doc.designMode="on"; - this.initEditor.defer(10, this); - } - }, - interval : 10, - duration:10000, - scope: this - }; - Ext.TaskMgr.start(task); - }, - - - checkDesignMode : function(){ - if(this.wrap && this.wrap.dom.offsetWidth){ - var doc = this.getDoc(); - if(!doc){ - return; - } - if(!doc.editorInitialized || String(doc.designMode).toLowerCase() != 'on'){ - this.initFrame(); - } - } - }, - - disableItems: function(disabled){ - if(this.fontSelect){ - this.fontSelect.dom.disabled = disabled; - } - this.tb.items.each(function(item){ - if(item.getItemId() != 'sourceedit'){ - item.setDisabled(disabled); - } - }); - }, - - // private - onResize : function(w, h){ - Ext.form.HtmlEditor.superclass.onResize.apply(this, arguments); - if(this.el && this.iframe){ - if(Ext.isNumber(w)){ - var aw = w - this.wrap.getFrameWidth('lr'); - this.el.setWidth(aw); - this.tb.setWidth(aw); - this.iframe.style.width = Math.max(aw, 0) + 'px'; - } - if(Ext.isNumber(h)){ - var ah = h - this.wrap.getFrameWidth('tb') - this.tb.el.getHeight(); - this.el.setHeight(ah); - this.iframe.style.height = Math.max(ah, 0) + 'px'; - if(this.doc){ - this.getEditorBody().style.height = Math.max((ah - (this.iframePad*2)), 0) + 'px'; - } - } - } - }, - - /** - * Toggles the editor between standard and source edit mode. - * @param {Boolean} sourceEdit (optional) True for source edit, false for standard - */ - toggleSourceEdit : function(sourceEditMode){ - if(sourceEditMode === undefined){ - sourceEditMode = !this.sourceEditMode; - } - this.sourceEditMode = sourceEditMode === true; - var btn = this.tb.items.get('sourceedit'); - if(btn.pressed !== this.sourceEditMode){ - btn.toggle(this.sourceEditMode); - if(!btn.xtbHidden){ - return; - } - } - if(this.sourceEditMode){ - this.disableItems(true); - this.syncValue(); - this.iframe.className = 'x-hidden'; - this.el.removeClass('x-hidden'); - this.el.dom.removeAttribute('tabIndex'); - this.el.focus(); - }else{ - if(this.initialized){ - this.disableItems(false); - } - this.pushValue(); - this.iframe.className = ''; - this.el.addClass('x-hidden'); - this.el.dom.setAttribute('tabIndex', -1); - this.deferFocus(); - } - var lastSize = this.lastSize; - if(lastSize){ - delete this.lastSize; - this.setSize(lastSize); - } - this.fireEvent('editmodechange', this, this.sourceEditMode); - }, - - // private used internally - createLink : function(){ - var url = prompt(this.createLinkText, this.defaultLinkValue); - if(url && url != 'http:/'+'/'){ - this.relayCmd('createlink', url); - } - }, - - // private - initEvents : function(){ - this.originalValue = this.getValue(); - }, - - /** - * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide - * @method - */ - markInvalid : Ext.emptyFn, - - /** - * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide - * @method - */ - clearInvalid : Ext.emptyFn, - - // docs inherit from Field - setValue : function(v){ - Ext.form.HtmlEditor.superclass.setValue.call(this, v); - this.pushValue(); - return this; - }, - - /** - * Protected method that will not generally be called directly. If you need/want - * custom HTML cleanup, this is the method you should override. - * @param {String} html The HTML to be cleaned - * @return {String} The cleaned HTML - */ - cleanHtml: function(html) { - html = String(html); - if(Ext.isWebKit){ // strip safari nonsense - html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, ''); - } - - /* - * Neat little hack. Strips out all the non-digit characters from the default - * value and compares it to the character code of the first character in the string - * because it can cause encoding issues when posted to the server. - */ - if(html.charCodeAt(0) == this.defaultValue.replace(/\D/g, '')){ - html = html.substring(1); - } - return html; - }, - - /** - * Protected method that will not generally be called directly. Syncs the contents - * of the editor iframe with the textarea. - */ - syncValue : function(){ - if(this.initialized){ - var bd = this.getEditorBody(); - var html = bd.innerHTML; - if(Ext.isWebKit){ - var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element! - var m = bs.match(/text-align:(.*?);/i); - if(m && m[1]){ - html = '
    ' + html + '
    '; - } - } - html = this.cleanHtml(html); - if(this.fireEvent('beforesync', this, html) !== false){ - this.el.dom.value = html; - this.fireEvent('sync', this, html); - } - } - }, - - //docs inherit from Field - getValue : function() { - this[this.sourceEditMode ? 'pushValue' : 'syncValue'](); - return Ext.form.HtmlEditor.superclass.getValue.call(this); - }, - - /** - * Protected method that will not generally be called directly. Pushes the value of the textarea - * into the iframe editor. - */ - pushValue : function(){ - if(this.initialized){ - var v = this.el.dom.value; - if(!this.activated && v.length < 1){ - v = this.defaultValue; - } - if(this.fireEvent('beforepush', this, v) !== false){ - this.getEditorBody().innerHTML = v; - if(Ext.isGecko){ - // Gecko hack, see: https://bugzilla.mozilla.org/show_bug.cgi?id=232791#c8 - var d = this.doc, - mode = d.designMode.toLowerCase(); - - d.designMode = mode.toggle('on', 'off'); - d.designMode = mode; - } - this.fireEvent('push', this, v); - } - } - }, - - // private - deferFocus : function(){ - this.focus.defer(10, this); - }, - - // docs inherit from Field - focus : function(){ - if(this.win && !this.sourceEditMode){ - this.win.focus(); - }else{ - this.el.focus(); - } - }, - - // private - initEditor : function(){ - //Destroying the component during/before initEditor can cause issues. - try{ - var dbody = this.getEditorBody(); - var ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat'); - ss['background-attachment'] = 'fixed'; // w3c - dbody.bgProperties = 'fixed'; // ie - - Ext.DomHelper.applyStyles(dbody, ss); - - if(this.doc){ - try{ - Ext.EventManager.removeAll(this.doc); - }catch(e){} - } - - this.doc = this.getDoc(); - - Ext.EventManager.on(this.doc, { - 'mousedown': this.onEditorEvent, - 'dblclick': this.onEditorEvent, - 'click': this.onEditorEvent, - 'keyup': this.onEditorEvent, - buffer:100, - scope: this - }); - - if(Ext.isGecko){ - Ext.EventManager.on(this.doc, 'keypress', this.applyCommand, this); - } - if(Ext.isIE || Ext.isWebKit || Ext.isOpera){ - Ext.EventManager.on(this.doc, 'keydown', this.fixKeys, this); - } - this.initialized = true; - this.fireEvent('initialize', this); - this.doc.editorInitialized = true; - this.pushValue(); - }catch(e){} - }, - - // private - onDestroy : function(){ - if(this.monitorTask){ - Ext.TaskMgr.stop(this.monitorTask); - } - if(this.rendered){ - Ext.destroy(this.tb); - if(this.wrap){ - this.wrap.dom.innerHTML = ''; - this.wrap.remove(); - } - } - if(this.el){ - this.el.removeAllListeners(); - this.el.remove(); - } - - if(this.doc){ - try{ - Ext.EventManager.removeAll(this.doc); - for (var prop in this.doc){ - delete this.doc[prop]; - } - }catch(e){} - } - this.purgeListeners(); - }, - - // private - onFirstFocus : function(){ - this.activated = true; - this.disableItems(false); - if(Ext.isGecko){ // prevent silly gecko errors - this.win.focus(); - var s = this.win.getSelection(); - if(!s.focusNode || s.focusNode.nodeType != 3){ - var r = s.getRangeAt(0); - r.selectNodeContents(this.getEditorBody()); - r.collapse(true); - this.deferFocus(); - } - try{ - this.execCmd('useCSS', true); - this.execCmd('styleWithCSS', false); - }catch(e){} - } - this.fireEvent('activate', this); +var form = new Ext.FormPanel({ + title: 'Simple Form with FieldSets', + labelWidth: 75, // label settings here cascade unless overridden + url: 'save-form.php', + frame:true, + bodyStyle:'padding:5px 5px 0', + width: 700, + renderTo: document.body, + layout:'column', // arrange items in columns + defaults: { // defaults applied to items + layout: 'form', + border: false, + bodyStyle: 'padding:4px' }, - - // private - adjustFont: function(btn){ - var adjust = btn.getItemId() == 'increasefontsize' ? 1 : -1; - - var v = parseInt(this.doc.queryCommandValue('FontSize') || 2, 10); - if((Ext.isSafari && !Ext.isSafari2) || Ext.isChrome || Ext.isAir){ - // Safari 3 values - // 1 = 10px, 2 = 13px, 3 = 16px, 4 = 18px, 5 = 24px, 6 = 32px - if(v <= 10){ - v = 1 + adjust; - }else if(v <= 13){ - v = 2 + adjust; - }else if(v <= 16){ - v = 3 + adjust; - }else if(v <= 18){ - v = 4 + adjust; - }else if(v <= 24){ - v = 5 + adjust; - }else { - v = 6 + adjust; - } - v = v.constrain(1, 6); - }else{ - if(Ext.isSafari){ // safari - adjust *= 2; + items: [{ + // Fieldset in Column 1 + xtype:'fieldset', + columnWidth: 0.5, + title: 'Fieldset 1', + collapsible: true, + autoHeight:true, + defaults: { + anchor: '-20' // leave room for error icon + }, + defaultType: 'textfield', + items :[{ + fieldLabel: 'Field 1' + }, { + fieldLabel: 'Field 2' + }, { + fieldLabel: 'Field 3' } - v = Math.max(1, v+adjust) + (Ext.isSafari ? 'px' : 0); - } - this.execCmd('FontSize', v); - }, - - // private - onEditorEvent : function(e){ - this.updateToolbar(); - }, - - + ] + },{ + // Fieldset in Column 2 - Panel inside + xtype:'fieldset', + title: 'Show Panel', // title, header, or checkboxToggle creates fieldset header + autoHeight:true, + columnWidth: 0.5, + checkboxToggle: true, + collapsed: true, // fieldset initially collapsed + layout:'anchor', + items :[{ + xtype: 'panel', + anchor: '100%', + title: 'Panel inside a fieldset', + frame: true, + height: 100 + }] + }] +}); + *
    + * @constructor + * @param {Object} config Configuration options + * @xtype fieldset + */ +Ext.form.FieldSet = Ext.extend(Ext.Panel, { /** - * Protected method that will not generally be called directly. It triggers - * a toolbar update by reading the markup state of the current selection in the editor. + * @cfg {Mixed} checkboxToggle true to render a checkbox into the fieldset frame just + * in front of the legend to expand/collapse the fieldset when the checkbox is toggled. (defaults + * to false). + *

    A {@link Ext.DomHelper DomHelper} element spec may also be specified to create the checkbox. + * If true is specified, the default DomHelper config object used to create the element + * is:

    
    +     * {tag: 'input', type: 'checkbox', name: this.checkboxName || this.id+'-checkbox'}
    +     * 
    */ - updateToolbar: function(){ - - if(!this.activated){ - this.onFirstFocus(); - return; - } - - var btns = this.tb.items.map, doc = this.doc; - - if(this.enableFont && !Ext.isSafari2){ - var name = (this.doc.queryCommandValue('FontName')||this.defaultFont).toLowerCase(); - if(name != this.fontSelect.dom.value){ - this.fontSelect.dom.value = name; - } - } - if(this.enableFormat){ - btns.bold.toggle(doc.queryCommandState('bold')); - btns.italic.toggle(doc.queryCommandState('italic')); - btns.underline.toggle(doc.queryCommandState('underline')); - } - if(this.enableAlignments){ - btns.justifyleft.toggle(doc.queryCommandState('justifyleft')); - btns.justifycenter.toggle(doc.queryCommandState('justifycenter')); - btns.justifyright.toggle(doc.queryCommandState('justifyright')); - } - if(!Ext.isSafari2 && this.enableLists){ - btns.insertorderedlist.toggle(doc.queryCommandState('insertorderedlist')); - btns.insertunorderedlist.toggle(doc.queryCommandState('insertunorderedlist')); - } - - Ext.menu.MenuMgr.hideAll(); - - this.syncValue(); - }, - - // private - relayBtnCmd : function(btn){ - this.relayCmd(btn.getItemId()); - }, - /** - * Executes a Midas editor command on the editor document and performs necessary focus and - * toolbar updates. This should only be called after the editor is initialized. - * @param {String} cmd The Midas command - * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null) + * @cfg {String} checkboxName The name to assign to the fieldset's checkbox if {@link #checkboxToggle} = true + * (defaults to '[checkbox id]-checkbox'). */ - relayCmd : function(cmd, value){ - (function(){ - this.focus(); - this.execCmd(cmd, value); - this.updateToolbar(); - }).defer(10, this); - }, - /** - * Executes a Midas editor command directly on the editor document. - * For visual commands, you should use {@link #relayCmd} instead. - * This should only be called after the editor is initialized. - * @param {String} cmd The Midas command - * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null) + * @cfg {Boolean} collapsible + * true to make the fieldset collapsible and have the expand/collapse toggle button automatically + * rendered into the legend element, false to keep the fieldset statically sized with no collapse + * button (defaults to false). Another option is to configure {@link #checkboxToggle}. */ - execCmd : function(cmd, value){ - this.doc.execCommand(cmd, false, value === undefined ? null : value); - this.syncValue(); - }, + /** + * @cfg {Number} labelWidth The width of labels. This property cascades to child containers. + */ + /** + * @cfg {String} itemCls A css class to apply to the x-form-item of fields (see + * {@link Ext.layout.FormLayout}.{@link Ext.layout.FormLayout#fieldTpl fieldTpl} for details). + * This property cascades to child containers. + */ + /** + * @cfg {String} baseCls The base CSS class applied to the fieldset (defaults to 'x-fieldset'). + */ + baseCls : 'x-fieldset', + /** + * @cfg {String} layout The {@link Ext.Container#layout} to use inside the fieldset (defaults to 'form'). + */ + layout : 'form', + /** + * @cfg {Boolean} animCollapse + * true to animate the transition when the panel is collapsed, false to skip the + * animation (defaults to false). + */ + animCollapse : false, // private - applyCommand : function(e){ - if(e.ctrlKey){ - var c = e.getCharCode(), cmd; - if(c > 0){ - c = String.fromCharCode(c); - switch(c){ - case 'b': - cmd = 'bold'; - break; - case 'i': - cmd = 'italic'; - break; - case 'u': - cmd = 'underline'; - break; - } - if(cmd){ - this.win.focus(); - this.execCmd(cmd); - this.deferFocus(); - e.preventDefault(); - } + onRender : function(ct, position){ + if(!this.el){ + this.el = document.createElement('fieldset'); + this.el.id = this.id; + if (this.title || this.header || this.checkboxToggle) { + this.el.appendChild(document.createElement('legend')).className = this.baseCls + '-header'; } } - }, - /** - * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated - * to insert text. - * @param {String} text - */ - insertAtCursor : function(text){ - if(!this.activated){ - return; - } - if(Ext.isIE){ - this.win.focus(); - var r = this.doc.selection.createRange(); - if(r){ - r.collapse(true); - r.pasteHTML(text); - this.syncValue(); - this.deferFocus(); - } - }else{ - this.win.focus(); - this.execCmd('InsertHTML', text); - this.deferFocus(); + Ext.form.FieldSet.superclass.onRender.call(this, ct, position); + + if(this.checkboxToggle){ + var o = typeof this.checkboxToggle == 'object' ? + this.checkboxToggle : + {tag: 'input', type: 'checkbox', name: this.checkboxName || this.id+'-checkbox'}; + this.checkbox = this.header.insertFirst(o); + this.checkbox.dom.checked = !this.collapsed; + this.mon(this.checkbox, 'click', this.onCheckClick, this); } }, // private - fixKeys : function(){ // load time branching for fastest keydown performance - if(Ext.isIE){ - return function(e){ - var k = e.getKey(), r; - if(k == e.TAB){ - e.stopEvent(); - r = this.doc.selection.createRange(); - if(r){ - r.collapse(true); - r.pasteHTML('    '); - this.deferFocus(); - } - }else if(k == e.ENTER){ - r = this.doc.selection.createRange(); - if(r){ - var target = r.parentElement(); - if(!target || target.tagName.toLowerCase() != 'li'){ - e.stopEvent(); - r.pasteHTML('
    '); - r.collapse(false); - r.select(); - } - } - } - }; - }else if(Ext.isOpera){ - return function(e){ - var k = e.getKey(); - if(k == e.TAB){ - e.stopEvent(); - this.win.focus(); - this.execCmd('InsertHTML','    '); - this.deferFocus(); - } - }; - }else if(Ext.isWebKit){ - return function(e){ - var k = e.getKey(); - if(k == e.TAB){ - e.stopEvent(); - this.execCmd('InsertText','\t'); - this.deferFocus(); - }else if(k == e.ENTER){ - e.stopEvent(); - this.execCmd('InsertHtml','

    '); - this.deferFocus(); - } - }; + onCollapse : function(doAnim, animArg){ + if(this.checkbox){ + this.checkbox.dom.checked = false; } - }(), + Ext.form.FieldSet.superclass.onCollapse.call(this, doAnim, animArg); - /** - * Returns the editor's toolbar. This is only available after the editor has been rendered. - * @return {Ext.Toolbar} - */ - getToolbar : function(){ - return this.tb; }, - /** - * Object collection of toolbar tooltips for the buttons in the editor. The key - * is the command id associated with that button and the value is a valid QuickTips object. - * For example: -
    
    -{
    -    bold : {
    -        title: 'Bold (Ctrl+B)',
    -        text: 'Make the selected text bold.',
    -        cls: 'x-html-editor-tip'
    -    },
    -    italic : {
    -        title: 'Italic (Ctrl+I)',
    -        text: 'Make the selected text italic.',
    -        cls: 'x-html-editor-tip'
    +    // private
    +    onExpand : function(doAnim, animArg){
    +        if(this.checkbox){
    +            this.checkbox.dom.checked = true;
    +        }
    +        Ext.form.FieldSet.superclass.onExpand.call(this, doAnim, animArg);
         },
    -    ...
    -
    - * @type Object + + /** + * This function is called by the fieldset's checkbox when it is toggled (only applies when + * checkboxToggle = true). This method should never be called externally, but can be + * overridden to provide custom behavior when the checkbox is toggled if needed. */ - buttonTips : { - bold : { - title: 'Bold (Ctrl+B)', - text: 'Make the selected text bold.', - cls: 'x-html-editor-tip' - }, - italic : { - title: 'Italic (Ctrl+I)', - text: 'Make the selected text italic.', - cls: 'x-html-editor-tip' - }, - underline : { - title: 'Underline (Ctrl+U)', - text: 'Underline the selected text.', - cls: 'x-html-editor-tip' - }, - increasefontsize : { - title: 'Grow Text', - text: 'Increase the font size.', - cls: 'x-html-editor-tip' - }, - decreasefontsize : { - title: 'Shrink Text', - text: 'Decrease the font size.', - cls: 'x-html-editor-tip' - }, - backcolor : { - title: 'Text Highlight Color', - text: 'Change the background color of the selected text.', - cls: 'x-html-editor-tip' - }, - forecolor : { - title: 'Font Color', - text: 'Change the color of the selected text.', - cls: 'x-html-editor-tip' - }, - justifyleft : { - title: 'Align Text Left', - text: 'Align text to the left.', - cls: 'x-html-editor-tip' - }, - justifycenter : { - title: 'Center Text', - text: 'Center text in the editor.', - cls: 'x-html-editor-tip' - }, - justifyright : { - title: 'Align Text Right', - text: 'Align text to the right.', - cls: 'x-html-editor-tip' - }, - insertunorderedlist : { - title: 'Bullet List', - text: 'Start a bulleted list.', - cls: 'x-html-editor-tip' - }, - insertorderedlist : { - title: 'Numbered List', - text: 'Start a numbered list.', - cls: 'x-html-editor-tip' - }, - createlink : { - title: 'Hyperlink', - text: 'Make the selected text a hyperlink.', - cls: 'x-html-editor-tip' - }, - sourceedit : { - title: 'Source Edit', - text: 'Switch to source editing mode.', - cls: 'x-html-editor-tip' - } + onCheckClick : function(){ + this[this.checkbox.dom.checked ? 'expand' : 'collapse'](); } - // hide stuff that is not compatible /** - * @event blur + * @cfg {String/Number} activeItem + * @hide + */ + /** + * @cfg {Mixed} applyTo * @hide */ /** - * @event change + * @cfg {Boolean} bodyBorder * @hide */ /** - * @event focus + * @cfg {Boolean} border * @hide */ /** - * @event specialkey + * @cfg {Boolean/Number} bufferResize * @hide */ /** - * @cfg {String} fieldClass @hide + * @cfg {Boolean} collapseFirst + * @hide */ /** - * @cfg {String} focusClass @hide + * @cfg {String} defaultType + * @hide */ /** - * @cfg {String} autoCreate @hide + * @cfg {String} disabledClass + * @hide */ /** - * @cfg {String} inputType @hide + * @cfg {String} elements + * @hide */ /** - * @cfg {String} invalidClass @hide + * @cfg {Boolean} floating + * @hide */ /** - * @cfg {String} invalidText @hide + * @cfg {Boolean} footer + * @hide */ /** - * @cfg {String} msgFx @hide + * @cfg {Boolean} frame + * @hide */ /** - * @cfg {String} validateOnBlur @hide + * @cfg {Boolean} header + * @hide */ /** - * @cfg {Boolean} allowDomMove @hide + * @cfg {Boolean} headerAsText + * @hide */ /** - * @cfg {String} applyTo @hide + * @cfg {Boolean} hideCollapseTool + * @hide */ /** - * @cfg {String} autoHeight @hide + * @cfg {String} iconCls + * @hide */ /** - * @cfg {String} autoWidth @hide + * @cfg {Boolean/String} shadow + * @hide */ /** - * @cfg {String} cls @hide + * @cfg {Number} shadowOffset + * @hide */ /** - * @cfg {String} disabled @hide + * @cfg {Boolean} shim + * @hide */ /** - * @cfg {String} disabledClass @hide + * @cfg {Object/Array} tbar + * @hide */ /** - * @cfg {String} msgTarget @hide + * @cfg {Array} tools + * @hide */ /** - * @cfg {String} readOnly @hide + * @cfg {Ext.Template/Ext.XTemplate} toolTemplate + * @hide */ /** - * @cfg {String} style @hide + * @cfg {String} xtype + * @hide */ /** - * @cfg {String} validationDelay @hide + * @property header + * @hide */ /** - * @cfg {String} validationEvent @hide + * @property footer + * @hide */ /** - * @cfg {String} tabIndex @hide + * @method focus + * @hide */ /** - * @property disabled + * @method getBottomToolbar * @hide */ /** - * @method applyToMarkup + * @method getTopToolbar * @hide */ /** - * @method disable + * @method setIconClass * @hide */ /** - * @method enable + * @event activate * @hide */ /** - * @method validate + * @event beforeclose * @hide */ /** - * @event valid + * @event bodyresize * @hide */ /** - * @method setDisabled + * @event close * @hide */ /** - * @cfg keys + * @event deactivate * @hide */ }); +Ext.reg('fieldset', Ext.form.FieldSet); +/** + * @class Ext.form.HtmlEditor + * @extends Ext.form.Field + * Provides a lightweight HTML Editor component. Some toolbar features are not supported by Safari and will be + * automatically hidden when needed. These are noted in the config options where appropriate. + *

    The editor's toolbar buttons have tooltips defined in the {@link #buttonTips} property, but they are not + * enabled by default unless the global {@link Ext.QuickTips} singleton is {@link Ext.QuickTips#init initialized}. + *

    Note: The focus/blur and validation marking functionality inherited from Ext.form.Field is NOT + * supported by this editor. + *

    An Editor is a sensitive component that can't be used in all spots standard fields can be used. Putting an Editor within + * any element that has display set to 'none' can cause problems in Safari and Firefox due to their default iframe reloading bugs. + *

    Example usage: + *
    
    +// Simple example rendered with default options:
    +Ext.QuickTips.init();  // enable tooltips
    +new Ext.form.HtmlEditor({
    +    renderTo: Ext.getBody(),
    +    width: 800,
    +    height: 300
    +});
    +
    +// Passed via xtype into a container and with custom options:
    +Ext.QuickTips.init();  // enable tooltips
    +new Ext.Panel({
    +    title: 'HTML Editor',
    +    renderTo: Ext.getBody(),
    +    width: 600,
    +    height: 300,
    +    frame: true,
    +    layout: 'fit',
    +    items: {
    +        xtype: 'htmleditor',
    +        enableColors: false,
    +        enableAlignments: false
    +    }
    +});
    +
    + * @constructor + * Create a new HtmlEditor + * @param {Object} config + * @xtype htmleditor + */ + +Ext.form.HtmlEditor = Ext.extend(Ext.form.Field, { + /** + * @cfg {Boolean} enableFormat Enable the bold, italic and underline buttons (defaults to true) + */ + enableFormat : true, + /** + * @cfg {Boolean} enableFontSize Enable the increase/decrease font size buttons (defaults to true) + */ + enableFontSize : true, + /** + * @cfg {Boolean} enableColors Enable the fore/highlight color buttons (defaults to true) + */ + enableColors : true, + /** + * @cfg {Boolean} enableAlignments Enable the left, center, right alignment buttons (defaults to true) + */ + enableAlignments : true, + /** + * @cfg {Boolean} enableLists Enable the bullet and numbered list buttons. Not available in Safari. (defaults to true) + */ + enableLists : true, + /** + * @cfg {Boolean} enableSourceEdit Enable the switch to source edit button. Not available in Safari. (defaults to true) + */ + enableSourceEdit : true, + /** + * @cfg {Boolean} enableLinks Enable the create link button. Not available in Safari. (defaults to true) + */ + enableLinks : true, + /** + * @cfg {Boolean} enableFont Enable font selection. Not available in Safari. (defaults to true) + */ + enableFont : true, + /** + * @cfg {String} createLinkText The default text for the create link prompt + */ + createLinkText : 'Please enter the URL for the link:', + /** + * @cfg {String} defaultLinkValue The default value for the create link prompt (defaults to http:/ /) + */ + defaultLinkValue : 'http:/'+'/', + /** + * @cfg {Array} fontFamilies An array of available font families + */ + fontFamilies : [ + 'Arial', + 'Courier New', + 'Tahoma', + 'Times New Roman', + 'Verdana' + ], + defaultFont: 'tahoma', + /** + * @cfg {String} defaultValue A default value to be put into the editor to resolve focus issues (defaults to   (Non-breaking space) in Opera and IE6, ​ (Zero-width space) in all other browsers). + */ + defaultValue: (Ext.isOpera || Ext.isIE6) ? ' ' : '​', + + // private properties + actionMode: 'wrap', + validationEvent : false, + deferHeight: true, + initialized : false, + activated : false, + sourceEditMode : false, + onFocus : Ext.emptyFn, + iframePad:3, + hideMode:'offsets', + defaultAutoCreate : { + tag: "textarea", + style:"width:500px;height:300px;", + autocomplete: "off" + }, + + // private + initComponent : function(){ + this.addEvents( + /** + * @event initialize + * Fires when the editor is fully initialized (including the iframe) + * @param {HtmlEditor} this + */ + 'initialize', + /** + * @event activate + * Fires when the editor is first receives the focus. Any insertion must wait + * until after this event. + * @param {HtmlEditor} this + */ + 'activate', + /** + * @event beforesync + * Fires before the textarea is updated with content from the editor iframe. Return false + * to cancel the sync. + * @param {HtmlEditor} this + * @param {String} html + */ + 'beforesync', + /** + * @event beforepush + * Fires before the iframe editor is updated with content from the textarea. Return false + * to cancel the push. + * @param {HtmlEditor} this + * @param {String} html + */ + 'beforepush', + /** + * @event sync + * Fires when the textarea is updated with content from the editor iframe. + * @param {HtmlEditor} this + * @param {String} html + */ + 'sync', + /** + * @event push + * Fires when the iframe editor is updated with content from the textarea. + * @param {HtmlEditor} this + * @param {String} html + */ + 'push', + /** + * @event editmodechange + * Fires when the editor switches edit modes + * @param {HtmlEditor} this + * @param {Boolean} sourceEdit True if source edit, false if standard editing. + */ + 'editmodechange' + ) + }, + + // private + createFontOptions : function(){ + var buf = [], fs = this.fontFamilies, ff, lc; + for(var i = 0, len = fs.length; i< len; i++){ + ff = fs[i]; + lc = ff.toLowerCase(); + buf.push( + '' + ); + } + return buf.join(''); + }, + + /* + * Protected method that will not generally be called directly. It + * is called when the editor creates its toolbar. Override this method if you need to + * add custom toolbar buttons. + * @param {HtmlEditor} editor + */ + createToolbar : function(editor){ + var items = []; + var tipsEnabled = Ext.QuickTips && Ext.QuickTips.isEnabled(); + + + function btn(id, toggle, handler){ + return { + itemId : id, + cls : 'x-btn-icon', + iconCls: 'x-edit-'+id, + enableToggle:toggle !== false, + scope: editor, + handler:handler||editor.relayBtnCmd, + clickEvent:'mousedown', + tooltip: tipsEnabled ? editor.buttonTips[id] || undefined : undefined, + overflowText: editor.buttonTips[id].title || undefined, + tabIndex:-1 + }; + } + + + if(this.enableFont && !Ext.isSafari2){ + var fontSelectItem = new Ext.Toolbar.Item({ + autoEl: { + tag:'select', + cls:'x-font-select', + html: this.createFontOptions() + } + }); + + items.push( + fontSelectItem, + '-' + ); + } + + if(this.enableFormat){ + items.push( + btn('bold'), + btn('italic'), + btn('underline') + ); + } + + if(this.enableFontSize){ + items.push( + '-', + btn('increasefontsize', false, this.adjustFont), + btn('decreasefontsize', false, this.adjustFont) + ); + } + + if(this.enableColors){ + items.push( + '-', { + itemId:'forecolor', + cls:'x-btn-icon', + iconCls: 'x-edit-forecolor', + clickEvent:'mousedown', + tooltip: tipsEnabled ? editor.buttonTips.forecolor || undefined : undefined, + tabIndex:-1, + menu : new Ext.menu.ColorMenu({ + allowReselect: true, + focus: Ext.emptyFn, + value:'000000', + plain:true, + listeners: { + scope: this, + select: function(cp, color){ + this.execCmd('forecolor', Ext.isWebKit || Ext.isIE ? '#'+color : color); + this.deferFocus(); + } + }, + clickEvent:'mousedown' + }) + }, { + itemId:'backcolor', + cls:'x-btn-icon', + iconCls: 'x-edit-backcolor', + clickEvent:'mousedown', + tooltip: tipsEnabled ? editor.buttonTips.backcolor || undefined : undefined, + tabIndex:-1, + menu : new Ext.menu.ColorMenu({ + focus: Ext.emptyFn, + value:'FFFFFF', + plain:true, + allowReselect: true, + listeners: { + scope: this, + select: function(cp, color){ + if(Ext.isGecko){ + this.execCmd('useCSS', false); + this.execCmd('hilitecolor', color); + this.execCmd('useCSS', true); + this.deferFocus(); + }else{ + this.execCmd(Ext.isOpera ? 'hilitecolor' : 'backcolor', Ext.isWebKit || Ext.isIE ? '#'+color : color); + this.deferFocus(); + } + } + }, + clickEvent:'mousedown' + }) + } + ); + } + + if(this.enableAlignments){ + items.push( + '-', + btn('justifyleft'), + btn('justifycenter'), + btn('justifyright') + ); + } + + if(!Ext.isSafari2){ + if(this.enableLinks){ + items.push( + '-', + btn('createlink', false, this.createLink) + ); + } + + if(this.enableLists){ + items.push( + '-', + btn('insertorderedlist'), + btn('insertunorderedlist') + ); + } + if(this.enableSourceEdit){ + items.push( + '-', + btn('sourceedit', true, function(btn){ + this.toggleSourceEdit(!this.sourceEditMode); + }) + ); + } + } + + // build the toolbar + var tb = new Ext.Toolbar({ + renderTo: this.wrap.dom.firstChild, + items: items + }); + + if (fontSelectItem) { + this.fontSelect = fontSelectItem.el; + + this.mon(this.fontSelect, 'change', function(){ + var font = this.fontSelect.dom.value; + this.relayCmd('fontname', font); + this.deferFocus(); + }, this); + } + + + // stop form submits + this.mon(tb.el, 'click', function(e){ + e.preventDefault(); + }); + + this.tb = tb; + }, + + onDisable: function(){ + this.wrap.mask(); + Ext.form.HtmlEditor.superclass.onDisable.call(this); + }, + + onEnable: function(){ + this.wrap.unmask(); + Ext.form.HtmlEditor.superclass.onEnable.call(this); + }, + + setReadOnly: function(readOnly){ + + Ext.form.HtmlEditor.superclass.setReadOnly.call(this, readOnly); + if(this.initialized){ + this.setDesignMode(!readOnly); + var bd = this.getEditorBody(); + if(bd){ + bd.style.cursor = this.readOnly ? 'default' : 'text'; + } + this.disableItems(readOnly); + } + }, + + /** + * Protected method that will not generally be called directly. It + * is called when the editor initializes the iframe with HTML contents. Override this method if you + * want to change the initialization markup of the iframe (e.g. to add stylesheets). + * + * Note: IE8-Standards has unwanted scroller behavior, so the default meta tag forces IE7 compatibility + */ + getDocMarkup : function(){ + return ''; + }, + + // private + getEditorBody : function(){ + var doc = this.getDoc(); + return doc.body || doc.documentElement; + }, + + // private + getDoc : function(){ + return Ext.isIE ? this.getWin().document : (this.iframe.contentDocument || this.getWin().document); + }, + + // private + getWin : function(){ + return Ext.isIE ? this.iframe.contentWindow : window.frames[this.iframe.name]; + }, + + // private + onRender : function(ct, position){ + Ext.form.HtmlEditor.superclass.onRender.call(this, ct, position); + this.el.dom.style.border = '0 none'; + this.el.dom.setAttribute('tabIndex', -1); + this.el.addClass('x-hidden'); + if(Ext.isIE){ // fix IE 1px bogus margin + this.el.applyStyles('margin-top:-1px;margin-bottom:-1px;') + } + this.wrap = this.el.wrap({ + cls:'x-html-editor-wrap', cn:{cls:'x-html-editor-tb'} + }); + + this.createToolbar(this); + + this.disableItems(true); + + this.tb.doLayout(); + + this.createIFrame(); + + if(!this.width){ + var sz = this.el.getSize(); + this.setSize(sz.width, this.height || sz.height); + } + this.resizeEl = this.positionEl = this.wrap; + }, + + createIFrame: function(){ + var iframe = document.createElement('iframe'); + iframe.name = Ext.id(); + iframe.frameBorder = '0'; + iframe.style.overflow = 'auto'; + + this.wrap.dom.appendChild(iframe); + this.iframe = iframe; + + this.monitorTask = Ext.TaskMgr.start({ + run: this.checkDesignMode, + scope: this, + interval:100 + }); + }, + + initFrame : function(){ + Ext.TaskMgr.stop(this.monitorTask); + var doc = this.getDoc(); + this.win = this.getWin(); + + doc.open(); + doc.write(this.getDocMarkup()); + doc.close(); + + var task = { // must defer to wait for browser to be ready + run : function(){ + var doc = this.getDoc(); + if(doc.body || doc.readyState == 'complete'){ + Ext.TaskMgr.stop(task); + this.setDesignMode(true); + this.initEditor.defer(10, this); + } + }, + interval : 10, + duration:10000, + scope: this + }; + Ext.TaskMgr.start(task); + }, + + + checkDesignMode : function(){ + if(this.wrap && this.wrap.dom.offsetWidth){ + var doc = this.getDoc(); + if(!doc){ + return; + } + if(!doc.editorInitialized || this.getDesignMode() != 'on'){ + this.initFrame(); + } + } + }, + + /* private + * set current design mode. To enable, mode can be true or 'on', off otherwise + */ + setDesignMode : function(mode){ + var doc ; + if(doc = this.getDoc()){ + if(this.readOnly){ + mode = false; + } + doc.designMode = (/on|true/i).test(String(mode).toLowerCase()) ?'on':'off'; + } + + }, + + // private + getDesignMode : function(){ + var doc = this.getDoc(); + if(!doc){ return ''; } + return String(doc.designMode).toLowerCase(); + + }, + + disableItems: function(disabled){ + if(this.fontSelect){ + this.fontSelect.dom.disabled = disabled; + } + this.tb.items.each(function(item){ + if(item.getItemId() != 'sourceedit'){ + item.setDisabled(disabled); + } + }); + }, + + // private + onResize : function(w, h){ + Ext.form.HtmlEditor.superclass.onResize.apply(this, arguments); + if(this.el && this.iframe){ + if(Ext.isNumber(w)){ + var aw = w - this.wrap.getFrameWidth('lr'); + this.el.setWidth(aw); + this.tb.setWidth(aw); + this.iframe.style.width = Math.max(aw, 0) + 'px'; + } + if(Ext.isNumber(h)){ + var ah = h - this.wrap.getFrameWidth('tb') - this.tb.el.getHeight(); + this.el.setHeight(ah); + this.iframe.style.height = Math.max(ah, 0) + 'px'; + var bd = this.getEditorBody(); + if(bd){ + bd.style.height = Math.max((ah - (this.iframePad*2)), 0) + 'px'; + } + } + } + }, + + /** + * Toggles the editor between standard and source edit mode. + * @param {Boolean} sourceEdit (optional) True for source edit, false for standard + */ + toggleSourceEdit : function(sourceEditMode){ + if(sourceEditMode === undefined){ + sourceEditMode = !this.sourceEditMode; + } + this.sourceEditMode = sourceEditMode === true; + var btn = this.tb.getComponent('sourceedit'); + + if(btn.pressed !== this.sourceEditMode){ + btn.toggle(this.sourceEditMode); + if(!btn.xtbHidden){ + return; + } + } + if(this.sourceEditMode){ + this.disableItems(true); + this.syncValue(); + this.iframe.className = 'x-hidden'; + this.el.removeClass('x-hidden'); + this.el.dom.removeAttribute('tabIndex'); + this.el.focus(); + }else{ + if(this.initialized){ + this.disableItems(this.readOnly); + } + this.pushValue(); + this.iframe.className = ''; + this.el.addClass('x-hidden'); + this.el.dom.setAttribute('tabIndex', -1); + this.deferFocus(); + } + var lastSize = this.lastSize; + if(lastSize){ + delete this.lastSize; + this.setSize(lastSize); + } + this.fireEvent('editmodechange', this, this.sourceEditMode); + }, + + // private used internally + createLink : function(){ + var url = prompt(this.createLinkText, this.defaultLinkValue); + if(url && url != 'http:/'+'/'){ + this.relayCmd('createlink', url); + } + }, + + // private + initEvents : function(){ + this.originalValue = this.getValue(); + }, + + /** + * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide + * @method + */ + markInvalid : Ext.emptyFn, + + /** + * Overridden and disabled. The editor element does not support standard valid/invalid marking. @hide + * @method + */ + clearInvalid : Ext.emptyFn, + + // docs inherit from Field + setValue : function(v){ + Ext.form.HtmlEditor.superclass.setValue.call(this, v); + this.pushValue(); + return this; + }, + + /** + * Protected method that will not generally be called directly. If you need/want + * custom HTML cleanup, this is the method you should override. + * @param {String} html The HTML to be cleaned + * @return {String} The cleaned HTML + */ + cleanHtml: function(html) { + html = String(html); + if(Ext.isWebKit){ // strip safari nonsense + html = html.replace(/\sclass="(?:Apple-style-span|khtml-block-placeholder)"/gi, ''); + } + + /* + * Neat little hack. Strips out all the non-digit characters from the default + * value and compares it to the character code of the first character in the string + * because it can cause encoding issues when posted to the server. + */ + if(html.charCodeAt(0) == this.defaultValue.replace(/\D/g, '')){ + html = html.substring(1); + } + return html; + }, + + /** + * Protected method that will not generally be called directly. Syncs the contents + * of the editor iframe with the textarea. + */ + syncValue : function(){ + if(this.initialized){ + var bd = this.getEditorBody(); + var html = bd.innerHTML; + if(Ext.isWebKit){ + var bs = bd.getAttribute('style'); // Safari puts text-align styles on the body element! + var m = bs.match(/text-align:(.*?);/i); + if(m && m[1]){ + html = '
    ' + html + '
    '; + } + } + html = this.cleanHtml(html); + if(this.fireEvent('beforesync', this, html) !== false){ + this.el.dom.value = html; + this.fireEvent('sync', this, html); + } + } + }, + + //docs inherit from Field + getValue : function() { + this[this.sourceEditMode ? 'pushValue' : 'syncValue'](); + return Ext.form.HtmlEditor.superclass.getValue.call(this); + }, + + /** + * Protected method that will not generally be called directly. Pushes the value of the textarea + * into the iframe editor. + */ + pushValue : function(){ + if(this.initialized){ + var v = this.el.dom.value; + if(!this.activated && v.length < 1){ + v = this.defaultValue; + } + if(this.fireEvent('beforepush', this, v) !== false){ + this.getEditorBody().innerHTML = v; + if(Ext.isGecko){ + // Gecko hack, see: https://bugzilla.mozilla.org/show_bug.cgi?id=232791#c8 + this.setDesignMode(false); //toggle off first + + } + this.setDesignMode(true); + this.fireEvent('push', this, v); + } + + } + }, + + // private + deferFocus : function(){ + this.focus.defer(10, this); + }, + + // docs inherit from Field + focus : function(){ + if(this.win && !this.sourceEditMode){ + this.win.focus(); + }else{ + this.el.focus(); + } + }, + + // private + initEditor : function(){ + //Destroying the component during/before initEditor can cause issues. + try{ + var dbody = this.getEditorBody(), + ss = this.el.getStyles('font-size', 'font-family', 'background-image', 'background-repeat'), + doc, + fn; + + ss['background-attachment'] = 'fixed'; // w3c + dbody.bgProperties = 'fixed'; // ie + + Ext.DomHelper.applyStyles(dbody, ss); + + doc = this.getDoc(); + + if(doc){ + try{ + Ext.EventManager.removeAll(doc); + }catch(e){} + } + + /* + * We need to use createDelegate here, because when using buffer, the delayed task is added + * as a property to the function. When the listener is removed, the task is deleted from the function. + * Since onEditorEvent is shared on the prototype, if we have multiple html editors, the first time one of the editors + * is destroyed, it causes the fn to be deleted from the prototype, which causes errors. Essentially, we're just anonymizing the function. + */ + fn = this.onEditorEvent.createDelegate(this); + Ext.EventManager.on(doc, { + mousedown: fn, + dblclick: fn, + click: fn, + keyup: fn, + buffer:100 + }); + + if(Ext.isGecko){ + Ext.EventManager.on(doc, 'keypress', this.applyCommand, this); + } + if(Ext.isIE || Ext.isWebKit || Ext.isOpera){ + Ext.EventManager.on(doc, 'keydown', this.fixKeys, this); + } + doc.editorInitialized = true; + this.initialized = true; + this.pushValue(); + this.setReadOnly(this.readOnly); + this.fireEvent('initialize', this); + }catch(e){} + }, + + // private + onDestroy : function(){ + if(this.monitorTask){ + Ext.TaskMgr.stop(this.monitorTask); + } + if(this.rendered){ + Ext.destroy(this.tb); + var doc = this.getDoc(); + if(doc){ + try{ + Ext.EventManager.removeAll(doc); + for (var prop in doc){ + delete doc[prop]; + } + }catch(e){} + } + if(this.wrap){ + this.wrap.dom.innerHTML = ''; + this.wrap.remove(); + } + } + + if(this.el){ + this.el.removeAllListeners(); + this.el.remove(); + } + this.purgeListeners(); + }, + + // private + onFirstFocus : function(){ + this.activated = true; + this.disableItems(this.readOnly); + if(Ext.isGecko){ // prevent silly gecko errors + this.win.focus(); + var s = this.win.getSelection(); + if(!s.focusNode || s.focusNode.nodeType != 3){ + var r = s.getRangeAt(0); + r.selectNodeContents(this.getEditorBody()); + r.collapse(true); + this.deferFocus(); + } + try{ + this.execCmd('useCSS', true); + this.execCmd('styleWithCSS', false); + }catch(e){} + } + this.fireEvent('activate', this); + }, + + // private + adjustFont: function(btn){ + var adjust = btn.getItemId() == 'increasefontsize' ? 1 : -1, + doc = this.getDoc(), + v = parseInt(doc.queryCommandValue('FontSize') || 2, 10); + if((Ext.isSafari && !Ext.isSafari2) || Ext.isChrome || Ext.isAir){ + // Safari 3 values + // 1 = 10px, 2 = 13px, 3 = 16px, 4 = 18px, 5 = 24px, 6 = 32px + if(v <= 10){ + v = 1 + adjust; + }else if(v <= 13){ + v = 2 + adjust; + }else if(v <= 16){ + v = 3 + adjust; + }else if(v <= 18){ + v = 4 + adjust; + }else if(v <= 24){ + v = 5 + adjust; + }else { + v = 6 + adjust; + } + v = v.constrain(1, 6); + }else{ + if(Ext.isSafari){ // safari + adjust *= 2; + } + v = Math.max(1, v+adjust) + (Ext.isSafari ? 'px' : 0); + } + this.execCmd('FontSize', v); + }, + + // private + onEditorEvent : function(e){ + this.updateToolbar(); + }, + + + /** + * Protected method that will not generally be called directly. It triggers + * a toolbar update by reading the markup state of the current selection in the editor. + */ + updateToolbar: function(){ + + if(this.readOnly){ + return; + } + + if(!this.activated){ + this.onFirstFocus(); + return; + } + + var btns = this.tb.items.map, + doc = this.getDoc(); + + if(this.enableFont && !Ext.isSafari2){ + var name = (doc.queryCommandValue('FontName')||this.defaultFont).toLowerCase(); + if(name != this.fontSelect.dom.value){ + this.fontSelect.dom.value = name; + } + } + if(this.enableFormat){ + btns.bold.toggle(doc.queryCommandState('bold')); + btns.italic.toggle(doc.queryCommandState('italic')); + btns.underline.toggle(doc.queryCommandState('underline')); + } + if(this.enableAlignments){ + btns.justifyleft.toggle(doc.queryCommandState('justifyleft')); + btns.justifycenter.toggle(doc.queryCommandState('justifycenter')); + btns.justifyright.toggle(doc.queryCommandState('justifyright')); + } + if(!Ext.isSafari2 && this.enableLists){ + btns.insertorderedlist.toggle(doc.queryCommandState('insertorderedlist')); + btns.insertunorderedlist.toggle(doc.queryCommandState('insertunorderedlist')); + } + + Ext.menu.MenuMgr.hideAll(); + + this.syncValue(); + }, + + // private + relayBtnCmd : function(btn){ + this.relayCmd(btn.getItemId()); + }, + + /** + * Executes a Midas editor command on the editor document and performs necessary focus and + * toolbar updates. This should only be called after the editor is initialized. + * @param {String} cmd The Midas command + * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null) + */ + relayCmd : function(cmd, value){ + (function(){ + this.focus(); + this.execCmd(cmd, value); + this.updateToolbar(); + }).defer(10, this); + }, + + /** + * Executes a Midas editor command directly on the editor document. + * For visual commands, you should use {@link #relayCmd} instead. + * This should only be called after the editor is initialized. + * @param {String} cmd The Midas command + * @param {String/Boolean} value (optional) The value to pass to the command (defaults to null) + */ + execCmd : function(cmd, value){ + var doc = this.getDoc(); + doc.execCommand(cmd, false, value === undefined ? null : value); + this.syncValue(); + }, + + // private + applyCommand : function(e){ + if(e.ctrlKey){ + var c = e.getCharCode(), cmd; + if(c > 0){ + c = String.fromCharCode(c); + switch(c){ + case 'b': + cmd = 'bold'; + break; + case 'i': + cmd = 'italic'; + break; + case 'u': + cmd = 'underline'; + break; + } + if(cmd){ + this.win.focus(); + this.execCmd(cmd); + this.deferFocus(); + e.preventDefault(); + } + } + } + }, + + /** + * Inserts the passed text at the current cursor position. Note: the editor must be initialized and activated + * to insert text. + * @param {String} text + */ + insertAtCursor : function(text){ + if(!this.activated){ + return; + } + if(Ext.isIE){ + this.win.focus(); + var doc = this.getDoc(), + r = doc.selection.createRange(); + if(r){ + r.pasteHTML(text); + this.syncValue(); + this.deferFocus(); + } + }else{ + this.win.focus(); + this.execCmd('InsertHTML', text); + this.deferFocus(); + } + }, + + // private + fixKeys : function(){ // load time branching for fastest keydown performance + if(Ext.isIE){ + return function(e){ + var k = e.getKey(), + doc = this.getDoc(), + r; + if(k == e.TAB){ + e.stopEvent(); + r = doc.selection.createRange(); + if(r){ + r.collapse(true); + r.pasteHTML('    '); + this.deferFocus(); + } + }else if(k == e.ENTER){ + r = doc.selection.createRange(); + if(r){ + var target = r.parentElement(); + if(!target || target.tagName.toLowerCase() != 'li'){ + e.stopEvent(); + r.pasteHTML('
    '); + r.collapse(false); + r.select(); + } + } + } + }; + }else if(Ext.isOpera){ + return function(e){ + var k = e.getKey(); + if(k == e.TAB){ + e.stopEvent(); + this.win.focus(); + this.execCmd('InsertHTML','    '); + this.deferFocus(); + } + }; + }else if(Ext.isWebKit){ + return function(e){ + var k = e.getKey(); + if(k == e.TAB){ + e.stopEvent(); + this.execCmd('InsertText','\t'); + this.deferFocus(); + }else if(k == e.ENTER){ + e.stopEvent(); + this.execCmd('InsertHtml','

    '); + this.deferFocus(); + } + }; + } + }(), + + /** + * Returns the editor's toolbar. This is only available after the editor has been rendered. + * @return {Ext.Toolbar} + */ + getToolbar : function(){ + return this.tb; + }, + + /** + * Object collection of toolbar tooltips for the buttons in the editor. The key + * is the command id associated with that button and the value is a valid QuickTips object. + * For example: +
    
    +{
    +    bold : {
    +        title: 'Bold (Ctrl+B)',
    +        text: 'Make the selected text bold.',
    +        cls: 'x-html-editor-tip'
    +    },
    +    italic : {
    +        title: 'Italic (Ctrl+I)',
    +        text: 'Make the selected text italic.',
    +        cls: 'x-html-editor-tip'
    +    },
    +    ...
    +
    + * @type Object + */ + buttonTips : { + bold : { + title: 'Bold (Ctrl+B)', + text: 'Make the selected text bold.', + cls: 'x-html-editor-tip' + }, + italic : { + title: 'Italic (Ctrl+I)', + text: 'Make the selected text italic.', + cls: 'x-html-editor-tip' + }, + underline : { + title: 'Underline (Ctrl+U)', + text: 'Underline the selected text.', + cls: 'x-html-editor-tip' + }, + increasefontsize : { + title: 'Grow Text', + text: 'Increase the font size.', + cls: 'x-html-editor-tip' + }, + decreasefontsize : { + title: 'Shrink Text', + text: 'Decrease the font size.', + cls: 'x-html-editor-tip' + }, + backcolor : { + title: 'Text Highlight Color', + text: 'Change the background color of the selected text.', + cls: 'x-html-editor-tip' + }, + forecolor : { + title: 'Font Color', + text: 'Change the color of the selected text.', + cls: 'x-html-editor-tip' + }, + justifyleft : { + title: 'Align Text Left', + text: 'Align text to the left.', + cls: 'x-html-editor-tip' + }, + justifycenter : { + title: 'Center Text', + text: 'Center text in the editor.', + cls: 'x-html-editor-tip' + }, + justifyright : { + title: 'Align Text Right', + text: 'Align text to the right.', + cls: 'x-html-editor-tip' + }, + insertunorderedlist : { + title: 'Bullet List', + text: 'Start a bulleted list.', + cls: 'x-html-editor-tip' + }, + insertorderedlist : { + title: 'Numbered List', + text: 'Start a numbered list.', + cls: 'x-html-editor-tip' + }, + createlink : { + title: 'Hyperlink', + text: 'Make the selected text a hyperlink.', + cls: 'x-html-editor-tip' + }, + sourceedit : { + title: 'Source Edit', + text: 'Switch to source editing mode.', + cls: 'x-html-editor-tip' + } + } + + // hide stuff that is not compatible + /** + * @event blur + * @hide + */ + /** + * @event change + * @hide + */ + /** + * @event focus + * @hide + */ + /** + * @event specialkey + * @hide + */ + /** + * @cfg {String} fieldClass @hide + */ + /** + * @cfg {String} focusClass @hide + */ + /** + * @cfg {String} autoCreate @hide + */ + /** + * @cfg {String} inputType @hide + */ + /** + * @cfg {String} invalidClass @hide + */ + /** + * @cfg {String} invalidText @hide + */ + /** + * @cfg {String} msgFx @hide + */ + /** + * @cfg {String} validateOnBlur @hide + */ + /** + * @cfg {Boolean} allowDomMove @hide + */ + /** + * @cfg {String} applyTo @hide + */ + /** + * @cfg {String} autoHeight @hide + */ + /** + * @cfg {String} autoWidth @hide + */ + /** + * @cfg {String} cls @hide + */ + /** + * @cfg {String} disabled @hide + */ + /** + * @cfg {String} disabledClass @hide + */ + /** + * @cfg {String} msgTarget @hide + */ + /** + * @cfg {String} readOnly @hide + */ + /** + * @cfg {String} style @hide + */ + /** + * @cfg {String} validationDelay @hide + */ + /** + * @cfg {String} validationEvent @hide + */ + /** + * @cfg {String} tabIndex @hide + */ + /** + * @property disabled + * @hide + */ + /** + * @method applyToMarkup + * @hide + */ + /** + * @method disable + * @hide + */ + /** + * @method enable + * @hide + */ + /** + * @method validate + * @hide + */ + /** + * @event valid + * @hide + */ + /** + * @method setDisabled + * @hide + */ + /** + * @cfg keys + * @hide + */ +}); Ext.reg('htmleditor', Ext.form.HtmlEditor);/** * @class Ext.form.TimeField * @extends Ext.form.ComboBox @@ -60086,15 +62718,15 @@ Ext.form.TimeField = Ext.extend(Ext.form.ComboBox, { /** * @cfg {Date/String} minValue * The minimum allowed time. Can be either a Javascript date object with a valid time value or a string - * time in a valid format -- see {@link #format} and {@link #altFormats} (defaults to null). + * time in a valid format -- see {@link #format} and {@link #altFormats} (defaults to undefined). */ - minValue : null, + minValue : undefined, /** * @cfg {Date/String} maxValue * The maximum allowed time. Can be either a Javascript date object with a valid time value or a string - * time in a valid format -- see {@link #format} and {@link #altFormats} (defaults to null). + * time in a valid format -- see {@link #format} and {@link #altFormats} (defaults to undefined). */ - maxValue : null, + maxValue : undefined, /** * @cfg {String} minText * The error text to display when the date in the cell is before minValue (defaults to @@ -60146,26 +62778,67 @@ Ext.form.TimeField = Ext.extend(Ext.form.ComboBox, { // private initComponent : function(){ - if(typeof this.minValue == "string"){ - this.minValue = this.parseDate(this.minValue); + if(Ext.isDefined(this.minValue)){ + this.setMinValue(this.minValue, true); } - if(typeof this.maxValue == "string"){ - this.maxValue = this.parseDate(this.maxValue); + if(Ext.isDefined(this.maxValue)){ + this.setMaxValue(this.maxValue, true); } - if(!this.store){ - var min = this.parseDate(this.minValue) || new Date(this.initDate).clearTime(); - var max = this.parseDate(this.maxValue) || new Date(this.initDate).clearTime().add('mi', (24 * 60) - 1); - var times = []; - while(min <= max){ - times.push(min.dateFormat(this.format)); - min = min.add('mi', this.increment); - } - this.store = times; + this.generateStore(true); } Ext.form.TimeField.superclass.initComponent.call(this); }, + + /** + * Replaces any existing {@link #minValue} with the new time and refreshes the store. + * @param {Date/String} value The minimum time that can be selected + */ + setMinValue: function(value, /* private */ initial){ + this.setLimit(value, true, initial); + return this; + }, + /** + * Replaces any existing {@link #maxValue} with the new time and refreshes the store. + * @param {Date/String} value The maximum time that can be selected + */ + setMaxValue: function(value, /* private */ initial){ + this.setLimit(value, false, initial); + return this; + }, + + // private + generateStore: function(initial){ + var min = this.minValue || new Date(this.initDate).clearTime(), + max = this.maxValue || new Date(this.initDate).clearTime().add('mi', (24 * 60) - 1), + times = []; + + while(min <= max){ + times.push(min.dateFormat(this.format)); + min = min.add('mi', this.increment); + } + this.bindStore(times, initial); + }, + + // private + setLimit: function(value, isMin, initial){ + var d; + if(Ext.isString(value)){ + d = this.parseDate(value); + }else if(Ext.isDate(value)){ + d = value; + } + if(d){ + var val = new Date(this.initDate).clearTime(); + val.setHours(d.getHours(), d.getMinutes(), isMin ? 0 : 59, 0); + this[isMin ? 'minValue' : 'maxValue'] = val; + if(!initial){ + this.generateStore(); + } + } + }, + // inherited docs getValue : function(){ var v = Ext.form.TimeField.superclass.getValue.call(this); @@ -60242,771 +62915,771 @@ Ext.form.Label = Ext.extend(Ext.BoxComponent, { } Ext.form.Label.superclass.onRender.call(this, ct, position); }, - - /** - * Updates the label's innerHTML with the specified string. - * @param {String} text The new label text - * @param {Boolean} encode (optional) False to skip HTML-encoding the text when rendering it - * to the label (defaults to true which encodes the value). This might be useful if you want to include - * tags in the label's innerHTML rather than rendering them as string literals per the default logic. - * @return {Label} this - */ - setText : function(t, encode){ - var e = encode === false; - this[!e ? 'text' : 'html'] = t; - delete this[e ? 'text' : 'html']; - if(this.rendered){ - this.el.dom.innerHTML = encode !== false ? Ext.util.Format.htmlEncode(t) : t; + + /** + * Updates the label's innerHTML with the specified string. + * @param {String} text The new label text + * @param {Boolean} encode (optional) False to skip HTML-encoding the text when rendering it + * to the label (defaults to true which encodes the value). This might be useful if you want to include + * tags in the label's innerHTML rather than rendering them as string literals per the default logic. + * @return {Label} this + */ + setText : function(t, encode){ + var e = encode === false; + this[!e ? 'text' : 'html'] = t; + delete this[e ? 'text' : 'html']; + if(this.rendered){ + this.el.dom.innerHTML = encode !== false ? Ext.util.Format.htmlEncode(t) : t; + } + return this; + } +}); + +Ext.reg('label', Ext.form.Label);/** + * @class Ext.form.Action + *

    The subclasses of this class provide actions to perform upon {@link Ext.form.BasicForm Form}s.

    + *

    Instances of this class are only created by a {@link Ext.form.BasicForm Form} when + * the Form needs to perform an action such as submit or load. The Configuration options + * listed for this class are set through the Form's action methods: {@link Ext.form.BasicForm#submit submit}, + * {@link Ext.form.BasicForm#load load} and {@link Ext.form.BasicForm#doAction doAction}

    + *

    The instance of Action which performed the action is passed to the success + * and failure callbacks of the Form's action methods ({@link Ext.form.BasicForm#submit submit}, + * {@link Ext.form.BasicForm#load load} and {@link Ext.form.BasicForm#doAction doAction}), + * and to the {@link Ext.form.BasicForm#actioncomplete actioncomplete} and + * {@link Ext.form.BasicForm#actionfailed actionfailed} event handlers.

    + */ +Ext.form.Action = function(form, options){ + this.form = form; + this.options = options || {}; +}; + +/** + * Failure type returned when client side validation of the Form fails + * thus aborting a submit action. Client side validation is performed unless + * {@link #clientValidation} is explicitly set to false. + * @type {String} + * @static + */ +Ext.form.Action.CLIENT_INVALID = 'client'; +/** + *

    Failure type returned when server side processing fails and the {@link #result}'s + * success property is set to false.

    + *

    In the case of a form submission, field-specific error messages may be returned in the + * {@link #result}'s errors property.

    + * @type {String} + * @static + */ +Ext.form.Action.SERVER_INVALID = 'server'; +/** + * Failure type returned when a communication error happens when attempting + * to send a request to the remote server. The {@link #response} may be examined to + * provide further information. + * @type {String} + * @static + */ +Ext.form.Action.CONNECT_FAILURE = 'connect'; +/** + * Failure type returned when the response's success + * property is set to false, or no field values are returned in the response's + * data property. + * @type {String} + * @static + */ +Ext.form.Action.LOAD_FAILURE = 'load'; + +Ext.form.Action.prototype = { +/** + * @cfg {String} url The URL that the Action is to invoke. + */ +/** + * @cfg {Boolean} reset When set to true, causes the Form to be + * {@link Ext.form.BasicForm.reset reset} on Action success. If specified, this happens + * before the {@link #success} callback is called and before the Form's + * {@link Ext.form.BasicForm.actioncomplete actioncomplete} event fires. + */ +/** + * @cfg {String} method The HTTP method to use to access the requested URL. Defaults to the + * {@link Ext.form.BasicForm}'s method, or if that is not specified, the underlying DOM form's method. + */ +/** + * @cfg {Mixed} params

    Extra parameter values to pass. These are added to the Form's + * {@link Ext.form.BasicForm#baseParams} and passed to the specified URL along with the Form's + * input fields.

    + *

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

    + */ +/** + * @cfg {Number} timeout The number of seconds to wait for a server response before + * failing with the {@link #failureType} as {@link #Action.CONNECT_FAILURE}. If not specified, + * defaults to the configured {@link Ext.form.BasicForm#timeout timeout} of the + * {@link Ext.form.BasicForm form}. + */ +/** + * @cfg {Function} success The function to call when a valid success return packet is recieved. + * The function is passed the following parameters:
      + *
    • form : Ext.form.BasicForm
      The form that requested the action
    • + *
    • action : Ext.form.Action
      The Action class. The {@link #result} + * property of this object may be examined to perform custom postprocessing.
    • + *
    + */ +/** + * @cfg {Function} failure The function to call when a failure packet was recieved, or when an + * error ocurred in the Ajax communication. + * The function is passed the following parameters:
      + *
    • form : Ext.form.BasicForm
      The form that requested the action
    • + *
    • action : Ext.form.Action
      The Action class. If an Ajax + * error ocurred, the failure type will be in {@link #failureType}. The {@link #result} + * property of this object may be examined to perform custom postprocessing.
    • + *
    + */ +/** + * @cfg {Object} scope The scope in which to call the callback functions (The this reference + * for the callback functions). + */ +/** + * @cfg {String} waitMsg The message to be displayed by a call to {@link Ext.MessageBox#wait} + * during the time the action is being processed. + */ +/** + * @cfg {String} waitTitle The title to be displayed by a call to {@link Ext.MessageBox#wait} + * during the time the action is being processed. + */ + +/** + * The type of action this Action instance performs. + * Currently only "submit" and "load" are supported. + * @type {String} + */ + type : 'default', +/** + * The type of failure detected will be one of these: {@link #CLIENT_INVALID}, + * {@link #SERVER_INVALID}, {@link #CONNECT_FAILURE}, or {@link #LOAD_FAILURE}. Usage: + *
    
    +var fp = new Ext.form.FormPanel({
    +...
    +buttons: [{
    +    text: 'Save',
    +    formBind: true,
    +    handler: function(){
    +        if(fp.getForm().isValid()){
    +            fp.getForm().submit({
    +                url: 'form-submit.php',
    +                waitMsg: 'Submitting your data...',
    +                success: function(form, action){
    +                    // server responded with success = true
    +                    var result = action.{@link #result};
    +                },
    +                failure: function(form, action){
    +                    if (action.{@link #failureType} === Ext.form.Action.{@link #CONNECT_FAILURE}) {
    +                        Ext.Msg.alert('Error',
    +                            'Status:'+action.{@link #response}.status+': '+
    +                            action.{@link #response}.statusText);
    +                    }
    +                    if (action.failureType === Ext.form.Action.{@link #SERVER_INVALID}){
    +                        // server responded with success = false
    +                        Ext.Msg.alert('Invalid', action.{@link #result}.errormsg);
    +                    }
    +                }
    +            });
    +        }
    +    }
    +},{
    +    text: 'Reset',
    +    handler: function(){
    +        fp.getForm().reset();
    +    }
    +}]
    + * 
    + * @property failureType + * @type {String} + */ + /** + * The XMLHttpRequest object used to perform the action. + * @property response + * @type {Object} + */ + /** + * The decoded response object containing a boolean success property and + * other, action-specific properties. + * @property result + * @type {Object} + */ + + // interface method + run : function(options){ + + }, + + // interface method + success : function(response){ + + }, + + // interface method + handleResponse : function(response){ + + }, + + // default connection failure + failure : function(response){ + this.response = response; + this.failureType = Ext.form.Action.CONNECT_FAILURE; + this.form.afterAction(this, false); + }, + + // private + // shared code among all Actions to validate that there was a response + // with either responseText or responseXml + processResponse : function(response){ + this.response = response; + if(!response.responseText && !response.responseXML){ + return true; + } + this.result = this.handleResponse(response); + return this.result; + }, + + // utility functions used internally + getUrl : function(appendParams){ + var url = this.options.url || this.form.url || this.form.el.dom.action; + if(appendParams){ + var p = this.getParams(); + if(p){ + url = Ext.urlAppend(url, p); + } + } + return url; + }, + + // private + getMethod : function(){ + return (this.options.method || this.form.method || this.form.el.dom.method || 'POST').toUpperCase(); + }, + + // private + getParams : function(){ + var bp = this.form.baseParams; + var p = this.options.params; + if(p){ + if(typeof p == "object"){ + p = Ext.urlEncode(Ext.applyIf(p, bp)); + }else if(typeof p == 'string' && bp){ + p += '&' + Ext.urlEncode(bp); + } + }else if(bp){ + p = Ext.urlEncode(bp); + } + return p; + }, + + // private + createCallback : function(opts){ + var opts = opts || {}; + return { + success: this.success, + failure: this.failure, + scope: this, + timeout: (opts.timeout*1000) || (this.form.timeout*1000), + upload: this.form.fileUpload ? this.success : undefined + }; + } +}; + +/** + * @class Ext.form.Action.Submit + * @extends Ext.form.Action + *

    A class which handles submission of data from {@link Ext.form.BasicForm Form}s + * and processes the returned response.

    + *

    Instances of this class are only created by a {@link Ext.form.BasicForm Form} when + * {@link Ext.form.BasicForm#submit submit}ting.

    + *

    Response Packet Criteria

    + *

    A response packet may contain: + *

      + *
    • success property : Boolean + *
      The success property is required.
    • + *
    • errors property : Object + *
      The errors property, + * which is optional, contains error messages for invalid fields.
    • + *
    + *

    JSON Packets

    + *

    By default, response packets are assumed to be JSON, so a typical response + * packet may look like this:

    
    +{
    +    success: false,
    +    errors: {
    +        clientCode: "Client not found",
    +        portOfLoading: "This field must not be null"
    +    }
    +}
    + *

    Other data may be placed into the response for processing by the {@link Ext.form.BasicForm}'s callback + * or event handler methods. The object decoded from this JSON is available in the + * {@link Ext.form.Action#result result} property.

    + *

    Alternatively, if an {@link #errorReader} is specified as an {@link Ext.data.XmlReader XmlReader}:

    
    +    errorReader: new Ext.data.XmlReader({
    +            record : 'field',
    +            success: '@success'
    +        }, [
    +            'id', 'msg'
    +        ]
    +    )
    +
    + *

    then the results may be sent back in XML format:

    
    +<?xml version="1.0" encoding="UTF-8"?>
    +<message success="false">
    +<errors>
    +    <field>
    +        <id>clientCode</id>
    +        <msg><![CDATA[Code not found. <br /><i>This is a test validation message from the server </i>]]></msg>
    +    </field>
    +    <field>
    +        <id>portOfLoading</id>
    +        <msg><![CDATA[Port not found. <br /><i>This is a test validation message from the server </i>]]></msg>
    +    </field>
    +</errors>
    +</message>
    +
    + *

    Other elements may be placed into the response XML for processing by the {@link Ext.form.BasicForm}'s callback + * or event handler methods. The XML document is available in the {@link #errorReader}'s {@link Ext.data.XmlReader#xmlData xmlData} property.

    + */ +Ext.form.Action.Submit = function(form, options){ + Ext.form.Action.Submit.superclass.constructor.call(this, form, options); +}; + +Ext.extend(Ext.form.Action.Submit, Ext.form.Action, { + /** + * @cfg {Ext.data.DataReader} errorReader

    Optional. JSON is interpreted with + * no need for an errorReader.

    + *

    A Reader which reads a single record from the returned data. The DataReader's + * success property specifies how submission success is determined. The Record's + * data provides the error messages to apply to any invalid form Fields.

    + */ + /** + * @cfg {boolean} clientValidation Determines whether a Form's fields are validated + * in a final call to {@link Ext.form.BasicForm#isValid isValid} prior to submission. + * Pass false in the Form's submit options to prevent this. If not defined, pre-submission field validation + * is performed. + */ + type : 'submit', + + // private + run : function(){ + var o = this.options; + var method = this.getMethod(); + var isGet = method == 'GET'; + if(o.clientValidation === false || this.form.isValid()){ + Ext.Ajax.request(Ext.apply(this.createCallback(o), { + form:this.form.el.dom, + url:this.getUrl(isGet), + method: method, + headers: o.headers, + params:!isGet ? this.getParams() : null, + isUpload: this.form.fileUpload + })); + }else if (o.clientValidation !== false){ // client validation failed + this.failureType = Ext.form.Action.CLIENT_INVALID; + this.form.afterAction(this, false); + } + }, + + // private + success : function(response){ + var result = this.processResponse(response); + if(result === true || result.success){ + this.form.afterAction(this, true); + return; + } + if(result.errors){ + this.form.markInvalid(result.errors); + } + this.failureType = Ext.form.Action.SERVER_INVALID; + this.form.afterAction(this, false); + }, + + // private + handleResponse : function(response){ + if(this.form.errorReader){ + var rs = this.form.errorReader.read(response); + var errors = []; + if(rs.records){ + for(var i = 0, len = rs.records.length; i < len; i++) { + var r = rs.records[i]; + errors[i] = r.data; + } + } + if(errors.length < 1){ + errors = null; + } + return { + success : rs.success, + errors : errors + }; + } + return Ext.decode(response.responseText); + } +}); + + +/** + * @class Ext.form.Action.Load + * @extends Ext.form.Action + *

    A class which handles loading of data from a server into the Fields of an {@link Ext.form.BasicForm}.

    + *

    Instances of this class are only created by a {@link Ext.form.BasicForm Form} when + * {@link Ext.form.BasicForm#load load}ing.

    + *

    Response Packet Criteria

    + *

    A response packet must contain: + *

      + *
    • success property : Boolean
    • + *
    • data property : Object
    • + *
      The data property contains the values of Fields to load. + * The individual value object for each Field is passed to the Field's + * {@link Ext.form.Field#setValue setValue} method.
      + *
    + *

    JSON Packets

    + *

    By default, response packets are assumed to be JSON, so for the following form load call:

    
    +var myFormPanel = new Ext.form.FormPanel({
    +    title: 'Client and routing info',
    +    items: [{
    +        fieldLabel: 'Client',
    +        name: 'clientName'
    +    }, {
    +        fieldLabel: 'Port of loading',
    +        name: 'portOfLoading'
    +    }, {
    +        fieldLabel: 'Port of discharge',
    +        name: 'portOfDischarge'
    +    }]
    +});
    +myFormPanel.{@link Ext.form.FormPanel#getForm getForm}().{@link Ext.form.BasicForm#load load}({
    +    url: '/getRoutingInfo.php',
    +    params: {
    +        consignmentRef: myConsignmentRef
    +    },
    +    failure: function(form, action) {
    +        Ext.Msg.alert("Load failed", action.result.errorMessage);
    +    }
    +});
    +
    + * a success response packet may look like this:

    
    +{
    +    success: true,
    +    data: {
    +        clientName: "Fred. Olsen Lines",
    +        portOfLoading: "FXT",
    +        portOfDischarge: "OSL"
    +    }
    +}
    + * while a failure response packet may look like this:

    
    +{
    +    success: false,
    +    errorMessage: "Consignment reference not found"
    +}
    + *

    Other data may be placed into the response for processing the {@link Ext.form.BasicForm Form}'s + * callback or event handler methods. The object decoded from this JSON is available in the + * {@link Ext.form.Action#result result} property.

    + */ +Ext.form.Action.Load = function(form, options){ + Ext.form.Action.Load.superclass.constructor.call(this, form, options); + this.reader = this.form.reader; +}; + +Ext.extend(Ext.form.Action.Load, Ext.form.Action, { + // private + type : 'load', + + // private + run : function(){ + Ext.Ajax.request(Ext.apply( + this.createCallback(this.options), { + method:this.getMethod(), + url:this.getUrl(false), + headers: this.options.headers, + params:this.getParams() + })); + }, + + // private + success : function(response){ + var result = this.processResponse(response); + if(result === true || !result.success || !result.data){ + this.failureType = Ext.form.Action.LOAD_FAILURE; + this.form.afterAction(this, false); + return; + } + this.form.clearInvalid(); + this.form.setValues(result.data); + this.form.afterAction(this, true); + }, + + // private + handleResponse : function(response){ + if(this.form.reader){ + var rs = this.form.reader.read(response); + var data = rs.records && rs.records[0] ? rs.records[0].data : null; + return { + success : rs.success, + data : data + }; + } + return Ext.decode(response.responseText); + } +}); + + + +/** + * @class Ext.form.Action.DirectLoad + * @extends Ext.form.Action.Load + *

    Provides Ext.direct support for loading form data.

    + *

    This example illustrates usage of Ext.Direct to load a form through Ext.Direct.

    + *
    
    +var myFormPanel = new Ext.form.FormPanel({
    +    // configs for FormPanel
    +    title: 'Basic Information',
    +    renderTo: document.body,
    +    width: 300, height: 160,
    +    padding: 10,
    +
    +    // configs apply to child items
    +    defaults: {anchor: '100%'},
    +    defaultType: 'textfield',
    +    items: [{
    +        fieldLabel: 'Name',
    +        name: 'name'
    +    },{
    +        fieldLabel: 'Email',
    +        name: 'email'
    +    },{
    +        fieldLabel: 'Company',
    +        name: 'company'
    +    }],
    +
    +    // configs for BasicForm
    +    api: {
    +        // The server-side method to call for load() requests
    +        load: Profile.getBasicInfo,
    +        // The server-side must mark the submit handler as a 'formHandler'
    +        submit: Profile.updateBasicInfo
    +    },
    +    // specify the order for the passed params
    +    paramOrder: ['uid', 'foo']
    +});
    +
    +// load the form
    +myFormPanel.getForm().load({
    +    // pass 2 arguments to server side getBasicInfo method (len=2)
    +    params: {
    +        foo: 'bar',
    +        uid: 34
    +    }
    +});
    + * 
    + * The data packet sent to the server will resemble something like: + *
    
    +[
    +    {
    +        "action":"Profile","method":"getBasicInfo","type":"rpc","tid":2,
    +        "data":[34,"bar"] // note the order of the params
    +    }
    +]
    + * 
    + * The form will process a data packet returned by the server that is similar + * to the following format: + *
    
    +[
    +    {
    +        "action":"Profile","method":"getBasicInfo","type":"rpc","tid":2,
    +        "result":{
    +            "success":true,
    +            "data":{
    +                "name":"Fred Flintstone",
    +                "company":"Slate Rock and Gravel",
    +                "email":"fred.flintstone@slaterg.com"
    +            }
    +        }
    +    }
    +]
    + * 
    + */ +Ext.form.Action.DirectLoad = Ext.extend(Ext.form.Action.Load, { + constructor: function(form, opts) { + Ext.form.Action.DirectLoad.superclass.constructor.call(this, form, opts); + }, + type : 'directload', + + run : function(){ + var args = this.getParams(); + args.push(this.success, this); + this.form.api.load.apply(window, args); + }, + + getParams : function() { + var buf = [], o = {}; + var bp = this.form.baseParams; + var p = this.options.params; + Ext.apply(o, p, bp); + var paramOrder = this.form.paramOrder; + if(paramOrder){ + for(var i = 0, len = paramOrder.length; i < len; i++){ + buf.push(o[paramOrder[i]]); + } + }else if(this.form.paramsAsHash){ + buf.push(o); + } + return buf; + }, + // Direct actions have already been processed and therefore + // we can directly set the result; Direct Actions do not have + // a this.response property. + processResponse : function(result) { + this.result = result; + return result; + }, + + success : function(response, trans){ + if(trans.type == Ext.Direct.exceptions.SERVER){ + response = {}; + } + Ext.form.Action.DirectLoad.superclass.success.call(this, response); + } +}); + +/** + * @class Ext.form.Action.DirectSubmit + * @extends Ext.form.Action.Submit + *

    Provides Ext.direct support for submitting form data.

    + *

    This example illustrates usage of Ext.Direct to submit a form through Ext.Direct.

    + *
    
    +var myFormPanel = new Ext.form.FormPanel({
    +    // configs for FormPanel
    +    title: 'Basic Information',
    +    renderTo: document.body,
    +    width: 300, height: 160,
    +    padding: 10,
    +    buttons:[{
    +        text: 'Submit',
    +        handler: function(){
    +            myFormPanel.getForm().submit({
    +                params: {
    +                    foo: 'bar',
    +                    uid: 34
    +                }
    +            });
    +        }
    +    }],
    +
    +    // configs apply to child items
    +    defaults: {anchor: '100%'},
    +    defaultType: 'textfield',
    +    items: [{
    +        fieldLabel: 'Name',
    +        name: 'name'
    +    },{
    +        fieldLabel: 'Email',
    +        name: 'email'
    +    },{
    +        fieldLabel: 'Company',
    +        name: 'company'
    +    }],
    +
    +    // configs for BasicForm
    +    api: {
    +        // The server-side method to call for load() requests
    +        load: Profile.getBasicInfo,
    +        // The server-side must mark the submit handler as a 'formHandler'
    +        submit: Profile.updateBasicInfo
    +    },
    +    // specify the order for the passed params
    +    paramOrder: ['uid', 'foo']
    +});
    + * 
    + * The data packet sent to the server will resemble something like: + *
    
    +{
    +    "action":"Profile","method":"updateBasicInfo","type":"rpc","tid":"6",
    +    "result":{
    +        "success":true,
    +        "id":{
    +            "extAction":"Profile","extMethod":"updateBasicInfo",
    +            "extType":"rpc","extTID":"6","extUpload":"false",
    +            "name":"Aaron Conran","email":"aaron@extjs.com","company":"Ext JS, LLC"
    +        }
    +    }
    +}
    + * 
    + * The form will process a data packet returned by the server that is similar + * to the following: + *
    
    +// sample success packet (batched requests)
    +[
    +    {
    +        "action":"Profile","method":"updateBasicInfo","type":"rpc","tid":3,
    +        "result":{
    +            "success":true
    +        }
    +    }
    +]
    +
    +// sample failure packet (one request)
    +{
    +        "action":"Profile","method":"updateBasicInfo","type":"rpc","tid":"6",
    +        "result":{
    +            "errors":{
    +                "email":"already taken"
    +            },
    +            "success":false,
    +            "foo":"bar"
    +        }
    +}
    + * 
    + * Also see the discussion in {@link Ext.form.Action.DirectLoad}. + */ +Ext.form.Action.DirectSubmit = Ext.extend(Ext.form.Action.Submit, { + constructor : function(form, opts) { + Ext.form.Action.DirectSubmit.superclass.constructor.call(this, form, opts); + }, + type : 'directsubmit', + // override of Submit + run : function(){ + var o = this.options; + if(o.clientValidation === false || this.form.isValid()){ + // tag on any additional params to be posted in the + // form scope + this.success.params = this.getParams(); + this.form.api.submit(this.form.el.dom, this.success, this); + }else if (o.clientValidation !== false){ // client validation failed + this.failureType = Ext.form.Action.CLIENT_INVALID; + this.form.afterAction(this, false); + } + }, + + getParams : function() { + var o = {}; + var bp = this.form.baseParams; + var p = this.options.params; + Ext.apply(o, p, bp); + return o; + }, + // Direct actions have already been processed and therefore + // we can directly set the result; Direct Actions do not have + // a this.response property. + processResponse : function(result) { + this.result = result; + return result; + }, + + success : function(response, trans){ + if(trans.type == Ext.Direct.exceptions.SERVER){ + response = {}; } - return this; + Ext.form.Action.DirectSubmit.superclass.success.call(this, response); } }); -Ext.reg('label', Ext.form.Label);/** - * @class Ext.form.Action - *

    The subclasses of this class provide actions to perform upon {@link Ext.form.BasicForm Form}s.

    - *

    Instances of this class are only created by a {@link Ext.form.BasicForm Form} when - * the Form needs to perform an action such as submit or load. The Configuration options - * listed for this class are set through the Form's action methods: {@link Ext.form.BasicForm#submit submit}, - * {@link Ext.form.BasicForm#load load} and {@link Ext.form.BasicForm#doAction doAction}

    - *

    The instance of Action which performed the action is passed to the success - * and failure callbacks of the Form's action methods ({@link Ext.form.BasicForm#submit submit}, - * {@link Ext.form.BasicForm#load load} and {@link Ext.form.BasicForm#doAction doAction}), - * and to the {@link Ext.form.BasicForm#actioncomplete actioncomplete} and - * {@link Ext.form.BasicForm#actionfailed actionfailed} event handlers.

    - */ -Ext.form.Action = function(form, options){ - this.form = form; - this.options = options || {}; -}; - -/** - * Failure type returned when client side validation of the Form fails - * thus aborting a submit action. Client side validation is performed unless - * {@link #clientValidation} is explicitly set to false. - * @type {String} - * @static - */ -Ext.form.Action.CLIENT_INVALID = 'client'; -/** - *

    Failure type returned when server side processing fails and the {@link #result}'s - * success property is set to false.

    - *

    In the case of a form submission, field-specific error messages may be returned in the - * {@link #result}'s errors property.

    - * @type {String} - * @static - */ -Ext.form.Action.SERVER_INVALID = 'server'; -/** - * Failure type returned when a communication error happens when attempting - * to send a request to the remote server. The {@link #response} may be examined to - * provide further information. - * @type {String} - * @static - */ -Ext.form.Action.CONNECT_FAILURE = 'connect'; -/** - * Failure type returned when the response's success - * property is set to false, or no field values are returned in the response's - * data property. - * @type {String} - * @static - */ -Ext.form.Action.LOAD_FAILURE = 'load'; - -Ext.form.Action.prototype = { -/** - * @cfg {String} url The URL that the Action is to invoke. - */ -/** - * @cfg {Boolean} reset When set to true, causes the Form to be - * {@link Ext.form.BasicForm.reset reset} on Action success. If specified, this happens - * before the {@link #success} callback is called and before the Form's - * {@link Ext.form.BasicForm.actioncomplete actioncomplete} event fires. - */ -/** - * @cfg {String} method The HTTP method to use to access the requested URL. Defaults to the - * {@link Ext.form.BasicForm}'s method, or if that is not specified, the underlying DOM form's method. - */ -/** - * @cfg {Mixed} params

    Extra parameter values to pass. These are added to the Form's - * {@link Ext.form.BasicForm#baseParams} and passed to the specified URL along with the Form's - * input fields.

    - *

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

    - */ -/** - * @cfg {Number} timeout The number of seconds to wait for a server response before - * failing with the {@link #failureType} as {@link #Action.CONNECT_FAILURE}. If not specified, - * defaults to the configured {@link Ext.form.BasicForm#timeout timeout} of the - * {@link Ext.form.BasicForm form}. - */ -/** - * @cfg {Function} success The function to call when a valid success return packet is recieved. - * The function is passed the following parameters:
      - *
    • form : Ext.form.BasicForm
      The form that requested the action
    • - *
    • action : Ext.form.Action
      The Action class. The {@link #result} - * property of this object may be examined to perform custom postprocessing.
    • - *
    - */ -/** - * @cfg {Function} failure The function to call when a failure packet was recieved, or when an - * error ocurred in the Ajax communication. - * The function is passed the following parameters:
      - *
    • form : Ext.form.BasicForm
      The form that requested the action
    • - *
    • action : Ext.form.Action
      The Action class. If an Ajax - * error ocurred, the failure type will be in {@link #failureType}. The {@link #result} - * property of this object may be examined to perform custom postprocessing.
    • - *
    - */ -/** - * @cfg {Object} scope The scope in which to call the callback functions (The this reference - * for the callback functions). - */ -/** - * @cfg {String} waitMsg The message to be displayed by a call to {@link Ext.MessageBox#wait} - * during the time the action is being processed. - */ -/** - * @cfg {String} waitTitle The title to be displayed by a call to {@link Ext.MessageBox#wait} - * during the time the action is being processed. - */ - -/** - * The type of action this Action instance performs. - * Currently only "submit" and "load" are supported. - * @type {String} - */ - type : 'default', -/** - * The type of failure detected will be one of these: {@link #CLIENT_INVALID}, - * {@link #SERVER_INVALID}, {@link #CONNECT_FAILURE}, or {@link #LOAD_FAILURE}. Usage: - *
    
    -var fp = new Ext.form.FormPanel({
    -...
    -buttons: [{
    -    text: 'Save',
    -    formBind: true,
    -    handler: function(){
    -        if(fp.getForm().isValid()){
    -            fp.getForm().submit({
    -                url: 'form-submit.php',
    -                waitMsg: 'Submitting your data...',
    -                success: function(form, action){
    -                    // server responded with success = true
    -                    var result = action.{@link #result};
    -                },
    -                failure: function(form, action){
    -                    if (action.{@link #failureType} === Ext.form.Action.{@link #CONNECT_FAILURE}) {
    -                        Ext.Msg.alert('Error',
    -                            'Status:'+action.{@link #response}.status+': '+
    -                            action.{@link #response}.statusText);
    -                    }
    -                    if (action.failureType === Ext.form.Action.{@link #SERVER_INVALID}){
    -                        // server responded with success = false
    -                        Ext.Msg.alert('Invalid', action.{@link #result}.errormsg);
    -                    }
    -                }
    -            });
    -        }
    -    }
    -},{
    -    text: 'Reset',
    -    handler: function(){
    -        fp.getForm().reset();
    -    }
    -}]
    - * 
    - * @property failureType - * @type {String} - */ - /** - * The XMLHttpRequest object used to perform the action. - * @property response - * @type {Object} - */ - /** - * The decoded response object containing a boolean success property and - * other, action-specific properties. - * @property result - * @type {Object} - */ - - // interface method - run : function(options){ - - }, - - // interface method - success : function(response){ - - }, - - // interface method - handleResponse : function(response){ - - }, - - // default connection failure - failure : function(response){ - this.response = response; - this.failureType = Ext.form.Action.CONNECT_FAILURE; - this.form.afterAction(this, false); - }, - - // private - // shared code among all Actions to validate that there was a response - // with either responseText or responseXml - processResponse : function(response){ - this.response = response; - if(!response.responseText && !response.responseXML){ - return true; - } - this.result = this.handleResponse(response); - return this.result; - }, - - // utility functions used internally - getUrl : function(appendParams){ - var url = this.options.url || this.form.url || this.form.el.dom.action; - if(appendParams){ - var p = this.getParams(); - if(p){ - url = Ext.urlAppend(url, p); - } - } - return url; - }, - - // private - getMethod : function(){ - return (this.options.method || this.form.method || this.form.el.dom.method || 'POST').toUpperCase(); - }, - - // private - getParams : function(){ - var bp = this.form.baseParams; - var p = this.options.params; - if(p){ - if(typeof p == "object"){ - p = Ext.urlEncode(Ext.applyIf(p, bp)); - }else if(typeof p == 'string' && bp){ - p += '&' + Ext.urlEncode(bp); - } - }else if(bp){ - p = Ext.urlEncode(bp); - } - return p; - }, - - // private - createCallback : function(opts){ - var opts = opts || {}; - return { - success: this.success, - failure: this.failure, - scope: this, - timeout: (opts.timeout*1000) || (this.form.timeout*1000), - upload: this.form.fileUpload ? this.success : undefined - }; - } -}; - -/** - * @class Ext.form.Action.Submit - * @extends Ext.form.Action - *

    A class which handles submission of data from {@link Ext.form.BasicForm Form}s - * and processes the returned response.

    - *

    Instances of this class are only created by a {@link Ext.form.BasicForm Form} when - * {@link Ext.form.BasicForm#submit submit}ting.

    - *

    Response Packet Criteria

    - *

    A response packet may contain: - *

      - *
    • success property : Boolean - *
      The success property is required.
    • - *
    • errors property : Object - *
      The errors property, - * which is optional, contains error messages for invalid fields.
    • - *
    - *

    JSON Packets

    - *

    By default, response packets are assumed to be JSON, so a typical response - * packet may look like this:

    
    -{
    -    success: false,
    -    errors: {
    -        clientCode: "Client not found",
    -        portOfLoading: "This field must not be null"
    -    }
    -}
    - *

    Other data may be placed into the response for processing by the {@link Ext.form.BasicForm}'s callback - * or event handler methods. The object decoded from this JSON is available in the - * {@link Ext.form.Action#result result} property.

    - *

    Alternatively, if an {@link #errorReader} is specified as an {@link Ext.data.XmlReader XmlReader}:

    
    -    errorReader: new Ext.data.XmlReader({
    -            record : 'field',
    -            success: '@success'
    -        }, [
    -            'id', 'msg'
    -        ]
    -    )
    -
    - *

    then the results may be sent back in XML format:

    
    -<?xml version="1.0" encoding="UTF-8"?>
    -<message success="false">
    -<errors>
    -    <field>
    -        <id>clientCode</id>
    -        <msg><![CDATA[Code not found. <br /><i>This is a test validation message from the server </i>]]></msg>
    -    </field>
    -    <field>
    -        <id>portOfLoading</id>
    -        <msg><![CDATA[Port not found. <br /><i>This is a test validation message from the server </i>]]></msg>
    -    </field>
    -</errors>
    -</message>
    -
    - *

    Other elements may be placed into the response XML for processing by the {@link Ext.form.BasicForm}'s callback - * or event handler methods. The XML document is available in the {@link #errorReader}'s {@link Ext.data.XmlReader#xmlData xmlData} property.

    - */ -Ext.form.Action.Submit = function(form, options){ - Ext.form.Action.Submit.superclass.constructor.call(this, form, options); -}; - -Ext.extend(Ext.form.Action.Submit, Ext.form.Action, { - /** - * @cfg {Ext.data.DataReader} errorReader

    Optional. JSON is interpreted with - * no need for an errorReader.

    - *

    A Reader which reads a single record from the returned data. The DataReader's - * success property specifies how submission success is determined. The Record's - * data provides the error messages to apply to any invalid form Fields.

    - */ - /** - * @cfg {boolean} clientValidation Determines whether a Form's fields are validated - * in a final call to {@link Ext.form.BasicForm#isValid isValid} prior to submission. - * Pass false in the Form's submit options to prevent this. If not defined, pre-submission field validation - * is performed. - */ - type : 'submit', - - // private - run : function(){ - var o = this.options; - var method = this.getMethod(); - var isGet = method == 'GET'; - if(o.clientValidation === false || this.form.isValid()){ - Ext.Ajax.request(Ext.apply(this.createCallback(o), { - form:this.form.el.dom, - url:this.getUrl(isGet), - method: method, - headers: o.headers, - params:!isGet ? this.getParams() : null, - isUpload: this.form.fileUpload - })); - }else if (o.clientValidation !== false){ // client validation failed - this.failureType = Ext.form.Action.CLIENT_INVALID; - this.form.afterAction(this, false); - } - }, - - // private - success : function(response){ - var result = this.processResponse(response); - if(result === true || result.success){ - this.form.afterAction(this, true); - return; - } - if(result.errors){ - this.form.markInvalid(result.errors); - } - this.failureType = Ext.form.Action.SERVER_INVALID; - this.form.afterAction(this, false); - }, - - // private - handleResponse : function(response){ - if(this.form.errorReader){ - var rs = this.form.errorReader.read(response); - var errors = []; - if(rs.records){ - for(var i = 0, len = rs.records.length; i < len; i++) { - var r = rs.records[i]; - errors[i] = r.data; - } - } - if(errors.length < 1){ - errors = null; - } - return { - success : rs.success, - errors : errors - }; - } - return Ext.decode(response.responseText); - } -}); - - -/** - * @class Ext.form.Action.Load - * @extends Ext.form.Action - *

    A class which handles loading of data from a server into the Fields of an {@link Ext.form.BasicForm}.

    - *

    Instances of this class are only created by a {@link Ext.form.BasicForm Form} when - * {@link Ext.form.BasicForm#load load}ing.

    - *

    Response Packet Criteria

    - *

    A response packet must contain: - *

      - *
    • success property : Boolean
    • - *
    • data property : Object
    • - *
      The data property contains the values of Fields to load. - * The individual value object for each Field is passed to the Field's - * {@link Ext.form.Field#setValue setValue} method.
      - *
    - *

    JSON Packets

    - *

    By default, response packets are assumed to be JSON, so for the following form load call:

    
    -var myFormPanel = new Ext.form.FormPanel({
    -    title: 'Client and routing info',
    -    items: [{
    -        fieldLabel: 'Client',
    -        name: 'clientName'
    -    }, {
    -        fieldLabel: 'Port of loading',
    -        name: 'portOfLoading'
    -    }, {
    -        fieldLabel: 'Port of discharge',
    -        name: 'portOfDischarge'
    -    }]
    -});
    -myFormPanel.{@link Ext.form.FormPanel#getForm getForm}().{@link Ext.form.BasicForm#load load}({
    -    url: '/getRoutingInfo.php',
    -    params: {
    -        consignmentRef: myConsignmentRef
    -    },
    -    failure: function(form, action) {
    -        Ext.Msg.alert("Load failed", action.result.errorMessage);
    -    }
    -});
    -
    - * a success response packet may look like this:

    
    -{
    -    success: true,
    -    data: {
    -        clientName: "Fred. Olsen Lines",
    -        portOfLoading: "FXT",
    -        portOfDischarge: "OSL"
    -    }
    -}
    - * while a failure response packet may look like this:

    
    -{
    -    success: false,
    -    errorMessage: "Consignment reference not found"
    -}
    - *

    Other data may be placed into the response for processing the {@link Ext.form.BasicForm Form}'s - * callback or event handler methods. The object decoded from this JSON is available in the - * {@link Ext.form.Action#result result} property.

    - */ -Ext.form.Action.Load = function(form, options){ - Ext.form.Action.Load.superclass.constructor.call(this, form, options); - this.reader = this.form.reader; -}; - -Ext.extend(Ext.form.Action.Load, Ext.form.Action, { - // private - type : 'load', - - // private - run : function(){ - Ext.Ajax.request(Ext.apply( - this.createCallback(this.options), { - method:this.getMethod(), - url:this.getUrl(false), - headers: this.options.headers, - params:this.getParams() - })); - }, - - // private - success : function(response){ - var result = this.processResponse(response); - if(result === true || !result.success || !result.data){ - this.failureType = Ext.form.Action.LOAD_FAILURE; - this.form.afterAction(this, false); - return; - } - this.form.clearInvalid(); - this.form.setValues(result.data); - this.form.afterAction(this, true); - }, - - // private - handleResponse : function(response){ - if(this.form.reader){ - var rs = this.form.reader.read(response); - var data = rs.records && rs.records[0] ? rs.records[0].data : null; - return { - success : rs.success, - data : data - }; - } - return Ext.decode(response.responseText); - } -}); - - - -/** - * @class Ext.form.Action.DirectLoad - * @extends Ext.form.Action.Load - *

    Provides Ext.direct support for loading form data.

    - *

    This example illustrates usage of Ext.Direct to load a form through Ext.Direct.

    - *
    
    -var myFormPanel = new Ext.form.FormPanel({
    -    // configs for FormPanel
    -    title: 'Basic Information',
    -    renderTo: document.body,
    -    width: 300, height: 160,
    -    padding: 10,
    -
    -    // configs apply to child items
    -    defaults: {anchor: '100%'},
    -    defaultType: 'textfield',
    -    items: [{
    -        fieldLabel: 'Name',
    -        name: 'name'
    -    },{
    -        fieldLabel: 'Email',
    -        name: 'email'
    -    },{
    -        fieldLabel: 'Company',
    -        name: 'company'
    -    }],
    -
    -    // configs for BasicForm
    -    api: {
    -        // The server-side method to call for load() requests
    -        load: Profile.getBasicInfo,
    -        // The server-side must mark the submit handler as a 'formHandler'
    -        submit: Profile.updateBasicInfo
    -    },
    -    // specify the order for the passed params
    -    paramOrder: ['uid', 'foo']
    -});
    -
    -// load the form
    -myFormPanel.getForm().load({
    -    // pass 2 arguments to server side getBasicInfo method (len=2)
    -    params: {
    -        foo: 'bar',
    -        uid: 34
    -    }
    -});
    - * 
    - * The data packet sent to the server will resemble something like: - *
    
    -[
    -    {
    -        "action":"Profile","method":"getBasicInfo","type":"rpc","tid":2,
    -        "data":[34,"bar"] // note the order of the params
    -    }
    -]
    - * 
    - * The form will process a data packet returned by the server that is similar - * to the following format: - *
    
    -[
    -    {
    -        "action":"Profile","method":"getBasicInfo","type":"rpc","tid":2,
    -        "result":{
    -            "success":true,
    -            "data":{
    -                "name":"Fred Flintstone",
    -                "company":"Slate Rock and Gravel",
    -                "email":"fred.flintstone@slaterg.com"
    -            }
    -        }
    -    }
    -]
    - * 
    - */ -Ext.form.Action.DirectLoad = Ext.extend(Ext.form.Action.Load, { - constructor: function(form, opts) { - Ext.form.Action.DirectLoad.superclass.constructor.call(this, form, opts); - }, - type : 'directload', - - run : function(){ - var args = this.getParams(); - args.push(this.success, this); - this.form.api.load.apply(window, args); - }, - - getParams : function() { - var buf = [], o = {}; - var bp = this.form.baseParams; - var p = this.options.params; - Ext.apply(o, p, bp); - var paramOrder = this.form.paramOrder; - if(paramOrder){ - for(var i = 0, len = paramOrder.length; i < len; i++){ - buf.push(o[paramOrder[i]]); - } - }else if(this.form.paramsAsHash){ - buf.push(o); - } - return buf; - }, - // Direct actions have already been processed and therefore - // we can directly set the result; Direct Actions do not have - // a this.response property. - processResponse : function(result) { - this.result = result; - return result; - }, - - success : function(response, trans){ - if(trans.type == Ext.Direct.exceptions.SERVER){ - response = {}; - } - Ext.form.Action.DirectLoad.superclass.success.call(this, response); - } -}); - -/** - * @class Ext.form.Action.DirectSubmit - * @extends Ext.form.Action.Submit - *

    Provides Ext.direct support for submitting form data.

    - *

    This example illustrates usage of Ext.Direct to submit a form through Ext.Direct.

    - *
    
    -var myFormPanel = new Ext.form.FormPanel({
    -    // configs for FormPanel
    -    title: 'Basic Information',
    -    renderTo: document.body,
    -    width: 300, height: 160,
    -    padding: 10,
    -    buttons:[{
    -        text: 'Submit',
    -        handler: function(){
    -            myFormPanel.getForm().submit({
    -                params: {
    -                    foo: 'bar',
    -                    uid: 34
    -                }
    -            });
    -        }
    -    }],
    -
    -    // configs apply to child items
    -    defaults: {anchor: '100%'},
    -    defaultType: 'textfield',
    -    items: [{
    -        fieldLabel: 'Name',
    -        name: 'name'
    -    },{
    -        fieldLabel: 'Email',
    -        name: 'email'
    -    },{
    -        fieldLabel: 'Company',
    -        name: 'company'
    -    }],
    -
    -    // configs for BasicForm
    -    api: {
    -        // The server-side method to call for load() requests
    -        load: Profile.getBasicInfo,
    -        // The server-side must mark the submit handler as a 'formHandler'
    -        submit: Profile.updateBasicInfo
    -    },
    -    // specify the order for the passed params
    -    paramOrder: ['uid', 'foo']
    -});
    - * 
    - * The data packet sent to the server will resemble something like: - *
    
    -{
    -    "action":"Profile","method":"updateBasicInfo","type":"rpc","tid":"6",
    -    "result":{
    -        "success":true,
    -        "id":{
    -            "extAction":"Profile","extMethod":"updateBasicInfo",
    -            "extType":"rpc","extTID":"6","extUpload":"false",
    -            "name":"Aaron Conran","email":"aaron@extjs.com","company":"Ext JS, LLC"
    -        }
    -    }
    -}
    - * 
    - * The form will process a data packet returned by the server that is similar - * to the following: - *
    
    -// sample success packet (batched requests)
    -[
    -    {
    -        "action":"Profile","method":"updateBasicInfo","type":"rpc","tid":3,
    -        "result":{
    -            "success":true
    -        }
    -    }
    -]
    -
    -// sample failure packet (one request)
    -{
    -        "action":"Profile","method":"updateBasicInfo","type":"rpc","tid":"6",
    -        "result":{
    -            "errors":{
    -                "email":"already taken"
    -            },
    -            "success":false,
    -            "foo":"bar"
    -        }
    -}
    - * 
    - * Also see the discussion in {@link Ext.form.Action.DirectLoad}. - */ -Ext.form.Action.DirectSubmit = Ext.extend(Ext.form.Action.Submit, { - constructor : function(form, opts) { - Ext.form.Action.DirectSubmit.superclass.constructor.call(this, form, opts); - }, - type : 'directsubmit', - // override of Submit - run : function(){ - var o = this.options; - if(o.clientValidation === false || this.form.isValid()){ - // tag on any additional params to be posted in the - // form scope - this.success.params = this.getParams(); - this.form.api.submit(this.form.el.dom, this.success, this); - }else if (o.clientValidation !== false){ // client validation failed - this.failureType = Ext.form.Action.CLIENT_INVALID; - this.form.afterAction(this, false); - } - }, - - getParams : function() { - var o = {}; - var bp = this.form.baseParams; - var p = this.options.params; - Ext.apply(o, p, bp); - return o; - }, - // Direct actions have already been processed and therefore - // we can directly set the result; Direct Actions do not have - // a this.response property. - processResponse : function(result) { - this.result = result; - return result; - }, - - success : function(response, trans){ - if(trans.type == Ext.Direct.exceptions.SERVER){ - response = {}; - } - Ext.form.Action.DirectSubmit.superclass.success.call(this, response); - } -}); - -Ext.form.Action.ACTION_TYPES = { - 'load' : Ext.form.Action.Load, - 'submit' : Ext.form.Action.Submit, - 'directload' : Ext.form.Action.DirectLoad, - 'directsubmit' : Ext.form.Action.DirectSubmit -}; +Ext.form.Action.ACTION_TYPES = { + 'load' : Ext.form.Action.Load, + 'submit' : Ext.form.Action.Submit, + 'directload' : Ext.form.Action.DirectLoad, + 'directsubmit' : Ext.form.Action.DirectSubmit +}; /** * @class Ext.form.VTypes *

    This is a singleton object which contains a set of commonly used field validation functions. @@ -61045,7 +63718,7 @@ Ext.form.VTypes = function(){ // closure these in so they are only created once. var alpha = /^[a-zA-Z_]+$/, alphanum = /^[a-zA-Z0-9_]+$/, - email = /^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,4}$/, + email = /^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,6}$/, url = /(((^https?)|(^ftp)):\/\/([\-\w]+\.)+\w{2,3}(\/[%\-\w]+(\.\w{2,})?)*(([\w\-\.\?\\\/+@&#;`~=%!]*)(\.\w{2,})?)*\/?)/i; // All these messages and functions are configurable @@ -61348,14 +64021,14 @@ Ext.grid.GridPanel = Ext.extend(Ext.Panel, { * @cfg {Array} stateEvents * An array of events that, when fired, should trigger this component to save its state. * Defaults to:

    
    -     * stateEvents: ['columnmove', 'columnresize', 'sortchange']
    +     * stateEvents: ['columnmove', 'columnresize', 'sortchange', 'groupchange']
          * 
    *

    These can be any types of events supported by this component, including browser or * custom events (e.g., ['click', 'customerchange']).

    *

    See {@link Ext.Component#stateful} for an explanation of saving and restoring * Component state.

    */ - stateEvents : ['columnmove', 'columnresize', 'sortchange'], + stateEvents : ['columnmove', 'columnresize', 'sortchange', 'groupchange'], /** * @cfg {Object} view The {@link Ext.grid.GridView} used by the grid. This can be set * before a call to {@link Ext.Component#render render()}. @@ -61365,6 +64038,7 @@ Ext.grid.GridPanel = Ext.extend(Ext.Panel, { /** * @cfg {Array} bubbleEvents *

    An array of events that, when fired, should be bubbled to any parent container. + * See {@link Ext.util.Observable#enableBubble}. * Defaults to []. */ bubbleEvents: [], @@ -61495,6 +64169,33 @@ Ext.grid.GridPanel = Ext.extend(Ext.Panel, { * @param {Ext.EventObject} e */ 'headermousedown', + + /** + * @event groupmousedown + * Fires before a group header is clicked. Only applies for grids with a {@link Ext.grid.GroupingView GroupingView}. + * @param {Grid} this + * @param {String} groupField + * @param {String} groupValue + * @param {Ext.EventObject} e + */ + 'groupmousedown', + + /** + * @event rowbodymousedown + * Fires before the row body is clicked. Only applies for grids with {@link Ext.grid.GridView#enableRowBody enableRowBody} configured. + * @param {Grid} this + * @param {Number} rowIndex + * @param {Ext.EventObject} e + */ + 'rowbodymousedown', + + /** + * @event containermousedown + * Fires before the container is clicked. The container consists of any part of the grid body that is not covered by a row. + * @param {Grid} this + * @param {Ext.EventObject} e + */ + 'containermousedown', /** * @event cellclick @@ -61556,6 +64257,56 @@ function(grid, rowIndex, columnIndex, e) { * @param {Ext.EventObject} e */ 'headerdblclick', + /** + * @event groupclick + * Fires when group header is clicked. Only applies for grids with a {@link Ext.grid.GroupingView GroupingView}. + * @param {Grid} this + * @param {String} groupField + * @param {String} groupValue + * @param {Ext.EventObject} e + */ + 'groupclick', + /** + * @event groupdblclick + * Fires when group header is double clicked. Only applies for grids with a {@link Ext.grid.GroupingView GroupingView}. + * @param {Grid} this + * @param {String} groupField + * @param {String} groupValue + * @param {Ext.EventObject} e + */ + 'groupdblclick', + /** + * @event containerclick + * Fires when the container is clicked. The container consists of any part of the grid body that is not covered by a row. + * @param {Grid} this + * @param {Ext.EventObject} e + */ + 'containerclick', + /** + * @event containerdblclick + * Fires when the container is double clicked. The container consists of any part of the grid body that is not covered by a row. + * @param {Grid} this + * @param {Ext.EventObject} e + */ + 'containerdblclick', + + /** + * @event rowbodyclick + * Fires when the row body is clicked. Only applies for grids with {@link Ext.grid.GridView#enableRowBody enableRowBody} configured. + * @param {Grid} this + * @param {Number} rowIndex + * @param {Ext.EventObject} e + */ + 'rowbodyclick', + /** + * @event rowbodydblclick + * Fires when the row body is double clicked. Only applies for grids with {@link Ext.grid.GridView#enableRowBody enableRowBody} configured. + * @param {Grid} this + * @param {Number} rowIndex + * @param {Ext.EventObject} e + */ + 'rowbodydblclick', + /** * @event rowcontextmenu * Fires when a row is right clicked @@ -61581,6 +64332,30 @@ function(grid, rowIndex, columnIndex, e) { * @param {Ext.EventObject} e */ 'headercontextmenu', + /** + * @event groupcontextmenu + * Fires when group header is right clicked. Only applies for grids with a {@link Ext.grid.GroupingView GroupingView}. + * @param {Grid} this + * @param {String} groupField + * @param {String} groupValue + * @param {Ext.EventObject} e + */ + 'groupcontextmenu', + /** + * @event containercontextmenu + * Fires when the container is right clicked. The container consists of any part of the grid body that is not covered by a row. + * @param {Grid} this + * @param {Ext.EventObject} e + */ + 'containercontextmenu', + /** + * @event rowbodycontextmenu + * Fires when the row body is right clicked. Only applies for grids with {@link Ext.grid.GridView#enableRowBody enableRowBody} configured. + * @param {Grid} this + * @param {Number} rowIndex + * @param {Ext.EventObject} e + */ + 'rowbodycontextmenu', /** * @event bodyscroll * Fires when the body element is scrolled @@ -61609,6 +64384,13 @@ function(grid, rowIndex, columnIndex, e) { * @param {Object} sortInfo An object with the keys field and direction */ 'sortchange', + /** + * @event groupchange + * Fires when the grid's grouping changes (only applies for grids with a {@link Ext.grid.GroupingView GroupingView}) + * @param {Grid} this + * @param {String} groupField A string with the grouping field, null if the store is not grouped. + */ + 'groupchange', /** * @event reconfigure * Fires when the grid is reconfigured with a new store and/or column model. @@ -61616,7 +64398,13 @@ function(grid, rowIndex, columnIndex, e) { * @param {Ext.data.Store} store The new store * @param {Ext.grid.ColumnModel} colModel The new column model */ - 'reconfigure' + 'reconfigure', + /** + * @event viewready + * Fires when the grid view is available (use this for selecting a default row). + * @param {Grid} this + */ + 'viewready' ); }, @@ -61661,23 +64449,40 @@ function(grid, rowIndex, columnIndex, e) { applyState : function(state){ var cm = this.colModel, - cs = state.columns; + cs = state.columns, + store = this.store, + s, + c, + oldIndex; + if(cs){ for(var i = 0, len = cs.length; i < len; i++){ - var s = cs[i]; - var c = cm.getColumnById(s.id); + s = cs[i]; + c = cm.getColumnById(s.id); if(c){ c.hidden = s.hidden; c.width = s.width; - var oldIndex = cm.getIndexById(s.id); + oldIndex = cm.getIndexById(s.id); if(oldIndex != i){ cm.moveColumn(oldIndex, i); } } } } - if(state.sort && this.store){ - this.store[this.store.remoteSort ? 'setDefaultSort' : 'sort'](state.sort.field, state.sort.direction); + if(store){ + s = state.sort; + if(s){ + store[store.remoteSort ? 'setDefaultSort' : 'sort'](s.field, s.direction); + } + s = state.group; + if(store.groupBy){ + if(s){ + store.groupBy(s); + }else{ + store.clearGrouping(); + } + } + } var o = Ext.apply({}, state); delete o.columns; @@ -61686,7 +64491,11 @@ function(grid, rowIndex, columnIndex, e) { }, getState : function(){ - var o = {columns: []}; + var o = {columns: []}, + store = this.store, + ss, + gs; + for(var i = 0, c; (c = this.colModel.config[i]); i++){ o.columns[i] = { id: c.id, @@ -61696,11 +64505,17 @@ function(grid, rowIndex, columnIndex, e) { o.columns[i].hidden = true; } } - if(this.store){ - var ss = this.store.getSortState(); + if(store){ + ss = store.getSortState(); if(ss){ o.sort = ss; } + if(store.getGroupState){ + gs = store.getGroupState(); + if(gs){ + o.group = gs; + } + } } return o; }, @@ -61732,15 +64547,20 @@ function(grid, rowIndex, columnIndex, e) { * @param {Ext.grid.ColumnModel} colModel The new {@link Ext.grid.ColumnModel} object */ reconfigure : function(store, colModel){ - if(this.loadMask){ - this.loadMask.destroy(); - this.loadMask = new Ext.LoadMask(this.bwrap, - Ext.apply({}, {store:store}, this.initialConfig.loadMask)); + var rendered = this.rendered; + if(rendered){ + if(this.loadMask){ + this.loadMask.destroy(); + this.loadMask = new Ext.LoadMask(this.bwrap, + Ext.apply({}, {store:store}, this.initialConfig.loadMask)); + } + } + if(this.view){ + this.view.initData(store, colModel); } - this.view.initData(store, colModel); this.store = store; this.colModel = colModel; - if(this.rendered){ + if(rendered){ this.view.refresh(true); } this.fireEvent('reconfigure', this, store, colModel); @@ -61749,9 +64569,6 @@ function(grid, rowIndex, columnIndex, e) { // private onDestroy : function(){ if(this.rendered){ - var c = this.body; - c.removeAllListeners(); - c.update(''); Ext.destroy(this.view, this.loadMask); }else if(this.store && this.store.autoDestroy){ this.store.destroy(); @@ -61764,21 +64581,31 @@ function(grid, rowIndex, columnIndex, e) { // private processEvent : function(name, e){ this.fireEvent(name, e); - var t = e.getTarget(); - var v = this.view; - var header = v.findHeaderIndex(t); + var t = e.getTarget(), + v = this.view, + header = v.findHeaderIndex(t); + if(header !== false){ this.fireEvent('header' + name, this, header, e); }else{ - var row = v.findRowIndex(t); - var cell = v.findCellIndex(t); + var row = v.findRowIndex(t), + cell, + body; if(row !== false){ this.fireEvent('row' + name, this, row, e); + cell = v.findCellIndex(t); + body = v.findRowBody(t); if(cell !== false){ this.fireEvent('cell' + name, this, row, cell, e); } + if(body){ + this.fireEvent('rowbody' + name, this, row, e); + } + }else{ + this.fireEvent('container' + name, this, e); } } + this.view.processEvent(name, e); }, // private @@ -61803,8 +64630,11 @@ function(grid, rowIndex, columnIndex, e) { // private walkCells : function(row, col, step, fn, scope){ - var cm = this.colModel, clen = cm.getColumnCount(); - var ds = this.store, rlen = ds.getCount(), first = true; + var cm = this.colModel, + clen = cm.getColumnCount(), + ds = this.store, + rlen = ds.getCount(), + first = true; if(step < 0){ if(col < 0){ row--; @@ -62022,7 +64852,7 @@ function(grid, rowIndex, columnIndex, e) { * @hide */ /** - * @event afterLayout + * @event afterlayout * @hide */ /** @@ -62106,67 +64936,7 @@ Ext.reg('grid', Ext.grid.GridPanel);/** * @constructor * @param {Object} config */ -Ext.grid.GridView = function(config){ - Ext.apply(this, config); - // These events are only used internally by the grid components - this.addEvents( - /** - * @event beforerowremoved - * Internal UI Event. Fired before a row is removed. - * @param {Ext.grid.GridView} view - * @param {Number} rowIndex The index of the row to be removed. - * @param {Ext.data.Record} record The Record to be removed - */ - 'beforerowremoved', - /** - * @event beforerowsinserted - * Internal UI Event. Fired before rows are inserted. - * @param {Ext.grid.GridView} view - * @param {Number} firstRow The index of the first row to be inserted. - * @param {Number} lastRow The index of the last row to be inserted. - */ - 'beforerowsinserted', - /** - * @event beforerefresh - * Internal UI Event. Fired before the view is refreshed. - * @param {Ext.grid.GridView} view - */ - 'beforerefresh', - /** - * @event rowremoved - * Internal UI Event. Fired after a row is removed. - * @param {Ext.grid.GridView} view - * @param {Number} rowIndex The index of the row that was removed. - * @param {Ext.data.Record} record The Record that was removed - */ - 'rowremoved', - /** - * @event rowsinserted - * Internal UI Event. Fired after rows are inserted. - * @param {Ext.grid.GridView} view - * @param {Number} firstRow The index of the first inserted. - * @param {Number} lastRow The index of the last row inserted. - */ - 'rowsinserted', - /** - * @event rowupdated - * Internal UI Event. Fired after a row has been updated. - * @param {Ext.grid.GridView} view - * @param {Number} firstRow The index of the row updated. - * @param {Ext.data.record} record The Record backing the row updated. - */ - 'rowupdated', - /** - * @event refresh - * Internal UI Event. Fired after the GridView's body has been refreshed. - * @param {Ext.grid.GridView} view - */ - 'refresh' - ); - Ext.grid.GridView.superclass.constructor.call(this); -}; - -Ext.extend(Ext.grid.GridView, Ext.util.Observable, { +Ext.grid.GridView = Ext.extend(Ext.util.Observable, { /** * Override this function to apply custom CSS classes to rows during rendering. You can also supply custom * parameters to the row template for the current row to customize how it is rendered using the rowParams @@ -62315,6 +65085,11 @@ viewConfig: { * @cfg {Number} rowSelectorDepth The number of levels to search for rows in event delegation (defaults to 10) */ rowSelectorDepth : 10, + + /** + * @cfg {Number} rowBodySelectorDepth The number of levels to search for row bodies in event delegation (defaults to 10) + */ + rowBodySelectorDepth : 10, /** * @cfg {String} cellSelector The selector used to find cells internally (defaults to 'td.x-grid3-cell') @@ -62325,10 +65100,75 @@ viewConfig: { */ rowSelector : 'div.x-grid3-row', + /** + * @cfg {String} rowBodySelector The selector used to find row bodies internally (defaults to 'div.x-grid3-row') + */ + rowBodySelector : 'div.x-grid3-row-body', + // private firstRowCls: 'x-grid3-row-first', lastRowCls: 'x-grid3-row-last', rowClsRe: /(?:^|\s+)x-grid3-row-(first|last|alt)(?:\s+|$)/g, + + constructor : function(config){ + Ext.apply(this, config); + // These events are only used internally by the grid components + this.addEvents( + /** + * @event beforerowremoved + * Internal UI Event. Fired before a row is removed. + * @param {Ext.grid.GridView} view + * @param {Number} rowIndex The index of the row to be removed. + * @param {Ext.data.Record} record The Record to be removed + */ + 'beforerowremoved', + /** + * @event beforerowsinserted + * Internal UI Event. Fired before rows are inserted. + * @param {Ext.grid.GridView} view + * @param {Number} firstRow The index of the first row to be inserted. + * @param {Number} lastRow The index of the last row to be inserted. + */ + 'beforerowsinserted', + /** + * @event beforerefresh + * Internal UI Event. Fired before the view is refreshed. + * @param {Ext.grid.GridView} view + */ + 'beforerefresh', + /** + * @event rowremoved + * Internal UI Event. Fired after a row is removed. + * @param {Ext.grid.GridView} view + * @param {Number} rowIndex The index of the row that was removed. + * @param {Ext.data.Record} record The Record that was removed + */ + 'rowremoved', + /** + * @event rowsinserted + * Internal UI Event. Fired after rows are inserted. + * @param {Ext.grid.GridView} view + * @param {Number} firstRow The index of the first inserted. + * @param {Number} lastRow The index of the last row inserted. + */ + 'rowsinserted', + /** + * @event rowupdated + * Internal UI Event. Fired after a row has been updated. + * @param {Ext.grid.GridView} view + * @param {Number} firstRow The index of the row updated. + * @param {Ext.data.record} record The Record backing the row updated. + */ + 'rowupdated', + /** + * @event refresh + * Internal UI Event. Fired after the GridView's body has been refreshed. + * @param {Ext.grid.GridView} view + */ + 'refresh' + ); + Ext.grid.GridView.superclass.constructor.call(this); + }, /* -------------------------------- UI Specific ----------------------------- */ @@ -62387,7 +65227,7 @@ viewConfig: { for(var k in ts){ var t = ts[k]; - if(t && typeof t.compile == 'function' && !t.compiled){ + if(t && Ext.isFunction(t.compile) && !t.compiled){ t.disableFormats = true; t.compile(); } @@ -62520,6 +65360,18 @@ viewConfig: { var r = this.findRow(el); return r ? r.rowIndex : false; }, + + /** + * Return the HtmlElement representing the grid row body which contains the passed element. + * @param {HTMLElement} el The target HTMLElement + * @return {HTMLElement} The row body element, or null if the target element is not within a row body of this GridView. + */ + findRowBody : function(el){ + if(!el){ + return false; + } + return this.fly(el).findParent(this.rowBodySelector, this.rowBodySelectorDepth); + }, // getter methods for fetching elements dynamically in the grid @@ -62725,12 +65577,12 @@ viewConfig: { p.id = c.id; p.css = i === 0 ? 'x-grid3-cell-first ' : (i == last ? 'x-grid3-cell-last ' : ''); p.attr = p.cellAttr = ''; - p.value = c.renderer(r.data[c.name], p, r, rowIndex, i, ds); + p.value = c.renderer.call(c.scope, r.data[c.name], p, r, rowIndex, i, ds); p.style = c.style; if(Ext.isEmpty(p.value)){ p.value = ' '; } - if(this.markDirty && r.dirty && typeof r.modified[c.name] !== 'undefined'){ + if(this.markDirty && r.dirty && Ext.isDefined(r.modified[c.name])){ p.css += ' x-grid3-dirty-cell'; } cb[cb.length] = ct.apply(p); @@ -62758,18 +65610,24 @@ viewConfig: { if(!this.ds || this.ds.getCount() < 1){ return; } - var rows = this.getRows(); + var rows = this.getRows(), + len = rows.length, + i, r; + skipStripe = skipStripe || !this.grid.stripeRows; startRow = startRow || 0; - Ext.each(rows, function(row, idx){ - row.rowIndex = idx; - if(!skipStripe){ - row.className = row.className.replace(this.rowClsRe, ' '); - if ((idx + 1) % 2 === 0){ - row.className += ' x-grid3-row-alt'; - } - } - }, this); + for(i = 0; i= last){ + this.fireEvent('beforerowsinserted', this, firstRow, lastRow); this.refresh(); + this.fireEvent('rowsinserted', this, firstRow, lastRow); }else{ if(!isUpdate){ this.fireEvent('beforerowsinserted', this, firstRow, lastRow); @@ -63190,7 +66054,7 @@ viewConfig: { // private getColumnWidth : function(col){ var w = this.cm.getColumnWidth(col); - if(typeof w == 'number'){ + if(Ext.isNumber(w)){ return (Ext.isBorderBox || (Ext.isWebKit && !Ext.isSafari2) ? w : (w - this.borderWidth > 0 ? w - this.borderWidth : 0)) + 'px'; } return w; @@ -63217,7 +66081,7 @@ viewConfig: { } var vc = cm.getColumnCount(true); - var ac = vc-(typeof omitColumn == 'number' ? 1 : 0); + var ac = vc-(Ext.isNumber(omitColumn) ? 1 : 0); if(ac === 0){ ac = 1; omitColumn = undefined; @@ -63284,8 +66148,9 @@ viewConfig: { for(var i = 0; i < colCount; i++){ var name = cm.getDataIndex(i); cs[i] = { - name : (typeof name == 'undefined' ? this.ds.fields.get(i).name : name), + name : (!Ext.isDefined(name) ? this.ds.fields.get(i).name : name), renderer : cm.getRenderer(i), + scope: cm.getRendererScope(i), id : cm.getColumnId(i), style : this.getColumnStyle(i) }; @@ -63306,7 +66171,7 @@ viewConfig: { var cs = this.getColumnData(); startRow = startRow || 0; - endRow = typeof endRow == "undefined"? ds.getCount()-1 : endRow; + endRow = !Ext.isDefined(endRow) ? ds.getCount()-1 : endRow; // records to render var rs = ds.getRange(startRow, endRow); @@ -63323,7 +66188,7 @@ viewConfig: { // private refreshRow : function(record){ var ds = this.ds, index; - if(typeof record == 'number'){ + if(Ext.isNumber(record)){ index = record; record = ds.getAt(index); if(!record){ @@ -63385,6 +66250,16 @@ viewConfig: { } }, + // private + clearHeaderSortState : function(){ + if(!this.sortState){ + return; + } + this.grid.fireEvent('sortchange', this.grid, null); + this.mainHd.select('td').removeClass(this.sortClasses); + delete this.sortState; + }, + // private destroy : function(){ if(this.colMenu){ @@ -63431,9 +66306,9 @@ viewConfig: { delete Ext.dd.DDM.ids[this.columnDrop.ddGroup]; } - if (this.splitone){ // enableColumnResize - this.splitone.destroy(); - delete this.splitone._domRef; + if (this.splitZone){ // enableColumnResize + this.splitZone.destroy(); + delete this.splitZone._domRef; delete Ext.dd.DDM.ids["gridSplitters" + this.grid.getGridEl().id]; } @@ -63562,6 +66437,7 @@ viewConfig: { // private onAdd : function(ds, records, index){ + this.insertRows(ds, index, index + (records.length-1)); }, @@ -63858,11 +66734,15 @@ Ext.extend(Ext.grid.GridView.SplitDragZone, Ext.dd.DDProxy, { this.startPos = x; Ext.dd.DDProxy.prototype.b4StartDrag.call(this, x, y); }, + + allowHeaderDrag : function(e){ + return true; + }, handleMouseDown : function(e){ var t = this.view.findHeaderCell(e.getTarget()); - if(t){ + if(t && this.allowHeaderDrag(e)){ var xy = this.view.fly(t).getXY(), x = xy[0], y = xy[1]; var exy = e.getXY(), ex = exy[0]; var w = t.offsetWidth, adjust = false; @@ -63913,19 +66793,21 @@ Ext.extend(Ext.grid.GridView.SplitDragZone, Ext.dd.DDProxy, { }); // private // This is a support class used internally by the Grid components -Ext.grid.HeaderDragZone = function(grid, hd, hd2){ - this.grid = grid; - this.view = grid.getView(); - this.ddGroup = "gridHeader" + this.grid.getGridEl().id; - Ext.grid.HeaderDragZone.superclass.constructor.call(this, hd); - if(hd2){ - this.setHandleElId(Ext.id(hd)); - this.setOuterHandleElId(Ext.id(hd2)); - } - this.scroll = false; -}; -Ext.extend(Ext.grid.HeaderDragZone, Ext.dd.DragZone, { +Ext.grid.HeaderDragZone = Ext.extend(Ext.dd.DragZone, { maxDragWidth: 120, + + constructor : function(grid, hd, hd2){ + this.grid = grid; + this.view = grid.getView(); + this.ddGroup = "gridHeader" + this.grid.getGridEl().id; + Ext.grid.HeaderDragZone.superclass.constructor.call(this, hd); + if(hd2){ + this.setHandleElId(Ext.id(hd)); + this.setOuterHandleElId(Ext.id(hd2)); + } + this.scroll = false; + }, + getDragData : function(e){ var t = Ext.lib.Event.getTarget(e); var h = this.view.findHeaderCell(t); @@ -63961,28 +66843,29 @@ Ext.extend(Ext.grid.HeaderDragZone, Ext.dd.DragZone, { // private // This is a support class used internally by the Grid components -Ext.grid.HeaderDropZone = function(grid, hd, hd2){ - this.grid = grid; - this.view = grid.getView(); - // split the proxies so they don't interfere with mouse events - this.proxyTop = Ext.DomHelper.append(document.body, { - cls:"col-move-top", html:" " - }, true); - this.proxyBottom = Ext.DomHelper.append(document.body, { - cls:"col-move-bottom", html:" " - }, true); - this.proxyTop.hide = this.proxyBottom.hide = function(){ - this.setLeftTop(-100,-100); - this.setStyle("visibility", "hidden"); - }; - this.ddGroup = "gridHeader" + this.grid.getGridEl().id; - // temporarily disabled - //Ext.dd.ScrollManager.register(this.view.scroller.dom); - Ext.grid.HeaderDropZone.superclass.constructor.call(this, grid.getGridEl().dom); -}; -Ext.extend(Ext.grid.HeaderDropZone, Ext.dd.DropZone, { +Ext.grid.HeaderDropZone = Ext.extend(Ext.dd.DropZone, { proxyOffsets : [-4, -9], fly: Ext.Element.fly, + + constructor : function(grid, hd, hd2){ + this.grid = grid; + this.view = grid.getView(); + // split the proxies so they don't interfere with mouse events + this.proxyTop = Ext.DomHelper.append(document.body, { + cls:"col-move-top", html:" " + }, true); + this.proxyBottom = Ext.DomHelper.append(document.body, { + cls:"col-move-bottom", html:" " + }, true); + this.proxyTop.hide = this.proxyBottom.hide = function(){ + this.setLeftTop(-100,-100); + this.setStyle("visibility", "hidden"); + }; + this.ddGroup = "gridHeader" + this.grid.getGridEl().id; + // temporarily disabled + //Ext.dd.ScrollManager.register(this.view.scroller.dom); + Ext.grid.HeaderDropZone.superclass.constructor.call(this, grid.getGridEl().dom); + }, getTargetFromEvent : function(e){ var t = Ext.lib.Event.getTarget(e); @@ -64088,15 +66971,14 @@ Ext.extend(Ext.grid.HeaderDropZone, Ext.dd.DropZone, { } }); - -Ext.grid.GridView.ColumnDragZone = function(grid, hd){ - Ext.grid.GridView.ColumnDragZone.superclass.constructor.call(this, grid, hd, null); - this.proxy.el.addClass('x-grid3-col-dd'); -}; - -Ext.extend(Ext.grid.GridView.ColumnDragZone, Ext.grid.HeaderDragZone, { +Ext.grid.GridView.ColumnDragZone = Ext.extend(Ext.grid.HeaderDragZone, { + + constructor : function(grid, hd){ + Ext.grid.GridView.ColumnDragZone.superclass.constructor.call(this, grid, hd, null); + this.proxy.el.addClass('x-grid3-col-dd'); + }, + handleMouseDown : function(e){ - }, callHandleMouseDown : function(e){ @@ -64104,20 +66986,21 @@ Ext.extend(Ext.grid.GridView.ColumnDragZone, Ext.grid.HeaderDragZone, { } });// private // This is a support class used internally by the Grid components -Ext.grid.SplitDragZone = function(grid, hd, hd2){ - this.grid = grid; - this.view = grid.getView(); - this.proxy = this.view.resizeProxy; - Ext.grid.SplitDragZone.superclass.constructor.call(this, hd, - "gridSplitters" + this.grid.getGridEl().id, { - dragElId : Ext.id(this.proxy.dom), resizeFrame:false - }); - this.setHandleElId(Ext.id(hd)); - this.setOuterHandleElId(Ext.id(hd2)); - this.scroll = false; -}; -Ext.extend(Ext.grid.SplitDragZone, Ext.dd.DDProxy, { +Ext.grid.SplitDragZone = Ext.extend(Ext.dd.DDProxy, { fly: Ext.Element.fly, + + constructor : function(grid, hd, hd2){ + this.grid = grid; + this.view = grid.getView(); + this.proxy = this.view.resizeProxy; + Ext.grid.SplitDragZone.superclass.constructor.call(this, hd, + "gridSplitters" + this.grid.getGridEl().id, { + dragElId : Ext.id(this.proxy.dom), resizeFrame:false + }); + this.setHandleElId(Ext.id(hd)); + this.setOuterHandleElId(Ext.id(hd2)); + this.scroll = false; + }, b4StartDrag : function(x, y){ this.view.headersDisabled = true; @@ -64330,66 +67213,7 @@ Ext.extend(Ext.grid.GridDragZone, Ext.dd.DragZone, { * @param {Mixed} config Specify either an Array of {@link Ext.grid.Column} configuration objects or specify * a configuration Object (see introductory section discussion utilizing Initialization Method 2 above). */ -Ext.grid.ColumnModel = function(config){ - /** - * An Array of {@link Ext.grid.Column Column definition} objects representing the configuration - * of this ColumnModel. See {@link Ext.grid.Column} for the configuration properties that may - * be specified. - * @property config - * @type Array - */ - if(config.columns){ - Ext.apply(this, config); - this.setConfig(config.columns, true); - }else{ - this.setConfig(config, true); - } - this.addEvents( - /** - * @event widthchange - * Fires when the width of a column is programmaticially changed using - * {@link #setColumnWidth}. - * Note internal resizing suppresses the event from firing. See also - * {@link Ext.grid.GridPanel}.{@link #columnresize}. - * @param {ColumnModel} this - * @param {Number} columnIndex The column index - * @param {Number} newWidth The new width - */ - "widthchange", - /** - * @event headerchange - * Fires when the text of a header changes. - * @param {ColumnModel} this - * @param {Number} columnIndex The column index - * @param {String} newText The new header text - */ - "headerchange", - /** - * @event hiddenchange - * Fires when a column is hidden or "unhidden". - * @param {ColumnModel} this - * @param {Number} columnIndex The column index - * @param {Boolean} hidden true if hidden, false otherwise - */ - "hiddenchange", - /** - * @event columnmoved - * Fires when a column is moved. - * @param {ColumnModel} this - * @param {Number} oldIndex - * @param {Number} newIndex - */ - "columnmoved", - /** - * @event configchange - * Fires when the configuration is changed - * @param {ColumnModel} this - */ - "configchange" - ); - Ext.grid.ColumnModel.superclass.constructor.call(this); -}; -Ext.extend(Ext.grid.ColumnModel, Ext.util.Observable, { +Ext.grid.ColumnModel = Ext.extend(Ext.util.Observable, { /** * @cfg {Number} defaultWidth (optional) The width of columns which have no {@link #width} * specified (defaults to 100). This property shall preferably be configured through the @@ -64412,6 +67236,66 @@ Ext.extend(Ext.grid.ColumnModel, Ext.util.Observable, { * configuration options to all {@link #columns}. Configuration options specified with * individual {@link Ext.grid.Column column} configs will supersede these {@link #defaults}. */ + + constructor : function(config){ + /** + * An Array of {@link Ext.grid.Column Column definition} objects representing the configuration + * of this ColumnModel. See {@link Ext.grid.Column} for the configuration properties that may + * be specified. + * @property config + * @type Array + */ + if(config.columns){ + Ext.apply(this, config); + this.setConfig(config.columns, true); + }else{ + this.setConfig(config, true); + } + this.addEvents( + /** + * @event widthchange + * Fires when the width of a column is programmaticially changed using + * {@link #setColumnWidth}. + * Note internal resizing suppresses the event from firing. See also + * {@link Ext.grid.GridPanel}.{@link #columnresize}. + * @param {ColumnModel} this + * @param {Number} columnIndex The column index + * @param {Number} newWidth The new width + */ + "widthchange", + /** + * @event headerchange + * Fires when the text of a header changes. + * @param {ColumnModel} this + * @param {Number} columnIndex The column index + * @param {String} newText The new header text + */ + "headerchange", + /** + * @event hiddenchange + * Fires when a column is hidden or "unhidden". + * @param {ColumnModel} this + * @param {Number} columnIndex The column index + * @param {Boolean} hidden true if hidden, false otherwise + */ + "hiddenchange", + /** + * @event columnmoved + * Fires when a column is moved. + * @param {ColumnModel} this + * @param {Number} oldIndex + * @param {Number} newIndex + */ + "columnmoved", + /** + * @event configchange + * Fires when the configuration is changed + * @param {ColumnModel} this + */ + "configchange" + ); + Ext.grid.ColumnModel.superclass.constructor.call(this); + }, /** * Returns the id of the column at the specified index. @@ -64441,10 +67325,7 @@ Ext.extend(Ext.grid.ColumnModel, Ext.util.Observable, { if(!initial){ // cleanup delete this.totalWidth; for(i = 0, len = this.config.length; i < len; i++){ - c = this.config[i]; - if(c.editor){ - c.editor.destroy(); - } + this.config[i].destroy(); } } @@ -64460,7 +67341,7 @@ Ext.extend(Ext.grid.ColumnModel, Ext.util.Observable, { for(i = 0, len = config.length; i < len; i++){ c = Ext.applyIf(config[i], this.defaults); // if no id, create one using column's ordinal position - if(typeof c.id == 'undefined'){ + if(Ext.isEmpty(c.id)){ c.id = i; } if(!c.isColumn){ @@ -64538,8 +67419,10 @@ var columns = grid.getColumnModel().getColumnsBy(function(c){ return c.hidden; }); - * @param {Function} fn - * @param {Object} scope (optional) + * @param {Function} fn A function which, when passed a {@link Ext.grid.Column Column} object, must + * return true if the column is to be included in the returned Array. + * @param {Object} scope (optional) The scope (this reference) in which the function + * is executed. Defaults to this ColumnModel. * @return {Array} result */ getColumnsBy : function(fn, scope){ @@ -64582,6 +67465,10 @@ var columns = grid.getColumnModel().getColumnsBy(function(c){ } return this.config[col].renderer; }, + + getRendererScope : function(col){ + return this.config[col].scope; + }, /** * Sets the rendering (formatting) function for a column. See {@link Ext.util.Format} for some @@ -64743,7 +67630,11 @@ var grid = new Ext.grid.GridPanel({ * @return {Boolean} */ isCellEditable : function(colIndex, rowIndex){ - return (this.config[colIndex].editable || (typeof this.config[colIndex].editable == "undefined" && this.config[colIndex].editor)) ? true : false; + var c = this.config[colIndex], + ed = c.editable; + + //force boolean + return !!(ed || (!Ext.isDefined(ed) && c.editor)); }, /** @@ -64816,16 +67707,15 @@ myGrid.getColumnModel().setHidden(0, true); // hide column 0 (0 = the first colu * @param {Object} editor The editor object */ setEditor : function(col, editor){ - Ext.destroy(this.config[col].editor); - this.config[col].editor = editor; + this.config[col].setEditor(editor); }, /** * Destroys this column model by purging any event listeners, and removing any editors. */ destroy : function(){ - for(var i = 0, c = this.config, len = c.length; i < len; i++){ - Ext.destroy(c[i].editor); + for(var i = 0, len = this.config.length; i < len; i++){ + this.config[i].destroy(); } this.purgeListeners(); } @@ -64844,17 +67734,17 @@ Ext.grid.ColumnModel.defaultRenderer = function(value){ * implemented by descendant classes. This class should not be directly instantiated. * @constructor */ -Ext.grid.AbstractSelectionModel = function(){ - this.locked = false; - Ext.grid.AbstractSelectionModel.superclass.constructor.call(this); -}; - -Ext.extend(Ext.grid.AbstractSelectionModel, Ext.util.Observable, { +Ext.grid.AbstractSelectionModel = Ext.extend(Ext.util.Observable, { /** * The GridPanel for which this SelectionModel is handling selection. Read-only. * @type Object * @property grid */ + + constructor : function(){ + this.locked = false; + Ext.grid.AbstractSelectionModel.superclass.constructor.call(this); + }, /** @ignore Called by the grid automatically. Do not call directly. */ init : function(grid){ @@ -64897,60 +67787,59 @@ Ext.extend(Ext.grid.AbstractSelectionModel, Ext.util.Observable, { * @constructor * @param {Object} config */ -Ext.grid.RowSelectionModel = function(config){ - Ext.apply(this, config); - this.selections = new Ext.util.MixedCollection(false, function(o){ - return o.id; - }); - - this.last = false; - this.lastActive = false; - - this.addEvents( - /** - * @event selectionchange - * Fires when the selection changes - * @param {SelectionModel} this - */ - 'selectionchange', - /** - * @event beforerowselect - * Fires before a row is selected, return false to cancel the selection. - * @param {SelectionModel} this - * @param {Number} rowIndex The index to be selected - * @param {Boolean} keepExisting False if other selections will be cleared - * @param {Record} record The record to be selected - */ - 'beforerowselect', - /** - * @event rowselect - * Fires when a row is selected. - * @param {SelectionModel} this - * @param {Number} rowIndex The selected index - * @param {Ext.data.Record} r The selected record - */ - 'rowselect', - /** - * @event rowdeselect - * Fires when a row is deselected. To prevent deselection - * {@link Ext.grid.AbstractSelectionModel#lock lock the selections}. - * @param {SelectionModel} this - * @param {Number} rowIndex - * @param {Record} record - */ - 'rowdeselect' - ); - - Ext.grid.RowSelectionModel.superclass.constructor.call(this); -}; - -Ext.extend(Ext.grid.RowSelectionModel, Ext.grid.AbstractSelectionModel, { +Ext.grid.RowSelectionModel = Ext.extend(Ext.grid.AbstractSelectionModel, { /** * @cfg {Boolean} singleSelect * true to allow selection of only one row at a time (defaults to false * allowing multiple selections) */ singleSelect : false, + + constructor : function(config){ + Ext.apply(this, config); + this.selections = new Ext.util.MixedCollection(false, function(o){ + return o.id; + }); + + this.last = false; + this.lastActive = false; + + this.addEvents( + /** + * @event selectionchange + * Fires when the selection changes + * @param {SelectionModel} this + */ + 'selectionchange', + /** + * @event beforerowselect + * Fires before a row is selected, return false to cancel the selection. + * @param {SelectionModel} this + * @param {Number} rowIndex The index to be selected + * @param {Boolean} keepExisting False if other selections will be cleared + * @param {Record} record The record to be selected + */ + 'beforerowselect', + /** + * @event rowselect + * Fires when a row is selected. + * @param {SelectionModel} this + * @param {Number} rowIndex The selected index + * @param {Ext.data.Record} r The selected record + */ + 'rowselect', + /** + * @event rowdeselect + * Fires when a row is deselected. To prevent deselection + * {@link Ext.grid.AbstractSelectionModel#lock lock the selections}. + * @param {SelectionModel} this + * @param {Number} rowIndex + * @param {Record} record + */ + 'rowdeselect' + ); + Ext.grid.RowSelectionModel.superclass.constructor.call(this); + }, /** * @cfg {Boolean} moveEditorOnEnter @@ -65137,8 +68026,8 @@ Ext.extend(Ext.grid.RowSelectionModel, Ext.grid.AbstractSelectionModel, { * Calls the passed function with each selection. If the function returns * false, iteration is stopped and this function returns * false. Otherwise it returns true. - * @param {Function} fn - * @param {Object} scope (optional) + * @param {Function} fn The function to call upon each iteration. It is passed the selected {@link Ext.data.Record Record}. + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this RowSelectionModel. * @return {Boolean} true if all selections were iterated */ each : function(fn, scope){ @@ -65427,23 +68316,7 @@ Ext.extend(Ext.grid.RowSelectionModel, Ext.grid.AbstractSelectionModel, { *

    While subclasses are provided to render data in different ways, this class renders a passed * data field unchanged and is usually used for textual columns.

    */ -Ext.grid.Column = function(config){ - Ext.apply(this, config); - - if(Ext.isString(this.renderer)){ - this.renderer = Ext.util.Format[this.renderer]; - } else if(Ext.isObject(this.renderer)){ - this.scope = this.renderer.scope; - this.renderer = this.renderer.fn; - } - this.renderer = this.renderer.createDelegate(this.scope || config); - - if(this.editor){ - this.editor = Ext.create(this.editor, 'textfield'); - } -}; - -Ext.grid.Column.prototype = { +Ext.grid.Column = Ext.extend(Object, { /** * @cfg {Boolean} editable Optional. Defaults to true, enabling the configured * {@link #editor}. Set to false to initially disable editing on this column. @@ -65648,6 +68521,24 @@ var grid = new Ext.grid.GridPanel({ * Defaults to true. */ isColumn : true, + + constructor : function(config){ + Ext.apply(this, config); + + if(Ext.isString(this.renderer)){ + this.renderer = Ext.util.Format[this.renderer]; + }else if(Ext.isObject(this.renderer)){ + this.scope = this.renderer.scope; + this.renderer = this.renderer.fn; + } + if(!this.scope){ + this.scope = this; + } + + var ed = this.editor; + delete this.editor; + this.setEditor(ed); + }, /** * Optional. A function which returns displayable data when passed the following parameters: @@ -65678,6 +68569,32 @@ var grid = new Ext.grid.GridPanel({ getEditor: function(rowIndex){ return this.editable !== false ? this.editor : null; }, + + /** + * Sets a new editor for this column. + * @param {Ext.Editor/Ext.form.Field} editor The editor to set + */ + setEditor : function(editor){ + if(this.editor){ + this.editor.destroy(); + } + this.editor = null; + if(editor){ + //not an instance, create it + if(!editor.isXType){ + editor = Ext.create(editor, 'textfield'); + } + //check if it's wrapped in an editor + if(!editor.startEdit){ + editor = new Ext.grid.GridEditor(editor); + } + this.editor = editor; + } + }, + + destroy : function(){ + this.setEditor(null); + }, /** * Returns the {@link Ext.Editor editor} defined for this column that was created to wrap the {@link Ext.form.Field Field} @@ -65686,20 +68603,9 @@ var grid = new Ext.grid.GridPanel({ * @return {Ext.Editor} */ getCellEditor: function(rowIndex){ - var editor = this.getEditor(rowIndex); - if(editor){ - if(!editor.startEdit){ - if(!editor.gridEditor){ - editor.gridEditor = new Ext.grid.GridEditor(editor); - } - return editor.gridEditor; - }else if(editor.startEdit){ - return editor; - } - } - return null; + return this.getEditor(rowIndex); } -}; +}); /** * @class Ext.grid.BooleanColumn @@ -65795,7 +68701,7 @@ Ext.grid.TemplateColumn = Ext.extend(Ext.grid.Column, { */ constructor: function(cfg){ Ext.grid.TemplateColumn.superclass.constructor.call(this, cfg); - var tpl = Ext.isObject(this.tpl) ? this.tpl : new Ext.XTemplate(this.tpl); + var tpl = (!Ext.isPrimitive(this.tpl) && this.tpl.compile) ? this.tpl : new Ext.XTemplate(this.tpl); this.renderer = function(value, p, r){ return tpl.apply(r.data); }; @@ -65841,14 +68747,7 @@ Ext.grid.Column.types = { * @constructor * @param {Object} config The configuration options */ -Ext.grid.RowNumberer = function(config){ - Ext.apply(this, config); - if(this.rowspan){ - this.renderer = this.renderer.createDelegate(this); - } -}; - -Ext.grid.RowNumberer.prototype = { +Ext.grid.RowNumberer = Ext.extend(Object, { /** * @cfg {String} header Any valid text or HTML fragment to display in the header cell for the row * number column (defaults to ''). @@ -65863,6 +68762,13 @@ Ext.grid.RowNumberer.prototype = { * @hide */ sortable: false, + + constructor : function(config){ + Ext.apply(this, config); + if(this.rowspan){ + this.renderer = this.renderer.createDelegate(this); + } + }, // private fixed:true, @@ -65878,7 +68784,7 @@ Ext.grid.RowNumberer.prototype = { } return rowIndex+1; } -};/** +});/** * @class Ext.grid.CheckboxSelectionModel * @extends Ext.grid.RowSelectionModel * A custom selection model that renders a column of checkboxes that can be toggled to select or deselect rows. @@ -65987,46 +68893,46 @@ Ext.grid.CheckboxSelectionModel = Ext.extend(Ext.grid.RowSelectionModel, { * @constructor * @param {Object} config The object containing the configuration of this model. */ -Ext.grid.CellSelectionModel = function(config){ - Ext.apply(this, config); - - this.selection = null; - - this.addEvents( - /** - * @event beforecellselect - * Fires before a cell is selected, return false to cancel the selection. - * @param {SelectionModel} this - * @param {Number} rowIndex The selected row index - * @param {Number} colIndex The selected cell index - */ - "beforecellselect", - /** - * @event cellselect - * Fires when a cell is selected. - * @param {SelectionModel} this - * @param {Number} rowIndex The selected row index - * @param {Number} colIndex The selected cell index - */ - "cellselect", - /** - * @event selectionchange - * Fires when the active selection changes. - * @param {SelectionModel} this - * @param {Object} selection null for no selection or an object with two properties - *
      - *
    • cell : see {@link #getSelectedCell} - *
    • record : Ext.data.record

      The {@link Ext.data.Record Record} - * which provides the data for the row containing the selection

    • - *
    - */ - "selectionchange" - ); - - Ext.grid.CellSelectionModel.superclass.constructor.call(this); -}; +Ext.grid.CellSelectionModel = Ext.extend(Ext.grid.AbstractSelectionModel, { + + constructor : function(config){ + Ext.apply(this, config); -Ext.extend(Ext.grid.CellSelectionModel, Ext.grid.AbstractSelectionModel, { + this.selection = null; + + this.addEvents( + /** + * @event beforecellselect + * Fires before a cell is selected, return false to cancel the selection. + * @param {SelectionModel} this + * @param {Number} rowIndex The selected row index + * @param {Number} colIndex The selected cell index + */ + "beforecellselect", + /** + * @event cellselect + * Fires when a cell is selected. + * @param {SelectionModel} this + * @param {Number} rowIndex The selected row index + * @param {Number} colIndex The selected cell index + */ + "cellselect", + /** + * @event selectionchange + * Fires when the active selection changes. + * @param {SelectionModel} this + * @param {Object} selection null for no selection or an object with two properties + *
      + *
    • cell : see {@link #getSelectedCell} + *
    • record : Ext.data.record

      The {@link Ext.data.Record Record} + * which provides the data for the row containing the selection

    • + *
    + */ + "selectionchange" + ); + + Ext.grid.CellSelectionModel.superclass.constructor.call(this); + }, /** @ignore */ initEvents : function(){ @@ -66284,7 +69190,7 @@ Ext.grid.EditorGridPanel = Ext.extend(Ext.grid.GridPanel, { * editing that cell.

    */ clicksToEdit: 2, - + /** * @cfg {Boolean} forceValidation * True to force validation even if the value is unmodified (defaults to false) @@ -66296,15 +69202,15 @@ Ext.grid.EditorGridPanel = Ext.extend(Ext.grid.GridPanel, { // private detectEdit: false, - /** - * @cfg {Boolean} autoEncode - * True to automatically HTML encode and decode values pre and post edit (defaults to false) - */ - autoEncode : false, + /** + * @cfg {Boolean} autoEncode + * True to automatically HTML encode and decode values pre and post edit (defaults to false) + */ + autoEncode : false, - /** - * @cfg {Boolean} trackMouseOver @hide - */ + /** + * @cfg {Boolean} trackMouseOver @hide + */ // private trackMouseOver: false, // causes very odd FF errors @@ -66322,7 +69228,7 @@ Ext.grid.EditorGridPanel = Ext.extend(Ext.grid.GridPanel, { this.activeEditor = null; - this.addEvents( + this.addEvents( /** * @event beforeedit * Fires before cell editing is triggered. The edit event object has the following properties
    @@ -66351,13 +69257,13 @@ Ext.grid.EditorGridPanel = Ext.extend(Ext.grid.GridPanel, { *
  • column - The grid column index
  • * * - *
     
    +             * 
    
     grid.on('afteredit', afterEdit, this );
     
     function afterEdit(e) {
         // execute an XHR to send/commit data to the server, in callback do (if successful):
         e.record.commit();
    -}; 
    +};
                  * 
    * @param {Object} e An edit event (see above for description) */ @@ -66380,10 +69286,10 @@ function afterEdit(e) { * records (not all). By observing the grid's validateedit event, it can be cancelled if * the edit occurs on a targeted row (for example) and then setting the field's new value * in the Record directly: - *
     
    +             * 
    
     grid.on('validateedit', function(e) {
       var myTargetRow = 6;
    - 
    +
       if (e.row == myTargetRow) {
         e.cancel = true;
         e.record.data[e.field] = e.value;
    @@ -66414,6 +69320,14 @@ grid.on('validateedit', function(e) {
             }
         },
     
    +    onResize : function(){
    +        Ext.grid.EditorGridPanel.superclass.onResize.apply(this, arguments);
    +        var ae = this.activeEditor;
    +        if(this.editing && ae){
    +            ae.realign(true);
    +        }
    +    },
    +
         // private
         onCellDblClick : function(g, row, col){
             this.startEditing(row, col);
    @@ -66424,8 +69338,8 @@ grid.on('validateedit', function(e) {
             if(e.button !== 0){
                 return;
             }
    -        var row = this.view.findRowIndex(t);
    -        var col = this.view.findCellIndex(t);
    +        var row = this.view.findRowIndex(t),
    +            col = this.view.findCellIndex(t);
             if(row !== false && col !== false){
                 this.stopEditing();
                 if(this.selModel.getSelectedCell){ // cell sm
    @@ -66445,9 +69359,9 @@ grid.on('validateedit', function(e) {
         onEditComplete : function(ed, value, startValue){
             this.editing = false;
             this.activeEditor = null;
    -        
    -		var r = ed.record;
    -        var field = this.colModel.getDataIndex(ed.col);
    +
    +        var r = ed.record,
    +            field = this.colModel.getDataIndex(ed.col);
             value = this.postEditValue(value, startValue, r, field);
             if(this.forceValidation === true || String(value) !== String(startValue)){
                 var e = {
    @@ -66478,17 +69392,17 @@ grid.on('validateedit', function(e) {
             this.stopEditing();
             if(this.colModel.isCellEditable(col, row)){
                 this.view.ensureVisible(row, col, true);
    -            var r = this.store.getAt(row);
    -            var field = this.colModel.getDataIndex(col);
    -            var e = {
    -                grid: this,
    -                record: r,
    -                field: field,
    -                value: r.data[field],
    -                row: row,
    -                column: col,
    -                cancel:false
    -            };
    +            var r = this.store.getAt(row),
    +                field = this.colModel.getDataIndex(col),
    +                e = {
    +                    grid: this,
    +                    record: r,
    +                    field: field,
    +                    value: r.data[field],
    +                    row: row,
    +                    column: col,
    +                    cancel:false
    +                };
                 if(this.fireEvent("beforeedit", e) !== false && !e.cancel){
                     this.editing = true;
                     var ed = this.colModel.getCellEditor(col, row);
    @@ -66523,8 +69437,16 @@ grid.on('validateedit', function(e) {
                         col: col
                     };
                     this.activeEditor = ed;
    +                // Set the selectSameEditor flag if we are reusing the same editor again and
    +                // need to prevent the editor from firing onBlur on itself.
    +                ed.selectSameEditor = (this.activeEditor == this.lastActiveEditor);
                     var v = this.preEditValue(r, field);
                     ed.startEdit(this.view.getCell(row, col).firstChild, Ext.isDefined(v) ? v : '');
    +
    +                // Clear the selectSameEditor flag
    +                (function(){
    +                    delete ed.selectSameEditor;
    +                }).defer(50);
                 }
             }
         },
    @@ -66536,9 +69458,9 @@ grid.on('validateedit', function(e) {
         },
     
         // private
    -	postEditValue : function(value, originalValue, r, field){
    -		return this.autoEncode && Ext.isString(value) ? Ext.util.Format.htmlEncode(value) : value;
    -	},
    +    postEditValue : function(value, originalValue, r, field){
    +        return this.autoEncode && Ext.isString(value) ? Ext.util.Format.htmlEncode(value) : value;
    +    },
     
         /**
          * Stops any active editing
    @@ -66546,7 +69468,8 @@ grid.on('validateedit', function(e) {
          */
         stopEditing : function(cancel){
             if(this.editing){
    -            var ae = this.activeEditor;
    +            // Store the lastActiveEditor to check if it is changing
    +            var ae = this.lastActiveEditor = this.activeEditor;
                 if(ae){
                     ae[cancel === true ? 'cancelEdit' : 'completeEdit']();
                     this.view.focusCell(ae.row, ae.col);
    @@ -66604,18 +69527,20 @@ Ext.grid.PropertyRecord = Ext.data.Record.create([
      * @param {Ext.grid.Grid} grid The grid this store will be bound to
      * @param {Object} source The source data config object
      */
    -Ext.grid.PropertyStore = function(grid, source){
    -    this.grid = grid;
    -    this.store = new Ext.data.Store({
    -        recordType : Ext.grid.PropertyRecord
    -    });
    -    this.store.on('update', this.onUpdate,  this);
    -    if(source){
    -        this.setSource(source);
    -    }
    -    Ext.grid.PropertyStore.superclass.constructor.call(this);
    -};
    -Ext.extend(Ext.grid.PropertyStore, Ext.util.Observable, {
    +Ext.grid.PropertyStore = Ext.extend(Ext.util.Observable, {
    +    
    +    constructor : function(grid, source){
    +        this.grid = grid;
    +        this.store = new Ext.data.Store({
    +            recordType : Ext.grid.PropertyRecord
    +        });
    +        this.store.on('update', this.onUpdate,  this);
    +        if(source){
    +            this.setSource(source);
    +        }
    +        Ext.grid.PropertyStore.superclass.constructor.call(this);    
    +    },
    +    
         // protected - should only be called by the grid.  Use grid.setSource instead.
         setSource : function(o){
             this.source = o;
    @@ -66651,16 +69576,36 @@ Ext.extend(Ext.grid.PropertyStore, Ext.util.Observable, {
     
         // private
         isEditableValue: function(val){
    -        if(Ext.isDate(val)){
    -            return true;
    -        }
    -        return !(Ext.isObject(val) || Ext.isFunction(val));
    +        return Ext.isPrimitive(val) || Ext.isDate(val);
         },
     
         // private
    -    setValue : function(prop, value){
    -        this.source[prop] = value;
    -        this.store.getById(prop).set('value', value);
    +    setValue : function(prop, value, create){
    +        var r = this.getRec(prop);
    +        if(r){
    +            r.set('value', value);
    +            this.source[prop] = value;
    +        }else if(create){
    +            // only create if specified.
    +            this.source[prop] = value;
    +            r = new Ext.grid.PropertyRecord({name: prop, value: value}, prop);
    +            this.store.add(r);
    +
    +        }
    +    },
    +    
    +    // private
    +    remove : function(prop){
    +        var r = this.getRec(prop);
    +        if(r){
    +            this.store.remove(r);
    +            delete this.source[prop];
    +        }
    +    },
    +    
    +    // private
    +    getRec : function(prop){
    +        return this.store.getById(prop);
         },
     
         // protected - should only be called by the grid.  Use grid.getSource instead.
    @@ -66677,43 +69622,45 @@ Ext.extend(Ext.grid.PropertyStore, Ext.util.Observable, {
      * @param {Ext.grid.Grid} grid The grid this store will be bound to
      * @param {Object} source The source data config object
      */
    -Ext.grid.PropertyColumnModel = function(grid, store){
    -    var g = Ext.grid,
    -        f = Ext.form;
    -        
    -    this.grid = grid;
    -    g.PropertyColumnModel.superclass.constructor.call(this, [
    -        {header: this.nameText, width:50, sortable: true, dataIndex:'name', id: 'name', menuDisabled:true},
    -        {header: this.valueText, width:50, resizable:false, dataIndex: 'value', id: 'value', menuDisabled:true}
    -    ]);
    -    this.store = store;
    -
    -    var bfield = new f.Field({
    -        autoCreate: {tag: 'select', children: [
    -            {tag: 'option', value: 'true', html: 'true'},
    -            {tag: 'option', value: 'false', html: 'false'}
    -        ]},
    -        getValue : function(){
    -            return this.el.dom.value == 'true';
    -        }
    -    });
    -    this.editors = {
    -        'date' : new g.GridEditor(new f.DateField({selectOnFocus:true})),
    -        'string' : new g.GridEditor(new f.TextField({selectOnFocus:true})),
    -        'number' : new g.GridEditor(new f.NumberField({selectOnFocus:true, style:'text-align:left;'})),
    -        'boolean' : new g.GridEditor(bfield, {
    -            autoSize: 'both'
    -        })
    -    };
    -    this.renderCellDelegate = this.renderCell.createDelegate(this);
    -    this.renderPropDelegate = this.renderProp.createDelegate(this);
    -};
    -
    -Ext.extend(Ext.grid.PropertyColumnModel, Ext.grid.ColumnModel, {
    +Ext.grid.PropertyColumnModel = Ext.extend(Ext.grid.ColumnModel, {
         // private - strings used for locale support
         nameText : 'Name',
         valueText : 'Value',
         dateFormat : 'm/j/Y',
    +    trueText: 'true',
    +    falseText: 'false',
    +    
    +    constructor : function(grid, store){
    +        var g = Ext.grid,
    +	        f = Ext.form;
    +	        
    +	    this.grid = grid;
    +	    g.PropertyColumnModel.superclass.constructor.call(this, [
    +	        {header: this.nameText, width:50, sortable: true, dataIndex:'name', id: 'name', menuDisabled:true},
    +	        {header: this.valueText, width:50, resizable:false, dataIndex: 'value', id: 'value', menuDisabled:true}
    +	    ]);
    +	    this.store = store;
    +	
    +	    var bfield = new f.Field({
    +	        autoCreate: {tag: 'select', children: [
    +	            {tag: 'option', value: 'true', html: this.trueText},
    +	            {tag: 'option', value: 'false', html: this.falseText}
    +	        ]},
    +	        getValue : function(){
    +	            return this.el.dom.value == 'true';
    +	        }
    +	    });
    +	    this.editors = {
    +	        'date' : new g.GridEditor(new f.DateField({selectOnFocus:true})),
    +	        'string' : new g.GridEditor(new f.TextField({selectOnFocus:true})),
    +	        'number' : new g.GridEditor(new f.NumberField({selectOnFocus:true, style:'text-align:left;'})),
    +	        'boolean' : new g.GridEditor(bfield, {
    +	            autoSize: 'both'
    +	        })
    +	    };
    +	    this.renderCellDelegate = this.renderCell.createDelegate(this);
    +	    this.renderPropDelegate = this.renderProp.createDelegate(this);
    +    },
     
         // private
         renderDate : function(dateVal){
    @@ -66722,7 +69669,7 @@ Ext.extend(Ext.grid.PropertyColumnModel, Ext.grid.ColumnModel, {
     
         // private
         renderBool : function(bVal){
    -        return bVal ? 'true' : 'false';
    +        return this[bVal ? 'trueText' : 'falseText'];
         },
     
         // private
    @@ -66742,7 +69689,11 @@ Ext.extend(Ext.grid.PropertyColumnModel, Ext.grid.ColumnModel, {
         },
     
         // private
    -    renderCell : function(val){
    +    renderCell : function(val, meta, rec){
    +        var renderer = this.grid.customRenderers[rec.get('name')];
    +        if(renderer){
    +            return renderer.apply(this, arguments);
    +        }
             var rv = val;
             if(Ext.isDate(val)){
                 rv = this.renderDate(val);
    @@ -66781,7 +69732,7 @@ Ext.extend(Ext.grid.PropertyColumnModel, Ext.grid.ColumnModel, {
         destroy : function(){
             Ext.grid.PropertyColumnModel.superclass.destroy.call(this);
             for(var ed in this.editors){
    -            Ext.destroy(ed);
    +            Ext.destroy(this.editors[ed]);
             }
         }
     });
    @@ -66834,6 +69785,33 @@ var grid = new Ext.grid.PropertyGrid({
             'Start Time': '10:00 AM'
         }
     });
    +
    + */ + /** + * @cfg {Object} source A data object to use as the data source of the grid (see {@link #setSource} for details). + */ + /** + * @cfg {Object} customRenderers An object containing name/value pairs of custom renderer type definitions that allow + * the grid to support custom rendering of fields. By default, the grid supports strongly-typed rendering + * of strings, dates, numbers and booleans using built-in form editors, but any custom type can be supported and + * associated with the type of the value. The name of the renderer type should correspond with the name of the property + * that it will render. Example usage: + *
    
    +var grid = new Ext.grid.PropertyGrid({
    +    ...
    +    customRenderers: {
    +        Available: function(v){
    +            if(v){
    +                return 'Yes';
    +            }else{
    +                return 'No';
    +            }
    +        }
    +    },
    +    source: {
    +        Available: true
    +    }
    +});
     
    */ @@ -66849,6 +69827,7 @@ var grid = new Ext.grid.PropertyGrid({ // private initComponent : function(){ + this.customRenderers = this.customRenderers || {}; this.customEditors = this.customEditors || {}; this.lastEditRow = null; var store = new Ext.grid.PropertyStore(this); @@ -66933,7 +69912,42 @@ grid.setSource({ */ getSource : function(){ return this.propStore.getSource(); + }, + + /** + * Sets the value of a property. + * @param {String} prop The name of the property to set + * @param {Mixed} value The value to test + * @param {Boolean} create (Optional) True to create the property if it doesn't already exist. Defaults to false. + */ + setProperty : function(prop, value, create){ + this.propStore.setValue(prop, value, create); + }, + + /** + * Removes a property from the grid. + * @param {String} prop The name of the property to remove + */ + removeProperty : function(prop){ + this.propStore.remove(prop); } + + /** + * @cfg store + * @hide + */ + /** + * @cfg colModel + * @hide + */ + /** + * @cfg cm + * @hide + */ + /** + * @cfg columns + * @hide + */ }); Ext.reg("propertygrid", Ext.grid.PropertyGrid); /** @@ -67071,13 +70085,13 @@ var grid = new Ext.grid.GridPanel({ *
    */ groupTextTpl : '{text}', - + /** * @cfg {String} groupMode Indicates how to construct the group identifier. 'value' constructs the id using * raw value, 'display' constructs the id using the rendered value. Defaults to 'value'. */ groupMode: 'value', - + /** * @cfg {Function} groupRenderer This property must be configured in the {@link Ext.grid.Column} for * each column. @@ -67103,6 +70117,10 @@ var grid = new Ext.grid.GridPanel({ ); } this.startGroup.compile(); + if(!this.endGroup){ + this.endGroup = ''; + } + this.endGroup = ''; }, @@ -67118,11 +70136,11 @@ var grid = new Ext.grid.GridPanel({ // private onAdd : function(){ - if(this.enableGrouping && !this.ignoreAdd){ + if(this.canGroup() && !this.ignoreAdd){ var ss = this.getScrollState(); this.refresh(); this.restoreScroll(ss); - }else if(!this.enableGrouping){ + }else if(!this.canGroup()){ Ext.grid.GroupingView.superclass.onAdd.apply(this, arguments); } }, @@ -67156,7 +70174,7 @@ var grid = new Ext.grid.GridPanel({ } if((item = items.get('showGroups'))){ item.setDisabled(disabled); - item.setChecked(!!this.getGroupField(), true); + item.setChecked(this.enableGrouping, true); } }, @@ -67186,18 +70204,55 @@ var grid = new Ext.grid.GridPanel({ } }, + processEvent: function(name, e){ + var hd = e.getTarget('.x-grid-group-hd', this.mainBody); + if(hd){ + // group value is at the end of the string + var field = this.getGroupField(), + prefix = this.getPrefix(field), + groupValue = hd.id.substring(prefix.length); + + // remove trailing '-hd' + groupValue = groupValue.substr(0, groupValue.length - 3); + if(groupValue){ + this.grid.fireEvent('group' + name, this.grid, field, groupValue, e); + } + } + + }, + // private onGroupByClick : function(){ + this.enableGrouping = true; this.grid.store.groupBy(this.cm.getDataIndex(this.hdCtxIndex)); + this.grid.fireEvent('groupchange', this, this.grid.store.getGroupState()); this.beforeMenuShow(); // Make sure the checkboxes get properly set when changing groups + this.refresh(); }, // private onShowGroupsClick : function(mi, checked){ + this.enableGrouping = checked; if(checked){ this.onGroupByClick(); }else{ this.grid.store.clearGrouping(); + this.grid.fireEvent('groupchange', this, null); + } + }, + + /** + * Toggle the group that contains the specific row. + * @param {Number} rowIndex The row inside the group + * @param {Boolean} expanded (optional) + */ + toggleRowIndex : function(rowIndex, expanded){ + if(!this.canGroup()){ + return; + } + var row = this.getRow(rowIndex); + if(row){ + this.toggleGroup(this.findGroup(row), expanded); } }, @@ -67207,14 +70262,13 @@ var grid = new Ext.grid.GridPanel({ * @param {Boolean} expanded (optional) */ toggleGroup : function(group, expanded){ - this.grid.stopEditing(true); - group = Ext.getDom(group); - var gel = Ext.fly(group); - expanded = expanded !== undefined ? - expanded : gel.hasClass('x-grid-group-collapsed'); - - this.state[gel.dom.id] = expanded; - gel[expanded ? 'removeClass' : 'addClass']('x-grid-group-collapsed'); + var gel = Ext.get(group); + expanded = Ext.isDefined(expanded) ? expanded : gel.hasClass('x-grid-group-collapsed'); + if(this.state[gel.id] !== expanded){ + this.grid.stopEditing(true); + this.state[gel.id] = expanded; + gel[expanded ? 'removeClass' : 'addClass']('x-grid-group-collapsed'); + } }, /** @@ -67264,9 +70318,12 @@ var grid = new Ext.grid.GridPanel({ getGroupField : function(){ return this.grid.store.getGroupState(); }, - + // private afterRender : function(){ + if(!this.ds || !this.cm){ + return; + } Ext.grid.GroupingView.superclass.afterRender.call(this); if(this.grid.deferRowRender){ this.updateGroupWidths(); @@ -67279,15 +70336,16 @@ var grid = new Ext.grid.GridPanel({ var eg = !!groupField; // if they turned off grouping and the last grouped field is hidden if(this.hideGroupedColumn) { - var colIndex = this.cm.findColumnIndex(groupField); - if(!eg && this.lastGroupField !== undefined) { + var colIndex = this.cm.findColumnIndex(groupField), + hasLastGroupField = Ext.isDefined(this.lastGroupField); + if(!eg && hasLastGroupField){ this.mainBody.update(''); this.cm.setHidden(this.cm.findColumnIndex(this.lastGroupField), false); delete this.lastGroupField; - }else if (eg && this.lastGroupField === undefined) { + }else if (eg && !hasLastGroupField){ this.lastGroupField = groupField; this.cm.setHidden(colIndex, true); - }else if (eg && this.lastGroupField !== undefined && groupField !== this.lastGroupField) { + }else if (eg && hasLastGroupField && groupField !== this.lastGroupField) { this.mainBody.update(''); var oldIndex = this.cm.findColumnIndex(this.lastGroupField); this.cm.setHidden(oldIndex, false); @@ -67304,18 +70362,16 @@ var grid = new Ext.grid.GridPanel({ if(rs.length < 1){ return ''; } - var groupField = this.getGroupField(), - colIndex = this.cm.findColumnIndex(groupField), - g; - this.enableGrouping = !!groupField; - - if(!this.enableGrouping || this.isUpdating){ + if(!this.canGroup() || this.isUpdating){ return Ext.grid.GroupingView.superclass.doRender.apply( this, arguments); } - var gstyle = 'width:' + this.getTotalWidth() + ';', - gidPrefix = this.grid.getGridEl().id, + + var groupField = this.getGroupField(), + colIndex = this.cm.findColumnIndex(groupField), + g, + gstyle = 'width:' + this.getTotalWidth() + ';', cfg = this.cm.config[colIndex], groupRenderer = cfg.groupRenderer || cfg.renderer, prefix = this.showGroupName ? (cfg.groupName || cfg.header)+': ' : '', @@ -67326,14 +70382,13 @@ var grid = new Ext.grid.GridPanel({ var rowIndex = startRow + i, r = rs[i], gvalue = r.data[groupField]; - + g = this.getGroup(gvalue, r, groupRenderer, rowIndex, colIndex, ds); if(!curGroup || curGroup.group != g){ - gid = this.constructId(gvalue, gidPrefix, groupField, colIndex); - // if state is defined use it, however state is in terms of expanded - // so negate it, otherwise use the default. - var isCollapsed = typeof this.state[gid] !== 'undefined' ? !this.state[gid] : this.startCollapsed; - var gcls = isCollapsed ? 'x-grid-group-collapsed' : ''; + gid = this.constructId(gvalue, groupField, colIndex); + // if state is defined use it, however state is in terms of expanded + // so negate it, otherwise use the default. + this.state[gid] = !(Ext.isDefined(this.state[gid]) ? !this.state[gid] : this.startCollapsed); curGroup = { group: g, gvalue: gvalue, @@ -67341,7 +70396,7 @@ var grid = new Ext.grid.GridPanel({ groupId: gid, startRow: rowIndex, rs: [r], - cls: gcls, + cls: this.state[gid] ? '' : 'x-grid-group-collapsed', style: gstyle }; groups.push(curGroup); @@ -67370,16 +70425,26 @@ var grid = new Ext.grid.GridPanel({ */ getGroupId : function(value){ var field = this.getGroupField(); - return this.constructId(value, this.grid.getGridEl().id, field, this.cm.findColumnIndex(field)); + return this.constructId(value, field, this.cm.findColumnIndex(field)); }, - + // private - constructId : function(value, prefix, field, idx){ + constructId : function(value, field, idx){ var cfg = this.cm.config[idx], groupRenderer = cfg.groupRenderer || cfg.renderer, val = (this.groupMode == 'value') ? value : this.getGroup(value, {data:{}}, groupRenderer, 0, idx, this.ds); - - return prefix + '-gp-' + field + '-' + Ext.util.Format.htmlEncode(val); + + return this.getPrefix(field) + Ext.util.Format.htmlEncode(val); + }, + + // private + canGroup : function(){ + return this.enableGrouping && !!this.getGroupField(); + }, + + // private + getPrefix: function(field){ + return this.grid.getGridEl().id + '-gp-' + field + '-'; }, // private @@ -67394,7 +70459,7 @@ var grid = new Ext.grid.GridPanel({ // private getRows : function(){ - if(!this.enableGrouping){ + if(!this.canGroup()){ return Ext.grid.GroupingView.superclass.getRows.call(this); } var r = []; @@ -67410,7 +70475,7 @@ var grid = new Ext.grid.GridPanel({ // private updateGroupWidths : function(){ - if(!this.enableGrouping || !this.hasRows()){ + if(!this.canGroup() || !this.hasRows()){ return; } var tw = Math.max(this.cm.getTotalWidth(), this.el.dom.offsetWidth-this.getScrollOffset()) +'px'; @@ -67445,14 +70510,7 @@ var grid = new Ext.grid.GridPanel({ // private onBeforeRowSelect : function(sm, rowIndex){ - if(!this.enableGrouping){ - return; - } - var row = this.getRow(rowIndex); - if(row && !row.offsetParent){ - var g = this.findGroup(row); - this.toggleGroup(g, true); - } + this.toggleRowIndex(rowIndex, true); } }); // private