--- /dev/null
+/*
+
+This file is part of Ext JS 4
+
+Copyright (c) 2011 Sencha Inc
+
+Contact: http://www.sencha.com/contact
+
+Commercial Usage
+Licensees holding valid commercial licenses may use this file in accordance with the Commercial Software License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and Sencha.
+
+If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
+
+*/
+/**
+ * @class Ext
+ * @singleton
+ */
+(function() {
+ var global = this,
+ objectPrototype = Object.prototype,
+ toString = objectPrototype.toString,
+ enumerables = true,
+ enumerablesTest = { toString: 1 },
+ i;
+
+ if (typeof Ext === 'undefined') {
+ global.Ext = {};
+ }
+
+ Ext.global = global;
+
+ for (i in enumerablesTest) {
+ enumerables = null;
+ }
+
+ if (enumerables) {
+ enumerables = ['hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable',
+ 'toLocaleString', 'toString', 'constructor'];
+ }
+
+ /**
+ * An array containing extra enumerables for old browsers
+ * @property {String[]}
+ */
+ Ext.enumerables = enumerables;
+
+ /**
+ * Copies all the properties of config to the specified object.
+ * Note that if recursive merging and cloning without referencing the original objects / arrays is needed, use
+ * {@link Ext.Object#merge} instead.
+ * @param {Object} object The receiver of the properties
+ * @param {Object} config The source of the properties
+ * @param {Object} defaults A different object that will also be applied for default values
+ * @return {Object} returns obj
+ */
+ Ext.apply = function(object, config, defaults) {
+ if (defaults) {
+ Ext.apply(object, defaults);
+ }
+
+ if (object && config && typeof config === 'object') {
+ var i, j, k;
+
+ for (i in config) {
+ object[i] = config[i];
+ }
+
+ if (enumerables) {
+ for (j = enumerables.length; j--;) {
+ k = enumerables[j];
+ if (config.hasOwnProperty(k)) {
+ object[k] = config[k];
+ }
+ }
+ }
+ }
+
+ return object;
+ };
+
+ Ext.buildSettings = Ext.apply({
+ baseCSSPrefix: 'x-',
+ scopeResetCSS: false
+ }, Ext.buildSettings || {});
+
+ Ext.apply(Ext, {
+ /**
+ * A reusable empty function
+ */
+ emptyFn: function() {},
+
+ baseCSSPrefix: Ext.buildSettings.baseCSSPrefix,
+
+ /**
+ * Copies all the properties of config to object if they don't already exist.
+ * @param {Object} object The receiver of the properties
+ * @param {Object} config The source of the properties
+ * @return {Object} returns obj
+ */
+ applyIf: function(object, config) {
+ var property;
+
+ if (object) {
+ for (property in config) {
+ if (object[property] === undefined) {
+ object[property] = config[property];
+ }
+ }
+ }
+
+ return object;
+ },
+
+ /**
+ * Iterates either an array or an object. This method delegates to
+ * {@link Ext.Array#each Ext.Array.each} if the given value is iterable, and {@link Ext.Object#each Ext.Object.each} otherwise.
+ *
+ * @param {Object/Array} object The object or array to be iterated.
+ * @param {Function} fn The function to be called for each iteration. See and {@link Ext.Array#each Ext.Array.each} and
+ * {@link Ext.Object#each Ext.Object.each} for detailed lists of arguments passed to this function depending on the given object
+ * type that is being iterated.
+ * @param {Object} scope (Optional) The scope (`this` reference) in which the specified function is executed.
+ * Defaults to the object being iterated itself.
+ * @markdown
+ */
+ iterate: function(object, fn, scope) {
+ if (Ext.isEmpty(object)) {
+ return;
+ }
+
+ if (scope === undefined) {
+ scope = object;
+ }
+
+ if (Ext.isIterable(object)) {
+ Ext.Array.each.call(Ext.Array, object, fn, scope);
+ }
+ else {
+ Ext.Object.each.call(Ext.Object, object, fn, scope);
+ }
+ }
+ });
+
+ Ext.apply(Ext, {
+
+ /**
+ * This method deprecated. Use {@link Ext#define Ext.define} instead.
+ * @method
+ * @param {Function} superclass
+ * @param {Object} overrides
+ * @return {Function} The subclass constructor from the <tt>overrides</tt> parameter, or a generated one if not provided.
+ * @deprecated 4.0.0 Use {@link Ext#define Ext.define} instead
+ */
+ extend: function() {
+ // inline overrides
+ var objectConstructor = objectPrototype.constructor,
+ inlineOverrides = function(o) {
+ for (var m in o) {
+ if (!o.hasOwnProperty(m)) {
+ continue;
+ }
+ this[m] = o[m];
+ }
+ };
+
+ return function(subclass, superclass, overrides) {
+ // First we check if the user passed in just the superClass with overrides
+ if (Ext.isObject(superclass)) {
+ overrides = superclass;
+ superclass = subclass;
+ subclass = overrides.constructor !== objectConstructor ? overrides.constructor : function() {
+ superclass.apply(this, arguments);
+ };
+ }
+
+
+ // We create a new temporary class
+ var F = function() {},
+ subclassProto, superclassProto = superclass.prototype;
+
+ F.prototype = superclassProto;
+ subclassProto = subclass.prototype = new F();
+ subclassProto.constructor = subclass;
+ subclass.superclass = superclassProto;
+
+ if (superclassProto.constructor === objectConstructor) {
+ superclassProto.constructor = superclass;
+ }
+
+ subclass.override = function(overrides) {
+ Ext.override(subclass, overrides);
+ };
+
+ subclassProto.override = inlineOverrides;
+ subclassProto.proto = subclassProto;
+
+ subclass.override(overrides);
+ subclass.extend = function(o) {
+ return Ext.extend(subclass, o);
+ };
+
+ return subclass;
+ };
+ }(),
+
+ /**
+ * Proxy to {@link Ext.Base#override}. Please refer {@link Ext.Base#override} for further details.
+
+ Ext.define('My.cool.Class', {
+ sayHi: function() {
+ alert('Hi!');
+ }
+ }
+
+ Ext.override(My.cool.Class, {
+ sayHi: function() {
+ alert('About to say...');
+
+ this.callOverridden();
+ }
+ });
+
+ var cool = new My.cool.Class();
+ cool.sayHi(); // alerts 'About to say...'
+ // alerts 'Hi!'
+
+ * Please note that `this.callOverridden()` only works if the class was previously
+ * created with {@link Ext#define)
+ *
+ * @param {Object} cls The class to override
+ * @param {Object} overrides The list of functions to add to origClass. This should be specified as an object literal
+ * containing one or more methods.
+ * @method override
+ * @markdown
+ */
+ override: function(cls, overrides) {
+ if (cls.prototype.$className) {
+ return cls.override(overrides);
+ }
+ else {
+ Ext.apply(cls.prototype, overrides);
+ }
+ }
+ });
+
+ // A full set of static methods to do type checking
+ Ext.apply(Ext, {
+
+ /**
+ * Returns the given value itself if it's not empty, as described in {@link Ext#isEmpty}; returns the default
+ * value (second argument) otherwise.
+ *
+ * @param {Object} value The value to test
+ * @param {Object} defaultValue The value to return if the original value is empty
+ * @param {Boolean} allowBlank (optional) true to allow zero length strings to qualify as non-empty (defaults to false)
+ * @return {Object} value, if non-empty, else defaultValue
+ */
+ valueFrom: function(value, defaultValue, allowBlank){
+ return Ext.isEmpty(value, allowBlank) ? defaultValue : value;
+ },
+
+ /**
+ * Returns the type of the given variable in string format. List of possible values are:
+ *
+ * - `undefined`: If the given value is `undefined`
+ * - `null`: If the given value is `null`
+ * - `string`: If the given value is a string
+ * - `number`: If the given value is a number
+ * - `boolean`: If the given value is a boolean value
+ * - `date`: If the given value is a `Date` object
+ * - `function`: If the given value is a function reference
+ * - `object`: If the given value is an object
+ * - `array`: If the given value is an array
+ * - `regexp`: If the given value is a regular expression
+ * - `element`: If the given value is a DOM Element
+ * - `textnode`: If the given value is a DOM text node and contains something other than whitespace
+ * - `whitespace`: If the given value is a DOM text node and contains only whitespace
+ *
+ * @param {Object} value
+ * @return {String}
+ * @markdown
+ */
+ typeOf: function(value) {
+ if (value === null) {
+ return 'null';
+ }
+
+ var type = typeof value;
+
+ if (type === 'undefined' || type === 'string' || type === 'number' || type === 'boolean') {
+ return type;
+ }
+
+ var typeToString = toString.call(value);
+
+ switch(typeToString) {
+ case '[object Array]':
+ return 'array';
+ case '[object Date]':
+ return 'date';
+ case '[object Boolean]':
+ return 'boolean';
+ case '[object Number]':
+ return 'number';
+ case '[object RegExp]':
+ return 'regexp';
+ }
+
+ if (type === 'function') {
+ return 'function';
+ }
+
+ if (type === 'object') {
+ if (value.nodeType !== undefined) {
+ if (value.nodeType === 3) {
+ return (/\S/).test(value.nodeValue) ? 'textnode' : 'whitespace';
+ }
+ else {
+ return 'element';
+ }
+ }
+
+ return 'object';
+ }
+
+ },
+
+ /**
+ * Returns true if the passed value is empty, false otherwise. The value is deemed to be empty if it is either:
+ *
+ * - `null`
+ * - `undefined`
+ * - a zero-length array
+ * - a zero-length string (Unless the `allowEmptyString` parameter is set to `true`)
+ *
+ * @param {Object} value The value to test
+ * @param {Boolean} allowEmptyString (optional) true to allow empty strings (defaults to false)
+ * @return {Boolean}
+ * @markdown
+ */
+ isEmpty: function(value, allowEmptyString) {
+ return (value === null) || (value === undefined) || (!allowEmptyString ? value === '' : false) || (Ext.isArray(value) && value.length === 0);
+ },
+
+ /**
+ * Returns true if the passed value is a JavaScript Array, false otherwise.
+ *
+ * @param {Object} target The target to test
+ * @return {Boolean}
+ * @method
+ */
+ isArray: ('isArray' in Array) ? Array.isArray : function(value) {
+ return toString.call(value) === '[object Array]';
+ },
+
+ /**
+ * Returns true if the passed value is a JavaScript Date object, false otherwise.
+ * @param {Object} object The object to test
+ * @return {Boolean}
+ */
+ isDate: function(value) {
+ return toString.call(value) === '[object Date]';
+ },
+
+ /**
+ * Returns true if the passed value is a JavaScript Object, false otherwise.
+ * @param {Object} value The value to test
+ * @return {Boolean}
+ * @method
+ */
+ isObject: (toString.call(null) === '[object Object]') ?
+ function(value) {
+ // check ownerDocument here as well to exclude DOM nodes
+ return value !== null && value !== undefined && toString.call(value) === '[object Object]' && value.ownerDocument === undefined;
+ } :
+ function(value) {
+ return toString.call(value) === '[object Object]';
+ },
+
+ /**
+ * Returns true if the passed value is a JavaScript 'primitive', a string, number or boolean.
+ * @param {Object} value The value to test
+ * @return {Boolean}
+ */
+ isPrimitive: function(value) {
+ var type = typeof value;
+
+ return type === 'string' || type === 'number' || type === 'boolean';
+ },
+
+ /**
+ * Returns true if the passed value is a JavaScript Function, false otherwise.
+ * @param {Object} value The value to test
+ * @return {Boolean}
+ * @method
+ */
+ isFunction:
+ // Safari 3.x and 4.x returns 'function' for typeof <NodeList>, hence we need to fall back to using
+ // Object.prorotype.toString (slower)
+ (typeof document !== 'undefined' && typeof document.getElementsByTagName('body') === 'function') ? function(value) {
+ return toString.call(value) === '[object Function]';
+ } : function(value) {
+ return typeof value === 'function';
+ },
+
+ /**
+ * Returns true if the passed value is a number. Returns false for non-finite numbers.
+ * @param {Object} value The value to test
+ * @return {Boolean}
+ */
+ isNumber: function(value) {
+ return typeof value === 'number' && isFinite(value);
+ },
+
+ /**
+ * Validates that a value is numeric.
+ * @param {Object} value Examples: 1, '1', '2.34'
+ * @return {Boolean} True if numeric, false otherwise
+ */
+ isNumeric: function(value) {
+ return !isNaN(parseFloat(value)) && isFinite(value);
+ },
+
+ /**
+ * Returns true if the passed value is a string.
+ * @param {Object} value The value to test
+ * @return {Boolean}
+ */
+ isString: function(value) {
+ return typeof value === 'string';
+ },
+
+ /**
+ * Returns true if the passed value is a boolean.
+ *
+ * @param {Object} value The value to test
+ * @return {Boolean}
+ */
+ isBoolean: function(value) {
+ return typeof value === 'boolean';
+ },
+
+ /**
+ * Returns true if the passed value is an HTMLElement
+ * @param {Object} value The value to test
+ * @return {Boolean}
+ */
+ isElement: function(value) {
+ return value ? value.nodeType === 1 : false;
+ },
+
+ /**
+ * Returns true if the passed value is a TextNode
+ * @param {Object} value The value to test
+ * @return {Boolean}
+ */
+ isTextNode: function(value) {
+ return value ? value.nodeName === "#text" : false;
+ },
+
+ /**
+ * Returns true if the passed value is defined.
+ * @param {Object} value The value to test
+ * @return {Boolean}
+ */
+ isDefined: function(value) {
+ return typeof value !== 'undefined';
+ },
+
+ /**
+ * Returns true if the passed value is iterable, false otherwise
+ * @param {Object} value The value to test
+ * @return {Boolean}
+ */
+ isIterable: function(value) {
+ return (value && typeof value !== 'string') ? value.length !== undefined : false;
+ }
+ });
+
+ Ext.apply(Ext, {
+
+ /**
+ * Clone almost any type of variable including array, object, DOM nodes and Date without keeping the old reference
+ * @param {Object} item The variable to clone
+ * @return {Object} clone
+ */
+ clone: function(item) {
+ if (item === null || item === undefined) {
+ return item;
+ }
+
+ // DOM nodes
+ // TODO proxy this to Ext.Element.clone to handle automatic id attribute changing
+ // recursively
+ if (item.nodeType && item.cloneNode) {
+ return item.cloneNode(true);
+ }
+
+ var type = toString.call(item);
+
+ // Date
+ if (type === '[object Date]') {
+ return new Date(item.getTime());
+ }
+
+ var i, j, k, clone, key;
+
+ // Array
+ if (type === '[object Array]') {
+ i = item.length;
+
+ clone = [];
+
+ while (i--) {
+ clone[i] = Ext.clone(item[i]);
+ }
+ }
+ // Object
+ else if (type === '[object Object]' && item.constructor === Object) {
+ clone = {};
+
+ for (key in item) {
+ clone[key] = Ext.clone(item[key]);
+ }
+
+ if (enumerables) {
+ for (j = enumerables.length; j--;) {
+ k = enumerables[j];
+ clone[k] = item[k];
+ }
+ }
+ }
+
+ return clone || item;
+ },
+
+ /**
+ * @private
+ * Generate a unique reference of Ext in the global scope, useful for sandboxing
+ */
+ getUniqueGlobalNamespace: function() {
+ var uniqueGlobalNamespace = this.uniqueGlobalNamespace;
+
+ if (uniqueGlobalNamespace === undefined) {
+ var i = 0;
+
+ do {
+ uniqueGlobalNamespace = 'ExtBox' + (++i);
+ } while (Ext.global[uniqueGlobalNamespace] !== undefined);
+
+ Ext.global[uniqueGlobalNamespace] = Ext;
+ this.uniqueGlobalNamespace = uniqueGlobalNamespace;
+ }
+
+ return uniqueGlobalNamespace;
+ },
+
+ /**
+ * @private
+ */
+ functionFactory: function() {
+ var args = Array.prototype.slice.call(arguments);
+
+ if (args.length > 0) {
+ args[args.length - 1] = 'var Ext=window.' + this.getUniqueGlobalNamespace() + ';' +
+ args[args.length - 1];
+ }
+
+ return Function.prototype.constructor.apply(Function.prototype, args);
+ }
+ });
+
+ /**
+ * Old alias to {@link Ext#typeOf}
+ * @deprecated 4.0.0 Use {@link Ext#typeOf} instead
+ * @method
+ * @alias Ext#typeOf
+ */
+ Ext.type = Ext.typeOf;
+
+})();
+
+/**
+ * @author Jacky Nguyen <jacky@sencha.com>
+ * @docauthor Jacky Nguyen <jacky@sencha.com>
+ * @class Ext.Version
+ *
+ * A utility class that wrap around a string version number and provide convenient
+ * method to perform comparison. See also: {@link Ext.Version#compare compare}. Example:
+
+ var version = new Ext.Version('1.0.2beta');
+ console.log("Version is " + version); // Version is 1.0.2beta
+
+ console.log(version.getMajor()); // 1
+ console.log(version.getMinor()); // 0
+ console.log(version.getPatch()); // 2
+ console.log(version.getBuild()); // 0
+ console.log(version.getRelease()); // beta
+
+ console.log(version.isGreaterThan('1.0.1')); // True
+ console.log(version.isGreaterThan('1.0.2alpha')); // True
+ console.log(version.isGreaterThan('1.0.2RC')); // False
+ console.log(version.isGreaterThan('1.0.2')); // False
+ console.log(version.isLessThan('1.0.2')); // True
+
+ console.log(version.match(1.0)); // True
+ console.log(version.match('1.0.2')); // True
+
+ * @markdown
+ */
+(function() {
+
+// Current core version
+var version = '4.0.7', Version;
+ Ext.Version = Version = Ext.extend(Object, {
+
+ /**
+ * @param {String/Number} version The version number in the follow standard format: major[.minor[.patch[.build[release]]]]
+ * Examples: 1.0 or 1.2.3beta or 1.2.3.4RC
+ * @return {Ext.Version} this
+ */
+ constructor: function(version) {
+ var parts, releaseStartIndex;
+
+ if (version instanceof Version) {
+ return version;
+ }
+
+ this.version = this.shortVersion = String(version).toLowerCase().replace(/_/g, '.').replace(/[\-+]/g, '');
+
+ releaseStartIndex = this.version.search(/([^\d\.])/);
+
+ if (releaseStartIndex !== -1) {
+ this.release = this.version.substr(releaseStartIndex, version.length);
+ this.shortVersion = this.version.substr(0, releaseStartIndex);
+ }
+
+ this.shortVersion = this.shortVersion.replace(/[^\d]/g, '');
+
+ parts = this.version.split('.');
+
+ this.major = parseInt(parts.shift() || 0, 10);
+ this.minor = parseInt(parts.shift() || 0, 10);
+ this.patch = parseInt(parts.shift() || 0, 10);
+ this.build = parseInt(parts.shift() || 0, 10);
+
+ return this;
+ },
+
+ /**
+ * Override the native toString method
+ * @private
+ * @return {String} version
+ */
+ toString: function() {
+ return this.version;
+ },
+
+ /**
+ * Override the native valueOf method
+ * @private
+ * @return {String} version
+ */
+ valueOf: function() {
+ return this.version;
+ },
+
+ /**
+ * Returns the major component value
+ * @return {Number} major
+ */
+ getMajor: function() {
+ return this.major || 0;
+ },
+
+ /**
+ * Returns the minor component value
+ * @return {Number} minor
+ */
+ getMinor: function() {
+ return this.minor || 0;
+ },
+
+ /**
+ * Returns the patch component value
+ * @return {Number} patch
+ */
+ getPatch: function() {
+ return this.patch || 0;
+ },
+
+ /**
+ * Returns the build component value
+ * @return {Number} build
+ */
+ getBuild: function() {
+ return this.build || 0;
+ },
+
+ /**
+ * Returns the release component value
+ * @return {Number} release
+ */
+ getRelease: function() {
+ return this.release || '';
+ },
+
+ /**
+ * Returns whether this version if greater than the supplied argument
+ * @param {String/Number} target The version to compare with
+ * @return {Boolean} True if this version if greater than the target, false otherwise
+ */
+ isGreaterThan: function(target) {
+ return Version.compare(this.version, target) === 1;
+ },
+
+ /**
+ * Returns whether this version if smaller than the supplied argument
+ * @param {String/Number} target The version to compare with
+ * @return {Boolean} True if this version if smaller than the target, false otherwise
+ */
+ isLessThan: function(target) {
+ return Version.compare(this.version, target) === -1;
+ },
+
+ /**
+ * Returns whether this version equals to the supplied argument
+ * @param {String/Number} target The version to compare with
+ * @return {Boolean} True if this version equals to the target, false otherwise
+ */
+ equals: function(target) {
+ return Version.compare(this.version, target) === 0;
+ },
+
+ /**
+ * Returns whether this version matches the supplied argument. Example:
+ * <pre><code>
+ * var version = new Ext.Version('1.0.2beta');
+ * console.log(version.match(1)); // True
+ * console.log(version.match(1.0)); // True
+ * console.log(version.match('1.0.2')); // True
+ * console.log(version.match('1.0.2RC')); // False
+ * </code></pre>
+ * @param {String/Number} target The version to compare with
+ * @return {Boolean} True if this version matches the target, false otherwise
+ */
+ match: function(target) {
+ target = String(target);
+ return this.version.substr(0, target.length) === target;
+ },
+
+ /**
+ * Returns this format: [major, minor, patch, build, release]. Useful for comparison
+ * @return {Number[]}
+ */
+ toArray: function() {
+ return [this.getMajor(), this.getMinor(), this.getPatch(), this.getBuild(), this.getRelease()];
+ },
+
+ /**
+ * Returns shortVersion version without dots and release
+ * @return {String}
+ */
+ getShortVersion: function() {
+ return this.shortVersion;
+ }
+ });
+
+ Ext.apply(Version, {
+ // @private
+ releaseValueMap: {
+ 'dev': -6,
+ 'alpha': -5,
+ 'a': -5,
+ 'beta': -4,
+ 'b': -4,
+ 'rc': -3,
+ '#': -2,
+ 'p': -1,
+ 'pl': -1
+ },
+
+ /**
+ * Converts a version component to a comparable value
+ *
+ * @static
+ * @param {Object} value The value to convert
+ * @return {Object}
+ */
+ getComponentValue: function(value) {
+ return !value ? 0 : (isNaN(value) ? this.releaseValueMap[value] || value : parseInt(value, 10));
+ },
+
+ /**
+ * Compare 2 specified versions, starting from left to right. If a part contains special version strings,
+ * they are handled in the following order:
+ * 'dev' < 'alpha' = 'a' < 'beta' = 'b' < 'RC' = 'rc' < '#' < 'pl' = 'p' < 'anything else'
+ *
+ * @static
+ * @param {String} current The current version to compare to
+ * @param {String} target The target version to compare to
+ * @return {Number} Returns -1 if the current version is smaller than the target version, 1 if greater, and 0 if they're equivalent
+ */
+ compare: function(current, target) {
+ var currentValue, targetValue, i;
+
+ current = new Version(current).toArray();
+ target = new Version(target).toArray();
+
+ for (i = 0; i < Math.max(current.length, target.length); i++) {
+ currentValue = this.getComponentValue(current[i]);
+ targetValue = this.getComponentValue(target[i]);
+
+ if (currentValue < targetValue) {
+ return -1;
+ } else if (currentValue > targetValue) {
+ return 1;
+ }
+ }
+
+ return 0;
+ }
+ });
+
+ Ext.apply(Ext, {
+ /**
+ * @private
+ */
+ versions: {},
+
+ /**
+ * @private
+ */
+ lastRegisteredVersion: null,
+
+ /**
+ * Set version number for the given package name.
+ *
+ * @param {String} packageName The package name, for example: 'core', 'touch', 'extjs'
+ * @param {String/Ext.Version} version The version, for example: '1.2.3alpha', '2.4.0-dev'
+ * @return {Ext}
+ */
+ setVersion: function(packageName, version) {
+ Ext.versions[packageName] = new Version(version);
+ Ext.lastRegisteredVersion = Ext.versions[packageName];
+
+ return this;
+ },
+
+ /**
+ * Get the version number of the supplied package name; will return the last registered version
+ * (last Ext.setVersion call) if there's no package name given.
+ *
+ * @param {String} packageName (Optional) The package name, for example: 'core', 'touch', 'extjs'
+ * @return {Ext.Version} The version
+ */
+ getVersion: function(packageName) {
+ if (packageName === undefined) {
+ return Ext.lastRegisteredVersion;
+ }
+
+ return Ext.versions[packageName];
+ },
+
+ /**
+ * Create a closure for deprecated code.
+ *
+ // This means Ext.oldMethod is only supported in 4.0.0beta and older.
+ // If Ext.getVersion('extjs') returns a version that is later than '4.0.0beta', for example '4.0.0RC',
+ // the closure will not be invoked
+ Ext.deprecate('extjs', '4.0.0beta', function() {
+ Ext.oldMethod = Ext.newMethod;
+
+ ...
+ });
+
+ * @param {String} packageName The package name
+ * @param {String} since The last version before it's deprecated
+ * @param {Function} closure The callback function to be executed with the specified version is less than the current version
+ * @param {Object} scope The execution scope (<tt>this</tt>) if the closure
+ * @markdown
+ */
+ deprecate: function(packageName, since, closure, scope) {
+ if (Version.compare(Ext.getVersion(packageName), since) < 1) {
+ closure.call(scope);
+ }
+ }
+ }); // End Versioning
+
+ Ext.setVersion('core', version);
+
+})();
+
+/**
+ * @class Ext.String
+ *
+ * A collection of useful static methods to deal with strings
+ * @singleton
+ */
+
+Ext.String = {
+ trimRegex: /^[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+|[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+$/g,
+ escapeRe: /('|\\)/g,
+ formatRe: /\{(\d+)\}/g,
+ escapeRegexRe: /([-.*+?^${}()|[\]\/\\])/g,
+
+ /**
+ * Convert certain characters (&, <, >, and ") to their HTML character equivalents for literal display in web pages.
+ * @param {String} value The string to encode
+ * @return {String} The encoded text
+ * @method
+ */
+ htmlEncode: (function() {
+ var entities = {
+ '&': '&',
+ '>': '>',
+ '<': '<',
+ '"': '"'
+ }, keys = [], p, regex;
+
+ for (p in entities) {
+ keys.push(p);
+ }
+
+ regex = new RegExp('(' + keys.join('|') + ')', 'g');
+
+ return function(value) {
+ return (!value) ? value : String(value).replace(regex, function(match, capture) {
+ return entities[capture];
+ });
+ };
+ })(),
+
+ /**
+ * Convert certain characters (&, <, >, and ") from their HTML character equivalents.
+ * @param {String} value The string to decode
+ * @return {String} The decoded text
+ * @method
+ */
+ htmlDecode: (function() {
+ var entities = {
+ '&': '&',
+ '>': '>',
+ '<': '<',
+ '"': '"'
+ }, keys = [], p, regex;
+
+ for (p in entities) {
+ keys.push(p);
+ }
+
+ regex = new RegExp('(' + keys.join('|') + '|&#[0-9]{1,5};' + ')', 'g');
+
+ return function(value) {
+ return (!value) ? value : String(value).replace(regex, function(match, capture) {
+ if (capture in entities) {
+ return entities[capture];
+ } else {
+ return String.fromCharCode(parseInt(capture.substr(2), 10));
+ }
+ });
+ };
+ })(),
+
+ /**
+ * Appends content to the query string of a URL, handling logic for whether to place
+ * a question mark or ampersand.
+ * @param {String} url The URL to append to.
+ * @param {String} string The content to append to the URL.
+ * @return (String) The resulting URL
+ */
+ urlAppend : function(url, string) {
+ if (!Ext.isEmpty(string)) {
+ return url + (url.indexOf('?') === -1 ? '?' : '&') + string;
+ }
+
+ return url;
+ },
+
+ /**
+ * Trims whitespace from either end of a string, leaving spaces within the string intact. Example:
+ * @example
+var s = ' foo bar ';
+alert('-' + s + '-'); //alerts "- foo bar -"
+alert('-' + Ext.String.trim(s) + '-'); //alerts "-foo bar-"
+
+ * @param {String} string The string to escape
+ * @return {String} The trimmed string
+ */
+ trim: function(string) {
+ return string.replace(Ext.String.trimRegex, "");
+ },
+
+ /**
+ * Capitalize the given string
+ * @param {String} string
+ * @return {String}
+ */
+ capitalize: function(string) {
+ return string.charAt(0).toUpperCase() + string.substr(1);
+ },
+
+ /**
+ * Truncate a string and add an ellipsis ('...') to the end if it exceeds the specified length
+ * @param {String} value The string to truncate
+ * @param {Number} length The maximum length to allow before truncating
+ * @param {Boolean} word True to try to find a common word break
+ * @return {String} The converted text
+ */
+ ellipsis: function(value, len, word) {
+ if (value && value.length > len) {
+ if (word) {
+ var vs = value.substr(0, len - 2),
+ index = Math.max(vs.lastIndexOf(' '), vs.lastIndexOf('.'), vs.lastIndexOf('!'), vs.lastIndexOf('?'));
+ if (index !== -1 && index >= (len - 15)) {
+ return vs.substr(0, index) + "...";
+ }
+ }
+ return value.substr(0, len - 3) + "...";
+ }
+ return value;
+ },
+
+ /**
+ * Escapes the passed string for use in a regular expression
+ * @param {String} string
+ * @return {String}
+ */
+ escapeRegex: function(string) {
+ return string.replace(Ext.String.escapeRegexRe, "\\$1");
+ },
+
+ /**
+ * Escapes the passed string for ' and \
+ * @param {String} string The string to escape
+ * @return {String} The escaped string
+ */
+ escape: function(string) {
+ return string.replace(Ext.String.escapeRe, "\\$1");
+ },
+
+ /**
+ * Utility function that allows you to easily switch a string between two alternating values. The passed value
+ * is compared to the current string, and if they are equal, the other value that was passed in is returned. If
+ * they are already different, the first value passed in is returned. Note that this method returns the new value
+ * but does not change the current string.
+ * <pre><code>
+ // alternate sort directions
+ sort = Ext.String.toggle(sort, 'ASC', 'DESC');
+
+ // instead of conditional logic:
+ sort = (sort == 'ASC' ? 'DESC' : 'ASC');
+ </code></pre>
+ * @param {String} string The current string
+ * @param {String} value The value to compare to the current string
+ * @param {String} other The new value to use if the string already equals the first value passed in
+ * @return {String} The new value
+ */
+ toggle: function(string, value, other) {
+ return string === value ? other : value;
+ },
+
+ /**
+ * Pads the left side of a string with a specified character. This is especially useful
+ * for normalizing number and date strings. Example usage:
+ *
+ * <pre><code>
+var s = Ext.String.leftPad('123', 5, '0');
+// s now contains the string: '00123'
+ </code></pre>
+ * @param {String} string The original string
+ * @param {Number} size The total length of the output string
+ * @param {String} character (optional) The character with which to pad the original string (defaults to empty string " ")
+ * @return {String} The padded string
+ */
+ leftPad: function(string, size, character) {
+ var result = String(string);
+ character = character || " ";
+ while (result.length < size) {
+ result = character + result;
+ }
+ return result;
+ },
+
+ /**
+ * Allows you to define a tokenized string and pass an arbitrary number of arguments to replace the tokens. Each
+ * token must be unique, and must increment in the format {0}, {1}, etc. Example usage:
+ * <pre><code>
+var cls = 'my-class', text = 'Some text';
+var s = Ext.String.format('<div class="{0}">{1}</div>', cls, text);
+// s now contains the string: '<div class="my-class">Some text</div>'
+ </code></pre>
+ * @param {String} string The tokenized string to be formatted
+ * @param {String} value1 The value to replace token {0}
+ * @param {String} value2 Etc...
+ * @return {String} The formatted string
+ */
+ format: function(format) {
+ var args = Ext.Array.toArray(arguments, 1);
+ return format.replace(Ext.String.formatRe, function(m, i) {
+ return args[i];
+ });
+ },
+
+ /**
+ * Returns a string with a specified number of repititions a given string pattern.
+ * The pattern be separated by a different string.
+ *
+ * var s = Ext.String.repeat('---', 4); // = '------------'
+ * var t = Ext.String.repeat('--', 3, '/'); // = '--/--/--'
+ *
+ * @param {String} pattern The pattern to repeat.
+ * @param {Number} count The number of times to repeat the pattern (may be 0).
+ * @param {String} sep An option string to separate each pattern.
+ */
+ repeat: function(pattern, count, sep) {
+ for (var buf = [], i = count; i--; ) {
+ buf.push(pattern);
+ }
+ return buf.join(sep || '');
+ }
+};
+
+/**
+ * @class Ext.Number
+ *
+ * A collection of useful static methods to deal with numbers
+ * @singleton
+ */
+
+(function() {
+
+var isToFixedBroken = (0.9).toFixed() !== '1';
+
+Ext.Number = {
+ /**
+ * Checks whether or not the passed number is within a desired range. If the number is already within the
+ * range it is returned, otherwise the min or max value is returned depending on which side of the range is
+ * exceeded. Note that this method returns the constrained value but does not change the current number.
+ * @param {Number} number The number to check
+ * @param {Number} min The minimum number in the range
+ * @param {Number} max The maximum number in the range
+ * @return {Number} The constrained value if outside the range, otherwise the current value
+ */
+ constrain: function(number, min, max) {
+ number = parseFloat(number);
+
+ if (!isNaN(min)) {
+ number = Math.max(number, min);
+ }
+ if (!isNaN(max)) {
+ number = Math.min(number, max);
+ }
+ return number;
+ },
+
+ /**
+ * Snaps the passed number between stopping points based upon a passed increment value.
+ * @param {Number} value The unsnapped value.
+ * @param {Number} increment The increment by which the value must move.
+ * @param {Number} minValue The minimum value to which the returned value must be constrained. Overrides the increment..
+ * @param {Number} maxValue The maximum value to which the returned value must be constrained. Overrides the increment..
+ * @return {Number} The value of the nearest snap target.
+ */
+ snap : function(value, increment, minValue, maxValue) {
+ var newValue = value,
+ m;
+
+ if (!(increment && value)) {
+ return value;
+ }
+ m = value % increment;
+ if (m !== 0) {
+ newValue -= m;
+ if (m * 2 >= increment) {
+ newValue += increment;
+ } else if (m * 2 < -increment) {
+ newValue -= increment;
+ }
+ }
+ return Ext.Number.constrain(newValue, minValue, maxValue);
+ },
+
+ /**
+ * Formats a number using fixed-point notation
+ * @param {Number} value The number to format
+ * @param {Number} precision The number of digits to show after the decimal point
+ */
+ toFixed: function(value, precision) {
+ if (isToFixedBroken) {
+ precision = precision || 0;
+ var pow = Math.pow(10, precision);
+ return (Math.round(value * pow) / pow).toFixed(precision);
+ }
+
+ return value.toFixed(precision);
+ },
+
+ /**
+ * Validate that a value is numeric and convert it to a number if necessary. Returns the specified default value if
+ * it is not.
+
+Ext.Number.from('1.23', 1); // returns 1.23
+Ext.Number.from('abc', 1); // returns 1
+
+ * @param {Object} value
+ * @param {Number} defaultValue The value to return if the original value is non-numeric
+ * @return {Number} value, if numeric, defaultValue otherwise
+ */
+ from: function(value, defaultValue) {
+ if (isFinite(value)) {
+ value = parseFloat(value);
+ }
+
+ return !isNaN(value) ? value : defaultValue;
+ }
+};
+
+})();
+
+/**
+ * @deprecated 4.0.0 Please use {@link Ext.Number#from} instead.
+ * @member Ext
+ * @method num
+ * @alias Ext.Number#from
+ */
+Ext.num = function() {
+ return Ext.Number.from.apply(this, arguments);
+};
+/**
+ * @class Ext.Array
+ * @singleton
+ * @author Jacky Nguyen <jacky@sencha.com>
+ * @docauthor Jacky Nguyen <jacky@sencha.com>
+ *
+ * A set of useful static methods to deal with arrays; provide missing methods for older browsers.
+ */
+(function() {
+
+ var arrayPrototype = Array.prototype,
+ slice = arrayPrototype.slice,
+ supportsSplice = function () {
+ var array = [],
+ lengthBefore,
+ j = 20;
+
+ if (!array.splice) {
+ return false;
+ }
+
+ // This detects a bug in IE8 splice method:
+ // see http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/
+
+ while (j--) {
+ array.push("A");
+ }
+
+ array.splice(15, 0, "F", "F", "F", "F", "F","F","F","F","F","F","F","F","F","F","F","F","F","F","F","F","F");
+
+ lengthBefore = array.length; //41
+ array.splice(13, 0, "XXX"); // add one element
+
+ if (lengthBefore+1 != array.length) {
+ return false;
+ }
+ // end IE8 bug
+
+ return true;
+ }(),
+ supportsForEach = 'forEach' in arrayPrototype,
+ supportsMap = 'map' in arrayPrototype,
+ supportsIndexOf = 'indexOf' in arrayPrototype,
+ supportsEvery = 'every' in arrayPrototype,
+ supportsSome = 'some' in arrayPrototype,
+ supportsFilter = 'filter' in arrayPrototype,
+ supportsSort = function() {
+ var a = [1,2,3,4,5].sort(function(){ return 0; });
+ return a[0] === 1 && a[1] === 2 && a[2] === 3 && a[3] === 4 && a[4] === 5;
+ }(),
+ supportsSliceOnNodeList = true,
+ ExtArray;
+
+ try {
+ // IE 6 - 8 will throw an error when using Array.prototype.slice on NodeList
+ if (typeof document !== 'undefined') {
+ slice.call(document.getElementsByTagName('body'));
+ }
+ } catch (e) {
+ supportsSliceOnNodeList = false;
+ }
+
+ function fixArrayIndex (array, index) {
+ return (index < 0) ? Math.max(0, array.length + index)
+ : Math.min(array.length, index);
+ }
+
+ /*
+ Does the same work as splice, but with a slightly more convenient signature. The splice
+ method has bugs in IE8, so this is the implementation we use on that platform.
+
+ The rippling of items in the array can be tricky. Consider two use cases:
+
+ index=2
+ removeCount=2
+ /=====\
+ +---+---+---+---+---+---+---+---+
+ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+ +---+---+---+---+---+---+---+---+
+ / \/ \/ \/ \
+ / /\ /\ /\ \
+ / / \/ \/ \ +--------------------------+
+ / / /\ /\ +--------------------------+ \
+ / / / \/ +--------------------------+ \ \
+ / / / /+--------------------------+ \ \ \
+ / / / / \ \ \ \
+ v v v v v v v v
+ +---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+
+ | 0 | 1 | 4 | 5 | 6 | 7 | | 0 | 1 | a | b | c | 4 | 5 | 6 | 7 |
+ +---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+
+ A B \=========/
+ insert=[a,b,c]
+
+ In case A, it is obvious that copying of [4,5,6,7] must be left-to-right so
+ that we don't end up with [0,1,6,7,6,7]. In case B, we have the opposite; we
+ must go right-to-left or else we would end up with [0,1,a,b,c,4,4,4,4].
+ */
+ function replaceSim (array, index, removeCount, insert) {
+ var add = insert ? insert.length : 0,
+ length = array.length,
+ pos = fixArrayIndex(array, index);
+
+ // we try to use Array.push when we can for efficiency...
+ if (pos === length) {
+ if (add) {
+ array.push.apply(array, insert);
+ }
+ } else {
+ var remove = Math.min(removeCount, length - pos),
+ tailOldPos = pos + remove,
+ tailNewPos = tailOldPos + add - remove,
+ tailCount = length - tailOldPos,
+ lengthAfterRemove = length - remove,
+ i;
+
+ if (tailNewPos < tailOldPos) { // case A
+ for (i = 0; i < tailCount; ++i) {
+ array[tailNewPos+i] = array[tailOldPos+i];
+ }
+ } else if (tailNewPos > tailOldPos) { // case B
+ for (i = tailCount; i--; ) {
+ array[tailNewPos+i] = array[tailOldPos+i];
+ }
+ } // else, add == remove (nothing to do)
+
+ if (add && pos === lengthAfterRemove) {
+ array.length = lengthAfterRemove; // truncate array
+ array.push.apply(array, insert);
+ } else {
+ array.length = lengthAfterRemove + add; // reserves space
+ for (i = 0; i < add; ++i) {
+ array[pos+i] = insert[i];
+ }
+ }
+ }
+
+ return array;
+ }
+
+ function replaceNative (array, index, removeCount, insert) {
+ if (insert && insert.length) {
+ if (index < array.length) {
+ array.splice.apply(array, [index, removeCount].concat(insert));
+ } else {
+ array.push.apply(array, insert);
+ }
+ } else {
+ array.splice(index, removeCount);
+ }
+ return array;
+ }
+
+ function eraseSim (array, index, removeCount) {
+ return replaceSim(array, index, removeCount);
+ }
+
+ function eraseNative (array, index, removeCount) {
+ array.splice(index, removeCount);
+ return array;
+ }
+
+ function spliceSim (array, index, removeCount) {
+ var pos = fixArrayIndex(array, index),
+ removed = array.slice(index, fixArrayIndex(array, pos+removeCount));
+
+ if (arguments.length < 4) {
+ replaceSim(array, pos, removeCount);
+ } else {
+ replaceSim(array, pos, removeCount, slice.call(arguments, 3));
+ }
+
+ return removed;
+ }
+
+ function spliceNative (array) {
+ return array.splice.apply(array, slice.call(arguments, 1));
+ }
+
+ var erase = supportsSplice ? eraseNative : eraseSim,
+ replace = supportsSplice ? replaceNative : replaceSim,
+ splice = supportsSplice ? spliceNative : spliceSim;
+
+ // NOTE: from here on, use erase, replace or splice (not native methods)...
+
+ ExtArray = Ext.Array = {
+ /**
+ * Iterates an array or an iterable value and invoke the given callback function for each item.
+ *
+ * var countries = ['Vietnam', 'Singapore', 'United States', 'Russia'];
+ *
+ * Ext.Array.each(countries, function(name, index, countriesItSelf) {
+ * console.log(name);
+ * });
+ *
+ * var sum = function() {
+ * var sum = 0;
+ *
+ * Ext.Array.each(arguments, function(value) {
+ * sum += value;
+ * });
+ *
+ * return sum;
+ * };
+ *
+ * sum(1, 2, 3); // returns 6
+ *
+ * The iteration can be stopped by returning false in the function callback.
+ *
+ * Ext.Array.each(countries, function(name, index, countriesItSelf) {
+ * if (name === 'Singapore') {
+ * return false; // break here
+ * }
+ * });
+ *
+ * {@link Ext#each Ext.each} is alias for {@link Ext.Array#each Ext.Array.each}
+ *
+ * @param {Array/NodeList/Object} iterable The value to be iterated. If this
+ * argument is not iterable, the callback function is called once.
+ * @param {Function} fn The callback function. If it returns false, the iteration stops and this method returns
+ * the current `index`.
+ * @param {Object} fn.item The item at the current `index` in the passed `array`
+ * @param {Number} fn.index The current `index` within the `array`
+ * @param {Array} fn.allItems The `array` itself which was passed as the first argument
+ * @param {Boolean} fn.return Return false to stop iteration.
+ * @param {Object} scope (Optional) The scope (`this` reference) in which the specified function is executed.
+ * @param {Boolean} reverse (Optional) Reverse the iteration order (loop from the end to the beginning)
+ * Defaults false
+ * @return {Boolean} See description for the `fn` parameter.
+ */
+ each: function(array, fn, scope, reverse) {
+ array = ExtArray.from(array);
+
+ var i,
+ ln = array.length;
+
+ if (reverse !== true) {
+ for (i = 0; i < ln; i++) {
+ if (fn.call(scope || array[i], array[i], i, array) === false) {
+ return i;
+ }
+ }
+ }
+ else {
+ for (i = ln - 1; i > -1; i--) {
+ if (fn.call(scope || array[i], array[i], i, array) === false) {
+ return i;
+ }
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Iterates an array and invoke the given callback function for each item. Note that this will simply
+ * delegate to the native Array.prototype.forEach method if supported. It doesn't support stopping the
+ * iteration by returning false in the callback function like {@link Ext.Array#each}. However, performance
+ * could be much better in modern browsers comparing with {@link Ext.Array#each}
+ *
+ * @param {Array} array The array to iterate
+ * @param {Function} fn The callback function.
+ * @param {Object} fn.item The item at the current `index` in the passed `array`
+ * @param {Number} fn.index The current `index` within the `array`
+ * @param {Array} fn.allItems The `array` itself which was passed as the first argument
+ * @param {Object} scope (Optional) The execution scope (`this`) in which the specified function is executed.
+ */
+ forEach: function(array, fn, scope) {
+ if (supportsForEach) {
+ return array.forEach(fn, scope);
+ }
+
+ var i = 0,
+ ln = array.length;
+
+ for (; i < ln; i++) {
+ fn.call(scope, array[i], i, array);
+ }
+ },
+
+ /**
+ * Get the index of the provided `item` in the given `array`, a supplement for the
+ * missing arrayPrototype.indexOf in Internet Explorer.
+ *
+ * @param {Array} array The array to check
+ * @param {Object} item The item to look for
+ * @param {Number} from (Optional) The index at which to begin the search
+ * @return {Number} The index of item in the array (or -1 if it is not found)
+ */
+ indexOf: function(array, item, from) {
+ if (supportsIndexOf) {
+ return array.indexOf(item, from);
+ }
+
+ var i, length = array.length;
+
+ for (i = (from < 0) ? Math.max(0, length + from) : from || 0; i < length; i++) {
+ if (array[i] === item) {
+ return i;
+ }
+ }
+
+ return -1;
+ },
+
+ /**
+ * Checks whether or not the given `array` contains the specified `item`
+ *
+ * @param {Array} array The array to check
+ * @param {Object} item The item to look for
+ * @return {Boolean} True if the array contains the item, false otherwise
+ */
+ contains: function(array, item) {
+ if (supportsIndexOf) {
+ return array.indexOf(item) !== -1;
+ }
+
+ var i, ln;
+
+ for (i = 0, ln = array.length; i < ln; i++) {
+ if (array[i] === item) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Converts any iterable (numeric indices and a length property) into a true array.
+ *
+ * function test() {
+ * var args = Ext.Array.toArray(arguments),
+ * fromSecondToLastArgs = Ext.Array.toArray(arguments, 1);
+ *
+ * alert(args.join(' '));
+ * alert(fromSecondToLastArgs.join(' '));
+ * }
+ *
+ * test('just', 'testing', 'here'); // alerts 'just testing here';
+ * // alerts 'testing here';
+ *
+ * Ext.Array.toArray(document.getElementsByTagName('div')); // will convert the NodeList into an array
+ * Ext.Array.toArray('splitted'); // returns ['s', 'p', 'l', 'i', 't', 't', 'e', 'd']
+ * Ext.Array.toArray('splitted', 0, 3); // returns ['s', 'p', 'l', 'i']
+ *
+ * {@link Ext#toArray Ext.toArray} is alias for {@link Ext.Array#toArray Ext.Array.toArray}
+ *
+ * @param {Object} iterable the iterable object to be turned into a true Array.
+ * @param {Number} start (Optional) a zero-based index that specifies the start of extraction. Defaults to 0
+ * @param {Number} end (Optional) a zero-based index that specifies the end of extraction. Defaults to the last
+ * index of the iterable value
+ * @return {Array} array
+ */
+ toArray: function(iterable, start, end){
+ if (!iterable || !iterable.length) {
+ return [];
+ }
+
+ if (typeof iterable === 'string') {
+ iterable = iterable.split('');
+ }
+
+ if (supportsSliceOnNodeList) {
+ return slice.call(iterable, start || 0, end || iterable.length);
+ }
+
+ var array = [],
+ i;
+
+ start = start || 0;
+ end = end ? ((end < 0) ? iterable.length + end : end) : iterable.length;
+
+ for (i = start; i < end; i++) {
+ array.push(iterable[i]);
+ }
+
+ return array;
+ },
+
+ /**
+ * Plucks the value of a property from each item in the Array. Example:
+ *
+ * Ext.Array.pluck(Ext.query("p"), "className"); // [el1.className, el2.className, ..., elN.className]
+ *
+ * @param {Array/NodeList} array The Array of items to pluck the value from.
+ * @param {String} propertyName The property name to pluck from each element.
+ * @return {Array} The value from each item in the Array.
+ */
+ pluck: function(array, propertyName) {
+ var ret = [],
+ i, ln, item;
+
+ for (i = 0, ln = array.length; i < ln; i++) {
+ item = array[i];
+
+ ret.push(item[propertyName]);
+ }
+
+ return ret;
+ },
+
+ /**
+ * Creates a new array with the results of calling a provided function on every element in this array.
+ *
+ * @param {Array} array
+ * @param {Function} fn Callback function for each item
+ * @param {Object} scope Callback function scope
+ * @return {Array} results
+ */
+ map: function(array, fn, scope) {
+ if (supportsMap) {
+ return array.map(fn, scope);
+ }
+
+ var results = [],
+ i = 0,
+ len = array.length;
+
+ for (; i < len; i++) {
+ results[i] = fn.call(scope, array[i], i, array);
+ }
+
+ return results;
+ },
+
+ /**
+ * Executes the specified function for each array element until the function returns a falsy value.
+ * If such an item is found, the function will return false immediately.
+ * Otherwise, it will return true.
+ *
+ * @param {Array} array
+ * @param {Function} fn Callback function for each item
+ * @param {Object} scope Callback function scope
+ * @return {Boolean} True if no false value is returned by the callback function.
+ */
+ every: function(array, fn, scope) {
+ if (supportsEvery) {
+ return array.every(fn, scope);
+ }
+
+ var i = 0,
+ ln = array.length;
+
+ for (; i < ln; ++i) {
+ if (!fn.call(scope, array[i], i, array)) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Executes the specified function for each array element until the function returns a truthy value.
+ * If such an item is found, the function will return true immediately. Otherwise, it will return false.
+ *
+ * @param {Array} array
+ * @param {Function} fn Callback function for each item
+ * @param {Object} scope Callback function scope
+ * @return {Boolean} True if the callback function returns a truthy value.
+ */
+ some: function(array, fn, scope) {
+ if (supportsSome) {
+ return array.some(fn, scope);
+ }
+
+ var i = 0,
+ ln = array.length;
+
+ for (; i < ln; ++i) {
+ if (fn.call(scope, array[i], i, array)) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Filter through an array and remove empty item as defined in {@link Ext#isEmpty Ext.isEmpty}
+ *
+ * See {@link Ext.Array#filter}
+ *
+ * @param {Array} array
+ * @return {Array} results
+ */
+ clean: function(array) {
+ var results = [],
+ i = 0,
+ ln = array.length,
+ item;
+
+ for (; i < ln; i++) {
+ item = array[i];
+
+ if (!Ext.isEmpty(item)) {
+ results.push(item);
+ }
+ }
+
+ return results;
+ },
+
+ /**
+ * Returns a new array with unique items
+ *
+ * @param {Array} array
+ * @return {Array} results
+ */
+ unique: function(array) {
+ var clone = [],
+ i = 0,
+ ln = array.length,
+ item;
+
+ for (; i < ln; i++) {
+ item = array[i];
+
+ if (ExtArray.indexOf(clone, item) === -1) {
+ clone.push(item);
+ }
+ }
+
+ return clone;
+ },
+
+ /**
+ * Creates a new array with all of the elements of this array for which
+ * the provided filtering function returns true.
+ *
+ * @param {Array} array
+ * @param {Function} fn Callback function for each item
+ * @param {Object} scope Callback function scope
+ * @return {Array} results
+ */
+ filter: function(array, fn, scope) {
+ if (supportsFilter) {
+ return array.filter(fn, scope);
+ }
+
+ var results = [],
+ i = 0,
+ ln = array.length;
+
+ for (; i < ln; i++) {
+ if (fn.call(scope, array[i], i, array)) {
+ results.push(array[i]);
+ }
+ }
+
+ return results;
+ },
+
+ /**
+ * Converts a value to an array if it's not already an array; returns:
+ *
+ * - An empty array if given value is `undefined` or `null`
+ * - Itself if given value is already an array
+ * - An array copy if given value is {@link Ext#isIterable iterable} (arguments, NodeList and alike)
+ * - An array with one item which is the given value, otherwise
+ *
+ * @param {Object} value The value to convert to an array if it's not already is an array
+ * @param {Boolean} newReference (Optional) True to clone the given array and return a new reference if necessary,
+ * defaults to false
+ * @return {Array} array
+ */
+ from: function(value, newReference) {
+ if (value === undefined || value === null) {
+ return [];
+ }
+
+ if (Ext.isArray(value)) {
+ return (newReference) ? slice.call(value) : value;
+ }
+
+ if (value && value.length !== undefined && typeof value !== 'string') {
+ return Ext.toArray(value);
+ }
+
+ return [value];
+ },
+
+ /**
+ * Removes the specified item from the array if it exists
+ *
+ * @param {Array} array The array
+ * @param {Object} item The item to remove
+ * @return {Array} The passed array itself
+ */
+ remove: function(array, item) {
+ var index = ExtArray.indexOf(array, item);
+
+ if (index !== -1) {
+ erase(array, index, 1);
+ }
+
+ return array;
+ },
+
+ /**
+ * Push an item into the array only if the array doesn't contain it yet
+ *
+ * @param {Array} array The array
+ * @param {Object} item The item to include
+ */
+ include: function(array, item) {
+ if (!ExtArray.contains(array, item)) {
+ array.push(item);
+ }
+ },
+
+ /**
+ * Clone a flat array without referencing the previous one. Note that this is different
+ * from Ext.clone since it doesn't handle recursive cloning. It's simply a convenient, easy-to-remember method
+ * for Array.prototype.slice.call(array)
+ *
+ * @param {Array} array The array
+ * @return {Array} The clone array
+ */
+ clone: function(array) {
+ return slice.call(array);
+ },
+
+ /**
+ * Merge multiple arrays into one with unique items.
+ *
+ * {@link Ext.Array#union} is alias for {@link Ext.Array#merge}
+ *
+ * @param {Array} array1
+ * @param {Array} array2
+ * @param {Array} etc
+ * @return {Array} merged
+ */
+ merge: function() {
+ var args = slice.call(arguments),
+ array = [],
+ i, ln;
+
+ for (i = 0, ln = args.length; i < ln; i++) {
+ array = array.concat(args[i]);
+ }
+
+ return ExtArray.unique(array);
+ },
+
+ /**
+ * Merge multiple arrays into one with unique items that exist in all of the arrays.
+ *
+ * @param {Array} array1
+ * @param {Array} array2
+ * @param {Array} etc
+ * @return {Array} intersect
+ */
+ intersect: function() {
+ var intersect = [],
+ arrays = slice.call(arguments),
+ i, j, k, minArray, array, x, y, ln, arraysLn, arrayLn;
+
+ if (!arrays.length) {
+ return intersect;
+ }
+
+ // Find the smallest array
+ for (i = x = 0,ln = arrays.length; i < ln,array = arrays[i]; i++) {
+ if (!minArray || array.length < minArray.length) {
+ minArray = array;
+ x = i;
+ }
+ }
+
+ minArray = ExtArray.unique(minArray);
+ erase(arrays, x, 1);
+
+ // Use the smallest unique'd array as the anchor loop. If the other array(s) do contain
+ // an item in the small array, we're likely to find it before reaching the end
+ // of the inner loop and can terminate the search early.
+ for (i = 0,ln = minArray.length; i < ln,x = minArray[i]; i++) {
+ var count = 0;
+
+ for (j = 0,arraysLn = arrays.length; j < arraysLn,array = arrays[j]; j++) {
+ for (k = 0,arrayLn = array.length; k < arrayLn,y = array[k]; k++) {
+ if (x === y) {
+ count++;
+ break;
+ }
+ }
+ }
+
+ if (count === arraysLn) {
+ intersect.push(x);
+ }
+ }
+
+ return intersect;
+ },
+
+ /**
+ * Perform a set difference A-B by subtracting all items in array B from array A.
+ *
+ * @param {Array} arrayA
+ * @param {Array} arrayB
+ * @return {Array} difference
+ */
+ difference: function(arrayA, arrayB) {
+ var clone = slice.call(arrayA),
+ ln = clone.length,
+ i, j, lnB;
+
+ for (i = 0,lnB = arrayB.length; i < lnB; i++) {
+ for (j = 0; j < ln; j++) {
+ if (clone[j] === arrayB[i]) {
+ erase(clone, j, 1);
+ j--;
+ ln--;
+ }
+ }
+ }
+
+ return clone;
+ },
+
+ /**
+ * Returns a shallow copy of a part of an array. This is equivalent to the native
+ * call "Array.prototype.slice.call(array, begin, end)". This is often used when "array"
+ * is "arguments" since the arguments object does not supply a slice method but can
+ * be the context object to Array.prototype.slice.
+ *
+ * @param {Array} array The array (or arguments object).
+ * @param {Number} begin The index at which to begin. Negative values are offsets from
+ * the end of the array.
+ * @param {Number} end The index at which to end. The copied items do not include
+ * end. Negative values are offsets from the end of the array. If end is omitted,
+ * all items up to the end of the array are copied.
+ * @return {Array} The copied piece of the array.
+ */
+ // Note: IE6 will return [] on slice.call(x, undefined).
+ slice: ([1,2].slice(1, undefined).length ?
+ function (array, begin, end) {
+ return slice.call(array, begin, end);
+ } :
+ // at least IE6 uses arguments.length for variadic signature
+ function (array, begin, end) {
+ // After tested for IE 6, the one below is of the best performance
+ // see http://jsperf.com/slice-fix
+ if (typeof begin === 'undefined') {
+ return slice.call(array);
+ }
+ if (typeof end === 'undefined') {
+ return slice.call(array, begin);
+ }
+ return slice.call(array, begin, end);
+ }
+ ),
+
+ /**
+ * Sorts the elements of an Array.
+ * By default, this method sorts the elements alphabetically and ascending.
+ *
+ * @param {Array} array The array to sort.
+ * @param {Function} sortFn (optional) The comparison function.
+ * @return {Array} The sorted array.
+ */
+ sort: function(array, sortFn) {
+ if (supportsSort) {
+ if (sortFn) {
+ return array.sort(sortFn);
+ } else {
+ return array.sort();
+ }
+ }
+
+ var length = array.length,
+ i = 0,
+ comparison,
+ j, min, tmp;
+
+ for (; i < length; i++) {
+ min = i;
+ for (j = i + 1; j < length; j++) {
+ if (sortFn) {
+ comparison = sortFn(array[j], array[min]);
+ if (comparison < 0) {
+ min = j;
+ }
+ } else if (array[j] < array[min]) {
+ min = j;
+ }
+ }
+ if (min !== i) {
+ tmp = array[i];
+ array[i] = array[min];
+ array[min] = tmp;
+ }
+ }
+
+ return array;
+ },
+
+ /**
+ * Recursively flattens into 1-d Array. Injects Arrays inline.
+ *
+ * @param {Array} array The array to flatten
+ * @return {Array} The 1-d array.
+ */
+ flatten: function(array) {
+ var worker = [];
+
+ function rFlatten(a) {
+ var i, ln, v;
+
+ for (i = 0, ln = a.length; i < ln; i++) {
+ v = a[i];
+
+ if (Ext.isArray(v)) {
+ rFlatten(v);
+ } else {
+ worker.push(v);
+ }
+ }
+
+ return worker;
+ }
+
+ return rFlatten(array);
+ },
+
+ /**
+ * Returns the minimum value in the Array.
+ *
+ * @param {Array/NodeList} array The Array from which to select the minimum value.
+ * @param {Function} comparisonFn (optional) a function to perform the comparision which determines minimization.
+ * If omitted the "<" operator will be used. Note: gt = 1; eq = 0; lt = -1
+ * @return {Object} minValue The minimum value
+ */
+ min: function(array, comparisonFn) {
+ var min = array[0],
+ i, ln, item;
+
+ for (i = 0, ln = array.length; i < ln; i++) {
+ item = array[i];
+
+ if (comparisonFn) {
+ if (comparisonFn(min, item) === 1) {
+ min = item;
+ }
+ }
+ else {
+ if (item < min) {
+ min = item;
+ }
+ }
+ }
+
+ return min;
+ },
+
+ /**
+ * Returns the maximum value in the Array.
+ *
+ * @param {Array/NodeList} array The Array from which to select the maximum value.
+ * @param {Function} comparisonFn (optional) a function to perform the comparision which determines maximization.
+ * If omitted the ">" operator will be used. Note: gt = 1; eq = 0; lt = -1
+ * @return {Object} maxValue The maximum value
+ */
+ max: function(array, comparisonFn) {
+ var max = array[0],
+ i, ln, item;
+
+ for (i = 0, ln = array.length; i < ln; i++) {
+ item = array[i];
+
+ if (comparisonFn) {
+ if (comparisonFn(max, item) === -1) {
+ max = item;
+ }
+ }
+ else {
+ if (item > max) {
+ max = item;
+ }
+ }
+ }
+
+ return max;
+ },
+
+ /**
+ * Calculates the mean of all items in the array.
+ *
+ * @param {Array} array The Array to calculate the mean value of.
+ * @return {Number} The mean.
+ */
+ mean: function(array) {
+ return array.length > 0 ? ExtArray.sum(array) / array.length : undefined;
+ },
+
+ /**
+ * Calculates the sum of all items in the given array.
+ *
+ * @param {Array} array The Array to calculate the sum value of.
+ * @return {Number} The sum.
+ */
+ sum: function(array) {
+ var sum = 0,
+ i, ln, item;
+
+ for (i = 0,ln = array.length; i < ln; i++) {
+ item = array[i];
+
+ sum += item;
+ }
+
+ return sum;
+ },
+
+
+ /**
+ * Removes items from an array. This is functionally equivalent to the splice method
+ * of Array, but works around bugs in IE8's splice method and does not copy the
+ * removed elements in order to return them (because very often they are ignored).
+ *
+ * @param {Array} array The Array on which to replace.
+ * @param {Number} index The index in the array at which to operate.
+ * @param {Number} removeCount The number of items to remove at index.
+ * @return {Array} The array passed.
+ * @method
+ */
+ erase: erase,
+
+ /**
+ * Inserts items in to an array.
+ *
+ * @param {Array} array The Array on which to replace.
+ * @param {Number} index The index in the array at which to operate.
+ * @param {Array} items The array of items to insert at index.
+ * @return {Array} The array passed.
+ */
+ insert: function (array, index, items) {
+ return replace(array, index, 0, items);
+ },
+
+ /**
+ * Replaces items in an array. This is functionally equivalent to the splice method
+ * of Array, but works around bugs in IE8's splice method and is often more convenient
+ * to call because it accepts an array of items to insert rather than use a variadic
+ * argument list.
+ *
+ * @param {Array} array The Array on which to replace.
+ * @param {Number} index The index in the array at which to operate.
+ * @param {Number} removeCount The number of items to remove at index (can be 0).
+ * @param {Array} insert (optional) An array of items to insert at index.
+ * @return {Array} The array passed.
+ * @method
+ */
+ replace: replace,
+
+ /**
+ * Replaces items in an array. This is equivalent to the splice method of Array, but
+ * works around bugs in IE8's splice method. The signature is exactly the same as the
+ * splice method except that the array is the first argument. All arguments following
+ * removeCount are inserted in the array at index.
+ *
+ * @param {Array} array The Array on which to replace.
+ * @param {Number} index The index in the array at which to operate.
+ * @param {Number} removeCount The number of items to remove at index (can be 0).
+ * @return {Array} An array containing the removed items.
+ * @method
+ */
+ splice: splice
+ };
+
+ /**
+ * @method
+ * @member Ext
+ * @alias Ext.Array#each
+ */
+ Ext.each = ExtArray.each;
+
+ /**
+ * @method
+ * @member Ext.Array
+ * @alias Ext.Array#merge
+ */
+ ExtArray.union = ExtArray.merge;
+
+ /**
+ * Old alias to {@link Ext.Array#min}
+ * @deprecated 4.0.0 Use {@link Ext.Array#min} instead
+ * @method
+ * @member Ext
+ * @alias Ext.Array#min
+ */
+ Ext.min = ExtArray.min;
+
+ /**
+ * Old alias to {@link Ext.Array#max}
+ * @deprecated 4.0.0 Use {@link Ext.Array#max} instead
+ * @method
+ * @member Ext
+ * @alias Ext.Array#max
+ */
+ Ext.max = ExtArray.max;
+
+ /**
+ * Old alias to {@link Ext.Array#sum}
+ * @deprecated 4.0.0 Use {@link Ext.Array#sum} instead
+ * @method
+ * @member Ext
+ * @alias Ext.Array#sum
+ */
+ Ext.sum = ExtArray.sum;
+
+ /**
+ * Old alias to {@link Ext.Array#mean}
+ * @deprecated 4.0.0 Use {@link Ext.Array#mean} instead
+ * @method
+ * @member Ext
+ * @alias Ext.Array#mean
+ */
+ Ext.mean = ExtArray.mean;
+
+ /**
+ * Old alias to {@link Ext.Array#flatten}
+ * @deprecated 4.0.0 Use {@link Ext.Array#flatten} instead
+ * @method
+ * @member Ext
+ * @alias Ext.Array#flatten
+ */
+ Ext.flatten = ExtArray.flatten;
+
+ /**
+ * Old alias to {@link Ext.Array#clean}
+ * @deprecated 4.0.0 Use {@link Ext.Array#clean} instead
+ * @method
+ * @member Ext
+ * @alias Ext.Array#clean
+ */
+ Ext.clean = ExtArray.clean;
+
+ /**
+ * Old alias to {@link Ext.Array#unique}
+ * @deprecated 4.0.0 Use {@link Ext.Array#unique} instead
+ * @method
+ * @member Ext
+ * @alias Ext.Array#unique
+ */
+ Ext.unique = ExtArray.unique;
+
+ /**
+ * Old alias to {@link Ext.Array#pluck Ext.Array.pluck}
+ * @deprecated 4.0.0 Use {@link Ext.Array#pluck Ext.Array.pluck} instead
+ * @method
+ * @member Ext
+ * @alias Ext.Array#pluck
+ */
+ Ext.pluck = ExtArray.pluck;
+
+ /**
+ * @method
+ * @member Ext
+ * @alias Ext.Array#toArray
+ */
+ Ext.toArray = function() {
+ return ExtArray.toArray.apply(ExtArray, arguments);
+ };
+})();
+
+/**
+ * @class Ext.Function
+ *
+ * A collection of useful static methods to deal with function callbacks
+ * @singleton
+ */
+Ext.Function = {
+
+ /**
+ * A very commonly used method throughout the framework. It acts as a wrapper around another method
+ * which originally accepts 2 arguments for `name` and `value`.
+ * The wrapped function then allows "flexible" value setting of either:
+ *
+ * - `name` and `value` as 2 arguments
+ * - one single object argument with multiple key - value pairs
+ *
+ * For example:
+ *
+ * var setValue = Ext.Function.flexSetter(function(name, value) {
+ * this[name] = value;
+ * });
+ *
+ * // Afterwards
+ * // Setting a single name - value
+ * setValue('name1', 'value1');
+ *
+ * // Settings multiple name - value pairs
+ * setValue({
+ * name1: 'value1',
+ * name2: 'value2',
+ * name3: 'value3'
+ * });
+ *
+ * @param {Function} setter
+ * @returns {Function} flexSetter
+ */
+ flexSetter: function(fn) {
+ return function(a, b) {
+ var k, i;
+
+ if (a === null) {
+ return this;
+ }
+
+ if (typeof a !== 'string') {
+ for (k in a) {
+ if (a.hasOwnProperty(k)) {
+ fn.call(this, k, a[k]);
+ }
+ }
+
+ if (Ext.enumerables) {
+ for (i = Ext.enumerables.length; i--;) {
+ k = Ext.enumerables[i];
+ if (a.hasOwnProperty(k)) {
+ fn.call(this, k, a[k]);
+ }
+ }
+ }
+ } else {
+ fn.call(this, a, b);
+ }
+
+ return this;
+ };
+ },
+
+ /**
+ * Create a new function from the provided `fn`, change `this` to the provided scope, optionally
+ * overrides arguments for the call. (Defaults to the arguments passed by the caller)
+ *
+ * {@link Ext#bind Ext.bind} is alias for {@link Ext.Function#bind Ext.Function.bind}
+ *
+ * @param {Function} fn The function to delegate.
+ * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
+ * **If omitted, defaults to the browser window.**
+ * @param {Array} args (optional) Overrides arguments for the call. (Defaults to the arguments passed by the caller)
+ * @param {Boolean/Number} appendArgs (optional) if True args are appended to call args instead of overriding,
+ * if a number the args are inserted at the specified position
+ * @return {Function} The new function
+ */
+ bind: function(fn, scope, args, appendArgs) {
+ if (arguments.length === 2) {
+ return function() {
+ return fn.apply(scope, arguments);
+ }
+ }
+
+ var method = fn,
+ slice = Array.prototype.slice;
+
+ return function() {
+ var callArgs = args || arguments;
+
+ if (appendArgs === true) {
+ callArgs = slice.call(arguments, 0);
+ callArgs = callArgs.concat(args);
+ }
+ else if (typeof appendArgs == 'number') {
+ callArgs = slice.call(arguments, 0); // copy arguments first
+ Ext.Array.insert(callArgs, appendArgs, args);
+ }
+
+ return method.apply(scope || window, callArgs);
+ };
+ },
+
+ /**
+ * Create a new function from the provided `fn`, the arguments of which are pre-set to `args`.
+ * New arguments passed to the newly created callback when it's invoked are appended after the pre-set ones.
+ * This is especially useful when creating callbacks.
+ *
+ * For example:
+ *
+ * var originalFunction = function(){
+ * alert(Ext.Array.from(arguments).join(' '));
+ * };
+ *
+ * var callback = Ext.Function.pass(originalFunction, ['Hello', 'World']);
+ *
+ * callback(); // alerts 'Hello World'
+ * callback('by Me'); // alerts 'Hello World by Me'
+ *
+ * {@link Ext#pass Ext.pass} is alias for {@link Ext.Function#pass Ext.Function.pass}
+ *
+ * @param {Function} fn The original function
+ * @param {Array} args The arguments to pass to new callback
+ * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
+ * @return {Function} The new callback function
+ */
+ pass: function(fn, args, scope) {
+ if (args) {
+ args = Ext.Array.from(args);
+ }
+
+ return function() {
+ return fn.apply(scope, args.concat(Ext.Array.toArray(arguments)));
+ };
+ },
+
+ /**
+ * Create an alias to the provided method property with name `methodName` of `object`.
+ * Note that the execution scope will still be bound to the provided `object` itself.
+ *
+ * @param {Object/Function} object
+ * @param {String} methodName
+ * @return {Function} aliasFn
+ */
+ alias: function(object, methodName) {
+ return function() {
+ return object[methodName].apply(object, arguments);
+ };
+ },
+
+ /**
+ * Creates an interceptor function. The passed function is called before the original one. If it returns false,
+ * the original one is not called. The resulting function returns the results of the original function.
+ * The passed function is called with the parameters of the original function. Example usage:
+ *
+ * var sayHi = function(name){
+ * alert('Hi, ' + name);
+ * }
+ *
+ * sayHi('Fred'); // alerts "Hi, Fred"
+ *
+ * // create a new function that validates input without
+ * // directly modifying the original function:
+ * var sayHiToFriend = Ext.Function.createInterceptor(sayHi, function(name){
+ * return name == 'Brian';
+ * });
+ *
+ * sayHiToFriend('Fred'); // no alert
+ * sayHiToFriend('Brian'); // alerts "Hi, Brian"
+ *
+ * @param {Function} origFn The original function.
+ * @param {Function} newFn The function to call before the original
+ * @param {Object} scope (optional) The scope (`this` reference) in which the passed function is executed.
+ * **If omitted, defaults to the scope in which the original function is called or the browser window.**
+ * @param {Object} returnValue (optional) The value to return if the passed function return false (defaults to null).
+ * @return {Function} The new function
+ */
+ createInterceptor: function(origFn, newFn, scope, returnValue) {
+ var method = origFn;
+ if (!Ext.isFunction(newFn)) {
+ return origFn;
+ }
+ else {
+ return function() {
+ var me = this,
+ args = arguments;
+ newFn.target = me;
+ newFn.method = origFn;
+ return (newFn.apply(scope || me || window, args) !== false) ? origFn.apply(me || window, args) : returnValue || null;
+ };
+ }
+ },
+
+ /**
+ * Creates a delegate (callback) which, when called, executes after a specific delay.
+ *
+ * @param {Function} fn The function which will be called on a delay when the returned function is called.
+ * Optionally, a replacement (or additional) argument list may be specified.
+ * @param {Number} delay The number of milliseconds to defer execution by whenever called.
+ * @param {Object} scope (optional) The scope (`this` reference) used by the function at execution time.
+ * @param {Array} args (optional) Override arguments for the call. (Defaults to the arguments passed by the caller)
+ * @param {Boolean/Number} appendArgs (optional) if True args are appended to call args instead of overriding,
+ * if a number the args are inserted at the specified position.
+ * @return {Function} A function which, when called, executes the original function after the specified delay.
+ */
+ createDelayed: function(fn, delay, scope, args, appendArgs) {
+ if (scope || args) {
+ fn = Ext.Function.bind(fn, scope, args, appendArgs);
+ }
+ return function() {
+ var me = this;
+ setTimeout(function() {
+ fn.apply(me, arguments);
+ }, delay);
+ };
+ },
+
+ /**
+ * Calls this function after the number of millseconds specified, optionally in a specific scope. Example usage:
+ *
+ * var sayHi = function(name){
+ * alert('Hi, ' + name);
+ * }
+ *
+ * // executes immediately:
+ * sayHi('Fred');
+ *
+ * // executes after 2 seconds:
+ * Ext.Function.defer(sayHi, 2000, this, ['Fred']);
+ *
+ * // this syntax is sometimes useful for deferring
+ * // execution of an anonymous function:
+ * Ext.Function.defer(function(){
+ * alert('Anonymous');
+ * }, 100);
+ *
+ * {@link Ext#defer Ext.defer} is alias for {@link Ext.Function#defer Ext.Function.defer}
+ *
+ * @param {Function} fn The function to defer.
+ * @param {Number} millis The number of milliseconds for the setTimeout call
+ * (if less than or equal to 0 the function is executed immediately)
+ * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
+ * **If omitted, defaults to the browser window.**
+ * @param {Array} args (optional) Overrides arguments for the call. (Defaults to the arguments passed by the caller)
+ * @param {Boolean/Number} appendArgs (optional) if True args are appended to call args instead of overriding,
+ * if a number the args are inserted at the specified position
+ * @return {Number} The timeout id that can be used with clearTimeout
+ */
+ defer: function(fn, millis, obj, args, appendArgs) {
+ fn = Ext.Function.bind(fn, obj, args, appendArgs);
+ if (millis > 0) {
+ return setTimeout(fn, millis);
+ }
+ fn();
+ return 0;
+ },
+
+ /**
+ * Create a combined function call sequence of the original function + the passed function.
+ * The resulting function returns the results of the original function.
+ * The passed function is called with the parameters of the original function. Example usage:
+ *
+ * var sayHi = function(name){
+ * alert('Hi, ' + name);
+ * }
+ *
+ * sayHi('Fred'); // alerts "Hi, Fred"
+ *
+ * var sayGoodbye = Ext.Function.createSequence(sayHi, function(name){
+ * alert('Bye, ' + name);
+ * });
+ *
+ * sayGoodbye('Fred'); // both alerts show
+ *
+ * @param {Function} origFn The original function.
+ * @param {Function} newFn The function to sequence
+ * @param {Object} scope (optional) The scope (`this` reference) in which the passed function is executed.
+ * If omitted, defaults to the scope in which the original function is called or the browser window.
+ * @return {Function} The new function
+ */
+ createSequence: function(origFn, newFn, scope) {
+ if (!Ext.isFunction(newFn)) {
+ return origFn;
+ }
+ else {
+ return function() {
+ var retval = origFn.apply(this || window, arguments);
+ newFn.apply(scope || this || window, arguments);
+ return retval;
+ };
+ }
+ },
+
+ /**
+ * Creates a delegate function, optionally with a bound scope which, when called, buffers
+ * the execution of the passed function for the configured number of milliseconds.
+ * If called again within that period, the impending invocation will be canceled, and the
+ * timeout period will begin again.
+ *
+ * @param {Function} fn The function to invoke on a buffered timer.
+ * @param {Number} buffer The number of milliseconds by which to buffer the invocation of the
+ * function.
+ * @param {Object} scope (optional) The scope (`this` reference) in which
+ * the passed function is executed. If omitted, defaults to the scope specified by the caller.
+ * @param {Array} args (optional) Override arguments for the call. Defaults to the arguments
+ * passed by the caller.
+ * @return {Function} A function which invokes the passed function after buffering for the specified time.
+ */
+ createBuffered: function(fn, buffer, scope, args) {
+ return function(){
+ var timerId;
+ return function() {
+ var me = this;
+ if (timerId) {
+ clearTimeout(timerId);
+ timerId = null;
+ }
+ timerId = setTimeout(function(){
+ fn.apply(scope || me, args || arguments);
+ }, buffer);
+ };
+ }();
+ },
+
+ /**
+ * Creates a throttled version of the passed function which, when called repeatedly and
+ * rapidly, invokes the passed function only after a certain interval has elapsed since the
+ * previous invocation.
+ *
+ * This is useful for wrapping functions which may be called repeatedly, such as
+ * a handler of a mouse move event when the processing is expensive.
+ *
+ * @param {Function} fn The function to execute at a regular time interval.
+ * @param {Number} interval The interval **in milliseconds** on which the passed function is executed.
+ * @param {Object} scope (optional) The scope (`this` reference) in which
+ * the passed function is executed. If omitted, defaults to the scope specified by the caller.
+ * @returns {Function} A function which invokes the passed function at the specified interval.
+ */
+ createThrottled: function(fn, interval, scope) {
+ var lastCallTime, elapsed, lastArgs, timer, execute = function() {
+ fn.apply(scope || this, lastArgs);
+ lastCallTime = new Date().getTime();
+ };
+
+ return function() {
+ elapsed = new Date().getTime() - lastCallTime;
+ lastArgs = arguments;
+
+ clearTimeout(timer);
+ if (!lastCallTime || (elapsed >= interval)) {
+ execute();
+ } else {
+ timer = setTimeout(execute, interval - elapsed);
+ }
+ };
+ },
+
+ /**
+ * Adds behavior to an existing method that is executed before the
+ * original behavior of the function. For example:
+ *
+ * var soup = {
+ * contents: [],
+ * add: function(ingredient) {
+ * this.contents.push(ingredient);
+ * }
+ * };
+ * Ext.Function.interceptBefore(soup, "add", function(ingredient){
+ * if (!this.contents.length && ingredient !== "water") {
+ * // Always add water to start with
+ * this.contents.push("water");
+ * }
+ * });
+ * soup.add("onions");
+ * soup.add("salt");
+ * soup.contents; // will contain: water, onions, salt
+ *
+ * @param {Object} object The target object
+ * @param {String} methodName Name of the method to override
+ * @param {Function} fn Function with the new behavior. It will
+ * be called with the same arguments as the original method. The
+ * return value of this function will be the return value of the
+ * new method.
+ * @return {Function} The new function just created.
+ */
+ interceptBefore: function(object, methodName, fn) {
+ var method = object[methodName] || Ext.emptyFn;
+
+ return object[methodName] = function() {
+ var ret = fn.apply(this, arguments);
+ method.apply(this, arguments);
+
+ return ret;
+ };
+ },
+
+ /**
+ * Adds behavior to an existing method that is executed after the
+ * original behavior of the function. For example:
+ *
+ * var soup = {
+ * contents: [],
+ * add: function(ingredient) {
+ * this.contents.push(ingredient);
+ * }
+ * };
+ * Ext.Function.interceptAfter(soup, "add", function(ingredient){
+ * // Always add a bit of extra salt
+ * this.contents.push("salt");
+ * });
+ * soup.add("water");
+ * soup.add("onions");
+ * soup.contents; // will contain: water, salt, onions, salt
+ *
+ * @param {Object} object The target object
+ * @param {String} methodName Name of the method to override
+ * @param {Function} fn Function with the new behavior. It will
+ * be called with the same arguments as the original method. The
+ * return value of this function will be the return value of the
+ * new method.
+ * @return {Function} The new function just created.
+ */
+ interceptAfter: function(object, methodName, fn) {
+ var method = object[methodName] || Ext.emptyFn;
+
+ return object[methodName] = function() {
+ method.apply(this, arguments);
+ return fn.apply(this, arguments);
+ };
+ }
+};
+
+/**
+ * @method
+ * @member Ext
+ * @alias Ext.Function#defer
+ */
+Ext.defer = Ext.Function.alias(Ext.Function, 'defer');
+
+/**
+ * @method
+ * @member Ext
+ * @alias Ext.Function#pass
+ */
+Ext.pass = Ext.Function.alias(Ext.Function, 'pass');
+
+/**
+ * @method
+ * @member Ext
+ * @alias Ext.Function#bind
+ */
+Ext.bind = Ext.Function.alias(Ext.Function, 'bind');
+
+/**
+ * @author Jacky Nguyen <jacky@sencha.com>
+ * @docauthor Jacky Nguyen <jacky@sencha.com>
+ * @class Ext.Object
+ *
+ * A collection of useful static methods to deal with objects.
+ *
+ * @singleton
+ */
+
+(function() {
+
+var ExtObject = Ext.Object = {
+
+ /**
+ * Converts a `name` - `value` pair to an array of objects with support for nested structures. Useful to construct
+ * query strings. For example:
+ *
+ * var objects = Ext.Object.toQueryObjects('hobbies', ['reading', 'cooking', 'swimming']);
+ *
+ * // objects then equals:
+ * [
+ * { name: 'hobbies', value: 'reading' },
+ * { name: 'hobbies', value: 'cooking' },
+ * { name: 'hobbies', value: 'swimming' },
+ * ];
+ *
+ * var objects = Ext.Object.toQueryObjects('dateOfBirth', {
+ * day: 3,
+ * month: 8,
+ * year: 1987,
+ * extra: {
+ * hour: 4
+ * minute: 30
+ * }
+ * }, true); // Recursive
+ *
+ * // objects then equals:
+ * [
+ * { name: 'dateOfBirth[day]', value: 3 },
+ * { name: 'dateOfBirth[month]', value: 8 },
+ * { name: 'dateOfBirth[year]', value: 1987 },
+ * { name: 'dateOfBirth[extra][hour]', value: 4 },
+ * { name: 'dateOfBirth[extra][minute]', value: 30 },
+ * ];
+ *
+ * @param {String} name
+ * @param {Object/Array} value
+ * @param {Boolean} [recursive=false] True to traverse object recursively
+ * @return {Array}
+ */
+ toQueryObjects: function(name, value, recursive) {
+ var self = ExtObject.toQueryObjects,
+ objects = [],
+ i, ln;
+
+ if (Ext.isArray(value)) {
+ for (i = 0, ln = value.length; i < ln; i++) {
+ if (recursive) {
+ objects = objects.concat(self(name + '[' + i + ']', value[i], true));
+ }
+ else {
+ objects.push({
+ name: name,
+ value: value[i]
+ });
+ }
+ }
+ }
+ else if (Ext.isObject(value)) {
+ for (i in value) {
+ if (value.hasOwnProperty(i)) {
+ if (recursive) {
+ objects = objects.concat(self(name + '[' + i + ']', value[i], true));
+ }
+ else {
+ objects.push({
+ name: name,
+ value: value[i]
+ });
+ }
+ }
+ }
+ }
+ else {
+ objects.push({
+ name: name,
+ value: value
+ });
+ }
+
+ return objects;
+ },
+
+ /**
+ * Takes an object and converts it to an encoded query string.
+ *
+ * Non-recursive:
+ *
+ * Ext.Object.toQueryString({foo: 1, bar: 2}); // returns "foo=1&bar=2"
+ * Ext.Object.toQueryString({foo: null, bar: 2}); // returns "foo=&bar=2"
+ * Ext.Object.toQueryString({'some price': '$300'}); // returns "some%20price=%24300"
+ * Ext.Object.toQueryString({date: new Date(2011, 0, 1)}); // returns "date=%222011-01-01T00%3A00%3A00%22"
+ * Ext.Object.toQueryString({colors: ['red', 'green', 'blue']}); // returns "colors=red&colors=green&colors=blue"
+ *
+ * Recursive:
+ *
+ * Ext.Object.toQueryString({
+ * username: 'Jacky',
+ * dateOfBirth: {
+ * day: 1,
+ * month: 2,
+ * year: 1911
+ * },
+ * hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
+ * }, true); // returns the following string (broken down and url-decoded for ease of reading purpose):
+ * // username=Jacky
+ * // &dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911
+ * // &hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&hobbies[3][0]=nested&hobbies[3][1]=stuff
+ *
+ * @param {Object} object The object to encode
+ * @param {Boolean} [recursive=false] Whether or not to interpret the object in recursive format.
+ * (PHP / Ruby on Rails servers and similar).
+ * @return {String} queryString
+ */
+ toQueryString: function(object, recursive) {
+ var paramObjects = [],
+ params = [],
+ i, j, ln, paramObject, value;
+
+ for (i in object) {
+ if (object.hasOwnProperty(i)) {
+ paramObjects = paramObjects.concat(ExtObject.toQueryObjects(i, object[i], recursive));
+ }
+ }
+
+ for (j = 0, ln = paramObjects.length; j < ln; j++) {
+ paramObject = paramObjects[j];
+ value = paramObject.value;
+
+ if (Ext.isEmpty(value)) {
+ value = '';
+ }
+ else if (Ext.isDate(value)) {
+ value = Ext.Date.toString(value);
+ }
+
+ params.push(encodeURIComponent(paramObject.name) + '=' + encodeURIComponent(String(value)));
+ }
+
+ return params.join('&');
+ },
+
+ /**
+ * Converts a query string back into an object.
+ *
+ * Non-recursive:
+ *
+ * Ext.Object.fromQueryString(foo=1&bar=2); // returns {foo: 1, bar: 2}
+ * Ext.Object.fromQueryString(foo=&bar=2); // returns {foo: null, bar: 2}
+ * Ext.Object.fromQueryString(some%20price=%24300); // returns {'some price': '$300'}
+ * Ext.Object.fromQueryString(colors=red&colors=green&colors=blue); // returns {colors: ['red', 'green', 'blue']}
+ *
+ * Recursive:
+ *
+ * Ext.Object.fromQueryString("username=Jacky&dateOfBirth[day]=1&dateOfBirth[month]=2&dateOfBirth[year]=1911&hobbies[0]=coding&hobbies[1]=eating&hobbies[2]=sleeping&hobbies[3][0]=nested&hobbies[3][1]=stuff", true);
+ * // returns
+ * {
+ * username: 'Jacky',
+ * dateOfBirth: {
+ * day: '1',
+ * month: '2',
+ * year: '1911'
+ * },
+ * hobbies: ['coding', 'eating', 'sleeping', ['nested', 'stuff']]
+ * }
+ *
+ * @param {String} queryString The query string to decode
+ * @param {Boolean} [recursive=false] Whether or not to recursively decode the string. This format is supported by
+ * PHP / Ruby on Rails servers and similar.
+ * @return {Object}
+ */
+ fromQueryString: function(queryString, recursive) {
+ var parts = queryString.replace(/^\?/, '').split('&'),
+ object = {},
+ temp, components, name, value, i, ln,
+ part, j, subLn, matchedKeys, matchedName,
+ keys, key, nextKey;
+
+ for (i = 0, ln = parts.length; i < ln; i++) {
+ part = parts[i];
+
+ if (part.length > 0) {
+ components = part.split('=');
+ name = decodeURIComponent(components[0]);
+ value = (components[1] !== undefined) ? decodeURIComponent(components[1]) : '';
+
+ if (!recursive) {
+ if (object.hasOwnProperty(name)) {
+ if (!Ext.isArray(object[name])) {
+ object[name] = [object[name]];
+ }
+
+ object[name].push(value);
+ }
+ else {
+ object[name] = value;
+ }
+ }
+ else {
+ matchedKeys = name.match(/(\[):?([^\]]*)\]/g);
+ matchedName = name.match(/^([^\[]+)/);
+
+
+ name = matchedName[0];
+ keys = [];
+
+ if (matchedKeys === null) {
+ object[name] = value;
+ continue;
+ }
+
+ for (j = 0, subLn = matchedKeys.length; j < subLn; j++) {
+ key = matchedKeys[j];
+ key = (key.length === 2) ? '' : key.substring(1, key.length - 1);
+ keys.push(key);
+ }
+
+ keys.unshift(name);
+
+ temp = object;
+
+ for (j = 0, subLn = keys.length; j < subLn; j++) {
+ key = keys[j];
+
+ if (j === subLn - 1) {
+ if (Ext.isArray(temp) && key === '') {
+ temp.push(value);
+ }
+ else {
+ temp[key] = value;
+ }
+ }
+ else {
+ if (temp[key] === undefined || typeof temp[key] === 'string') {
+ nextKey = keys[j+1];
+
+ temp[key] = (Ext.isNumeric(nextKey) || nextKey === '') ? [] : {};
+ }
+
+ temp = temp[key];
+ }
+ }
+ }
+ }
+ }
+
+ return object;
+ },
+
+ /**
+ * Iterates through an object and invokes the given callback function for each iteration.
+ * The iteration can be stopped by returning `false` in the callback function. For example:
+ *
+ * var person = {
+ * name: 'Jacky'
+ * hairColor: 'black'
+ * loves: ['food', 'sleeping', 'wife']
+ * };
+ *
+ * Ext.Object.each(person, function(key, value, myself) {
+ * console.log(key + ":" + value);
+ *
+ * if (key === 'hairColor') {
+ * return false; // stop the iteration
+ * }
+ * });
+ *
+ * @param {Object} object The object to iterate
+ * @param {Function} fn The callback function.
+ * @param {String} fn.key
+ * @param {Object} fn.value
+ * @param {Object} fn.object The object itself
+ * @param {Object} [scope] The execution scope (`this`) of the callback function
+ */
+ each: function(object, fn, scope) {
+ for (var property in object) {
+ if (object.hasOwnProperty(property)) {
+ if (fn.call(scope || object, property, object[property], object) === false) {
+ return;
+ }
+ }
+ }
+ },
+
+ /**
+ * Merges any number of objects recursively without referencing them or their children.
+ *
+ * var extjs = {
+ * companyName: 'Ext JS',
+ * products: ['Ext JS', 'Ext GWT', 'Ext Designer'],
+ * isSuperCool: true
+ * office: {
+ * size: 2000,
+ * location: 'Palo Alto',
+ * isFun: true
+ * }
+ * };
+ *
+ * var newStuff = {
+ * companyName: 'Sencha Inc.',
+ * products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
+ * office: {
+ * size: 40000,
+ * location: 'Redwood City'
+ * }
+ * };
+ *
+ * var sencha = Ext.Object.merge(extjs, newStuff);
+ *
+ * // extjs and sencha then equals to
+ * {
+ * companyName: 'Sencha Inc.',
+ * products: ['Ext JS', 'Ext GWT', 'Ext Designer', 'Sencha Touch', 'Sencha Animator'],
+ * isSuperCool: true
+ * office: {
+ * size: 30000,
+ * location: 'Redwood City'
+ * isFun: true
+ * }
+ * }
+ *
+ * @param {Object...} object Any number of objects to merge.
+ * @return {Object} merged The object that is created as a result of merging all the objects passed in.
+ */
+ merge: function(source, key, value) {
+ if (typeof key === 'string') {
+ if (value && value.constructor === Object) {
+ if (source[key] && source[key].constructor === Object) {
+ ExtObject.merge(source[key], value);
+ }
+ else {
+ source[key] = Ext.clone(value);
+ }
+ }
+ else {
+ source[key] = value;
+ }
+
+ return source;
+ }
+
+ var i = 1,
+ ln = arguments.length,
+ object, property;
+
+ for (; i < ln; i++) {
+ object = arguments[i];
+
+ for (property in object) {
+ if (object.hasOwnProperty(property)) {
+ ExtObject.merge(source, property, object[property]);
+ }
+ }
+ }
+
+ return source;
+ },
+
+ /**
+ * Returns the first matching key corresponding to the given value.
+ * If no matching value is found, null is returned.
+ *
+ * var person = {
+ * name: 'Jacky',
+ * loves: 'food'
+ * };
+ *
+ * alert(Ext.Object.getKey(person, 'food')); // alerts 'loves'
+ *
+ * @param {Object} object
+ * @param {Object} value The value to find
+ */
+ getKey: function(object, value) {
+ for (var property in object) {
+ if (object.hasOwnProperty(property) && object[property] === value) {
+ return property;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Gets all values of the given object as an array.
+ *
+ * var values = Ext.Object.getValues({
+ * name: 'Jacky',
+ * loves: 'food'
+ * }); // ['Jacky', 'food']
+ *
+ * @param {Object} object
+ * @return {Array} An array of values from the object
+ */
+ getValues: function(object) {
+ var values = [],
+ property;
+
+ for (property in object) {
+ if (object.hasOwnProperty(property)) {
+ values.push(object[property]);
+ }
+ }
+
+ return values;
+ },
+
+ /**
+ * Gets all keys of the given object as an array.
+ *
+ * var values = Ext.Object.getKeys({
+ * name: 'Jacky',
+ * loves: 'food'
+ * }); // ['name', 'loves']
+ *
+ * @param {Object} object
+ * @return {String[]} An array of keys from the object
+ * @method
+ */
+ getKeys: ('keys' in Object.prototype) ? Object.keys : function(object) {
+ var keys = [],
+ property;
+
+ for (property in object) {
+ if (object.hasOwnProperty(property)) {
+ keys.push(property);
+ }
+ }
+
+ return keys;
+ },
+
+ /**
+ * Gets the total number of this object's own properties
+ *
+ * var size = Ext.Object.getSize({
+ * name: 'Jacky',
+ * loves: 'food'
+ * }); // size equals 2
+ *
+ * @param {Object} object
+ * @return {Number} size
+ */
+ getSize: function(object) {
+ var size = 0,
+ property;
+
+ for (property in object) {
+ if (object.hasOwnProperty(property)) {
+ size++;
+ }
+ }
+
+ return size;
+ }
+};
+
+
+/**
+ * A convenient alias method for {@link Ext.Object#merge}.
+ *
+ * @member Ext
+ * @method merge
+ * @alias Ext.Object#merge
+ */
+Ext.merge = Ext.Object.merge;
+
+/**
+ * Alias for {@link Ext.Object#toQueryString}.
+ *
+ * @member Ext
+ * @method urlEncode
+ * @alias Ext.Object#toQueryString
+ * @deprecated 4.0.0 Use {@link Ext.Object#toQueryString} instead
+ */
+Ext.urlEncode = function() {
+ var args = Ext.Array.from(arguments),
+ prefix = '';
+
+ // Support for the old `pre` argument
+ if ((typeof args[1] === 'string')) {
+ prefix = args[1] + '&';
+ args[1] = false;
+ }
+
+ return prefix + Ext.Object.toQueryString.apply(Ext.Object, args);
+};
+
+/**
+ * Alias for {@link Ext.Object#fromQueryString}.
+ *
+ * @member Ext
+ * @method urlDecode
+ * @alias Ext.Object#fromQueryString
+ * @deprecated 4.0.0 Use {@link Ext.Object#fromQueryString} instead
+ */
+Ext.urlDecode = function() {
+ return Ext.Object.fromQueryString.apply(Ext.Object, arguments);
+};
+
+})();
+
+/**
+ * @class Ext.Date
+ * A set of useful static methods to deal with date
+ * Note that if Ext.Date is required and loaded, it will copy all methods / properties to
+ * this object for convenience
+ *
+ * The date parsing and formatting syntax contains a subset of
+ * <a href="http://www.php.net/date">PHP's date() function</a>, and the formats that are
+ * supported will provide results equivalent to their PHP versions.
+ *
+ * The following is a list of all currently supported formats:
+ * <pre class="">
+Format Description Example returned values
+------ ----------------------------------------------------------------------- -----------------------
+ d Day of the month, 2 digits with leading zeros 01 to 31
+ D A short textual representation of the day of the week Mon to Sun
+ j Day of the month without leading zeros 1 to 31
+ l A full textual representation of the day of the week Sunday to Saturday
+ N ISO-8601 numeric representation of the day of the week 1 (for Monday) through 7 (for Sunday)
+ S English ordinal suffix for the day of the month, 2 characters st, nd, rd or th. Works well with j
+ w Numeric representation of the day of the week 0 (for Sunday) to 6 (for Saturday)
+ z The day of the year (starting from 0) 0 to 364 (365 in leap years)
+ W ISO-8601 week number of year, weeks starting on Monday 01 to 53
+ F A full textual representation of a month, such as January or March January to December
+ m Numeric representation of a month, with leading zeros 01 to 12
+ M A short textual representation of a month Jan to Dec
+ n Numeric representation of a month, without leading zeros 1 to 12
+ t Number of days in the given month 28 to 31
+ L Whether it's a leap year 1 if it is a leap year, 0 otherwise.
+ o ISO-8601 year number (identical to (Y), but if the ISO week number (W) Examples: 1998 or 2004
+ belongs to the previous or next year, that year is used instead)
+ Y A full numeric representation of a year, 4 digits Examples: 1999 or 2003
+ y A two digit representation of a year Examples: 99 or 03
+ a Lowercase Ante meridiem and Post meridiem am or pm
+ A Uppercase Ante meridiem and Post meridiem AM or PM
+ g 12-hour format of an hour without leading zeros 1 to 12
+ G 24-hour format of an hour without leading zeros 0 to 23
+ h 12-hour format of an hour with leading zeros 01 to 12
+ H 24-hour format of an hour with leading zeros 00 to 23
+ i Minutes, with leading zeros 00 to 59
+ s Seconds, with leading zeros 00 to 59
+ u Decimal fraction of a second Examples:
+ (minimum 1 digit, arbitrary number of digits allowed) 001 (i.e. 0.001s) or
+ 100 (i.e. 0.100s) or
+ 999 (i.e. 0.999s) or
+ 999876543210 (i.e. 0.999876543210s)
+ O Difference to Greenwich time (GMT) in hours and minutes Example: +1030
+ P Difference to Greenwich time (GMT) with colon between hours and minutes Example: -08:00
+ T Timezone abbreviation of the machine running the code Examples: EST, MDT, PDT ...
+ Z Timezone offset in seconds (negative if west of UTC, positive if east) -43200 to 50400
+ c ISO 8601 date
+ Notes: Examples:
+ 1) If unspecified, the month / day defaults to the current month / day, 1991 or
+ the time defaults to midnight, while the timezone defaults to the 1992-10 or
+ browser's timezone. If a time is specified, it must include both hours 1993-09-20 or
+ and minutes. The "T" delimiter, seconds, milliseconds and timezone 1994-08-19T16:20+01:00 or
+ are optional. 1995-07-18T17:21:28-02:00 or
+ 2) The decimal fraction of a second, if specified, must contain at 1996-06-17T18:22:29.98765+03:00 or
+ least 1 digit (there is no limit to the maximum number 1997-05-16T19:23:30,12345-0400 or
+ of digits allowed), and may be delimited by either a '.' or a ',' 1998-04-15T20:24:31.2468Z or
+ Refer to the examples on the right for the various levels of 1999-03-14T20:24:32Z or
+ date-time granularity which are supported, or see 2000-02-13T21:25:33
+ http://www.w3.org/TR/NOTE-datetime for more info. 2001-01-12 22:26:34
+ U Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) 1193432466 or -2138434463
+ MS Microsoft AJAX serialized dates \/Date(1238606590509)\/ (i.e. UTC milliseconds since epoch) or
+ \/Date(1238606590509+0800)\/
+</pre>
+ *
+ * Example usage (note that you must escape format specifiers with '\\' to render them as character literals):
+ * <pre><code>
+// Sample date:
+// 'Wed Jan 10 2007 15:05:01 GMT-0600 (Central Standard Time)'
+
+var dt = new Date('1/10/2007 03:05:01 PM GMT-0600');
+console.log(Ext.Date.format(dt, 'Y-m-d')); // 2007-01-10
+console.log(Ext.Date.format(dt, 'F j, Y, g:i a')); // January 10, 2007, 3:05 pm
+console.log(Ext.Date.format(dt, 'l, \\t\\he jS \\of F Y h:i:s A')); // Wednesday, the 10th of January 2007 03:05:01 PM
+</code></pre>
+ *
+ * Here are some standard date/time patterns that you might find helpful. They
+ * are not part of the source of Ext.Date, but to use them you can simply copy this
+ * block of code into any script that is included after Ext.Date and they will also become
+ * globally available on the Date object. Feel free to add or remove patterns as needed in your code.
+ * <pre><code>
+Ext.Date.patterns = {
+ ISO8601Long:"Y-m-d H:i:s",
+ ISO8601Short:"Y-m-d",
+ ShortDate: "n/j/Y",
+ LongDate: "l, F d, Y",
+ FullDateTime: "l, F d, Y g:i:s A",
+ MonthDay: "F d",
+ ShortTime: "g:i A",
+ LongTime: "g:i:s A",
+ SortableDateTime: "Y-m-d\\TH:i:s",
+ UniversalSortableDateTime: "Y-m-d H:i:sO",
+ YearMonth: "F, Y"
+};
+</code></pre>
+ *
+ * Example usage:
+ * <pre><code>
+var dt = new Date();
+console.log(Ext.Date.format(dt, Ext.Date.patterns.ShortDate));
+</code></pre>
+ * <p>Developer-written, custom formats may be used by supplying both a formatting and a parsing function
+ * which perform to specialized requirements. The functions are stored in {@link #parseFunctions} and {@link #formatFunctions}.</p>
+ * @singleton
+ */
+
+/*
+ * Most of the date-formatting functions below are the excellent work of Baron Schwartz.
+ * (see http://www.xaprb.com/blog/2005/12/12/javascript-closures-for-runtime-efficiency/)
+ * They generate precompiled functions from format patterns instead of parsing and
+ * processing each pattern every time a date is formatted. These functions are available
+ * on every Date object.
+ */
+
+(function() {
+
+// create private copy of Ext's Ext.util.Format.format() method
+// - to remove unnecessary dependency
+// - to resolve namespace conflict with MS-Ajax's implementation
+function xf(format) {
+ var args = Array.prototype.slice.call(arguments, 1);
+ return format.replace(/\{(\d+)\}/g, function(m, i) {
+ return args[i];
+ });
+}
+
+Ext.Date = {
+ /**
+ * Returns the current timestamp
+ * @return {Date} The current timestamp
+ * @method
+ */
+ now: Date.now || function() {
+ return +new Date();
+ },
+
+ /**
+ * @private
+ * Private for now
+ */
+ toString: function(date) {
+ var pad = Ext.String.leftPad;
+
+ return date.getFullYear() + "-"
+ + pad(date.getMonth() + 1, 2, '0') + "-"
+ + pad(date.getDate(), 2, '0') + "T"
+ + pad(date.getHours(), 2, '0') + ":"
+ + pad(date.getMinutes(), 2, '0') + ":"
+ + pad(date.getSeconds(), 2, '0');
+ },
+
+ /**
+ * Returns the number of milliseconds between two dates
+ * @param {Date} dateA The first date
+ * @param {Date} dateB (optional) The second date, defaults to now
+ * @return {Number} The difference in milliseconds
+ */
+ getElapsed: function(dateA, dateB) {
+ return Math.abs(dateA - (dateB || new Date()));
+ },
+
+ /**
+ * Global flag which determines if strict date parsing should be used.
+ * Strict date parsing will not roll-over invalid dates, which is the
+ * default behaviour of javascript Date objects.
+ * (see {@link #parse} for more information)
+ * Defaults to <tt>false</tt>.
+ * @type Boolean
+ */
+ useStrict: false,
+
+ // private
+ formatCodeToRegex: function(character, currentGroup) {
+ // Note: currentGroup - position in regex result array (see notes for Ext.Date.parseCodes below)
+ var p = utilDate.parseCodes[character];
+
+ if (p) {
+ p = typeof p == 'function'? p() : p;
+ utilDate.parseCodes[character] = p; // reassign function result to prevent repeated execution
+ }
+
+ return p ? Ext.applyIf({
+ c: p.c ? xf(p.c, currentGroup || "{0}") : p.c
+ }, p) : {
+ g: 0,
+ c: null,
+ s: Ext.String.escapeRegex(character) // treat unrecognised characters as literals
+ };
+ },
+
+ /**
+ * <p>An object hash in which each property is a date parsing function. The property name is the
+ * format string which that function parses.</p>
+ * <p>This object is automatically populated with date parsing functions as
+ * date formats are requested for Ext standard formatting strings.</p>
+ * <p>Custom parsing functions may be inserted into this object, keyed by a name which from then on
+ * may be used as a format string to {@link #parse}.<p>
+ * <p>Example:</p><pre><code>
+Ext.Date.parseFunctions['x-date-format'] = myDateParser;
+</code></pre>
+ * <p>A parsing function should return a Date object, and is passed the following parameters:<div class="mdetail-params"><ul>
+ * <li><code>date</code> : String<div class="sub-desc">The date string to parse.</div></li>
+ * <li><code>strict</code> : Boolean<div class="sub-desc">True to validate date strings while parsing
+ * (i.e. prevent javascript Date "rollover") (The default must be false).
+ * Invalid date strings should return null when parsed.</div></li>
+ * </ul></div></p>
+ * <p>To enable Dates to also be <i>formatted</i> according to that format, a corresponding
+ * formatting function must be placed into the {@link #formatFunctions} property.
+ * @property parseFunctions
+ * @type Object
+ */
+ parseFunctions: {
+ "MS": function(input, strict) {
+ // note: the timezone offset is ignored since the MS Ajax server sends
+ // a UTC milliseconds-since-Unix-epoch value (negative values are allowed)
+ var re = new RegExp('\\/Date\\(([-+])?(\\d+)(?:[+-]\\d{4})?\\)\\/');
+ var r = (input || '').match(re);
+ return r? new Date(((r[1] || '') + r[2]) * 1) : null;
+ }
+ },
+ parseRegexes: [],
+
+ /**
+ * <p>An object hash in which each property is a date formatting function. The property name is the
+ * format string which corresponds to the produced formatted date string.</p>
+ * <p>This object is automatically populated with date formatting functions as
+ * date formats are requested for Ext standard formatting strings.</p>
+ * <p>Custom formatting functions may be inserted into this object, keyed by a name which from then on
+ * may be used as a format string to {@link #format}. Example:</p><pre><code>
+Ext.Date.formatFunctions['x-date-format'] = myDateFormatter;
+</code></pre>
+ * <p>A formatting function should return a string representation of the passed Date object, and is passed the following parameters:<div class="mdetail-params"><ul>
+ * <li><code>date</code> : Date<div class="sub-desc">The Date to format.</div></li>
+ * </ul></div></p>
+ * <p>To enable date strings to also be <i>parsed</i> according to that format, a corresponding
+ * parsing function must be placed into the {@link #parseFunctions} property.
+ * @property formatFunctions
+ * @type Object
+ */
+ formatFunctions: {
+ "MS": function() {
+ // UTC milliseconds since Unix epoch (MS-AJAX serialized date format (MRSF))
+ return '\\/Date(' + this.getTime() + ')\\/';
+ }
+ },
+
+ y2kYear : 50,
+
+ /**
+ * Date interval constant
+ * @type String
+ */
+ MILLI : "ms",
+
+ /**
+ * Date interval constant
+ * @type String
+ */
+ SECOND : "s",
+
+ /**
+ * Date interval constant
+ * @type String
+ */
+ MINUTE : "mi",
+
+ /** Date interval constant
+ * @type String
+ */
+ HOUR : "h",
+
+ /**
+ * Date interval constant
+ * @type String
+ */
+ DAY : "d",
+
+ /**
+ * Date interval constant
+ * @type String
+ */
+ MONTH : "mo",
+
+ /**
+ * Date interval constant
+ * @type String
+ */
+ YEAR : "y",
+
+ /**
+ * <p>An object hash containing default date values used during date parsing.</p>
+ * <p>The following properties are available:<div class="mdetail-params"><ul>
+ * <li><code>y</code> : Number<div class="sub-desc">The default year value. (defaults to undefined)</div></li>
+ * <li><code>m</code> : Number<div class="sub-desc">The default 1-based month value. (defaults to undefined)</div></li>
+ * <li><code>d</code> : Number<div class="sub-desc">The default day value. (defaults to undefined)</div></li>
+ * <li><code>h</code> : Number<div class="sub-desc">The default hour value. (defaults to undefined)</div></li>
+ * <li><code>i</code> : Number<div class="sub-desc">The default minute value. (defaults to undefined)</div></li>
+ * <li><code>s</code> : Number<div class="sub-desc">The default second value. (defaults to undefined)</div></li>
+ * <li><code>ms</code> : Number<div class="sub-desc">The default millisecond value. (defaults to undefined)</div></li>
+ * </ul></div></p>
+ * <p>Override these properties to customize the default date values used by the {@link #parse} method.</p>
+ * <p><b>Note: In countries which experience Daylight Saving Time (i.e. DST), the <tt>h</tt>, <tt>i</tt>, <tt>s</tt>
+ * and <tt>ms</tt> properties may coincide with the exact time in which DST takes effect.
+ * It is the responsiblity of the developer to account for this.</b></p>
+ * Example Usage:
+ * <pre><code>
+// set default day value to the first day of the month
+Ext.Date.defaults.d = 1;
+
+// parse a February date string containing only year and month values.
+// setting the default day value to 1 prevents weird date rollover issues
+// when attempting to parse the following date string on, for example, March 31st 2009.
+Ext.Date.parse('2009-02', 'Y-m'); // returns a Date object representing February 1st 2009
+</code></pre>
+ * @property defaults
+ * @type Object
+ */
+ defaults: {},
+
+ /**
+ * @property {String[]} dayNames
+ * An array of textual day names.
+ * Override these values for international dates.
+ * Example:
+ * <pre><code>
+Ext.Date.dayNames = [
+ 'SundayInYourLang',
+ 'MondayInYourLang',
+ ...
+];
+</code></pre>
+ */
+ dayNames : [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday"
+ ],
+
+ /**
+ * @property {String[]} monthNames
+ * An array of textual month names.
+ * Override these values for international dates.
+ * Example:
+ * <pre><code>
+Ext.Date.monthNames = [
+ 'JanInYourLang',
+ 'FebInYourLang',
+ ...
+];
+</code></pre>
+ */
+ monthNames : [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December"
+ ],
+
+ /**
+ * @property {Object} monthNumbers
+ * An object hash of zero-based javascript month numbers (with short month names as keys. note: keys are case-sensitive).
+ * Override these values for international dates.
+ * Example:
+ * <pre><code>
+Ext.Date.monthNumbers = {
+ 'ShortJanNameInYourLang':0,
+ 'ShortFebNameInYourLang':1,
+ ...
+};
+</code></pre>
+ */
+ monthNumbers : {
+ Jan:0,
+ Feb:1,
+ Mar:2,
+ Apr:3,
+ May:4,
+ Jun:5,
+ Jul:6,
+ Aug:7,
+ Sep:8,
+ Oct:9,
+ Nov:10,
+ Dec:11
+ },
+ /**
+ * @property {String} defaultFormat
+ * <p>The date format string that the {@link Ext.util.Format#dateRenderer}
+ * and {@link Ext.util.Format#date} functions use. See {@link Ext.Date} for details.</p>
+ * <p>This may be overridden in a locale file.</p>
+ */
+ defaultFormat : "m/d/Y",
+ /**
+ * Get the short month name for the given month number.
+ * Override this function for international dates.
+ * @param {Number} month A zero-based javascript month number.
+ * @return {String} The short month name.
+ */
+ getShortMonthName : function(month) {
+ return utilDate.monthNames[month].substring(0, 3);
+ },
+
+ /**
+ * Get the short day name for the given day number.
+ * Override this function for international dates.
+ * @param {Number} day A zero-based javascript day number.
+ * @return {String} The short day name.
+ */
+ getShortDayName : function(day) {
+ return utilDate.dayNames[day].substring(0, 3);
+ },
+
+ /**
+ * Get the zero-based javascript month number for the given short/full month name.
+ * Override this function for international dates.
+ * @param {String} name The short/full month name.
+ * @return {Number} The zero-based javascript month number.
+ */
+ getMonthNumber : function(name) {
+ // handle camel casing for english month names (since the keys for the Ext.Date.monthNumbers hash are case sensitive)
+ return utilDate.monthNumbers[name.substring(0, 1).toUpperCase() + name.substring(1, 3).toLowerCase()];
+ },
+
+ /**
+ * Checks if the specified format contains hour information
+ * @param {String} format The format to check
+ * @return {Boolean} True if the format contains hour information
+ * @method
+ */
+ formatContainsHourInfo : (function(){
+ var stripEscapeRe = /(\\.)/g,
+ hourInfoRe = /([gGhHisucUOPZ]|MS)/;
+ return function(format){
+ return hourInfoRe.test(format.replace(stripEscapeRe, ''));
+ };
+ })(),
+
+ /**
+ * Checks if the specified format contains information about
+ * anything other than the time.
+ * @param {String} format The format to check
+ * @return {Boolean} True if the format contains information about
+ * date/day information.
+ * @method
+ */
+ formatContainsDateInfo : (function(){
+ var stripEscapeRe = /(\\.)/g,
+ dateInfoRe = /([djzmnYycU]|MS)/;
+
+ return function(format){
+ return dateInfoRe.test(format.replace(stripEscapeRe, ''));
+ };
+ })(),
+
+ /**
+ * The base format-code to formatting-function hashmap used by the {@link #format} method.
+ * Formatting functions are strings (or functions which return strings) which
+ * will return the appropriate value when evaluated in the context of the Date object
+ * from which the {@link #format} method is called.
+ * Add to / override these mappings for custom date formatting.
+ * Note: Ext.Date.format() treats characters as literals if an appropriate mapping cannot be found.
+ * Example:
+ * <pre><code>
+Ext.Date.formatCodes.x = "Ext.util.Format.leftPad(this.getDate(), 2, '0')";
+console.log(Ext.Date.format(new Date(), 'X'); // returns the current day of the month
+</code></pre>
+ * @type Object
+ */
+ formatCodes : {
+ d: "Ext.String.leftPad(this.getDate(), 2, '0')",
+ D: "Ext.Date.getShortDayName(this.getDay())", // get localised short day name
+ j: "this.getDate()",
+ l: "Ext.Date.dayNames[this.getDay()]",
+ N: "(this.getDay() ? this.getDay() : 7)",
+ S: "Ext.Date.getSuffix(this)",
+ w: "this.getDay()",
+ z: "Ext.Date.getDayOfYear(this)",
+ W: "Ext.String.leftPad(Ext.Date.getWeekOfYear(this), 2, '0')",
+ F: "Ext.Date.monthNames[this.getMonth()]",
+ m: "Ext.String.leftPad(this.getMonth() + 1, 2, '0')",
+ M: "Ext.Date.getShortMonthName(this.getMonth())", // get localised short month name
+ n: "(this.getMonth() + 1)",
+ t: "Ext.Date.getDaysInMonth(this)",
+ L: "(Ext.Date.isLeapYear(this) ? 1 : 0)",
+ o: "(this.getFullYear() + (Ext.Date.getWeekOfYear(this) == 1 && this.getMonth() > 0 ? +1 : (Ext.Date.getWeekOfYear(this) >= 52 && this.getMonth() < 11 ? -1 : 0)))",
+ Y: "Ext.String.leftPad(this.getFullYear(), 4, '0')",
+ y: "('' + this.getFullYear()).substring(2, 4)",
+ a: "(this.getHours() < 12 ? 'am' : 'pm')",
+ A: "(this.getHours() < 12 ? 'AM' : 'PM')",
+ g: "((this.getHours() % 12) ? this.getHours() % 12 : 12)",
+ G: "this.getHours()",
+ h: "Ext.String.leftPad((this.getHours() % 12) ? this.getHours() % 12 : 12, 2, '0')",
+ H: "Ext.String.leftPad(this.getHours(), 2, '0')",
+ i: "Ext.String.leftPad(this.getMinutes(), 2, '0')",
+ s: "Ext.String.leftPad(this.getSeconds(), 2, '0')",
+ u: "Ext.String.leftPad(this.getMilliseconds(), 3, '0')",
+ O: "Ext.Date.getGMTOffset(this)",
+ P: "Ext.Date.getGMTOffset(this, true)",
+ T: "Ext.Date.getTimezone(this)",
+ Z: "(this.getTimezoneOffset() * -60)",
+
+ c: function() { // ISO-8601 -- GMT format
+ for (var c = "Y-m-dTH:i:sP", code = [], i = 0, l = c.length; i < l; ++i) {
+ var e = c.charAt(i);
+ code.push(e == "T" ? "'T'" : utilDate.getFormatCode(e)); // treat T as a character literal
+ }
+ return code.join(" + ");
+ },
+ /*
+ c: function() { // ISO-8601 -- UTC format
+ return [
+ "this.getUTCFullYear()", "'-'",
+ "Ext.util.Format.leftPad(this.getUTCMonth() + 1, 2, '0')", "'-'",
+ "Ext.util.Format.leftPad(this.getUTCDate(), 2, '0')",
+ "'T'",
+ "Ext.util.Format.leftPad(this.getUTCHours(), 2, '0')", "':'",
+ "Ext.util.Format.leftPad(this.getUTCMinutes(), 2, '0')", "':'",
+ "Ext.util.Format.leftPad(this.getUTCSeconds(), 2, '0')",
+ "'Z'"
+ ].join(" + ");
+ },
+ */
+
+ U: "Math.round(this.getTime() / 1000)"
+ },
+
+ /**
+ * Checks if the passed Date parameters will cause a javascript Date "rollover".
+ * @param {Number} year 4-digit year
+ * @param {Number} month 1-based month-of-year
+ * @param {Number} day Day of month
+ * @param {Number} hour (optional) Hour
+ * @param {Number} minute (optional) Minute
+ * @param {Number} second (optional) Second
+ * @param {Number} millisecond (optional) Millisecond
+ * @return {Boolean} true if the passed parameters do not cause a Date "rollover", false otherwise.
+ */
+ isValid : function(y, m, d, h, i, s, ms) {
+ // setup defaults
+ h = h || 0;
+ i = i || 0;
+ s = s || 0;
+ ms = ms || 0;
+
+ // Special handling for year < 100
+ var dt = utilDate.add(new Date(y < 100 ? 100 : y, m - 1, d, h, i, s, ms), utilDate.YEAR, y < 100 ? y - 100 : 0);
+
+ return y == dt.getFullYear() &&
+ m == dt.getMonth() + 1 &&
+ d == dt.getDate() &&
+ h == dt.getHours() &&
+ i == dt.getMinutes() &&
+ s == dt.getSeconds() &&
+ ms == dt.getMilliseconds();
+ },
+
+ /**
+ * Parses the passed string using the specified date format.
+ * Note that this function expects normal calendar dates, meaning that months are 1-based (i.e. 1 = January).
+ * The {@link #defaults} hash will be used for any date value (i.e. year, month, day, hour, minute, second or millisecond)
+ * which cannot be found in the passed string. If a corresponding default date value has not been specified in the {@link #defaults} hash,
+ * the current date's year, month, day or DST-adjusted zero-hour time value will be used instead.
+ * Keep in mind that the input date string must precisely match the specified format string
+ * in order for the parse operation to be successful (failed parse operations return a null value).
+ * <p>Example:</p><pre><code>
+//dt = Fri May 25 2007 (current date)
+var dt = new Date();
+
+//dt = Thu May 25 2006 (today's month/day in 2006)
+dt = Ext.Date.parse("2006", "Y");
+
+//dt = Sun Jan 15 2006 (all date parts specified)
+dt = Ext.Date.parse("2006-01-15", "Y-m-d");
+
+//dt = Sun Jan 15 2006 15:20:01
+dt = Ext.Date.parse("2006-01-15 3:20:01 PM", "Y-m-d g:i:s A");
+
+// attempt to parse Sun Feb 29 2006 03:20:01 in strict mode
+dt = Ext.Date.parse("2006-02-29 03:20:01", "Y-m-d H:i:s", true); // returns null
+</code></pre>
+ * @param {String} input The raw date string.
+ * @param {String} format The expected date string format.
+ * @param {Boolean} strict (optional) True to validate date strings while parsing (i.e. prevents javascript Date "rollover")
+ (defaults to false). Invalid date strings will return null when parsed.
+ * @return {Date} The parsed Date.
+ */
+ parse : function(input, format, strict) {
+ var p = utilDate.parseFunctions;
+ if (p[format] == null) {
+ utilDate.createParser(format);
+ }
+ return p[format](input, Ext.isDefined(strict) ? strict : utilDate.useStrict);
+ },
+
+ // Backwards compat
+ parseDate: function(input, format, strict){
+ return utilDate.parse(input, format, strict);
+ },
+
+
+ // private
+ getFormatCode : function(character) {
+ var f = utilDate.formatCodes[character];
+
+ if (f) {
+ f = typeof f == 'function'? f() : f;
+ utilDate.formatCodes[character] = f; // reassign function result to prevent repeated execution
+ }
+
+ // note: unknown characters are treated as literals
+ return f || ("'" + Ext.String.escape(character) + "'");
+ },
+
+ // private
+ createFormat : function(format) {
+ var code = [],
+ special = false,
+ ch = '';
+
+ for (var i = 0; i < format.length; ++i) {
+ ch = format.charAt(i);
+ if (!special && ch == "\\") {
+ special = true;
+ } else if (special) {
+ special = false;
+ code.push("'" + Ext.String.escape(ch) + "'");
+ } else {
+ code.push(utilDate.getFormatCode(ch));
+ }
+ }
+ utilDate.formatFunctions[format] = Ext.functionFactory("return " + code.join('+'));
+ },
+
+ // private
+ createParser : (function() {
+ var code = [
+ "var dt, y, m, d, h, i, s, ms, o, z, zz, u, v,",
+ "def = Ext.Date.defaults,",
+ "results = String(input).match(Ext.Date.parseRegexes[{0}]);", // either null, or an array of matched strings
+
+ "if(results){",
+ "{1}",
+
+ "if(u != null){", // i.e. unix time is defined
+ "v = new Date(u * 1000);", // give top priority to UNIX time
+ "}else{",
+ // create Date object representing midnight of the current day;
+ // this will provide us with our date defaults
+ // (note: clearTime() handles Daylight Saving Time automatically)
+ "dt = Ext.Date.clearTime(new Date);",
+
+ // date calculations (note: these calculations create a dependency on Ext.Number.from())
+ "y = Ext.Number.from(y, Ext.Number.from(def.y, dt.getFullYear()));",
+ "m = Ext.Number.from(m, Ext.Number.from(def.m - 1, dt.getMonth()));",
+ "d = Ext.Number.from(d, Ext.Number.from(def.d, dt.getDate()));",
+
+ // time calculations (note: these calculations create a dependency on Ext.Number.from())
+ "h = Ext.Number.from(h, Ext.Number.from(def.h, dt.getHours()));",
+ "i = Ext.Number.from(i, Ext.Number.from(def.i, dt.getMinutes()));",
+ "s = Ext.Number.from(s, Ext.Number.from(def.s, dt.getSeconds()));",
+ "ms = Ext.Number.from(ms, Ext.Number.from(def.ms, dt.getMilliseconds()));",
+
+ "if(z >= 0 && y >= 0){",
+ // both the year and zero-based day of year are defined and >= 0.
+ // these 2 values alone provide sufficient info to create a full date object
+
+ // create Date object representing January 1st for the given year
+ // handle years < 100 appropriately
+ "v = Ext.Date.add(new Date(y < 100 ? 100 : y, 0, 1, h, i, s, ms), Ext.Date.YEAR, y < 100 ? y - 100 : 0);",
+
+ // then add day of year, checking for Date "rollover" if necessary
+ "v = !strict? v : (strict === true && (z <= 364 || (Ext.Date.isLeapYear(v) && z <= 365))? Ext.Date.add(v, Ext.Date.DAY, z) : null);",
+ "}else if(strict === true && !Ext.Date.isValid(y, m + 1, d, h, i, s, ms)){", // check for Date "rollover"
+ "v = null;", // invalid date, so return null
+ "}else{",
+ // plain old Date object
+ // handle years < 100 properly
+ "v = Ext.Date.add(new Date(y < 100 ? 100 : y, m, d, h, i, s, ms), Ext.Date.YEAR, y < 100 ? y - 100 : 0);",
+ "}",
+ "}",
+ "}",
+
+ "if(v){",
+ // favour UTC offset over GMT offset
+ "if(zz != null){",
+ // reset to UTC, then add offset
+ "v = Ext.Date.add(v, Ext.Date.SECOND, -v.getTimezoneOffset() * 60 - zz);",
+ "}else if(o){",
+ // reset to GMT, then add offset
+ "v = Ext.Date.add(v, Ext.Date.MINUTE, -v.getTimezoneOffset() + (sn == '+'? -1 : 1) * (hr * 60 + mn));",
+ "}",
+ "}",
+
+ "return v;"
+ ].join('\n');
+
+ return function(format) {
+ var regexNum = utilDate.parseRegexes.length,
+ currentGroup = 1,
+ calc = [],
+ regex = [],
+ special = false,
+ ch = "";
+
+ for (var i = 0; i < format.length; ++i) {
+ ch = format.charAt(i);
+ if (!special && ch == "\\") {
+ special = true;
+ } else if (special) {
+ special = false;
+ regex.push(Ext.String.escape(ch));
+ } else {
+ var obj = utilDate.formatCodeToRegex(ch, currentGroup);
+ currentGroup += obj.g;
+ regex.push(obj.s);
+ if (obj.g && obj.c) {
+ calc.push(obj.c);
+ }
+ }
+ }
+
+ utilDate.parseRegexes[regexNum] = new RegExp("^" + regex.join('') + "$", 'i');
+ utilDate.parseFunctions[format] = Ext.functionFactory("input", "strict", xf(code, regexNum, calc.join('')));
+ };
+ })(),
+
+ // private
+ parseCodes : {
+ /*
+ * Notes:
+ * g = {Number} calculation group (0 or 1. only group 1 contributes to date calculations.)
+ * c = {String} calculation method (required for group 1. null for group 0. {0} = currentGroup - position in regex result array)
+ * s = {String} regex pattern. all matches are stored in results[], and are accessible by the calculation mapped to 'c'
+ */
+ d: {
+ g:1,
+ c:"d = parseInt(results[{0}], 10);\n",
+ s:"(\\d{2})" // day of month with leading zeroes (01 - 31)
+ },
+ j: {
+ g:1,
+ c:"d = parseInt(results[{0}], 10);\n",
+ s:"(\\d{1,2})" // day of month without leading zeroes (1 - 31)
+ },
+ D: function() {
+ for (var a = [], i = 0; i < 7; a.push(utilDate.getShortDayName(i)), ++i); // get localised short day names
+ return {
+ g:0,
+ c:null,
+ s:"(?:" + a.join("|") +")"
+ };
+ },
+ l: function() {
+ return {
+ g:0,
+ c:null,
+ s:"(?:" + utilDate.dayNames.join("|") + ")"
+ };
+ },
+ N: {
+ g:0,
+ c:null,
+ s:"[1-7]" // ISO-8601 day number (1 (monday) - 7 (sunday))
+ },
+ S: {
+ g:0,
+ c:null,
+ s:"(?:st|nd|rd|th)"
+ },
+ w: {
+ g:0,
+ c:null,
+ s:"[0-6]" // javascript day number (0 (sunday) - 6 (saturday))
+ },
+ z: {
+ g:1,
+ c:"z = parseInt(results[{0}], 10);\n",
+ s:"(\\d{1,3})" // day of the year (0 - 364 (365 in leap years))
+ },
+ W: {
+ g:0,
+ c:null,
+ s:"(?:\\d{2})" // ISO-8601 week number (with leading zero)
+ },
+ F: function() {
+ return {
+ g:1,
+ c:"m = parseInt(Ext.Date.getMonthNumber(results[{0}]), 10);\n", // get localised month number
+ s:"(" + utilDate.monthNames.join("|") + ")"
+ };
+ },
+ M: function() {
+ for (var a = [], i = 0; i < 12; a.push(utilDate.getShortMonthName(i)), ++i); // get localised short month names
+ return Ext.applyIf({
+ s:"(" + a.join("|") + ")"
+ }, utilDate.formatCodeToRegex("F"));
+ },
+ m: {
+ g:1,
+ c:"m = parseInt(results[{0}], 10) - 1;\n",
+ s:"(\\d{2})" // month number with leading zeros (01 - 12)
+ },
+ n: {
+ g:1,
+ c:"m = parseInt(results[{0}], 10) - 1;\n",
+ s:"(\\d{1,2})" // month number without leading zeros (1 - 12)
+ },
+ t: {
+ g:0,
+ c:null,
+ s:"(?:\\d{2})" // no. of days in the month (28 - 31)
+ },
+ L: {
+ g:0,
+ c:null,
+ s:"(?:1|0)"
+ },
+ o: function() {
+ return utilDate.formatCodeToRegex("Y");
+ },
+ Y: {
+ g:1,
+ c:"y = parseInt(results[{0}], 10);\n",
+ s:"(\\d{4})" // 4-digit year
+ },
+ y: {
+ g:1,
+ c:"var ty = parseInt(results[{0}], 10);\n"
+ + "y = ty > Ext.Date.y2kYear ? 1900 + ty : 2000 + ty;\n", // 2-digit year
+ s:"(\\d{1,2})"
+ },
+ /*
+ * In the am/pm parsing routines, we allow both upper and lower case
+ * even though it doesn't exactly match the spec. It gives much more flexibility
+ * in being able to specify case insensitive regexes.
+ */
+ a: {
+ g:1,
+ c:"if (/(am)/i.test(results[{0}])) {\n"
+ + "if (!h || h == 12) { h = 0; }\n"
+ + "} else { if (!h || h < 12) { h = (h || 0) + 12; }}",
+ s:"(am|pm|AM|PM)"
+ },
+ A: {
+ g:1,
+ c:"if (/(am)/i.test(results[{0}])) {\n"
+ + "if (!h || h == 12) { h = 0; }\n"
+ + "} else { if (!h || h < 12) { h = (h || 0) + 12; }}",
+ s:"(AM|PM|am|pm)"
+ },
+ g: function() {
+ return utilDate.formatCodeToRegex("G");
+ },
+ G: {
+ g:1,
+ c:"h = parseInt(results[{0}], 10);\n",
+ s:"(\\d{1,2})" // 24-hr format of an hour without leading zeroes (0 - 23)
+ },
+ h: function() {
+ return utilDate.formatCodeToRegex("H");
+ },
+ H: {
+ g:1,
+ c:"h = parseInt(results[{0}], 10);\n",
+ s:"(\\d{2})" // 24-hr format of an hour with leading zeroes (00 - 23)
+ },
+ i: {
+ g:1,
+ c:"i = parseInt(results[{0}], 10);\n",
+ s:"(\\d{2})" // minutes with leading zeros (00 - 59)
+ },
+ s: {
+ g:1,
+ c:"s = parseInt(results[{0}], 10);\n",
+ s:"(\\d{2})" // seconds with leading zeros (00 - 59)
+ },
+ u: {
+ g:1,
+ c:"ms = results[{0}]; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n",
+ s:"(\\d+)" // decimal fraction of a second (minimum = 1 digit, maximum = unlimited)
+ },
+ O: {
+ g:1,
+ c:[
+ "o = results[{0}];",
+ "var sn = o.substring(0,1),", // get + / - sign
+ "hr = o.substring(1,3)*1 + Math.floor(o.substring(3,5) / 60),", // get hours (performs minutes-to-hour conversion also, just in case)
+ "mn = o.substring(3,5) % 60;", // get minutes
+ "o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + Ext.String.leftPad(hr, 2, '0') + Ext.String.leftPad(mn, 2, '0')) : null;\n" // -12hrs <= GMT offset <= 14hrs
+ ].join("\n"),
+ s: "([+\-]\\d{4})" // GMT offset in hrs and mins
+ },
+ P: {
+ g:1,
+ c:[
+ "o = results[{0}];",
+ "var sn = o.substring(0,1),", // get + / - sign
+ "hr = o.substring(1,3)*1 + Math.floor(o.substring(4,6) / 60),", // get hours (performs minutes-to-hour conversion also, just in case)
+ "mn = o.substring(4,6) % 60;", // get minutes
+ "o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + Ext.String.leftPad(hr, 2, '0') + Ext.String.leftPad(mn, 2, '0')) : null;\n" // -12hrs <= GMT offset <= 14hrs
+ ].join("\n"),
+ s: "([+\-]\\d{2}:\\d{2})" // GMT offset in hrs and mins (with colon separator)
+ },
+ T: {
+ g:0,
+ c:null,
+ s:"[A-Z]{1,4}" // timezone abbrev. may be between 1 - 4 chars
+ },
+ Z: {
+ g:1,
+ c:"zz = results[{0}] * 1;\n" // -43200 <= UTC offset <= 50400
+ + "zz = (-43200 <= zz && zz <= 50400)? zz : null;\n",
+ s:"([+\-]?\\d{1,5})" // leading '+' sign is optional for UTC offset
+ },
+ c: function() {
+ var calc = [],
+ arr = [
+ utilDate.formatCodeToRegex("Y", 1), // year
+ utilDate.formatCodeToRegex("m", 2), // month
+ utilDate.formatCodeToRegex("d", 3), // day
+ utilDate.formatCodeToRegex("h", 4), // hour
+ utilDate.formatCodeToRegex("i", 5), // minute
+ utilDate.formatCodeToRegex("s", 6), // second
+ {c:"ms = results[7] || '0'; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n"}, // decimal fraction of a second (minimum = 1 digit, maximum = unlimited)
+ {c:[ // allow either "Z" (i.e. UTC) or "-0530" or "+08:00" (i.e. UTC offset) timezone delimiters. assumes local timezone if no timezone is specified
+ "if(results[8]) {", // timezone specified
+ "if(results[8] == 'Z'){",
+ "zz = 0;", // UTC
+ "}else if (results[8].indexOf(':') > -1){",
+ utilDate.formatCodeToRegex("P", 8).c, // timezone offset with colon separator
+ "}else{",
+ utilDate.formatCodeToRegex("O", 8).c, // timezone offset without colon separator
+ "}",
+ "}"
+ ].join('\n')}
+ ];
+
+ for (var i = 0, l = arr.length; i < l; ++i) {
+ calc.push(arr[i].c);
+ }
+
+ return {
+ g:1,
+ c:calc.join(""),
+ s:[
+ arr[0].s, // year (required)
+ "(?:", "-", arr[1].s, // month (optional)
+ "(?:", "-", arr[2].s, // day (optional)
+ "(?:",
+ "(?:T| )?", // time delimiter -- either a "T" or a single blank space
+ arr[3].s, ":", arr[4].s, // hour AND minute, delimited by a single colon (optional). MUST be preceded by either a "T" or a single blank space
+ "(?::", arr[5].s, ")?", // seconds (optional)
+ "(?:(?:\\.|,)(\\d+))?", // decimal fraction of a second (e.g. ",12345" or ".98765") (optional)
+ "(Z|(?:[-+]\\d{2}(?::)?\\d{2}))?", // "Z" (UTC) or "-0530" (UTC offset without colon delimiter) or "+08:00" (UTC offset with colon delimiter) (optional)
+ ")?",
+ ")?",
+ ")?"
+ ].join("")
+ };
+ },
+ U: {
+ g:1,
+ c:"u = parseInt(results[{0}], 10);\n",
+ s:"(-?\\d+)" // leading minus sign indicates seconds before UNIX epoch
+ }
+ },
+
+ //Old Ext.Date prototype methods.
+ // private
+ dateFormat: function(date, format) {
+ return utilDate.format(date, format);
+ },
+
+ /**
+ * Formats a date given the supplied format string.
+ * @param {Date} date The date to format
+ * @param {String} format The format string
+ * @return {String} The formatted date
+ */
+ format: function(date, format) {
+ if (utilDate.formatFunctions[format] == null) {
+ utilDate.createFormat(format);
+ }
+ var result = utilDate.formatFunctions[format].call(date);
+ return result + '';
+ },
+
+ /**
+ * Get the timezone abbreviation of the current date (equivalent to the format specifier 'T').
+ *
+ * Note: The date string returned by the javascript Date object's toString() method varies
+ * between browsers (e.g. FF vs IE) and system region settings (e.g. IE in Asia vs IE in America).
+ * For a given date string e.g. "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula Standard Time)",
+ * getTimezone() first tries to get the timezone abbreviation from between a pair of parentheses
+ * (which may or may not be present), failing which it proceeds to get the timezone abbreviation
+ * from the GMT offset portion of the date string.
+ * @param {Date} date The date
+ * @return {String} The abbreviated timezone name (e.g. 'CST', 'PDT', 'EDT', 'MPST' ...).
+ */
+ getTimezone : function(date) {
+ // the following list shows the differences between date strings from different browsers on a WinXP SP2 machine from an Asian locale:
+ //
+ // Opera : "Thu, 25 Oct 2007 22:53:45 GMT+0800" -- shortest (weirdest) date string of the lot
+ // Safari : "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula Standard Time)" -- value in parentheses always gives the correct timezone (same as FF)
+ // FF : "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula Standard Time)" -- value in parentheses always gives the correct timezone
+ // IE : "Thu Oct 25 22:54:35 UTC+0800 2007" -- (Asian system setting) look for 3-4 letter timezone abbrev
+ // IE : "Thu Oct 25 17:06:37 PDT 2007" -- (American system setting) look for 3-4 letter timezone abbrev
+ //
+ // this crazy regex attempts to guess the correct timezone abbreviation despite these differences.
+ // step 1: (?:\((.*)\) -- find timezone in parentheses
+ // step 2: ([A-Z]{1,4})(?:[\-+][0-9]{4})?(?: -?\d+)?) -- if nothing was found in step 1, find timezone from timezone offset portion of date string
+ // step 3: remove all non uppercase characters found in step 1 and 2
+ return date.toString().replace(/^.* (?:\((.*)\)|([A-Z]{1,4})(?:[\-+][0-9]{4})?(?: -?\d+)?)$/, "$1$2").replace(/[^A-Z]/g, "");
+ },
+
+ /**
+ * Get the offset from GMT of the current date (equivalent to the format specifier 'O').
+ * @param {Date} date The date
+ * @param {Boolean} colon (optional) true to separate the hours and minutes with a colon (defaults to false).
+ * @return {String} The 4-character offset string prefixed with + or - (e.g. '-0600').
+ */
+ getGMTOffset : function(date, colon) {
+ var offset = date.getTimezoneOffset();
+ return (offset > 0 ? "-" : "+")
+ + Ext.String.leftPad(Math.floor(Math.abs(offset) / 60), 2, "0")
+ + (colon ? ":" : "")
+ + Ext.String.leftPad(Math.abs(offset % 60), 2, "0");
+ },
+
+ /**
+ * Get the numeric day number of the year, adjusted for leap year.
+ * @param {Date} date The date
+ * @return {Number} 0 to 364 (365 in leap years).
+ */
+ getDayOfYear: function(date) {
+ var num = 0,
+ d = Ext.Date.clone(date),
+ m = date.getMonth(),
+ i;
+
+ for (i = 0, d.setDate(1), d.setMonth(0); i < m; d.setMonth(++i)) {
+ num += utilDate.getDaysInMonth(d);
+ }
+ return num + date.getDate() - 1;
+ },
+
+ /**
+ * Get the numeric ISO-8601 week number of the year.
+ * (equivalent to the format specifier 'W', but without a leading zero).
+ * @param {Date} date The date
+ * @return {Number} 1 to 53
+ * @method
+ */
+ getWeekOfYear : (function() {
+ // adapted from http://www.merlyn.demon.co.uk/weekcalc.htm
+ var ms1d = 864e5, // milliseconds in a day
+ ms7d = 7 * ms1d; // milliseconds in a week
+
+ return function(date) { // return a closure so constants get calculated only once
+ var DC3 = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate() + 3) / ms1d, // an Absolute Day Number
+ AWN = Math.floor(DC3 / 7), // an Absolute Week Number
+ Wyr = new Date(AWN * ms7d).getUTCFullYear();
+
+ return AWN - Math.floor(Date.UTC(Wyr, 0, 7) / ms7d) + 1;
+ };
+ })(),
+
+ /**
+ * Checks if the current date falls within a leap year.
+ * @param {Date} date The date
+ * @return {Boolean} True if the current date falls within a leap year, false otherwise.
+ */
+ isLeapYear : function(date) {
+ var year = date.getFullYear();
+ return !!((year & 3) == 0 && (year % 100 || (year % 400 == 0 && year)));
+ },
+
+ /**
+ * Get the first day of the current month, adjusted for leap year. The returned value
+ * is the numeric day index within the week (0-6) which can be used in conjunction with
+ * the {@link #monthNames} array to retrieve the textual day name.
+ * Example:
+ * <pre><code>
+var dt = new Date('1/10/2007'),
+ firstDay = Ext.Date.getFirstDayOfMonth(dt);
+console.log(Ext.Date.dayNames[firstDay]); //output: 'Monday'
+ * </code></pre>
+ * @param {Date} date The date
+ * @return {Number} The day number (0-6).
+ */
+ getFirstDayOfMonth : function(date) {
+ var day = (date.getDay() - (date.getDate() - 1)) % 7;
+ return (day < 0) ? (day + 7) : day;
+ },
+
+ /**
+ * Get the last day of the current month, adjusted for leap year. The returned value
+ * is the numeric day index within the week (0-6) which can be used in conjunction with
+ * the {@link #monthNames} array to retrieve the textual day name.
+ * Example:
+ * <pre><code>
+var dt = new Date('1/10/2007'),
+ lastDay = Ext.Date.getLastDayOfMonth(dt);
+console.log(Ext.Date.dayNames[lastDay]); //output: 'Wednesday'
+ * </code></pre>
+ * @param {Date} date The date
+ * @return {Number} The day number (0-6).
+ */
+ getLastDayOfMonth : function(date) {
+ return utilDate.getLastDateOfMonth(date).getDay();
+ },
+
+
+ /**
+ * Get the date of the first day of the month in which this date resides.
+ * @param {Date} date The date
+ * @return {Date}
+ */
+ getFirstDateOfMonth : function(date) {
+ return new Date(date.getFullYear(), date.getMonth(), 1);
+ },
+
+ /**
+ * Get the date of the last day of the month in which this date resides.
+ * @param {Date} date The date
+ * @return {Date}
+ */
+ getLastDateOfMonth : function(date) {
+ return new Date(date.getFullYear(), date.getMonth(), utilDate.getDaysInMonth(date));
+ },
+
+ /**
+ * Get the number of days in the current month, adjusted for leap year.
+ * @param {Date} date The date
+ * @return {Number} The number of days in the month.
+ * @method
+ */
+ getDaysInMonth: (function() {
+ var daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
+
+ return function(date) { // return a closure for efficiency
+ var m = date.getMonth();
+
+ return m == 1 && utilDate.isLeapYear(date) ? 29 : daysInMonth[m];
+ };
+ })(),
+
+ /**
+ * Get the English ordinal suffix of the current day (equivalent to the format specifier 'S').
+ * @param {Date} date The date
+ * @return {String} 'st, 'nd', 'rd' or 'th'.
+ */
+ getSuffix : function(date) {
+ switch (date.getDate()) {
+ case 1:
+ case 21:
+ case 31:
+ return "st";
+ case 2:
+ case 22:
+ return "nd";
+ case 3:
+ case 23:
+ return "rd";
+ default:
+ return "th";
+ }
+ },
+
+ /**
+ * Creates and returns a new Date instance with the exact same date value as the called instance.
+ * Dates are copied and passed by reference, so if a copied date variable is modified later, the original
+ * variable will also be changed. When the intention is to create a new variable that will not
+ * modify the original instance, you should create a clone.
+ *
+ * Example of correctly cloning a date:
+ * <pre><code>
+//wrong way:
+var orig = new Date('10/1/2006');
+var copy = orig;
+copy.setDate(5);
+console.log(orig); //returns 'Thu Oct 05 2006'!
+
+//correct way:
+var orig = new Date('10/1/2006'),
+ copy = Ext.Date.clone(orig);
+copy.setDate(5);
+console.log(orig); //returns 'Thu Oct 01 2006'
+ * </code></pre>
+ * @param {Date} date The date
+ * @return {Date} The new Date instance.
+ */
+ clone : function(date) {
+ return new Date(date.getTime());
+ },
+
+ /**
+ * Checks if the current date is affected by Daylight Saving Time (DST).
+ * @param {Date} date The date
+ * @return {Boolean} True if the current date is affected by DST.
+ */
+ isDST : function(date) {
+ // adapted from http://sencha.com/forum/showthread.php?p=247172#post247172
+ // courtesy of @geoffrey.mcgill
+ return new Date(date.getFullYear(), 0, 1).getTimezoneOffset() != date.getTimezoneOffset();
+ },
+
+ /**
+ * Attempts to clear all time information from this Date by setting the time to midnight of the same day,
+ * automatically adjusting for Daylight Saving Time (DST) where applicable.
+ * (note: DST timezone information for the browser's host operating system is assumed to be up-to-date)
+ * @param {Date} date The date
+ * @param {Boolean} clone true to create a clone of this date, clear the time and return it (defaults to false).
+ * @return {Date} this or the clone.
+ */
+ clearTime : function(date, clone) {
+ if (clone) {
+ return Ext.Date.clearTime(Ext.Date.clone(date));
+ }
+
+ // get current date before clearing time
+ var d = date.getDate();
+
+ // clear time
+ date.setHours(0);
+ date.setMinutes(0);
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+
+ if (date.getDate() != d) { // account for DST (i.e. day of month changed when setting hour = 0)
+ // note: DST adjustments are assumed to occur in multiples of 1 hour (this is almost always the case)
+ // refer to http://www.timeanddate.com/time/aboutdst.html for the (rare) exceptions to this rule
+
+ // increment hour until cloned date == current date
+ for (var hr = 1, c = utilDate.add(date, Ext.Date.HOUR, hr); c.getDate() != d; hr++, c = utilDate.add(date, Ext.Date.HOUR, hr));
+
+ date.setDate(d);
+ date.setHours(c.getHours());
+ }
+
+ return date;
+ },
+
+ /**
+ * Provides a convenient method for performing basic date arithmetic. This method
+ * does not modify the Date instance being called - it creates and returns
+ * a new Date instance containing the resulting date value.
+ *
+ * Examples:
+ * <pre><code>
+// Basic usage:
+var dt = Ext.Date.add(new Date('10/29/2006'), Ext.Date.DAY, 5);
+console.log(dt); //returns 'Fri Nov 03 2006 00:00:00'
+
+// Negative values will be subtracted:
+var dt2 = Ext.Date.add(new Date('10/1/2006'), Ext.Date.DAY, -5);
+console.log(dt2); //returns 'Tue Sep 26 2006 00:00:00'
+
+ * </code></pre>
+ *
+ * @param {Date} date The date to modify
+ * @param {String} interval A valid date interval enum value.
+ * @param {Number} value The amount to add to the current date.
+ * @return {Date} The new Date instance.
+ */
+ add : function(date, interval, value) {
+ var d = Ext.Date.clone(date),
+ Date = Ext.Date;
+ if (!interval || value === 0) return d;
+
+ switch(interval.toLowerCase()) {
+ case Ext.Date.MILLI:
+ d.setMilliseconds(d.getMilliseconds() + value);
+ break;
+ case Ext.Date.SECOND:
+ d.setSeconds(d.getSeconds() + value);
+ break;
+ case Ext.Date.MINUTE:
+ d.setMinutes(d.getMinutes() + value);
+ break;
+ case Ext.Date.HOUR:
+ d.setHours(d.getHours() + value);
+ break;
+ case Ext.Date.DAY:
+ d.setDate(d.getDate() + value);
+ break;
+ case Ext.Date.MONTH:
+ var day = date.getDate();
+ if (day > 28) {
+ day = Math.min(day, Ext.Date.getLastDateOfMonth(Ext.Date.add(Ext.Date.getFirstDateOfMonth(date), 'mo', value)).getDate());
+ }
+ d.setDate(day);
+ d.setMonth(date.getMonth() + value);
+ break;
+ case Ext.Date.YEAR:
+ d.setFullYear(date.getFullYear() + value);
+ break;
+ }
+ return d;
+ },
+
+ /**
+ * Checks if a date falls on or between the given start and end dates.
+ * @param {Date} date The date to check
+ * @param {Date} start Start date
+ * @param {Date} end End date
+ * @return {Boolean} true if this date falls on or between the given start and end dates.
+ */
+ between : function(date, start, end) {
+ var t = date.getTime();
+ return start.getTime() <= t && t <= end.getTime();
+ },
+
+ //Maintains compatibility with old static and prototype window.Date methods.
+ compat: function() {
+ var nativeDate = window.Date,
+ p, u,
+ statics = ['useStrict', 'formatCodeToRegex', 'parseFunctions', 'parseRegexes', 'formatFunctions', 'y2kYear', 'MILLI', 'SECOND', 'MINUTE', 'HOUR', 'DAY', 'MONTH', 'YEAR', 'defaults', 'dayNames', 'monthNames', 'monthNumbers', 'getShortMonthName', 'getShortDayName', 'getMonthNumber', 'formatCodes', 'isValid', 'parseDate', 'getFormatCode', 'createFormat', 'createParser', 'parseCodes'],
+ proto = ['dateFormat', 'format', 'getTimezone', 'getGMTOffset', 'getDayOfYear', 'getWeekOfYear', 'isLeapYear', 'getFirstDayOfMonth', 'getLastDayOfMonth', 'getDaysInMonth', 'getSuffix', 'clone', 'isDST', 'clearTime', 'add', 'between'];
+
+ //Append statics
+ Ext.Array.forEach(statics, function(s) {
+ nativeDate[s] = utilDate[s];
+ });
+
+ //Append to prototype
+ Ext.Array.forEach(proto, function(s) {
+ nativeDate.prototype[s] = function() {
+ var args = Array.prototype.slice.call(arguments);
+ args.unshift(this);
+ return utilDate[s].apply(utilDate, args);
+ };
+ });
+ }
+};
+
+var utilDate = Ext.Date;
+
+})();
+
+/**
+ * @author Jacky Nguyen <jacky@sencha.com>
+ * @docauthor Jacky Nguyen <jacky@sencha.com>
+ * @class Ext.Base
+ *
+ * The root of all classes created with {@link Ext#define}.
+ *
+ * Ext.Base is the building block of all Ext classes. All classes in Ext inherit from Ext.Base.
+ * All prototype and static members of this class are inherited by all other classes.
+ */
+(function(flexSetter) {
+
+var Base = Ext.Base = function() {};
+ Base.prototype = {
+ $className: 'Ext.Base',
+
+ $class: Base,
+
+ /**
+ * Get the reference to the current class from which this object was instantiated. Unlike {@link Ext.Base#statics},
+ * `this.self` is scope-dependent and it's meant to be used for dynamic inheritance. See {@link Ext.Base#statics}
+ * for a detailed comparison
+ *
+ * Ext.define('My.Cat', {
+ * statics: {
+ * speciesName: 'Cat' // My.Cat.speciesName = 'Cat'
+ * },
+ *
+ * constructor: function() {
+ * alert(this.self.speciesName); / dependent on 'this'
+ *
+ * return this;
+ * },
+ *
+ * clone: function() {
+ * return new this.self();
+ * }
+ * });
+ *
+ *
+ * Ext.define('My.SnowLeopard', {
+ * extend: 'My.Cat',
+ * statics: {
+ * speciesName: 'Snow Leopard' // My.SnowLeopard.speciesName = 'Snow Leopard'
+ * }
+ * });
+ *
+ * var cat = new My.Cat(); // alerts 'Cat'
+ * var snowLeopard = new My.SnowLeopard(); // alerts 'Snow Leopard'
+ *
+ * var clone = snowLeopard.clone();
+ * alert(Ext.getClassName(clone)); // alerts 'My.SnowLeopard'
+ *
+ * @type Ext.Class
+ * @protected
+ */
+ self: Base,
+
+ // Default constructor, simply returns `this`
+ constructor: function() {
+ return this;
+ },
+
+ //<feature classSystem.config>
+ /**
+ * Initialize configuration for this class. a typical example:
+ *
+ * Ext.define('My.awesome.Class', {
+ * // The default config
+ * config: {
+ * name: 'Awesome',
+ * isAwesome: true
+ * },
+ *
+ * constructor: function(config) {
+ * this.initConfig(config);
+ *
+ * return this;
+ * }
+ * });
+ *
+ * var awesome = new My.awesome.Class({
+ * name: 'Super Awesome'
+ * });
+ *
+ * alert(awesome.getName()); // 'Super Awesome'
+ *
+ * @protected
+ * @param {Object} config
+ * @return {Object} mixins The mixin prototypes as key - value pairs
+ */
+ initConfig: function(config) {
+ if (!this.$configInited) {
+ this.config = Ext.Object.merge({}, this.config || {}, config || {});
+
+ this.applyConfig(this.config);
+
+ this.$configInited = true;
+ }
+
+ return this;
+ },
+
+ /**
+ * @private
+ */
+ setConfig: function(config) {
+ this.applyConfig(config || {});
+
+ return this;
+ },
+
+ /**
+ * @private
+ */
+ applyConfig: flexSetter(function(name, value) {
+ var setter = 'set' + Ext.String.capitalize(name);
+
+ if (typeof this[setter] === 'function') {
+ this[setter].call(this, value);
+ }
+
+ return this;
+ }),
+ //</feature>
+
+ /**
+ * Call the parent's overridden method. For example:
+ *
+ * Ext.define('My.own.A', {
+ * constructor: function(test) {
+ * alert(test);
+ * }
+ * });
+ *
+ * Ext.define('My.own.B', {
+ * extend: 'My.own.A',
+ *
+ * constructor: function(test) {
+ * alert(test);
+ *
+ * this.callParent([test + 1]);
+ * }
+ * });
+ *
+ * Ext.define('My.own.C', {
+ * extend: 'My.own.B',
+ *
+ * constructor: function() {
+ * alert("Going to call parent's overriden constructor...");
+ *
+ * this.callParent(arguments);
+ * }
+ * });
+ *
+ * var a = new My.own.A(1); // alerts '1'
+ * var b = new My.own.B(1); // alerts '1', then alerts '2'
+ * var c = new My.own.C(2); // alerts "Going to call parent's overriden constructor..."
+ * // alerts '2', then alerts '3'
+ *
+ * @protected
+ * @param {Array/Arguments} args The arguments, either an array or the `arguments` object
+ * from the current method, for example: `this.callParent(arguments)`
+ * @return {Object} Returns the result from the superclass' method
+ */
+ callParent: function(args) {
+ var method = this.callParent.caller,
+ parentClass, methodName;
+
+ if (!method.$owner) {
+
+ method = method.caller;
+ }
+
+ parentClass = method.$owner.superclass;
+ methodName = method.$name;
+
+
+ return parentClass[methodName].apply(this, args || []);
+ },
+
+
+ /**
+ * Get the reference to the class from which this object was instantiated. Note that unlike {@link Ext.Base#self},
+ * `this.statics()` is scope-independent and it always returns the class from which it was called, regardless of what
+ * `this` points to during run-time
+ *
+ * Ext.define('My.Cat', {
+ * statics: {
+ * totalCreated: 0,
+ * speciesName: 'Cat' // My.Cat.speciesName = 'Cat'
+ * },
+ *
+ * constructor: function() {
+ * var statics = this.statics();
+ *
+ * alert(statics.speciesName); // always equals to 'Cat' no matter what 'this' refers to
+ * // equivalent to: My.Cat.speciesName
+ *
+ * alert(this.self.speciesName); // dependent on 'this'
+ *
+ * statics.totalCreated++;
+ *
+ * return this;
+ * },
+ *
+ * clone: function() {
+ * var cloned = new this.self; // dependent on 'this'
+ *
+ * cloned.groupName = this.statics().speciesName; // equivalent to: My.Cat.speciesName
+ *
+ * return cloned;
+ * }
+ * });
+ *
+ *
+ * Ext.define('My.SnowLeopard', {
+ * extend: 'My.Cat',
+ *
+ * statics: {
+ * speciesName: 'Snow Leopard' // My.SnowLeopard.speciesName = 'Snow Leopard'
+ * },
+ *
+ * constructor: function() {
+ * this.callParent();
+ * }
+ * });
+ *
+ * var cat = new My.Cat(); // alerts 'Cat', then alerts 'Cat'
+ *
+ * var snowLeopard = new My.SnowLeopard(); // alerts 'Cat', then alerts 'Snow Leopard'
+ *
+ * var clone = snowLeopard.clone();
+ * alert(Ext.getClassName(clone)); // alerts 'My.SnowLeopard'
+ * alert(clone.groupName); // alerts 'Cat'
+ *
+ * alert(My.Cat.totalCreated); // alerts 3
+ *
+ * @protected
+ * @return {Ext.Class}
+ */
+ statics: function() {
+ var method = this.statics.caller,
+ self = this.self;
+
+ if (!method) {
+ return self;
+ }
+
+ return method.$owner;
+ },
+
+ /**
+ * Call the original method that was previously overridden with {@link Ext.Base#override}
+ *
+ * Ext.define('My.Cat', {
+ * constructor: function() {
+ * alert("I'm a cat!");
+ *
+ * return this;
+ * }
+ * });
+ *
+ * My.Cat.override({
+ * constructor: function() {
+ * alert("I'm going to be a cat!");
+ *
+ * var instance = this.callOverridden();
+ *
+ * alert("Meeeeoooowwww");
+ *
+ * return instance;
+ * }
+ * });
+ *
+ * var kitty = new My.Cat(); // alerts "I'm going to be a cat!"
+ * // alerts "I'm a cat!"
+ * // alerts "Meeeeoooowwww"
+ *
+ * @param {Array/Arguments} args The arguments, either an array or the `arguments` object
+ * @return {Object} Returns the result after calling the overridden method
+ * @protected
+ */
+ callOverridden: function(args) {
+ var method = this.callOverridden.caller;
+
+
+ return method.$previous.apply(this, args || []);
+ },
+
+ destroy: function() {}
+ };
+
+ // These static properties will be copied to every newly created class with {@link Ext#define}
+ Ext.apply(Ext.Base, {
+ /**
+ * Create a new instance of this Class.
+ *
+ * Ext.define('My.cool.Class', {
+ * ...
+ * });
+ *
+ * My.cool.Class.create({
+ * someConfig: true
+ * });
+ *
+ * All parameters are passed to the constructor of the class.
+ *
+ * @return {Object} the created instance.
+ * @static
+ * @inheritable
+ */
+ create: function() {
+ return Ext.create.apply(Ext, [this].concat(Array.prototype.slice.call(arguments, 0)));
+ },
+
+ /**
+ * @private
+ * @inheritable
+ */
+ own: function(name, value) {
+ if (typeof value == 'function') {
+ this.ownMethod(name, value);
+ }
+ else {
+ this.prototype[name] = value;
+ }
+ },
+
+ /**
+ * @private
+ * @inheritable
+ */
+ ownMethod: function(name, fn) {
+ var originalFn;
+
+ if (typeof fn.$owner !== 'undefined' && fn !== Ext.emptyFn) {
+ originalFn = fn;
+
+ fn = function() {
+ return originalFn.apply(this, arguments);
+ };
+ }
+
+ fn.$owner = this;
+ fn.$name = name;
+
+ this.prototype[name] = fn;
+ },
+
+ /**
+ * Add / override static properties of this class.
+ *
+ * Ext.define('My.cool.Class', {
+ * ...
+ * });
+ *
+ * My.cool.Class.addStatics({
+ * someProperty: 'someValue', // My.cool.Class.someProperty = 'someValue'
+ * method1: function() { ... }, // My.cool.Class.method1 = function() { ... };
+ * method2: function() { ... } // My.cool.Class.method2 = function() { ... };
+ * });
+ *
+ * @param {Object} members
+ * @return {Ext.Base} this
+ * @static
+ * @inheritable
+ */
+ addStatics: function(members) {
+ for (var name in members) {
+ if (members.hasOwnProperty(name)) {
+ this[name] = members[name];
+ }
+ }
+
+ return this;
+ },
+
+ /**
+ * @private
+ * @param {Object} members
+ */
+ addInheritableStatics: function(members) {
+ var inheritableStatics,
+ hasInheritableStatics,
+ prototype = this.prototype,
+ name, member;
+
+ inheritableStatics = prototype.$inheritableStatics;
+ hasInheritableStatics = prototype.$hasInheritableStatics;
+
+ if (!inheritableStatics) {
+ inheritableStatics = prototype.$inheritableStatics = [];
+ hasInheritableStatics = prototype.$hasInheritableStatics = {};
+ }
+
+
+ for (name in members) {
+ if (members.hasOwnProperty(name)) {
+ member = members[name];
+ this[name] = member;
+
+ if (!hasInheritableStatics[name]) {
+ hasInheritableStatics[name] = true;
+ inheritableStatics.push(name);
+ }
+ }
+ }
+
+ return this;
+ },
+
+ /**
+ * Add methods / properties to the prototype of this class.
+ *
+ * Ext.define('My.awesome.Cat', {
+ * constructor: function() {
+ * ...
+ * }
+ * });
+ *
+ * My.awesome.Cat.implement({
+ * meow: function() {
+ * alert('Meowww...');
+ * }
+ * });
+ *
+ * var kitty = new My.awesome.Cat;
+ * kitty.meow();
+ *
+ * @param {Object} members
+ * @static
+ * @inheritable
+ */
+ implement: function(members) {
+ var prototype = this.prototype,
+ enumerables = Ext.enumerables,
+ name, i, member;
+ for (name in members) {
+ if (members.hasOwnProperty(name)) {
+ member = members[name];
+
+ if (typeof member === 'function') {
+ member.$owner = this;
+ member.$name = name;
+ }
+
+ prototype[name] = member;
+ }
+ }
+
+ if (enumerables) {
+ for (i = enumerables.length; i--;) {
+ name = enumerables[i];
+
+ if (members.hasOwnProperty(name)) {
+ member = members[name];
+ member.$owner = this;
+ member.$name = name;
+ prototype[name] = member;
+ }
+ }
+ }
+ },
+
+ /**
+ * Borrow another class' members to the prototype of this class.
+ *
+ * Ext.define('Bank', {
+ * money: '$$$',
+ * printMoney: function() {
+ * alert('$$$$$$$');
+ * }
+ * });
+ *
+ * Ext.define('Thief', {
+ * ...
+ * });
+ *
+ * Thief.borrow(Bank, ['money', 'printMoney']);
+ *
+ * var steve = new Thief();
+ *
+ * alert(steve.money); // alerts '$$$'
+ * steve.printMoney(); // alerts '$$$$$$$'
+ *
+ * @param {Ext.Base} fromClass The class to borrow members from
+ * @param {String/String[]} members The names of the members to borrow
+ * @return {Ext.Base} this
+ * @static
+ * @inheritable
+ */
+ borrow: function(fromClass, members) {
+ var fromPrototype = fromClass.prototype,
+ i, ln, member;
+
+ members = Ext.Array.from(members);
+
+ for (i = 0, ln = members.length; i < ln; i++) {
+ member = members[i];
+
+ this.own(member, fromPrototype[member]);
+ }
+
+ return this;
+ },
+
+ /**
+ * Override prototype members of this class. Overridden methods can be invoked via
+ * {@link Ext.Base#callOverridden}
+ *
+ * Ext.define('My.Cat', {
+ * constructor: function() {
+ * alert("I'm a cat!");
+ *
+ * return this;
+ * }
+ * });
+ *
+ * My.Cat.override({
+ * constructor: function() {
+ * alert("I'm going to be a cat!");
+ *
+ * var instance = this.callOverridden();
+ *
+ * alert("Meeeeoooowwww");
+ *
+ * return instance;
+ * }
+ * });
+ *
+ * var kitty = new My.Cat(); // alerts "I'm going to be a cat!"
+ * // alerts "I'm a cat!"
+ * // alerts "Meeeeoooowwww"
+ *
+ * @param {Object} members
+ * @return {Ext.Base} this
+ * @static
+ * @inheritable
+ */
+ override: function(members) {
+ var prototype = this.prototype,
+ enumerables = Ext.enumerables,
+ name, i, member, previous;
+
+ if (arguments.length === 2) {
+ name = members;
+ member = arguments[1];
+
+ if (typeof member == 'function') {
+ if (typeof prototype[name] == 'function') {
+ previous = prototype[name];
+ member.$previous = previous;
+ }
+
+ this.ownMethod(name, member);
+ }
+ else {
+ prototype[name] = member;
+ }
+
+ return this;
+ }
+
+ for (name in members) {
+ if (members.hasOwnProperty(name)) {
+ member = members[name];
+
+ if (typeof member === 'function') {
+ if (typeof prototype[name] === 'function') {
+ previous = prototype[name];
+ member.$previous = previous;
+ }
+
+ this.ownMethod(name, member);
+ }
+ else {
+ prototype[name] = member;
+ }
+ }
+ }
+
+ if (enumerables) {
+ for (i = enumerables.length; i--;) {
+ name = enumerables[i];
+
+ if (members.hasOwnProperty(name)) {
+ if (typeof prototype[name] !== 'undefined') {
+ previous = prototype[name];
+ members[name].$previous = previous;
+ }
+
+ this.ownMethod(name, members[name]);
+ }
+ }
+ }
+
+ return this;
+ },
+
+ //<feature classSystem.mixins>
+ /**
+ * Used internally by the mixins pre-processor
+ * @private
+ * @inheritable
+ */
+ mixin: function(name, cls) {
+ var mixin = cls.prototype,
+ my = this.prototype,
+ key, fn;
+
+ for (key in mixin) {
+ if (mixin.hasOwnProperty(key)) {
+ if (typeof my[key] === 'undefined' && key !== 'mixins' && key !== 'mixinId') {
+ if (typeof mixin[key] === 'function') {
+ fn = mixin[key];
+
+ if (typeof fn.$owner === 'undefined') {
+ this.ownMethod(key, fn);
+ }
+ else {
+ my[key] = fn;
+ }
+ }
+ else {
+ my[key] = mixin[key];
+ }
+ }
+ //<feature classSystem.config>
+ else if (key === 'config' && my.config && mixin.config) {
+ Ext.Object.merge(my.config, mixin.config);
+ }
+ //</feature>
+ }
+ }
+
+ if (typeof mixin.onClassMixedIn !== 'undefined') {
+ mixin.onClassMixedIn.call(cls, this);
+ }
+
+ if (!my.hasOwnProperty('mixins')) {
+ if ('mixins' in my) {
+ my.mixins = Ext.Object.merge({}, my.mixins);
+ }
+ else {
+ my.mixins = {};
+ }
+ }
+
+ my.mixins[name] = mixin;
+ },
+ //</feature>
+
+ /**
+ * Get the current class' name in string format.
+ *
+ * Ext.define('My.cool.Class', {
+ * constructor: function() {
+ * alert(this.self.getName()); // alerts 'My.cool.Class'
+ * }
+ * });
+ *
+ * My.cool.Class.getName(); // 'My.cool.Class'
+ *
+ * @return {String} className
+ * @static
+ * @inheritable
+ */
+ getName: function() {
+ return Ext.getClassName(this);
+ },
+
+ /**
+ * Create aliases for existing prototype methods. Example:
+ *
+ * Ext.define('My.cool.Class', {
+ * method1: function() { ... },
+ * method2: function() { ... }
+ * });
+ *
+ * var test = new My.cool.Class();
+ *
+ * My.cool.Class.createAlias({
+ * method3: 'method1',
+ * method4: 'method2'
+ * });
+ *
+ * test.method3(); // test.method1()
+ *
+ * My.cool.Class.createAlias('method5', 'method3');
+ *
+ * test.method5(); // test.method3() -> test.method1()
+ *
+ * @param {String/Object} alias The new method name, or an object to set multiple aliases. See
+ * {@link Ext.Function#flexSetter flexSetter}
+ * @param {String/Object} origin The original method name
+ * @static
+ * @inheritable
+ * @method
+ */
+ createAlias: flexSetter(function(alias, origin) {
+ this.prototype[alias] = function() {
+ return this[origin].apply(this, arguments);
+ }
+ })
+ });
+
+})(Ext.Function.flexSetter);
+
+/**
+ * @author Jacky Nguyen <jacky@sencha.com>
+ * @docauthor Jacky Nguyen <jacky@sencha.com>
+ * @class Ext.Class
+ *
+ * Handles class creation throughout the framework. This is a low level factory that is used by Ext.ClassManager and generally
+ * should not be used directly. If you choose to use Ext.Class you will lose out on the namespace, aliasing and depency loading
+ * features made available by Ext.ClassManager. The only time you would use Ext.Class directly is to create an anonymous class.
+ *
+ * If you wish to create a class you should use {@link Ext#define Ext.define} which aliases
+ * {@link Ext.ClassManager#create Ext.ClassManager.create} to enable namespacing and dynamic dependency resolution.
+ *
+ * Ext.Class is the factory and **not** the superclass of everything. For the base class that **all** Ext classes inherit
+ * from, see {@link Ext.Base}.
+ */
+(function() {
+
+ var Class,
+ Base = Ext.Base,
+ baseStaticProperties = [],
+ baseStaticProperty;
+
+ for (baseStaticProperty in Base) {
+ if (Base.hasOwnProperty(baseStaticProperty)) {
+ baseStaticProperties.push(baseStaticProperty);
+ }
+ }
+
+ /**
+ * @method constructor
+ * Creates new class.
+ * @param {Object} classData An object represent the properties of this class
+ * @param {Function} createdFn (Optional) The callback function to be executed when this class is fully created.
+ * Note that the creation process can be asynchronous depending on the pre-processors used.
+ * @return {Ext.Base} The newly created class
+ */
+ Ext.Class = Class = function(newClass, classData, onClassCreated) {
+ if (typeof newClass != 'function') {
+ onClassCreated = classData;
+ classData = newClass;
+ newClass = function() {
+ return this.constructor.apply(this, arguments);
+ };
+ }
+
+ if (!classData) {
+ classData = {};
+ }
+
+ var preprocessorStack = classData.preprocessors || Class.getDefaultPreprocessors(),
+ registeredPreprocessors = Class.getPreprocessors(),
+ index = 0,
+ preprocessors = [],
+ preprocessor, staticPropertyName, process, i, j, ln;
+
+ for (i = 0, ln = baseStaticProperties.length; i < ln; i++) {
+ staticPropertyName = baseStaticProperties[i];
+ newClass[staticPropertyName] = Base[staticPropertyName];
+ }
+
+ delete classData.preprocessors;
+
+ for (j = 0, ln = preprocessorStack.length; j < ln; j++) {
+ preprocessor = preprocessorStack[j];
+
+ if (typeof preprocessor == 'string') {
+ preprocessor = registeredPreprocessors[preprocessor];
+
+ if (!preprocessor.always) {
+ if (classData.hasOwnProperty(preprocessor.name)) {
+ preprocessors.push(preprocessor.fn);
+ }
+ }
+ else {
+ preprocessors.push(preprocessor.fn);
+ }
+ }
+ else {
+ preprocessors.push(preprocessor);
+ }
+ }
+
+ classData.onClassCreated = onClassCreated || Ext.emptyFn;
+
+ classData.onBeforeClassCreated = function(cls, data) {
+ onClassCreated = data.onClassCreated;
+
+ delete data.onBeforeClassCreated;
+ delete data.onClassCreated;
+
+ cls.implement(data);
+
+ onClassCreated.call(cls, cls);
+ };
+
+ process = function(cls, data) {
+ preprocessor = preprocessors[index++];
+
+ if (!preprocessor) {
+ data.onBeforeClassCreated.apply(this, arguments);
+ return;
+ }
+
+ if (preprocessor.call(this, cls, data, process) !== false) {
+ process.apply(this, arguments);
+ }
+ };
+
+ process.call(Class, newClass, classData);
+
+ return newClass;
+ };
+
+ Ext.apply(Class, {
+
+ /** @private */
+ preprocessors: {},
+
+ /**
+ * Register a new pre-processor to be used during the class creation process
+ *
+ * @member Ext.Class
+ * @param {String} name The pre-processor's name
+ * @param {Function} fn The callback function to be executed. Typical format:
+ *
+ * function(cls, data, fn) {
+ * // Your code here
+ *
+ * // Execute this when the processing is finished.
+ * // Asynchronous processing is perfectly ok
+ * if (fn) {
+ * fn.call(this, cls, data);
+ * }
+ * });
+ *
+ * @param {Function} fn.cls The created class
+ * @param {Object} fn.data The set of properties passed in {@link Ext.Class} constructor
+ * @param {Function} fn.fn The callback function that **must** to be executed when this pre-processor finishes,
+ * regardless of whether the processing is synchronous or aynchronous
+ *
+ * @return {Ext.Class} this
+ * @static
+ */
+ registerPreprocessor: function(name, fn, always) {
+ this.preprocessors[name] = {
+ name: name,
+ always: always || false,
+ fn: fn
+ };
+
+ return this;
+ },
+
+ /**
+ * Retrieve a pre-processor callback function by its name, which has been registered before
+ *
+ * @param {String} name
+ * @return {Function} preprocessor
+ * @static
+ */
+ getPreprocessor: function(name) {
+ return this.preprocessors[name];
+ },
+
+ getPreprocessors: function() {
+ return this.preprocessors;
+ },
+
+ /**
+ * Retrieve the array stack of default pre-processors
+ *
+ * @return {Function[]} defaultPreprocessors
+ * @static
+ */
+ getDefaultPreprocessors: function() {
+ return this.defaultPreprocessors || [];
+ },
+
+ /**
+ * Set the default array stack of default pre-processors
+ *
+ * @param {Function/Function[]} preprocessors
+ * @return {Ext.Class} this
+ * @static
+ */
+ setDefaultPreprocessors: function(preprocessors) {
+ this.defaultPreprocessors = Ext.Array.from(preprocessors);
+
+ return this;
+ },
+
+ /**
+ * Inserts this pre-processor at a specific position in the stack, optionally relative to
+ * any existing pre-processor. For example:
+ *
+ * Ext.Class.registerPreprocessor('debug', function(cls, data, fn) {
+ * // Your code here
+ *
+ * if (fn) {
+ * fn.call(this, cls, data);
+ * }
+ * }).setDefaultPreprocessorPosition('debug', 'last');
+ *
+ * @param {String} name The pre-processor name. Note that it needs to be registered with
+ * {@link #registerPreprocessor registerPreprocessor} before this
+ * @param {String} offset The insertion position. Four possible values are:
+ * 'first', 'last', or: 'before', 'after' (relative to the name provided in the third argument)
+ * @param {String} relativeName
+ * @return {Ext.Class} this
+ * @static
+ */
+ setDefaultPreprocessorPosition: function(name, offset, relativeName) {
+ var defaultPreprocessors = this.defaultPreprocessors,
+ index;
+
+ if (typeof offset == 'string') {
+ if (offset === 'first') {
+ defaultPreprocessors.unshift(name);
+
+ return this;
+ }
+ else if (offset === 'last') {
+ defaultPreprocessors.push(name);
+
+ return this;
+ }
+
+ offset = (offset === 'after') ? 1 : -1;
+ }
+
+ index = Ext.Array.indexOf(defaultPreprocessors, relativeName);
+
+ if (index !== -1) {
+ Ext.Array.splice(defaultPreprocessors, Math.max(0, index + offset), 0, name);
+ }
+
+ return this;
+ }
+ });
+
+ /**
+ * @cfg {String} extend
+ * The parent class that this class extends. For example:
+ *
+ * Ext.define('Person', {
+ * say: function(text) { alert(text); }
+ * });
+ *
+ * Ext.define('Developer', {
+ * extend: 'Person',
+ * say: function(text) { this.callParent(["print "+text]); }
+ * });
+ */
+ Class.registerPreprocessor('extend', function(cls, data) {
+ var extend = data.extend,
+ base = Ext.Base,
+ basePrototype = base.prototype,
+ prototype = function() {},
+ parent, i, k, ln, staticName, parentStatics,
+ parentPrototype, clsPrototype;
+
+ if (extend && extend !== Object) {
+ parent = extend;
+ }
+ else {
+ parent = base;
+ }
+
+ parentPrototype = parent.prototype;
+
+ prototype.prototype = parentPrototype;
+ clsPrototype = cls.prototype = new prototype();
+
+ if (!('$class' in parent)) {
+ for (i in basePrototype) {
+ if (!parentPrototype[i]) {
+ parentPrototype[i] = basePrototype[i];
+ }
+ }
+ }
+
+ clsPrototype.self = cls;
+
+ cls.superclass = clsPrototype.superclass = parentPrototype;
+
+ delete data.extend;
+
+ //<feature classSystem.inheritableStatics>
+ // Statics inheritance
+ parentStatics = parentPrototype.$inheritableStatics;
+
+ if (parentStatics) {
+ for (k = 0, ln = parentStatics.length; k < ln; k++) {
+ staticName = parentStatics[k];
+
+ if (!cls.hasOwnProperty(staticName)) {
+ cls[staticName] = parent[staticName];
+ }
+ }
+ }
+ //</feature>
+
+ //<feature classSystem.config>
+ // Merge the parent class' config object without referencing it
+ if (parentPrototype.config) {
+ clsPrototype.config = Ext.Object.merge({}, parentPrototype.config);
+ }
+ else {
+ clsPrototype.config = {};
+ }
+ //</feature>
+
+ //<feature classSystem.onClassExtended>
+ if (clsPrototype.$onExtended) {
+ clsPrototype.$onExtended.call(cls, cls, data);
+ }
+
+ if (data.onClassExtended) {
+ clsPrototype.$onExtended = data.onClassExtended;
+ delete data.onClassExtended;
+ }
+ //</feature>
+
+ }, true);
+
+ //<feature classSystem.statics>
+ /**
+ * @cfg {Object} statics
+ * List of static methods for this class. For example:
+ *
+ * Ext.define('Computer', {
+ * statics: {
+ * factory: function(brand) {
+ * // 'this' in static methods refer to the class itself
+ * return new this(brand);
+ * }
+ * },
+ *
+ * constructor: function() { ... }
+ * });
+ *
+ * var dellComputer = Computer.factory('Dell');
+ */
+ Class.registerPreprocessor('statics', function(cls, data) {
+ cls.addStatics(data.statics);
+
+ delete data.statics;
+ });
+ //</feature>
+
+ //<feature classSystem.inheritableStatics>
+ /**
+ * @cfg {Object} inheritableStatics
+ * List of inheritable static methods for this class.
+ * Otherwise just like {@link #statics} but subclasses inherit these methods.
+ */
+ Class.registerPreprocessor('inheritableStatics', function(cls, data) {
+ cls.addInheritableStatics(data.inheritableStatics);
+
+ delete data.inheritableStatics;
+ });
+ //</feature>
+
+ //<feature classSystem.config>
+ /**
+ * @cfg {Object} config
+ * List of configuration options with their default values, for which automatically
+ * accessor methods are generated. For example:
+ *
+ * Ext.define('SmartPhone', {
+ * config: {
+ * hasTouchScreen: false,
+ * operatingSystem: 'Other',
+ * price: 500
+ * },
+ * constructor: function(cfg) {
+ * this.initConfig(cfg);
+ * }
+ * });
+ *
+ * var iPhone = new SmartPhone({
+ * hasTouchScreen: true,
+ * operatingSystem: 'iOS'
+ * });
+ *
+ * iPhone.getPrice(); // 500;
+ * iPhone.getOperatingSystem(); // 'iOS'
+ * iPhone.getHasTouchScreen(); // true;
+ * iPhone.hasTouchScreen(); // true
+ */
+ Class.registerPreprocessor('config', function(cls, data) {
+ var prototype = cls.prototype;
+
+ Ext.Object.each(data.config, function(name) {
+ var cName = name.charAt(0).toUpperCase() + name.substr(1),
+ pName = name,
+ apply = 'apply' + cName,
+ setter = 'set' + cName,
+ getter = 'get' + cName;
+
+ if (!(apply in prototype) && !data.hasOwnProperty(apply)) {
+ data[apply] = function(val) {
+ return val;
+ };
+ }
+
+ if (!(setter in prototype) && !data.hasOwnProperty(setter)) {
+ data[setter] = function(val) {
+ var ret = this[apply].call(this, val, this[pName]);
+
+ if (typeof ret != 'undefined') {
+ this[pName] = ret;
+ }
+
+ return this;
+ };
+ }
+
+ if (!(getter in prototype) && !data.hasOwnProperty(getter)) {
+ data[getter] = function() {
+ return this[pName];
+ };
+ }
+ });
+
+ Ext.Object.merge(prototype.config, data.config);
+ delete data.config;
+ });
+ //</feature>
+
+ //<feature classSystem.mixins>
+ /**
+ * @cfg {Object} mixins
+ * List of classes to mix into this class. For example:
+ *
+ * Ext.define('CanSing', {
+ * sing: function() {
+ * alert("I'm on the highway to hell...")
+ * }
+ * });
+ *
+ * Ext.define('Musician', {
+ * extend: 'Person',
+ *
+ * mixins: {
+ * canSing: 'CanSing'
+ * }
+ * })
+ */
+ Class.registerPreprocessor('mixins', function(cls, data) {
+ var mixins = data.mixins,
+ name, mixin, i, ln;
+
+ delete data.mixins;
+
+ Ext.Function.interceptBefore(data, 'onClassCreated', function(cls) {
+ if (mixins instanceof Array) {
+ for (i = 0,ln = mixins.length; i < ln; i++) {
+ mixin = mixins[i];
+ name = mixin.prototype.mixinId || mixin.$className;
+
+ cls.mixin(name, mixin);
+ }
+ }
+ else {
+ for (name in mixins) {
+ if (mixins.hasOwnProperty(name)) {
+ cls.mixin(name, mixins[name]);
+ }
+ }
+ }
+ });
+ });
+
+ //</feature>
+
+ Class.setDefaultPreprocessors([
+ 'extend'
+ //<feature classSystem.statics>
+ ,'statics'
+ //</feature>
+ //<feature classSystem.inheritableStatics>
+ ,'inheritableStatics'
+ //</feature>
+ //<feature classSystem.config>
+ ,'config'
+ //</feature>
+ //<feature classSystem.mixins>
+ ,'mixins'
+ //</feature>
+ ]);
+
+ //<feature classSystem.backwardsCompatible>
+ // Backwards compatible
+ Ext.extend = function(subclass, superclass, members) {
+ if (arguments.length === 2 && Ext.isObject(superclass)) {
+ members = superclass;
+ superclass = subclass;
+ subclass = null;
+ }
+
+ var cls;
+
+ if (!superclass) {
+ Ext.Error.raise("Attempting to extend from a class which has not been loaded on the page.");
+ }
+
+ members.extend = superclass;
+ members.preprocessors = [
+ 'extend'
+ //<feature classSystem.statics>
+ ,'statics'
+ //</feature>
+ //<feature classSystem.inheritableStatics>
+ ,'inheritableStatics'
+ //</feature>
+ //<feature classSystem.mixins>
+ ,'mixins'
+ //</feature>
+ //<feature classSystem.config>
+ ,'config'
+ //</feature>
+ ];
+
+ if (subclass) {
+ cls = new Class(subclass, members);
+ }
+ else {
+ cls = new Class(members);
+ }
+
+ cls.prototype.override = function(o) {
+ for (var m in o) {
+ if (o.hasOwnProperty(m)) {
+ this[m] = o[m];
+ }
+ }
+ };
+
+ return cls;
+ };
+ //</feature>
+
+})();
+
+/**
+ * @author Jacky Nguyen <jacky@sencha.com>
+ * @docauthor Jacky Nguyen <jacky@sencha.com>
+ * @class Ext.ClassManager
+ *
+ * Ext.ClassManager manages all classes and handles mapping from string class name to
+ * actual class objects throughout the whole framework. It is not generally accessed directly, rather through
+ * these convenient shorthands:
+ *
+ * - {@link Ext#define Ext.define}
+ * - {@link Ext#create Ext.create}
+ * - {@link Ext#widget Ext.widget}
+ * - {@link Ext#getClass Ext.getClass}
+ * - {@link Ext#getClassName Ext.getClassName}
+ *
+ * # Basic syntax:
+ *
+ * Ext.define(className, properties);
+ *
+ * in which `properties` is an object represent a collection of properties that apply to the class. See
+ * {@link Ext.ClassManager#create} for more detailed instructions.
+ *
+ * Ext.define('Person', {
+ * name: 'Unknown',
+ *
+ * constructor: function(name) {
+ * if (name) {
+ * this.name = name;
+ * }
+ *
+ * return this;
+ * },
+ *
+ * eat: function(foodType) {
+ * alert("I'm eating: " + foodType);
+ *
+ * return this;
+ * }
+ * });
+ *
+ * var aaron = new Person("Aaron");
+ * aaron.eat("Sandwich"); // alert("I'm eating: Sandwich");
+ *
+ * Ext.Class has a powerful set of extensible {@link Ext.Class#registerPreprocessor pre-processors} which takes care of
+ * everything related to class creation, including but not limited to inheritance, mixins, configuration, statics, etc.
+ *
+ * # Inheritance:
+ *
+ * Ext.define('Developer', {
+ * extend: 'Person',
+ *
+ * constructor: function(name, isGeek) {
+ * this.isGeek = isGeek;
+ *
+ * // Apply a method from the parent class' prototype
+ * this.callParent([name]);
+ *
+ * return this;
+ *
+ * },
+ *
+ * code: function(language) {
+ * alert("I'm coding in: " + language);
+ *
+ * this.eat("Bugs");
+ *
+ * return this;
+ * }
+ * });
+ *
+ * var jacky = new Developer("Jacky", true);
+ * jacky.code("JavaScript"); // alert("I'm coding in: JavaScript");
+ * // alert("I'm eating: Bugs");
+ *
+ * See {@link Ext.Base#callParent} for more details on calling superclass' methods
+ *
+ * # Mixins:
+ *
+ * Ext.define('CanPlayGuitar', {
+ * playGuitar: function() {
+ * alert("F#...G...D...A");
+ * }
+ * });
+ *
+ * Ext.define('CanComposeSongs', {
+ * composeSongs: function() { ... }
+ * });
+ *
+ * Ext.define('CanSing', {
+ * sing: function() {
+ * alert("I'm on the highway to hell...")
+ * }
+ * });
+ *
+ * Ext.define('Musician', {
+ * extend: 'Person',
+ *
+ * mixins: {
+ * canPlayGuitar: 'CanPlayGuitar',
+ * canComposeSongs: 'CanComposeSongs',
+ * canSing: 'CanSing'
+ * }
+ * })
+ *
+ * Ext.define('CoolPerson', {
+ * extend: 'Person',
+ *
+ * mixins: {
+ * canPlayGuitar: 'CanPlayGuitar',
+ * canSing: 'CanSing'
+ * },
+ *
+ * sing: function() {
+ * alert("Ahem....");
+ *
+ * this.mixins.canSing.sing.call(this);
+ *
+ * alert("[Playing guitar at the same time...]");
+ *
+ * this.playGuitar();
+ * }
+ * });
+ *
+ * var me = new CoolPerson("Jacky");
+ *
+ * me.sing(); // alert("Ahem...");
+ * // alert("I'm on the highway to hell...");
+ * // alert("[Playing guitar at the same time...]");
+ * // alert("F#...G...D...A");
+ *
+ * # Config:
+ *
+ * Ext.define('SmartPhone', {
+ * config: {
+ * hasTouchScreen: false,
+ * operatingSystem: 'Other',
+ * price: 500
+ * },
+ *
+ * isExpensive: false,
+ *
+ * constructor: function(config) {
+ * this.initConfig(config);
+ *
+ * return this;
+ * },
+ *
+ * applyPrice: function(price) {
+ * this.isExpensive = (price > 500);
+ *
+ * return price;
+ * },
+ *
+ * applyOperatingSystem: function(operatingSystem) {
+ * if (!(/^(iOS|Android|BlackBerry)$/i).test(operatingSystem)) {
+ * return 'Other';
+ * }
+ *
+ * return operatingSystem;
+ * }
+ * });
+ *
+ * var iPhone = new SmartPhone({
+ * hasTouchScreen: true,
+ * operatingSystem: 'iOS'
+ * });
+ *
+ * iPhone.getPrice(); // 500;
+ * iPhone.getOperatingSystem(); // 'iOS'
+ * iPhone.getHasTouchScreen(); // true;
+ * iPhone.hasTouchScreen(); // true
+ *
+ * iPhone.isExpensive; // false;
+ * iPhone.setPrice(600);
+ * iPhone.getPrice(); // 600
+ * iPhone.isExpensive; // true;
+ *
+ * iPhone.setOperatingSystem('AlienOS');
+ * iPhone.getOperatingSystem(); // 'Other'
+ *
+ * # Statics:
+ *
+ * Ext.define('Computer', {
+ * statics: {
+ * factory: function(brand) {
+ * // 'this' in static methods refer to the class itself
+ * return new this(brand);
+ * }
+ * },
+ *
+ * constructor: function() { ... }
+ * });
+ *
+ * var dellComputer = Computer.factory('Dell');
+ *
+ * Also see {@link Ext.Base#statics} and {@link Ext.Base#self} for more details on accessing
+ * static properties within class methods
+ *
+ * @singleton
+ */
+(function(Class, alias) {
+
+ var slice = Array.prototype.slice;
+
+ var Manager = Ext.ClassManager = {
+
+ /**
+ * @property {Object} classes
+ * All classes which were defined through the ClassManager. Keys are the
+ * name of the classes and the values are references to the classes.
+ * @private
+ */
+ classes: {},
+
+ /**
+ * @private
+ */
+ existCache: {},
+
+ /**
+ * @private
+ */
+ namespaceRewrites: [{
+ from: 'Ext.',
+ to: Ext
+ }],
+
+ /**
+ * @private
+ */
+ maps: {
+ alternateToName: {},
+ aliasToName: {},
+ nameToAliases: {}
+ },
+
+ /** @private */
+ enableNamespaceParseCache: true,
+
+ /** @private */
+ namespaceParseCache: {},
+
+ /** @private */
+ instantiators: [],
+
+
+ /**
+ * Checks if a class has already been created.
+ *
+ * @param {String} className
+ * @return {Boolean} exist
+ */
+ isCreated: function(className) {
+ var i, ln, part, root, parts;
+
+
+ if (this.classes.hasOwnProperty(className) || this.existCache.hasOwnProperty(className)) {
+ return true;
+ }
+
+ root = Ext.global;
+ parts = this.parseNamespace(className);
+
+ for (i = 0, ln = parts.length; i < ln; i++) {
+ part = parts[i];
+
+ if (typeof part !== 'string') {
+ root = part;
+ } else {
+ if (!root || !root[part]) {
+ return false;
+ }
+
+ root = root[part];
+ }
+ }
+
+ Ext.Loader.historyPush(className);
+
+ this.existCache[className] = true;
+
+ return true;
+ },
+
+ /**
+ * Supports namespace rewriting
+ * @private
+ */
+ parseNamespace: function(namespace) {
+
+ var cache = this.namespaceParseCache;
+
+ if (this.enableNamespaceParseCache) {
+ if (cache.hasOwnProperty(namespace)) {
+ return cache[namespace];
+ }
+ }
+
+ var parts = [],
+ rewrites = this.namespaceRewrites,
+ rewrite, from, to, i, ln, root = Ext.global;
+
+ for (i = 0, ln = rewrites.length; i < ln; i++) {
+ rewrite = rewrites[i];
+ from = rewrite.from;
+ to = rewrite.to;
+
+ if (namespace === from || namespace.substring(0, from.length) === from) {
+ namespace = namespace.substring(from.length);
+
+ if (typeof to !== 'string') {
+ root = to;
+ } else {
+ parts = parts.concat(to.split('.'));
+ }
+
+ break;
+ }
+ }
+
+ parts.push(root);
+
+ parts = parts.concat(namespace.split('.'));
+
+ if (this.enableNamespaceParseCache) {
+ cache[namespace] = parts;
+ }
+
+ return parts;
+ },
+
+ /**
+ * Creates a namespace and assign the `value` to the created object
+ *
+ * Ext.ClassManager.setNamespace('MyCompany.pkg.Example', someObject);
+ *
+ * alert(MyCompany.pkg.Example === someObject); // alerts true
+ *
+ * @param {String} name
+ * @param {Object} value
+ */
+ setNamespace: function(name, value) {
+ var root = Ext.global,
+ parts = this.parseNamespace(name),
+ ln = parts.length - 1,
+ leaf = parts[ln],
+ i, part;
+
+ for (i = 0; i < ln; i++) {
+ part = parts[i];
+
+ if (typeof part !== 'string') {
+ root = part;
+ } else {
+ if (!root[part]) {
+ root[part] = {};
+ }
+
+ root = root[part];
+ }
+ }
+
+ root[leaf] = value;
+
+ return root[leaf];
+ },
+
+ /**
+ * The new Ext.ns, supports namespace rewriting
+ * @private
+ */
+ createNamespaces: function() {
+ var root = Ext.global,
+ parts, part, i, j, ln, subLn;
+
+ for (i = 0, ln = arguments.length; i < ln; i++) {
+ parts = this.parseNamespace(arguments[i]);
+
+ for (j = 0, subLn = parts.length; j < subLn; j++) {
+ part = parts[j];
+
+ if (typeof part !== 'string') {
+ root = part;
+ } else {
+ if (!root[part]) {
+ root[part] = {};
+ }
+
+ root = root[part];
+ }
+ }
+ }
+
+ return root;
+ },
+
+ /**
+ * Sets a name reference to a class.
+ *
+ * @param {String} name
+ * @param {Object} value
+ * @return {Ext.ClassManager} this
+ */
+ set: function(name, value) {
+ var targetName = this.getName(value);
+
+ this.classes[name] = this.setNamespace(name, value);
+
+ if (targetName && targetName !== name) {
+ this.maps.alternateToName[name] = targetName;
+ }
+
+ return this;
+ },
+
+ /**
+ * Retrieve a class by its name.
+ *
+ * @param {String} name
+ * @return {Ext.Class} class
+ */
+ get: function(name) {
+ if (this.classes.hasOwnProperty(name)) {
+ return this.classes[name];
+ }
+
+ var root = Ext.global,
+ parts = this.parseNamespace(name),
+ part, i, ln;
+
+ for (i = 0, ln = parts.length; i < ln; i++) {
+ part = parts[i];
+
+ if (typeof part !== 'string') {
+ root = part;
+ } else {
+ if (!root || !root[part]) {
+ return null;
+ }
+
+ root = root[part];
+ }
+ }
+
+ return root;
+ },
+
+ /**
+ * Register the alias for a class.
+ *
+ * @param {Ext.Class/String} cls a reference to a class or a className
+ * @param {String} alias Alias to use when referring to this class
+ */
+ setAlias: function(cls, alias) {
+ var aliasToNameMap = this.maps.aliasToName,
+ nameToAliasesMap = this.maps.nameToAliases,
+ className;
+
+ if (typeof cls === 'string') {
+ className = cls;
+ } else {
+ className = this.getName(cls);
+ }
+
+ if (alias && aliasToNameMap[alias] !== className) {
+
+ aliasToNameMap[alias] = className;
+ }
+
+ if (!nameToAliasesMap[className]) {
+ nameToAliasesMap[className] = [];
+ }
+
+ if (alias) {
+ Ext.Array.include(nameToAliasesMap[className], alias);
+ }
+
+ return this;
+ },
+
+ /**
+ * Get a reference to the class by its alias.
+ *
+ * @param {String} alias
+ * @return {Ext.Class} class
+ */
+ getByAlias: function(alias) {
+ return this.get(this.getNameByAlias(alias));
+ },
+
+ /**
+ * Get the name of a class by its alias.
+ *
+ * @param {String} alias
+ * @return {String} className
+ */
+ getNameByAlias: function(alias) {
+ return this.maps.aliasToName[alias] || '';
+ },
+
+ /**
+ * Get the name of a class by its alternate name.
+ *
+ * @param {String} alternate
+ * @return {String} className
+ */
+ getNameByAlternate: function(alternate) {
+ return this.maps.alternateToName[alternate] || '';
+ },
+
+ /**
+ * Get the aliases of a class by the class name
+ *
+ * @param {String} name
+ * @return {String[]} aliases
+ */
+ getAliasesByName: function(name) {
+ return this.maps.nameToAliases[name] || [];
+ },
+
+ /**
+ * Get the name of the class by its reference or its instance.
+ *
+ * Ext.ClassManager.getName(Ext.Action); // returns "Ext.Action"
+ *
+ * {@link Ext#getClassName Ext.getClassName} is alias for {@link Ext.ClassManager#getName Ext.ClassManager.getName}.
+ *
+ * @param {Ext.Class/Object} object
+ * @return {String} className
+ */
+ getName: function(object) {
+ return object && object.$className || '';
+ },
+
+ /**
+ * Get the class of the provided object; returns null if it's not an instance
+ * of any class created with Ext.define.
+ *
+ * var component = new Ext.Component();
+ *
+ * Ext.ClassManager.getClass(component); // returns Ext.Component
+ *
+ * {@link Ext#getClass Ext.getClass} is alias for {@link Ext.ClassManager#getClass Ext.ClassManager.getClass}.
+ *
+ * @param {Object} object
+ * @return {Ext.Class} class
+ */
+ getClass: function(object) {
+ return object && object.self || null;
+ },
+
+ /**
+ * Defines a class.
+ *
+ * {@link Ext#define Ext.define} and {@link Ext.ClassManager#create Ext.ClassManager.create} are almost aliases
+ * of each other, with the only exception that Ext.define allows definition of {@link Ext.Class#override overrides}.
+ * To avoid trouble, always use Ext.define.
+ *
+ * Ext.define('My.awesome.Class', {
+ * someProperty: 'something',
+ * someMethod: function() { ... }
+ * ...
+ *
+ * }, function() {
+ * alert('Created!');
+ * alert(this === My.awesome.Class); // alerts true
+ *
+ * var myInstance = new this();
+ * });
+ *
+ * @param {String} className The class name to create in string dot-namespaced format, for example:
+ * `My.very.awesome.Class`, `FeedViewer.plugin.CoolPager`. It is highly recommended to follow this simple convention:
+ *
+ * - The root and the class name are 'CamelCased'
+ * - Everything else is lower-cased
+ *
+ * @param {Object} data The key-value pairs of properties to apply to this class. Property names can be of any valid
+ * strings, except those in the reserved list below:
+ *
+ * - {@link Ext.Base#self self}
+ * - {@link Ext.Class#alias alias}
+ * - {@link Ext.Class#alternateClassName alternateClassName}
+ * - {@link Ext.Class#config config}
+ * - {@link Ext.Class#extend extend}
+ * - {@link Ext.Class#inheritableStatics inheritableStatics}
+ * - {@link Ext.Class#mixins mixins}
+ * - {@link Ext.Class#override override} (only when using {@link Ext#define Ext.define})
+ * - {@link Ext.Class#requires requires}
+ * - {@link Ext.Class#singleton singleton}
+ * - {@link Ext.Class#statics statics}
+ * - {@link Ext.Class#uses uses}
+ *
+ * @param {Function} [createdFn] callback to execute after the class is created, the execution scope of which
+ * (`this`) will be the newly created class itself.
+ *
+ * @return {Ext.Base}
+ */
+ create: function(className, data, createdFn) {
+ var manager = this;
+
+
+ data.$className = className;
+
+ return new Class(data, function() {
+ var postprocessorStack = data.postprocessors || manager.defaultPostprocessors,
+ registeredPostprocessors = manager.postprocessors,
+ index = 0,
+ postprocessors = [],
+ postprocessor, process, i, ln;
+
+ delete data.postprocessors;
+
+ for (i = 0, ln = postprocessorStack.length; i < ln; i++) {
+ postprocessor = postprocessorStack[i];
+
+ if (typeof postprocessor === 'string') {
+ postprocessor = registeredPostprocessors[postprocessor];
+
+ if (!postprocessor.always) {
+ if (data[postprocessor.name] !== undefined) {
+ postprocessors.push(postprocessor.fn);
+ }
+ }
+ else {
+ postprocessors.push(postprocessor.fn);
+ }
+ }
+ else {
+ postprocessors.push(postprocessor);
+ }
+ }
+
+ process = function(clsName, cls, clsData) {
+ postprocessor = postprocessors[index++];
+
+ if (!postprocessor) {
+ manager.set(className, cls);
+
+ Ext.Loader.historyPush(className);
+
+ if (createdFn) {
+ createdFn.call(cls, cls);
+ }
+
+ return;
+ }
+
+ if (postprocessor.call(this, clsName, cls, clsData, process) !== false) {
+ process.apply(this, arguments);
+ }
+ };
+
+ process.call(manager, className, this, data);
+ });
+ },
+
+ /**
+ * Instantiate a class by its alias.
+ *
+ * If {@link Ext.Loader} is {@link Ext.Loader#setConfig enabled} and the class has not been defined yet, it will
+ * attempt to load the class via synchronous loading.
+ *
+ * var window = Ext.ClassManager.instantiateByAlias('widget.window', { width: 600, height: 800, ... });
+ *
+ * {@link Ext#createByAlias Ext.createByAlias} is alias for {@link Ext.ClassManager#instantiateByAlias Ext.ClassManager.instantiateByAlias}.
+ *
+ * @param {String} alias
+ * @param {Object...} args Additional arguments after the alias will be passed to the
+ * class constructor.
+ * @return {Object} instance
+ */
+ instantiateByAlias: function() {
+ var alias = arguments[0],
+ args = slice.call(arguments),
+ className = this.getNameByAlias(alias);
+
+ if (!className) {
+ className = this.maps.aliasToName[alias];
+
+
+
+ Ext.syncRequire(className);
+ }
+
+ args[0] = className;
+
+ return this.instantiate.apply(this, args);
+ },
+
+ /**
+ * Instantiate a class by either full name, alias or alternate name.
+ *
+ * If {@link Ext.Loader} is {@link Ext.Loader#setConfig enabled} and the class has not been defined yet, it will
+ * attempt to load the class via synchronous loading.
+ *
+ * For example, all these three lines return the same result:
+ *
+ * // alias
+ * var window = Ext.ClassManager.instantiate('widget.window', { width: 600, height: 800, ... });
+ *
+ * // alternate name
+ * var window = Ext.ClassManager.instantiate('Ext.Window', { width: 600, height: 800, ... });
+ *
+ * // full class name
+ * var window = Ext.ClassManager.instantiate('Ext.window.Window', { width: 600, height: 800, ... });
+ *
+ * {@link Ext#create Ext.create} is alias for {@link Ext.ClassManager#instantiate Ext.ClassManager.instantiate}.
+ *
+ * @param {String} name
+ * @param {Object...} args Additional arguments after the name will be passed to the class' constructor.
+ * @return {Object} instance
+ */
+ instantiate: function() {
+ var name = arguments[0],
+ args = slice.call(arguments, 1),
+ alias = name,
+ possibleName, cls;
+
+ if (typeof name !== 'function') {
+
+ cls = this.get(name);
+ }
+ else {
+ cls = name;
+ }
+
+ // No record of this class name, it's possibly an alias, so look it up
+ if (!cls) {
+ possibleName = this.getNameByAlias(name);
+
+ if (possibleName) {
+ name = possibleName;
+
+ cls = this.get(name);
+ }
+ }
+
+ // Still no record of this class name, it's possibly an alternate name, so look it up
+ if (!cls) {
+ possibleName = this.getNameByAlternate(name);
+
+ if (possibleName) {
+ name = possibleName;
+
+ cls = this.get(name);
+ }
+ }
+
+ // Still not existing at this point, try to load it via synchronous mode as the last resort
+ if (!cls) {
+
+ Ext.syncRequire(name);
+
+ cls = this.get(name);
+ }
+
+
+
+ return this.getInstantiator(args.length)(cls, args);
+ },
+
+ /**
+ * @private
+ * @param name
+ * @param args
+ */
+ dynInstantiate: function(name, args) {
+ args = Ext.Array.from(args, true);
+ args.unshift(name);
+
+ return this.instantiate.apply(this, args);
+ },
+
+ /**
+ * @private
+ * @param length
+ */
+ getInstantiator: function(length) {
+ if (!this.instantiators[length]) {
+ var i = length,
+ args = [];
+
+ for (i = 0; i < length; i++) {
+ args.push('a['+i+']');
+ }
+
+ this.instantiators[length] = new Function('c', 'a', 'return new c('+args.join(',')+')');
+ }
+
+ return this.instantiators[length];
+ },
+
+ /**
+ * @private
+ */
+ postprocessors: {},
+
+ /**
+ * @private
+ */
+ defaultPostprocessors: [],
+
+ /**
+ * Register a post-processor function.
+ *
+ * @param {String} name
+ * @param {Function} postprocessor
+ */
+ registerPostprocessor: function(name, fn, always) {
+ this.postprocessors[name] = {
+ name: name,
+ always: always || false,
+ fn: fn
+ };
+
+ return this;
+ },
+
+ /**
+ * Set the default post processors array stack which are applied to every class.
+ *
+ * @param {String/String[]} The name of a registered post processor or an array of registered names.
+ * @return {Ext.ClassManager} this
+ */
+ setDefaultPostprocessors: function(postprocessors) {
+ this.defaultPostprocessors = Ext.Array.from(postprocessors);
+
+ return this;
+ },
+
+ /**
+ * Insert this post-processor at a specific position in the stack, optionally relative to
+ * any existing post-processor
+ *
+ * @param {String} name The post-processor name. Note that it needs to be registered with
+ * {@link Ext.ClassManager#registerPostprocessor} before this
+ * @param {String} offset The insertion position. Four possible values are:
+ * 'first', 'last', or: 'before', 'after' (relative to the name provided in the third argument)
+ * @param {String} relativeName
+ * @return {Ext.ClassManager} this
+ */
+ setDefaultPostprocessorPosition: function(name, offset, relativeName) {
+ var defaultPostprocessors = this.defaultPostprocessors,
+ index;
+
+ if (typeof offset === 'string') {
+ if (offset === 'first') {
+ defaultPostprocessors.unshift(name);
+
+ return this;
+ }
+ else if (offset === 'last') {
+ defaultPostprocessors.push(name);
+
+ return this;
+ }
+
+ offset = (offset === 'after') ? 1 : -1;
+ }
+
+ index = Ext.Array.indexOf(defaultPostprocessors, relativeName);
+
+ if (index !== -1) {
+ Ext.Array.splice(defaultPostprocessors, Math.max(0, index + offset), 0, name);
+ }
+
+ return this;
+ },
+
+ /**
+ * Converts a string expression to an array of matching class names. An expression can either refers to class aliases
+ * or class names. Expressions support wildcards:
+ *
+ * // returns ['Ext.window.Window']
+ * var window = Ext.ClassManager.getNamesByExpression('widget.window');
+ *
+ * // returns ['widget.panel', 'widget.window', ...]
+ * var allWidgets = Ext.ClassManager.getNamesByExpression('widget.*');
+ *
+ * // returns ['Ext.data.Store', 'Ext.data.ArrayProxy', ...]
+ * var allData = Ext.ClassManager.getNamesByExpression('Ext.data.*');
+ *
+ * @param {String} expression
+ * @return {String[]} classNames
+ */
+ getNamesByExpression: function(expression) {
+ var nameToAliasesMap = this.maps.nameToAliases,
+ names = [],
+ name, alias, aliases, possibleName, regex, i, ln;
+
+
+ if (expression.indexOf('*') !== -1) {
+ expression = expression.replace(/\*/g, '(.*?)');
+ regex = new RegExp('^' + expression + '$');
+
+ for (name in nameToAliasesMap) {
+ if (nameToAliasesMap.hasOwnProperty(name)) {
+ aliases = nameToAliasesMap[name];
+
+ if (name.search(regex) !== -1) {
+ names.push(name);
+ }
+ else {
+ for (i = 0, ln = aliases.length; i < ln; i++) {
+ alias = aliases[i];
+
+ if (alias.search(regex) !== -1) {
+ names.push(name);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ } else {
+ possibleName = this.getNameByAlias(expression);
+
+ if (possibleName) {
+ names.push(possibleName);
+ } else {
+ possibleName = this.getNameByAlternate(expression);
+
+ if (possibleName) {
+ names.push(possibleName);
+ } else {
+ names.push(expression);
+ }
+ }
+ }
+
+ return names;
+ }
+ };
+
+ var defaultPostprocessors = Manager.defaultPostprocessors;
+ //<feature classSystem.alias>
+
+ /**
+ * @cfg {String[]} alias
+ * @member Ext.Class
+ * List of short aliases for class names. Most useful for defining xtypes for widgets:
+ *
+ * Ext.define('MyApp.CoolPanel', {
+ * extend: 'Ext.panel.Panel',
+ * alias: ['widget.coolpanel'],
+ * title: 'Yeah!'
+ * });
+ *
+ * // Using Ext.create
+ * Ext.widget('widget.coolpanel');
+ * // Using the shorthand for widgets and in xtypes
+ * Ext.widget('panel', {
+ * items: [
+ * {xtype: 'coolpanel', html: 'Foo'},
+ * {xtype: 'coolpanel', html: 'Bar'}
+ * ]
+ * });
+ */
+ Manager.registerPostprocessor('alias', function(name, cls, data) {
+ var aliases = data.alias,
+ i, ln;
+
+ delete data.alias;
+
+ for (i = 0, ln = aliases.length; i < ln; i++) {
+ alias = aliases[i];
+
+ this.setAlias(cls, alias);
+ }
+ });
+
+ /**
+ * @cfg {Boolean} singleton
+ * @member Ext.Class
+ * When set to true, the class will be instantiated as singleton. For example:
+ *
+ * Ext.define('Logger', {
+ * singleton: true,
+ * log: function(msg) {
+ * console.log(msg);
+ * }
+ * });
+ *
+ * Logger.log('Hello');
+ */
+ Manager.registerPostprocessor('singleton', function(name, cls, data, fn) {
+ fn.call(this, name, new cls(), data);
+ return false;
+ });
+
+ /**
+ * @cfg {String/String[]} alternateClassName
+ * @member Ext.Class
+ * Defines alternate names for this class. For example:
+ *
+ * Ext.define('Developer', {
+ * alternateClassName: ['Coder', 'Hacker'],
+ * code: function(msg) {
+ * alert('Typing... ' + msg);
+ * }
+ * });
+ *
+ * var joe = Ext.create('Developer');
+ * joe.code('stackoverflow');
+ *
+ * var rms = Ext.create('Hacker');
+ * rms.code('hack hack');
+ */
+ Manager.registerPostprocessor('alternateClassName', function(name, cls, data) {
+ var alternates = data.alternateClassName,
+ i, ln, alternate;
+
+ if (!(alternates instanceof Array)) {
+ alternates = [alternates];
+ }
+
+ for (i = 0, ln = alternates.length; i < ln; i++) {
+ alternate = alternates[i];
+
+
+ this.set(alternate, cls);
+ }
+ });
+
+ Manager.setDefaultPostprocessors(['alias', 'singleton', 'alternateClassName']);
+
+ Ext.apply(Ext, {
+ /**
+ * @method
+ * @member Ext
+ * @alias Ext.ClassManager#instantiate
+ */
+ create: alias(Manager, 'instantiate'),
+
+ /**
+ * @private
+ * API to be stablized
+ *
+ * @param {Object} item
+ * @param {String} namespace
+ */
+ factory: function(item, namespace) {
+ if (item instanceof Array) {
+ var i, ln;
+
+ for (i = 0, ln = item.length; i < ln; i++) {
+ item[i] = Ext.factory(item[i], namespace);
+ }
+
+ return item;
+ }
+
+ var isString = (typeof item === 'string');
+
+ if (isString || (item instanceof Object && item.constructor === Object)) {
+ var name, config = {};
+
+ if (isString) {
+ name = item;
+ }
+ else {
+ name = item.className;
+ config = item;
+ delete config.className;
+ }
+
+ if (namespace !== undefined && name.indexOf(namespace) === -1) {
+ name = namespace + '.' + Ext.String.capitalize(name);
+ }
+
+ return Ext.create(name, config);
+ }
+
+ if (typeof item === 'function') {
+ return Ext.create(item);
+ }
+
+ return item;
+ },
+
+ /**
+ * Convenient shorthand to create a widget by its xtype, also see {@link Ext.ClassManager#instantiateByAlias}
+ *
+ * var button = Ext.widget('button'); // Equivalent to Ext.create('widget.button')
+ * var panel = Ext.widget('panel'); // Equivalent to Ext.create('widget.panel')
+ *
+ * @method
+ * @member Ext
+ * @param {String} name xtype of the widget to create.
+ * @param {Object...} args arguments for the widget constructor.
+ * @return {Object} widget instance
+ */
+ widget: function(name) {
+ var args = slice.call(arguments);
+ args[0] = 'widget.' + name;
+
+ return Manager.instantiateByAlias.apply(Manager, args);
+ },
+
+ /**
+ * @method
+ * @member Ext
+ * @alias Ext.ClassManager#instantiateByAlias
+ */
+ createByAlias: alias(Manager, 'instantiateByAlias'),
+
+ /**
+ * @cfg {String} override
+ * @member Ext.Class
+ *
+ * Defines an override applied to a class. Note that **overrides can only be created using
+ * {@link Ext#define}.** {@link Ext.ClassManager#create} only creates classes.
+ *
+ * To define an override, include the override property. The content of an override is
+ * aggregated with the specified class in order to extend or modify that class. This can be
+ * as simple as setting default property values or it can extend and/or replace methods.
+ * This can also extend the statics of the class.
+ *
+ * One use for an override is to break a large class into manageable pieces.
+ *
+ * // File: /src/app/Panel.js
+ *
+ * Ext.define('My.app.Panel', {
+ * extend: 'Ext.panel.Panel',
+ * requires: [
+ * 'My.app.PanelPart2',
+ * 'My.app.PanelPart3'
+ * ]
+ *
+ * constructor: function (config) {
+ * this.callSuper(arguments); // calls Ext.panel.Panel's constructor
+ * //...
+ * },
+ *
+ * statics: {
+ * method: function () {
+ * return 'abc';
+ * }
+ * }
+ * });
+ *
+ * // File: /src/app/PanelPart2.js
+ * Ext.define('My.app.PanelPart2', {
+ * override: 'My.app.Panel',
+ *
+ * constructor: function (config) {
+ * this.callSuper(arguments); // calls My.app.Panel's constructor
+ * //...
+ * }
+ * });
+ *
+ * Another use of overrides is to provide optional parts of classes that can be
+ * independently required. In this case, the class may even be unaware of the
+ * override altogether.
+ *
+ * Ext.define('My.ux.CoolTip', {
+ * override: 'Ext.tip.ToolTip',
+ *
+ * constructor: function (config) {
+ * this.callSuper(arguments); // calls Ext.tip.ToolTip's constructor
+ * //...
+ * }
+ * });
+ *
+ * The above override can now be required as normal.
+ *
+ * Ext.define('My.app.App', {
+ * requires: [
+ * 'My.ux.CoolTip'
+ * ]
+ * });
+ *
+ * Overrides can also contain statics:
+ *
+ * Ext.define('My.app.BarMod', {
+ * override: 'Ext.foo.Bar',
+ *
+ * statics: {
+ * method: function (x) {
+ * return this.callSuper([x * 2]); // call Ext.foo.Bar.method
+ * }
+ * }
+ * });
+ *
+ * IMPORTANT: An override is only included in a build if the class it overrides is
+ * required. Otherwise, the override, like the target class, is not included.
+ */
+
+ /**
+ * @method
+ *
+ * @member Ext
+ * @alias Ext.ClassManager#create
+ */
+ define: function (className, data, createdFn) {
+ if (!data.override) {
+ return Manager.create.apply(Manager, arguments);
+ }
+
+ var requires = data.requires,
+ uses = data.uses,
+ overrideName = className;
+
+ className = data.override;
+
+ // hoist any 'requires' or 'uses' from the body onto the faux class:
+ data = Ext.apply({}, data);
+ delete data.requires;
+ delete data.uses;
+ delete data.override;
+
+ // make sure className is in the requires list:
+ if (typeof requires == 'string') {
+ requires = [ className, requires ];
+ } else if (requires) {
+ requires = requires.slice(0);
+ requires.unshift(className);
+ } else {
+ requires = [ className ];
+ }
+
+// TODO - we need to rework this to allow the override to not require the target class
+// and rather 'wait' for it in such a way that if the target class is not in the build,
+// neither are any of its overrides.
+//
+// Also, this should process the overrides for a class ASAP (ideally before any derived
+// classes) if the target class 'requires' the overrides. Without some special handling, the
+// overrides so required will be processed before the class and have to be bufferred even
+// in a build.
+//
+// TODO - we should probably support the "config" processor on an override (to config new
+// functionaliy like Aria) and maybe inheritableStatics (although static is now supported
+// by callSuper). If inheritableStatics causes those statics to be included on derived class
+// constructors, that probably means "no" to this since an override can come after other
+// classes extend the target.
+ return Manager.create(overrideName, {
+ requires: requires,
+ uses: uses,
+ isPartial: true,
+ constructor: function () {
+ }
+ }, function () {
+ var cls = Manager.get(className);
+ if (cls.override) { // if (normal class)
+ cls.override(data);
+ } else { // else (singleton)
+ cls.self.override(data);
+ }
+
+ if (createdFn) {
+ // called once the override is applied and with the context of the
+ // overridden class (the override itself is a meaningless, name-only
+ // thing).
+ createdFn.call(cls);
+ }
+ });
+ },
+
+ /**
+ * @method
+ * @member Ext
+ * @alias Ext.ClassManager#getName
+ */
+ getClassName: alias(Manager, 'getName'),
+
+ /**
+ * Returns the displayName property or className or object.
+ * When all else fails, returns "Anonymous".
+ * @param {Object} object
+ * @return {String}
+ */
+ getDisplayName: function(object) {
+ if (object.displayName) {
+ return object.displayName;
+ }
+
+ if (object.$name && object.$class) {
+ return Ext.getClassName(object.$class) + '#' + object.$name;
+ }
+
+ if (object.$className) {
+ return object.$className;
+ }
+
+ return 'Anonymous';
+ },
+
+ /**
+ * @method
+ * @member Ext
+ * @alias Ext.ClassManager#getClass
+ */
+ getClass: alias(Manager, 'getClass'),
+
+ /**
+ * Creates namespaces to be used for scoping variables and classes so that they are not global.
+ * Specifying the last node of a namespace implicitly creates all other nodes. Usage:
+ *
+ * Ext.namespace('Company', 'Company.data');
+ *
+ * // equivalent and preferable to the above syntax
+ * Ext.namespace('Company.data');
+ *
+ * Company.Widget = function() { ... };
+ *
+ * Company.data.CustomStore = function(config) { ... };
+ *
+ * @method
+ * @member Ext
+ * @param {String} namespace1
+ * @param {String} namespace2
+ * @param {String} etc
+ * @return {Object} The namespace object. (If multiple arguments are passed, this will be the last namespace created)
+ */
+ namespace: alias(Manager, 'createNamespaces')
+ });
+
+ /**
+ * Old name for {@link Ext#widget}.
+ * @deprecated 4.0.0 Use {@link Ext#widget} instead.
+ * @method
+ * @member Ext
+ * @alias Ext#widget
+ */
+ Ext.createWidget = Ext.widget;
+
+ /**
+ * Convenient alias for {@link Ext#namespace Ext.namespace}
+ * @method
+ * @member Ext
+ * @alias Ext#namespace
+ */
+ Ext.ns = Ext.namespace;
+
+ Class.registerPreprocessor('className', function(cls, data) {
+ if (data.$className) {
+ cls.$className = data.$className;
+ }
+ }, true);
+
+ Class.setDefaultPreprocessorPosition('className', 'first');
+
+ Class.registerPreprocessor('xtype', function(cls, data) {
+ var xtypes = Ext.Array.from(data.xtype),
+ widgetPrefix = 'widget.',
+ aliases = Ext.Array.from(data.alias),
+ i, ln, xtype;
+
+ data.xtype = xtypes[0];
+ data.xtypes = xtypes;
+
+ aliases = data.alias = Ext.Array.from(data.alias);
+
+ for (i = 0,ln = xtypes.length; i < ln; i++) {
+ xtype = xtypes[i];
+
+
+ aliases.push(widgetPrefix + xtype);
+ }
+
+ data.alias = aliases;
+ });
+
+ Class.setDefaultPreprocessorPosition('xtype', 'last');
+
+ Class.registerPreprocessor('alias', function(cls, data) {
+ var aliases = Ext.Array.from(data.alias),
+ xtypes = Ext.Array.from(data.xtypes),
+ widgetPrefix = 'widget.',
+ widgetPrefixLength = widgetPrefix.length,
+ i, ln, alias, xtype;
+
+ for (i = 0, ln = aliases.length; i < ln; i++) {
+ alias = aliases[i];
+
+
+ if (alias.substring(0, widgetPrefixLength) === widgetPrefix) {
+ xtype = alias.substring(widgetPrefixLength);
+ Ext.Array.include(xtypes, xtype);
+
+ if (!cls.xtype) {
+ cls.xtype = data.xtype = xtype;
+ }
+ }
+ }
+
+ data.alias = aliases;
+ data.xtypes = xtypes;
+ });
+
+ Class.setDefaultPreprocessorPosition('alias', 'last');
+
+})(Ext.Class, Ext.Function.alias);
+
+/**
+ * @class Ext.Loader
+ * @singleton
+ * @author Jacky Nguyen <jacky@sencha.com>
+ * @docauthor Jacky Nguyen <jacky@sencha.com>
+ *
+ * Ext.Loader is the heart of the new dynamic dependency loading capability in Ext JS 4+. It is most commonly used
+ * via the {@link Ext#require} shorthand. Ext.Loader supports both asynchronous and synchronous loading
+ * approaches, and leverage their advantages for the best development flow. We'll discuss about the pros and cons
+ * of each approach:
+ *
+ * # Asynchronous Loading
+ *
+ * - Advantages:
+ * + Cross-domain
+ * + No web server needed: you can run the application via the file system protocol
+ * (i.e: `file://path/to/your/index.html`)
+ * + Best possible debugging experience: error messages come with the exact file name and line number
+ *
+ * - Disadvantages:
+ * + Dependencies need to be specified before-hand
+ *
+ * ### Method 1: Explicitly include what you need:
+ *
+ * // Syntax
+ * Ext.require({String/Array} expressions);
+ *
+ * // Example: Single alias
+ * Ext.require('widget.window');
+ *
+ * // Example: Single class name
+ * Ext.require('Ext.window.Window');
+ *
+ * // Example: Multiple aliases / class names mix
+ * Ext.require(['widget.window', 'layout.border', 'Ext.data.Connection']);
+ *
+ * // Wildcards
+ * Ext.require(['widget.*', 'layout.*', 'Ext.data.*']);
+ *
+ * ### Method 2: Explicitly exclude what you don't need:
+ *
+ * // Syntax: Note that it must be in this chaining format.
+ * Ext.exclude({String/Array} expressions)
+ * .require({String/Array} expressions);
+ *
+ * // Include everything except Ext.data.*
+ * Ext.exclude('Ext.data.*').require('*');Â
+ *
+ * // Include all widgets except widget.checkbox*,
+ * // which will match widget.checkbox, widget.checkboxfield, widget.checkboxgroup, etc.
+ * Ext.exclude('widget.checkbox*').require('widget.*');
+ *
+ * # Synchronous Loading on Demand
+ *
+ * - Advantages:
+ * + There's no need to specify dependencies before-hand, which is always the convenience of including
+ * ext-all.js before
+ *
+ * - Disadvantages:
+ * + Not as good debugging experience since file name won't be shown (except in Firebug at the moment)
+ * + Must be from the same domain due to XHR restriction
+ * + Need a web server, same reason as above
+ *
+ * There's one simple rule to follow: Instantiate everything with Ext.create instead of the `new` keyword
+ *
+ * Ext.create('widget.window', { ... }); // Instead of new Ext.window.Window({...});
+ *
+ * Ext.create('Ext.window.Window', {}); // Same as above, using full class name instead of alias
+ *
+ * Ext.widget('window', {}); // Same as above, all you need is the traditional `xtype`
+ *
+ * Behind the scene, {@link Ext.ClassManager} will automatically check whether the given class name / alias has already
+ * existed on the page. If it's not, Ext.Loader will immediately switch itself to synchronous mode and automatic load
+ * the given class and all its dependencies.
+ *
+ * # Hybrid Loading - The Best of Both Worlds
+ *
+ * It has all the advantages combined from asynchronous and synchronous loading. The development flow is simple:
+ *
+ * ### Step 1: Start writing your application using synchronous approach.
+ *
+ * Ext.Loader will automatically fetch all dependencies on demand as they're needed during run-time. For example:
+ *
+ * Ext.onReady(function(){
+ * var window = Ext.createWidget('window', {
+ * width: 500,
+ * height: 300,
+ * layout: {
+ * type: 'border',
+ * padding: 5
+ * },
+ * title: 'Hello Dialog',
+ * items: [{
+ * title: 'Navigation',
+ * collapsible: true,
+ * region: 'west',
+ * width: 200,
+ * html: 'Hello',
+ * split: true
+ * }, {
+ * title: 'TabPanel',
+ * region: 'center'
+ * }]
+ * });
+ *
+ * window.show();
+ * })
+ *
+ * ### Step 2: Along the way, when you need better debugging ability, watch the console for warnings like these:
+ *
+ * [Ext.Loader] Synchronously loading 'Ext.window.Window'; consider adding Ext.require('Ext.window.Window') before your application's code ClassManager.js:432
+ * [Ext.Loader] Synchronously loading 'Ext.layout.container.Border'; consider adding Ext.require('Ext.layout.container.Border') before your application's code
+ *
+ * Simply copy and paste the suggested code above `Ext.onReady`, e.g.:
+ *
+ * Ext.require('Ext.window.Window');
+ * Ext.require('Ext.layout.container.Border');
+ *
+ * Ext.onReady(...);
+ *
+ * Everything should now load via asynchronous mode.
+ *
+ * # Deployment
+ *
+ * It's important to note that dynamic loading should only be used during development on your local machines.
+ * During production, all dependencies should be combined into one single JavaScript file. Ext.Loader makes
+ * the whole process of transitioning from / to between development / maintenance and production as easy as
+ * possible. Internally {@link Ext.Loader#history Ext.Loader.history} maintains the list of all dependencies
+ * your application needs in the exact loading sequence. It's as simple as concatenating all files in this
+ * array into one, then include it on top of your application.
+ *
+ * This process will be automated with Sencha Command, to be released and documented towards Ext JS 4 Final.
+ */
+(function(Manager, Class, flexSetter, alias) {
+
+ var
+ dependencyProperties = ['extend', 'mixins', 'requires'],
+ Loader;
+
+ Loader = Ext.Loader = {
+ /**
+ * @private
+ */
+ documentHead: typeof document !== 'undefined' && (document.head || document.getElementsByTagName('head')[0]),
+
+ /**
+ * Flag indicating whether there are still files being loaded
+ * @private
+ */
+ isLoading: false,
+
+ /**
+ * Maintain the queue for all dependencies. Each item in the array is an object of the format:
+ * {
+ * requires: [...], // The required classes for this queue item
+ * callback: function() { ... } // The function to execute when all classes specified in requires exist
+ * }
+ * @private
+ */
+ queue: [],
+
+ /**
+ * Maintain the list of files that have already been handled so that they never get double-loaded
+ * @private
+ */
+ isFileLoaded: {},
+
+ /**
+ * Maintain the list of listeners to execute when all required scripts are fully loaded
+ * @private
+ */
+ readyListeners: [],
+
+ /**
+ * Contains optional dependencies to be loaded last
+ * @private
+ */
+ optionalRequires: [],
+
+ /**
+ * Map of fully qualified class names to an array of dependent classes.
+ * @private
+ */
+ requiresMap: {},
+
+ /**
+ * @private
+ */
+ numPendingFiles: 0,
+
+ /**
+ * @private
+ */
+ numLoadedFiles: 0,
+
+ /** @private */
+ hasFileLoadError: false,
+
+ /**
+ * @private
+ */
+ classNameToFilePathMap: {},
+
+ /**
+ * @property {String[]} history
+ * An array of class names to keep track of the dependency loading order.
+ * This is not guaranteed to be the same everytime due to the asynchronous nature of the Loader.
+ */
+ history: [],
+
+ /**
+ * Configuration
+ * @private
+ */
+ config: {
+ /**
+ * @cfg {Boolean} enabled
+ * Whether or not to enable the dynamic dependency loading feature.
+ */
+ enabled: false,
+
+ /**
+ * @cfg {Boolean} disableCaching
+ * Appends current timestamp to script files to prevent caching.
+ */
+ disableCaching: true,
+
+ /**
+ * @cfg {String} disableCachingParam
+ * The get parameter name for the cache buster's timestamp.
+ */
+ disableCachingParam: '_dc',
+
+ /**
+ * @cfg {Object} paths
+ * The mapping from namespaces to file paths
+ *
+ * {
+ * 'Ext': '.', // This is set by default, Ext.layout.container.Container will be
+ * // loaded from ./layout/Container.js
+ *
+ * 'My': './src/my_own_folder' // My.layout.Container will be loaded from
+ * // ./src/my_own_folder/layout/Container.js
+ * }
+ *
+ * Note that all relative paths are relative to the current HTML document.
+ * If not being specified, for example, `Other.awesome.Class`
+ * will simply be loaded from `./Other/awesome/Class.js`
+ */
+ paths: {
+ 'Ext': '.'
+ }
+ },
+
+ /**
+ * Set the configuration for the loader. This should be called right after ext-core.js
+ * (or ext-core-debug.js) is included in the page, e.g.:
+ *
+ * <script type="text/javascript" src="ext-core-debug.js"></script>
+ * <script type="text/javascript">
+ * Ext.Loader.setConfig({
+ * enabled: true,
+ * paths: {
+ * 'My': 'my_own_path'
+ * }
+ * });
+ * <script>
+ * <script type="text/javascript">
+ * Ext.require(...);
+ *
+ * Ext.onReady(function() {
+ * // application code here
+ * });
+ * </script>
+ *
+ * Refer to config options of {@link Ext.Loader} for the list of possible properties.
+ *
+ * @param {String/Object} name Name of the value to override, or a config object to override multiple values.
+ * @param {Object} value (optional) The new value to set, needed if first parameter is String.
+ * @return {Ext.Loader} this
+ */
+ setConfig: function(name, value) {
+ if (Ext.isObject(name) && arguments.length === 1) {
+ Ext.Object.merge(this.config, name);
+ }
+ else {
+ this.config[name] = (Ext.isObject(value)) ? Ext.Object.merge(this.config[name], value) : value;
+ }
+
+ return this;
+ },
+
+ /**
+ * Get the config value corresponding to the specified name.
+ * If no name is given, will return the config object.
+ * @param {String} name The config property name
+ * @return {Object}
+ */
+ getConfig: function(name) {
+ if (name) {
+ return this.config[name];
+ }
+
+ return this.config;
+ },
+
+ /**
+ * Sets the path of a namespace. For Example:
+ *
+ * Ext.Loader.setPath('Ext', '.');
+ *
+ * @param {String/Object} name See {@link Ext.Function#flexSetter flexSetter}
+ * @param {String} path See {@link Ext.Function#flexSetter flexSetter}
+ * @return {Ext.Loader} this
+ * @method
+ */
+ setPath: flexSetter(function(name, path) {
+ this.config.paths[name] = path;
+
+ return this;
+ }),
+
+ /**
+ * Translates a className to a file path by adding the the proper prefix and converting the .'s to /'s.
+ * For example:
+ *
+ * Ext.Loader.setPath('My', '/path/to/My');
+ *
+ * alert(Ext.Loader.getPath('My.awesome.Class')); // alerts '/path/to/My/awesome/Class.js'
+ *
+ * Note that the deeper namespace levels, if explicitly set, are always resolved first. For example:
+ *
+ * Ext.Loader.setPath({
+ * 'My': '/path/to/lib',
+ * 'My.awesome': '/other/path/for/awesome/stuff',
+ * 'My.awesome.more': '/more/awesome/path'
+ * });
+ *
+ * alert(Ext.Loader.getPath('My.awesome.Class')); // alerts '/other/path/for/awesome/stuff/Class.js'
+ *
+ * alert(Ext.Loader.getPath('My.awesome.more.Class')); // alerts '/more/awesome/path/Class.js'
+ *
+ * alert(Ext.Loader.getPath('My.cool.Class')); // alerts '/path/to/lib/cool/Class.js'
+ *
+ * alert(Ext.Loader.getPath('Unknown.strange.Stuff')); // alerts 'Unknown/strange/Stuff.js'
+ *
+ * @param {String} className
+ * @return {String} path
+ */
+ getPath: function(className) {
+ var path = '',
+ paths = this.config.paths,
+ prefix = this.getPrefix(className);
+
+ if (prefix.length > 0) {
+ if (prefix === className) {
+ return paths[prefix];
+ }
+
+ path = paths[prefix];
+ className = className.substring(prefix.length + 1);
+ }
+
+ if (path.length > 0) {
+ path += '/';
+ }
+
+ return path.replace(/\/\.\//g, '/') + className.replace(/\./g, "/") + '.js';
+ },
+
+ /**
+ * @private
+ * @param {String} className
+ */
+ getPrefix: function(className) {
+ var paths = this.config.paths,
+ prefix, deepestPrefix = '';
+
+ if (paths.hasOwnProperty(className)) {
+ return className;
+ }
+
+ for (prefix in paths) {
+ if (paths.hasOwnProperty(prefix) && prefix + '.' === className.substring(0, prefix.length + 1)) {
+ if (prefix.length > deepestPrefix.length) {
+ deepestPrefix = prefix;
+ }
+ }
+ }
+
+ return deepestPrefix;
+ },
+
+ /**
+ * Refresh all items in the queue. If all dependencies for an item exist during looping,
+ * it will execute the callback and call refreshQueue again. Triggers onReady when the queue is
+ * empty
+ * @private
+ */
+ refreshQueue: function() {
+ var ln = this.queue.length,
+ i, item, j, requires;
+
+ if (ln === 0) {
+ this.triggerReady();
+ return;
+ }
+
+ for (i = 0; i < ln; i++) {
+ item = this.queue[i];
+
+ if (item) {
+ requires = item.requires;
+
+ // Don't bother checking when the number of files loaded
+ // is still less than the array length
+ if (requires.length > this.numLoadedFiles) {
+ continue;
+ }
+
+ j = 0;
+
+ do {
+ if (Manager.isCreated(requires[j])) {
+ // Take out from the queue
+ Ext.Array.erase(requires, j, 1);
+ }
+ else {
+ j++;
+ }
+ } while (j < requires.length);
+
+ if (item.requires.length === 0) {
+ Ext.Array.erase(this.queue, i, 1);
+ item.callback.call(item.scope);
+ this.refreshQueue();
+ break;
+ }
+ }
+ }
+
+ return this;
+ },
+
+ /**
+ * Inject a script element to document's head, call onLoad and onError accordingly
+ * @private
+ */
+ injectScriptElement: function(url, onLoad, onError, scope) {
+ var script = document.createElement('script'),
+ me = this,
+ onLoadFn = function() {
+ me.cleanupScriptElement(script);
+ onLoad.call(scope);
+ },
+ onErrorFn = function() {
+ me.cleanupScriptElement(script);
+ onError.call(scope);
+ };
+
+ script.type = 'text/javascript';
+ script.src = url;
+ script.onload = onLoadFn;
+ script.onerror = onErrorFn;
+ script.onreadystatechange = function() {
+ if (this.readyState === 'loaded' || this.readyState === 'complete') {
+ onLoadFn();
+ }
+ };
+
+ this.documentHead.appendChild(script);
+
+ return script;
+ },
+
+ /**
+ * @private
+ */
+ cleanupScriptElement: function(script) {
+ script.onload = null;
+ script.onreadystatechange = null;
+ script.onerror = null;
+
+ return this;
+ },
+
+ /**
+ * Load a script file, supports both asynchronous and synchronous approaches
+ *
+ * @param {String} url
+ * @param {Function} onLoad
+ * @param {Object} scope
+ * @param {Boolean} synchronous
+ * @private
+ */
+ loadScriptFile: function(url, onLoad, onError, scope, synchronous) {
+ var me = this,
+ noCacheUrl = url + (this.getConfig('disableCaching') ? ('?' + this.getConfig('disableCachingParam') + '=' + Ext.Date.now()) : ''),
+ fileName = url.split('/').pop(),
+ isCrossOriginRestricted = false,
+ xhr, status, onScriptError;
+
+ scope = scope || this;
+
+ this.isLoading = true;
+
+ if (!synchronous) {
+ onScriptError = function() {
+ onError.call(scope, "Failed loading '" + url + "', please verify that the file exists", synchronous);
+ };
+
+ if (!Ext.isReady && Ext.onDocumentReady) {
+ Ext.onDocumentReady(function() {
+ me.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope);
+ });
+ }
+ else {
+ this.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope);
+ }
+ }
+ else {
+ if (typeof XMLHttpRequest !== 'undefined') {
+ xhr = new XMLHttpRequest();
+ } else {
+ xhr = new ActiveXObject('Microsoft.XMLHTTP');
+ }
+
+ try {
+ xhr.open('GET', noCacheUrl, false);
+ xhr.send(null);
+ } catch (e) {
+ isCrossOriginRestricted = true;
+ }
+
+ status = (xhr.status === 1223) ? 204 : xhr.status;
+
+ if (!isCrossOriginRestricted) {
+ isCrossOriginRestricted = (status === 0);
+ }
+
+ if (isCrossOriginRestricted
+ ) {
+ onError.call(this, "Failed loading synchronously via XHR: '" + url + "'; It's likely that the file is either " +
+ "being loaded from a different domain or from the local file system whereby cross origin " +
+ "requests are not allowed due to security reasons. Use asynchronous loading with " +
+ "Ext.require instead.", synchronous);
+ }
+ else if (status >= 200 && status < 300
+ ) {
+ // Firebug friendly, file names are still shown even though they're eval'ed code
+ new Function(xhr.responseText + "\n//@ sourceURL=" + fileName)();
+
+ onLoad.call(scope);
+ }
+ else {
+ onError.call(this, "Failed loading synchronously via XHR: '" + url + "'; please " +
+ "verify that the file exists. " +
+ "XHR status code: " + status, synchronous);
+ }
+
+ // Prevent potential IE memory leak
+ xhr = null;
+ }
+ },
+
+ /**
+ * Explicitly exclude files from being loaded. Useful when used in conjunction with a broad include expression.
+ * Can be chained with more `require` and `exclude` methods, e.g.:
+ *
+ * Ext.exclude('Ext.data.*').require('*');
+ *
+ * Ext.exclude('widget.button*').require('widget.*');
+ *
+ * {@link Ext#exclude Ext.exclude} is alias for {@link Ext.Loader#exclude Ext.Loader.exclude} for convenience.
+ *
+ * @param {String/String[]} excludes
+ * @return {Object} object contains `require` method for chaining
+ */
+ exclude: function(excludes) {
+ var me = this;
+
+ return {
+ require: function(expressions, fn, scope) {
+ return me.require(expressions, fn, scope, excludes);
+ },
+
+ syncRequire: function(expressions, fn, scope) {
+ return me.syncRequire(expressions, fn, scope, excludes);
+ }
+ };
+ },
+
+ /**
+ * Synchronously loads all classes by the given names and all their direct dependencies;
+ * optionally executes the given callback function when finishes, within the optional scope.
+ *
+ * {@link Ext#syncRequire Ext.syncRequire} is alias for {@link Ext.Loader#syncRequire Ext.Loader.syncRequire} for convenience.
+ *
+ * @param {String/String[]} expressions Can either be a string or an array of string
+ * @param {Function} fn (Optional) The callback function
+ * @param {Object} scope (Optional) The execution scope (`this`) of the callback function
+ * @param {String/String[]} excludes (Optional) Classes to be excluded, useful when being used with expressions
+ */
+ syncRequire: function() {
+ this.syncModeEnabled = true;
+ this.require.apply(this, arguments);
+ this.refreshQueue();
+ this.syncModeEnabled = false;
+ },
+
+ /**
+ * Loads all classes by the given names and all their direct dependencies;
+ * optionally executes the given callback function when finishes, within the optional scope.
+ *
+ * {@link Ext#require Ext.require} is alias for {@link Ext.Loader#require Ext.Loader.require} for convenience.
+ *
+ * @param {String/String[]} expressions Can either be a string or an array of string
+ * @param {Function} fn (Optional) The callback function
+ * @param {Object} scope (Optional) The execution scope (`this`) of the callback function
+ * @param {String/String[]} excludes (Optional) Classes to be excluded, useful when being used with expressions
+ */
+ require: function(expressions, fn, scope, excludes) {
+ var filePath, expression, exclude, className, excluded = {},
+ excludedClassNames = [],
+ possibleClassNames = [],
+ possibleClassName, classNames = [],
+ i, j, ln, subLn;
+
+ expressions = Ext.Array.from(expressions);
+ excludes = Ext.Array.from(excludes);
+
+ fn = fn || Ext.emptyFn;
+
+ scope = scope || Ext.global;
+
+ for (i = 0, ln = excludes.length; i < ln; i++) {
+ exclude = excludes[i];
+
+ if (typeof exclude === 'string' && exclude.length > 0) {
+ excludedClassNames = Manager.getNamesByExpression(exclude);
+
+ for (j = 0, subLn = excludedClassNames.length; j < subLn; j++) {
+ excluded[excludedClassNames[j]] = true;
+ }
+ }
+ }
+
+ for (i = 0, ln = expressions.length; i < ln; i++) {
+ expression = expressions[i];
+
+ if (typeof expression === 'string' && expression.length > 0) {
+ possibleClassNames = Manager.getNamesByExpression(expression);
+
+ for (j = 0, subLn = possibleClassNames.length; j < subLn; j++) {
+ possibleClassName = possibleClassNames[j];
+
+ if (!excluded.hasOwnProperty(possibleClassName) && !Manager.isCreated(possibleClassName)) {
+ Ext.Array.include(classNames, possibleClassName);
+ }
+ }
+ }
+ }
+
+ // If the dynamic dependency feature is not being used, throw an error
+ // if the dependencies are not defined
+ if (!this.config.enabled) {
+ if (classNames.length > 0) {
+ Ext.Error.raise({
+ sourceClass: "Ext.Loader",
+ sourceMethod: "require",
+ msg: "Ext.Loader is not enabled, so dependencies cannot be resolved dynamically. " +
+ "Missing required class" + ((classNames.length > 1) ? "es" : "") + ": " + classNames.join(', ')
+ });
+ }
+ }
+
+ if (classNames.length === 0) {
+ fn.call(scope);
+ return this;
+ }
+
+ this.queue.push({
+ requires: classNames,
+ callback: fn,
+ scope: scope
+ });
+
+ classNames = classNames.slice();
+
+ for (i = 0, ln = classNames.length; i < ln; i++) {
+ className = classNames[i];
+
+ if (!this.isFileLoaded.hasOwnProperty(className)) {
+ this.isFileLoaded[className] = false;
+
+ filePath = this.getPath(className);
+
+ this.classNameToFilePathMap[className] = filePath;
+
+ this.numPendingFiles++;
+
+ this.loadScriptFile(
+ filePath,
+ Ext.Function.pass(this.onFileLoaded, [className, filePath], this),
+ Ext.Function.pass(this.onFileLoadError, [className, filePath]),
+ this,
+ this.syncModeEnabled
+ );
+ }
+ }
+
+ return this;
+ },
+
+ /**
+ * @private
+ * @param {String} className
+ * @param {String} filePath
+ */
+ onFileLoaded: function(className, filePath) {
+ this.numLoadedFiles++;
+
+ this.isFileLoaded[className] = true;
+
+ this.numPendingFiles--;
+
+ if (this.numPendingFiles === 0) {
+ this.refreshQueue();
+ }
+
+
+ },
+
+ /**
+ * @private
+ */
+ onFileLoadError: function(className, filePath, errorMessage, isSynchronous) {
+ this.numPendingFiles--;
+ this.hasFileLoadError = true;
+
+ },
+
+ /**
+ * @private
+ */
+ addOptionalRequires: function(requires) {
+ var optionalRequires = this.optionalRequires,
+ i, ln, require;
+
+ requires = Ext.Array.from(requires);
+
+ for (i = 0, ln = requires.length; i < ln; i++) {
+ require = requires[i];
+
+ Ext.Array.include(optionalRequires, require);
+ }
+
+ return this;
+ },
+
+ /**
+ * @private
+ */
+ triggerReady: function(force) {
+ var readyListeners = this.readyListeners,
+ optionalRequires, listener;
+
+ if (this.isLoading || force) {
+ this.isLoading = false;
+
+ if (this.optionalRequires.length) {
+ // Clone then empty the array to eliminate potential recursive loop issue
+ optionalRequires = Ext.Array.clone(this.optionalRequires);
+
+ // Empty the original array
+ this.optionalRequires.length = 0;
+
+ this.require(optionalRequires, Ext.Function.pass(this.triggerReady, [true], this), this);
+ return this;
+ }
+
+ while (readyListeners.length) {
+ listener = readyListeners.shift();
+ listener.fn.call(listener.scope);
+
+ if (this.isLoading) {
+ return this;
+ }
+ }
+ }
+
+ return this;
+ },
+
+ /**
+ * Adds new listener to be executed when all required scripts are fully loaded.
+ *
+ * @param {Function} fn The function callback to be executed
+ * @param {Object} scope The execution scope (`this`) of the callback function
+ * @param {Boolean} withDomReady Whether or not to wait for document dom ready as well
+ */
+ onReady: function(fn, scope, withDomReady, options) {
+ var oldFn;
+
+ if (withDomReady !== false && Ext.onDocumentReady) {
+ oldFn = fn;
+
+ fn = function() {
+ Ext.onDocumentReady(oldFn, scope, options);
+ };
+ }
+
+ if (!this.isLoading) {
+ fn.call(scope);
+ }
+ else {
+ this.readyListeners.push({
+ fn: fn,
+ scope: scope
+ });
+ }
+ },
+
+ /**
+ * @private
+ * @param {String} className
+ */
+ historyPush: function(className) {
+ if (className && this.isFileLoaded.hasOwnProperty(className)) {
+ Ext.Array.include(this.history, className);
+ }
+
+ return this;
+ }
+ };
+
+ /**
+ * @member Ext
+ * @method require
+ * @alias Ext.Loader#require
+ */
+ Ext.require = alias(Loader, 'require');
+
+ /**
+ * @member Ext
+ * @method syncRequire
+ * @alias Ext.Loader#syncRequire
+ */
+ Ext.syncRequire = alias(Loader, 'syncRequire');
+
+ /**
+ * @member Ext
+ * @method exclude
+ * @alias Ext.Loader#exclude
+ */
+ Ext.exclude = alias(Loader, 'exclude');
+
+ /**
+ * @member Ext
+ * @method onReady
+ * @alias Ext.Loader#onReady
+ */
+ Ext.onReady = function(fn, scope, options) {
+ Loader.onReady(fn, scope, true, options);
+ };
+
+ /**
+ * @cfg {String[]} requires
+ * @member Ext.Class
+ * List of classes that have to be loaded before instantiating this class.
+ * For example:
+ *
+ * Ext.define('Mother', {
+ * requires: ['Child'],
+ * giveBirth: function() {
+ * // we can be sure that child class is available.
+ * return new Child();
+ * }
+ * });
+ */
+ Class.registerPreprocessor('loader', function(cls, data, continueFn) {
+ var me = this,
+ dependencies = [],
+ className = Manager.getName(cls),
+ i, j, ln, subLn, value, propertyName, propertyValue;
+
+ /*
+ Basically loop through the dependencyProperties, look for string class names and push
+ them into a stack, regardless of whether the property's value is a string, array or object. For example:
+ {
+ extend: 'Ext.MyClass',
+ requires: ['Ext.some.OtherClass'],
+ mixins: {
+ observable: 'Ext.util.Observable';
+ }
+ }
+ which will later be transformed into:
+ {
+ extend: Ext.MyClass,
+ requires: [Ext.some.OtherClass],
+ mixins: {
+ observable: Ext.util.Observable;
+ }
+ }
+ */
+
+ for (i = 0, ln = dependencyProperties.length; i < ln; i++) {
+ propertyName = dependencyProperties[i];
+
+ if (data.hasOwnProperty(propertyName)) {
+ propertyValue = data[propertyName];
+
+ if (typeof propertyValue === 'string') {
+ dependencies.push(propertyValue);
+ }
+ else if (propertyValue instanceof Array) {
+ for (j = 0, subLn = propertyValue.length; j < subLn; j++) {
+ value = propertyValue[j];
+
+ if (typeof value === 'string') {
+ dependencies.push(value);
+ }
+ }
+ }
+ else if (typeof propertyValue != 'function') {
+ for (j in propertyValue) {
+ if (propertyValue.hasOwnProperty(j)) {
+ value = propertyValue[j];
+
+ if (typeof value === 'string') {
+ dependencies.push(value);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (dependencies.length === 0) {
+// Loader.historyPush(className);
+ return;
+ }
+
+
+ Loader.require(dependencies, function() {
+ for (i = 0, ln = dependencyProperties.length; i < ln; i++) {
+ propertyName = dependencyProperties[i];
+
+ if (data.hasOwnProperty(propertyName)) {
+ propertyValue = data[propertyName];
+
+ if (typeof propertyValue === 'string') {
+ data[propertyName] = Manager.get(propertyValue);
+ }
+ else if (propertyValue instanceof Array) {
+ for (j = 0, subLn = propertyValue.length; j < subLn; j++) {
+ value = propertyValue[j];
+
+ if (typeof value === 'string') {
+ data[propertyName][j] = Manager.get(value);
+ }
+ }
+ }
+ else if (typeof propertyValue != 'function') {
+ for (var k in propertyValue) {
+ if (propertyValue.hasOwnProperty(k)) {
+ value = propertyValue[k];
+
+ if (typeof value === 'string') {
+ data[propertyName][k] = Manager.get(value);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ continueFn.call(me, cls, data);
+ });
+
+ return false;
+ }, true);
+
+ Class.setDefaultPreprocessorPosition('loader', 'after', 'className');
+
+ /**
+ * @cfg {String[]} uses
+ * @member Ext.Class
+ * List of classes to load together with this class. These aren't neccessarily loaded before
+ * this class is instantiated. For example:
+ *
+ * Ext.define('Mother', {
+ * uses: ['Child'],
+ * giveBirth: function() {
+ * // This code might, or might not work:
+ * // return new Child();
+ *
+ * // Instead use Ext.create() to load the class at the spot if not loaded already:
+ * return Ext.create('Child');
+ * }
+ * });
+ */
+ Manager.registerPostprocessor('uses', function(name, cls, data) {
+ var uses = Ext.Array.from(data.uses),
+ items = [],
+ i, ln, item;
+
+ for (i = 0, ln = uses.length; i < ln; i++) {
+ item = uses[i];
+
+ if (typeof item === 'string') {
+ items.push(item);
+ }
+ }
+
+ Loader.addOptionalRequires(items);
+ });
+
+ Manager.setDefaultPostprocessorPosition('uses', 'last');
+
+})(Ext.ClassManager, Ext.Class, Ext.Function.flexSetter, Ext.Function.alias);
+
+/**
+ * @author Brian Moeskau <brian@sencha.com>
+ * @docauthor Brian Moeskau <brian@sencha.com>
+ *
+ * A wrapper class for the native JavaScript Error object that adds a few useful capabilities for handling
+ * errors in an Ext application. When you use Ext.Error to {@link #raise} an error from within any class that
+ * uses the Ext 4 class system, the Error class can automatically add the source class and method from which
+ * the error was raised. It also includes logic to automatically log the eroor to the console, if available,
+ * with additional metadata about the error. In all cases, the error will always be thrown at the end so that
+ * execution will halt.
+ *
+ * Ext.Error also offers a global error {@link #handle handling} method that can be overridden in order to
+ * handle application-wide errors in a single spot. You can optionally {@link #ignore} errors altogether,
+ * although in a real application it's usually a better idea to override the handling function and perform
+ * logging or some other method of reporting the errors in a way that is meaningful to the application.
+ *
+ * At its simplest you can simply raise an error as a simple string from within any code:
+ *
+ * Example usage:
+ *
+ * Ext.Error.raise('Something bad happened!');
+ *
+ * If raised from plain JavaScript code, the error will be logged to the console (if available) and the message
+ * displayed. In most cases however you'll be raising errors from within a class, and it may often be useful to add
+ * additional metadata about the error being raised. The {@link #raise} method can also take a config object.
+ * In this form the `msg` attribute becomes the error description, and any other data added to the config gets
+ * added to the error object and, if the console is available, logged to the console for inspection.
+ *
+ * Example usage:
+ *
+ * Ext.define('Ext.Foo', {
+ * doSomething: function(option){
+ * if (someCondition === false) {
+ * Ext.Error.raise({
+ * msg: 'You cannot do that!',
+ * option: option, // whatever was passed into the method
+ * 'error code': 100 // other arbitrary info
+ * });
+ * }
+ * }
+ * });
+ *
+ * If a console is available (that supports the `console.dir` function) you'll see console output like:
+ *
+ * An error was raised with the following data:
+ * option: Object { foo: "bar"}
+ * foo: "bar"
+ * error code: 100
+ * msg: "You cannot do that!"
+ * sourceClass: "Ext.Foo"
+ * sourceMethod: "doSomething"
+ *
+ * uncaught exception: You cannot do that!
+ *
+ * As you can see, the error will report exactly where it was raised and will include as much information as the
+ * raising code can usefully provide.
+ *
+ * If you want to handle all application errors globally you can simply override the static {@link #handle} method
+ * and provide whatever handling logic you need. If the method returns true then the error is considered handled
+ * and will not be thrown to the browser. If anything but true is returned then the error will be thrown normally.
+ *
+ * Example usage:
+ *
+ * Ext.Error.handle = function(err) {
+ * if (err.someProperty == 'NotReallyAnError') {
+ * // maybe log something to the application here if applicable
+ * return true;
+ * }
+ * // any non-true return value (including none) will cause the error to be thrown
+ * }
+ *
+ */
+Ext.Error = Ext.extend(Error, {
+ statics: {
+ /**
+ * @property {Boolean} ignore
+ * Static flag that can be used to globally disable error reporting to the browser if set to true
+ * (defaults to false). Note that if you ignore Ext errors it's likely that some other code may fail
+ * and throw a native JavaScript error thereafter, so use with caution. In most cases it will probably
+ * be preferable to supply a custom error {@link #handle handling} function instead.
+ *
+ * Example usage:
+ *
+ * Ext.Error.ignore = true;
+ *
+ * @static
+ */
+ ignore: false,
+
+ /**
+ * @property {Boolean} notify
+ * Static flag that can be used to globally control error notification to the user. Unlike
+ * Ex.Error.ignore, this does not effect exceptions. They are still thrown. This value can be
+ * set to false to disable the alert notification (default is true for IE6 and IE7).
+ *
+ * Only the first error will generate an alert. Internally this flag is set to false when the
+ * first error occurs prior to displaying the alert.
+ *
+ * This flag is not used in a release build.
+ *
+ * Example usage:
+ *
+ * Ext.Error.notify = false;
+ *
+ * @static
+ */
+ //notify: Ext.isIE6 || Ext.isIE7,
+
+ /**
+ * Raise an error that can include additional data and supports automatic console logging if available.
+ * You can pass a string error message or an object with the `msg` attribute which will be used as the
+ * error message. The object can contain any other name-value attributes (or objects) to be logged
+ * along with the error.
+ *
+ * Note that after displaying the error message a JavaScript error will ultimately be thrown so that
+ * execution will halt.
+ *
+ * Example usage:
+ *
+ * Ext.Error.raise('A simple string error message');
+ *
+ * // or...
+ *
+ * Ext.define('Ext.Foo', {
+ * doSomething: function(option){
+ * if (someCondition === false) {
+ * Ext.Error.raise({
+ * msg: 'You cannot do that!',
+ * option: option, // whatever was passed into the method
+ * 'error code': 100 // other arbitrary info
+ * });
+ * }
+ * }
+ * });
+ *
+ * @param {String/Object} err The error message string, or an object containing the attribute "msg" that will be
+ * used as the error message. Any other data included in the object will also be logged to the browser console,
+ * if available.
+ * @static
+ */
+ raise: function(err){
+ err = err || {};
+ if (Ext.isString(err)) {
+ err = { msg: err };
+ }
+
+ var method = this.raise.caller;
+
+ if (method) {
+ if (method.$name) {
+ err.sourceMethod = method.$name;
+ }
+ if (method.$owner) {
+ err.sourceClass = method.$owner.$className;
+ }
+ }
+
+ if (Ext.Error.handle(err) !== true) {
+ var msg = Ext.Error.prototype.toString.call(err);
+
+ Ext.log({
+ msg: msg,
+ level: 'error',
+ dump: err,
+ stack: true
+ });
+
+ throw new Ext.Error(err);
+ }
+ },
+
+ /**
+ * Globally handle any Ext errors that may be raised, optionally providing custom logic to
+ * handle different errors individually. Return true from the function to bypass throwing the
+ * error to the browser, otherwise the error will be thrown and execution will halt.
+ *
+ * Example usage:
+ *
+ * Ext.Error.handle = function(err) {
+ * if (err.someProperty == 'NotReallyAnError') {
+ * // maybe log something to the application here if applicable
+ * return true;
+ * }
+ * // any non-true return value (including none) will cause the error to be thrown
+ * }
+ *
+ * @param {Ext.Error} err The Ext.Error object being raised. It will contain any attributes that were originally
+ * raised with it, plus properties about the method and class from which the error originated (if raised from a
+ * class that uses the Ext 4 class system).
+ * @static
+ */
+ handle: function(){
+ return Ext.Error.ignore;
+ }
+ },
+
+ // This is the standard property that is the name of the constructor.
+ name: 'Ext.Error',
+
+ /**
+ * Creates new Error object.
+ * @param {String/Object} config The error message string, or an object containing the
+ * attribute "msg" that will be used as the error message. Any other data included in
+ * the object will be applied to the error instance and logged to the browser console, if available.
+ */
+ constructor: function(config){
+ if (Ext.isString(config)) {
+ config = { msg: config };
+ }
+
+ var me = this;
+
+ Ext.apply(me, config);
+
+ me.message = me.message || me.msg; // 'message' is standard ('msg' is non-standard)
+ // note: the above does not work in old WebKit (me.message is readonly) (Safari 4)
+ },
+
+ /**
+ * Provides a custom string representation of the error object. This is an override of the base JavaScript
+ * `Object.toString` method, which is useful so that when logged to the browser console, an error object will
+ * be displayed with a useful message instead of `[object Object]`, the default `toString` result.
+ *
+ * The default implementation will include the error message along with the raising class and method, if available,
+ * but this can be overridden with a custom implementation either at the prototype level (for all errors) or on
+ * a particular error instance, if you want to provide a custom description that will show up in the console.
+ * @return {String} The error message. If raised from within the Ext 4 class system, the error message will also
+ * include the raising class and method names, if available.
+ */
+ toString: function(){
+ var me = this,
+ className = me.className ? me.className : '',
+ methodName = me.methodName ? '.' + me.methodName + '(): ' : '',
+ msg = me.msg || '(No description provided)';
+
+ return className + methodName + msg;
+ }
+});
+
+/*
+ * This mechanism is used to notify the user of the first error encountered on the page. This
+ * was previously internal to Ext.Error.raise and is a desirable feature since errors often
+ * slip silently under the radar. It cannot live in Ext.Error.raise since there are times
+ * where exceptions are handled in a try/catch.
+ */
+
+
+
+/*
+
+This file is part of Ext JS 4
+
+Copyright (c) 2011 Sencha Inc
+
+Contact: http://www.sencha.com/contact
+
+Commercial Usage
+Licensees holding valid commercial licenses may use this file in accordance with the Commercial Software License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and Sencha.
+
+If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
+
+*/
+/**
+ * @class Ext.JSON
+ * Modified version of Douglas Crockford's JSON.js that doesn't
+ * mess with the Object prototype
+ * http://www.json.org/js.html
+ * @singleton
+ */
+Ext.JSON = new(function() {
+ var useHasOwn = !! {}.hasOwnProperty,
+ isNative = function() {
+ var useNative = null;
+
+ return function() {
+ if (useNative === null) {
+ useNative = Ext.USE_NATIVE_JSON && window.JSON && JSON.toString() == '[object JSON]';
+ }
+
+ return useNative;
+ };
+ }(),
+ pad = function(n) {
+ return n < 10 ? "0" + n : n;
+ },
+ doDecode = function(json) {
+ return eval("(" + json + ')');
+ },
+ doEncode = function(o) {
+ if (!Ext.isDefined(o) || o === null) {
+ return "null";
+ } else if (Ext.isArray(o)) {
+ return encodeArray(o);
+ } else if (Ext.isDate(o)) {
+ return Ext.JSON.encodeDate(o);
+ } else if (Ext.isString(o)) {
+ return encodeString(o);
+ } else if (typeof o == "number") {
+ //don't use isNumber here, since finite checks happen inside isNumber
+ return isFinite(o) ? String(o) : "null";
+ } else if (Ext.isBoolean(o)) {
+ return String(o);
+ } else if (Ext.isObject(o)) {
+ return encodeObject(o);
+ } else if (typeof o === "function") {
+ return "null";
+ }
+ return 'undefined';
+ },
+ m = {
+ "\b": '\\b',
+ "\t": '\\t',
+ "\n": '\\n',
+ "\f": '\\f',
+ "\r": '\\r',
+ '"': '\\"',
+ "\\": '\\\\',
+ '\x0b': '\\u000b' //ie doesn't handle \v
+ },
+ charToReplace = /[\\\"\x00-\x1f\x7f-\uffff]/g,
+ encodeString = function(s) {
+ return '"' + s.replace(charToReplace, function(a) {
+ var c = m[a];
+ return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ }) + '"';
+ },
+ encodeArray = function(o) {
+ var a = ["[", ""],
+ // Note empty string in case there are no serializable members.
+ len = o.length,
+ i;
+ for (i = 0; i < len; i += 1) {
+ a.push(doEncode(o[i]), ',');
+ }
+ // Overwrite trailing comma (or empty string)
+ a[a.length - 1] = ']';
+ return a.join("");
+ },
+ encodeObject = function(o) {
+ var a = ["{", ""],
+ // Note empty string in case there are no serializable members.
+ i;
+ for (i in o) {
+ if (!useHasOwn || o.hasOwnProperty(i)) {
+ a.push(doEncode(i), ":", doEncode(o[i]), ',');
+ }
+ }
+ // Overwrite trailing comma (or empty string)
+ a[a.length - 1] = '}';
+ return a.join("");
+ };
+
+ /**
+ * <p>Encodes a Date. This returns the actual string which is inserted into the JSON string as the literal expression.
+ * <b>The returned value includes enclosing double quotation marks.</b></p>
+ * <p>The default return format is "yyyy-mm-ddThh:mm:ss".</p>
+ * <p>To override this:</p><pre><code>
+Ext.JSON.encodeDate = function(d) {
+ return Ext.Date.format(d, '"Y-m-d"');
+};
+ </code></pre>
+ * @param {Date} d The Date to encode
+ * @return {String} The string literal to use in a JSON string.
+ */
+ this.encodeDate = function(o) {
+ return '"' + o.getFullYear() + "-"
+ + pad(o.getMonth() + 1) + "-"
+ + pad(o.getDate()) + "T"
+ + pad(o.getHours()) + ":"
+ + pad(o.getMinutes()) + ":"
+ + pad(o.getSeconds()) + '"';
+ };
+
+ /**
+ * Encodes an Object, Array or other value
+ * @param {Object} o The variable to encode
+ * @return {String} The JSON string
+ */
+ this.encode = function() {
+ var ec;
+ return function(o) {
+ if (!ec) {
+ // setup encoding function on first access
+ ec = isNative() ? JSON.stringify : doEncode;
+ }
+ return ec(o);
+ };
+ }();
+
+
+ /**
+ * Decodes (parses) a JSON string to an object. If the JSON is invalid, this function throws a SyntaxError unless the safe option is set.
+ * @param {String} json The JSON string
+ * @param {Boolean} safe (optional) Whether to return null or throw an exception if the JSON is invalid.
+ * @return {Object} The resulting object
+ */
+ this.decode = function() {
+ var dc;
+ return function(json, safe) {
+ if (!dc) {
+ // setup decoding function on first access
+ dc = isNative() ? JSON.parse : doDecode;
+ }
+ try {
+ return dc(json);
+ } catch (e) {
+ if (safe === true) {
+ return null;
+ }
+ Ext.Error.raise({
+ sourceClass: "Ext.JSON",
+ sourceMethod: "decode",
+ msg: "You're trying to decode an invalid JSON String: " + json
+ });
+ }
+ };
+ }();
+
+})();
+/**
+ * Shorthand for {@link Ext.JSON#encode}
+ * @member Ext
+ * @method encode
+ * @alias Ext.JSON#encode
+ */
+Ext.encode = Ext.JSON.encode;
+/**
+ * Shorthand for {@link Ext.JSON#decode}
+ * @member Ext
+ * @method decode
+ * @alias Ext.JSON#decode
+ */
+Ext.decode = Ext.JSON.decode;
+
+
+/**
+ * @class Ext
+
+ The Ext namespace (global object) encapsulates all classes, singletons, and utility methods provided by Sencha's libraries.</p>
+ Most user interface Components are at a lower level of nesting in the namespace, but many common utility functions are provided
+ as direct properties of the Ext namespace.
+
+ Also many frequently used methods from other classes are provided as shortcuts within the Ext namespace.
+ For example {@link Ext#getCmp Ext.getCmp} aliases {@link Ext.ComponentManager#get Ext.ComponentManager.get}.
+
+ Many applications are initiated with {@link Ext#onReady Ext.onReady} which is called once the DOM is ready.
+ This ensures all scripts have been loaded, preventing dependency issues. For example
+
+ Ext.onReady(function(){
+ new Ext.Component({
+ renderTo: document.body,
+ html: 'DOM ready!'
+ });
+ });
+
+For more information about how to use the Ext classes, see
+
+- <a href="http://www.sencha.com/learn/">The Learning Center</a>
+- <a href="http://www.sencha.com/learn/Ext_FAQ">The FAQ</a>
+- <a href="http://www.sencha.com/forum/">The forums</a>
+
+ * @singleton
+ * @markdown
+ */
+Ext.apply(Ext, {
+ userAgent: navigator.userAgent.toLowerCase(),
+ cache: {},
+ idSeed: 1000,
+ windowId: 'ext-window',
+ documentId: 'ext-document',
+
+ /**
+ * True when the document is fully initialized and ready for action
+ * @type Boolean
+ */
+ isReady: false,
+
+ /**
+ * True to automatically uncache orphaned Ext.Elements periodically
+ * @type Boolean
+ */
+ enableGarbageCollector: true,
+
+ /**
+ * True to automatically purge event listeners during garbageCollection.
+ * @type Boolean
+ */
+ enableListenerCollection: true,
+
+ /**
+ * Generates unique ids. If the element already has an id, it is unchanged
+ * @param {HTMLElement/Ext.Element} el (optional) The element to generate an id for
+ * @param {String} prefix (optional) Id prefix (defaults "ext-gen")
+ * @return {String} The generated Id.
+ */
+ id: function(el, prefix) {
+ var me = this,
+ sandboxPrefix = '';
+ el = Ext.getDom(el, true) || {};
+ if (el === document) {
+ el.id = me.documentId;
+ }
+ else if (el === window) {
+ el.id = me.windowId;
+ }
+ if (!el.id) {
+ if (me.isSandboxed) {
+ if (!me.uniqueGlobalNamespace) {
+ me.getUniqueGlobalNamespace();
+ }
+ sandboxPrefix = me.uniqueGlobalNamespace + '-';
+ }
+ el.id = sandboxPrefix + (prefix || "ext-gen") + (++Ext.idSeed);
+ }
+ return el.id;
+ },
+
+ /**
+ * Returns the current document body as an {@link Ext.Element}.
+ * @return Ext.Element The document body
+ */
+ getBody: function() {
+ return Ext.get(document.body || false);
+ },
+
+ /**
+ * Returns the current document head as an {@link Ext.Element}.
+ * @return Ext.Element The document head
+ * @method
+ */
+ getHead: function() {
+ var head;
+
+ return function() {
+ if (head == undefined) {
+ head = Ext.get(document.getElementsByTagName("head")[0]);
+ }
+
+ return head;
+ };
+ }(),
+
+ /**
+ * Returns the current HTML document object as an {@link Ext.Element}.
+ * @return Ext.Element The document
+ */
+ getDoc: function() {
+ return Ext.get(document);
+ },
+
+ /**
+ * This is shorthand reference to {@link Ext.ComponentManager#get}.
+ * Looks up an existing {@link Ext.Component Component} by {@link Ext.Component#id id}
+ * @param {String} id The component {@link Ext.Component#id id}
+ * @return Ext.Component The Component, <tt>undefined</tt> if not found, or <tt>null</tt> if a
+ * Class was found.
+ */
+ getCmp: function(id) {
+ return Ext.ComponentManager.get(id);
+ },
+
+ /**
+ * Returns the current orientation of the mobile device
+ * @return {String} Either 'portrait' or 'landscape'
+ */
+ getOrientation: function() {
+ return window.innerHeight > window.innerWidth ? 'portrait' : 'landscape';
+ },
+
+ /**
+ * Attempts to destroy any objects passed to it by removing all event listeners, removing them from the
+ * DOM (if applicable) and calling their destroy functions (if available). This method is primarily
+ * intended for arguments of type {@link Ext.Element} and {@link Ext.Component}, but any subclass of
+ * {@link Ext.util.Observable} can be passed in. Any number of elements and/or components can be
+ * passed into this function in a single call as separate arguments.
+ * @param {Ext.Element/Ext.Component/Ext.Element[]/Ext.Component[]...} arg1
+ * An {@link Ext.Element}, {@link Ext.Component}, or an Array of either of these to destroy
+ */
+ destroy: function() {
+ var ln = arguments.length,
+ i, arg;
+
+ for (i = 0; i < ln; i++) {
+ arg = arguments[i];
+ if (arg) {
+ if (Ext.isArray(arg)) {
+ this.destroy.apply(this, arg);
+ }
+ else if (Ext.isFunction(arg.destroy)) {
+ arg.destroy();
+ }
+ else if (arg.dom) {
+ arg.remove();
+ }
+ }
+ }
+ },
+
+ /**
+ * Execute a callback function in a particular scope. If no function is passed the call is ignored.
+ *
+ * For example, these lines are equivalent:
+ *
+ * Ext.callback(myFunc, this, [arg1, arg2]);
+ * Ext.isFunction(myFunc) && myFunc.apply(this, [arg1, arg2]);
+ *
+ * @param {Function} callback The callback to execute
+ * @param {Object} scope (optional) The scope to execute in
+ * @param {Array} args (optional) The arguments to pass to the function
+ * @param {Number} delay (optional) Pass a number to delay the call by a number of milliseconds.
+ */
+ callback: function(callback, scope, args, delay){
+ if(Ext.isFunction(callback)){
+ args = args || [];
+ scope = scope || window;
+ if (delay) {
+ Ext.defer(callback, delay, scope, args);
+ } else {
+ callback.apply(scope, args);
+ }
+ }
+ },
+
+ /**
+ * Convert certain characters (&, <, >, and ') to their HTML character equivalents for literal display in web pages.
+ * @param {String} value The string to encode
+ * @return {String} The encoded text
+ */
+ htmlEncode : function(value) {
+ return Ext.String.htmlEncode(value);
+ },
+
+ /**
+ * Convert certain characters (&, <, >, and ') from their HTML character equivalents.
+ * @param {String} value The string to decode
+ * @return {String} The decoded text
+ */
+ htmlDecode : function(value) {
+ return Ext.String.htmlDecode(value);
+ },
+
+ /**
+ * Appends content to the query string of a URL, handling logic for whether to place
+ * a question mark or ampersand.
+ * @param {String} url The URL to append to.
+ * @param {String} s The content to append to the URL.
+ * @return (String) The resulting URL
+ */
+ urlAppend : function(url, s) {
+ if (!Ext.isEmpty(s)) {
+ return url + (url.indexOf('?') === -1 ? '?' : '&') + s;
+ }
+ return url;
+ }
+});
+
+
+Ext.ns = Ext.namespace;
+
+// for old browsers
+window.undefined = window.undefined;
+
+/**
+ * @class Ext
+ * Ext core utilities and functions.
+ * @singleton
+ */
+(function(){
+/*
+FF 3.6 - Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.17) Gecko/20110420 Firefox/3.6.17
+FF 4.0.1 - Mozilla/5.0 (Windows NT 5.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1
+FF 5.0 - Mozilla/5.0 (Windows NT 6.1; WOW64; rv:5.0) Gecko/20100101 Firefox/5.0
+
+IE6 - Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1;)
+IE7 - Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; SV1;)
+IE8 - Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)
+IE9 - Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)
+
+Chrome 11 - Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.60 Safari/534.24
+
+Safari 5 - Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1
+
+Opera 11.11 - Opera/9.80 (Windows NT 6.1; U; en) Presto/2.8.131 Version/11.11
+*/
+ var check = function(regex){
+ return regex.test(Ext.userAgent);
+ },
+ isStrict = document.compatMode == "CSS1Compat",
+ version = function (is, regex) {
+ var m;
+ return (is && (m = regex.exec(Ext.userAgent))) ? parseFloat(m[1]) : 0;
+ },
+ docMode = document.documentMode,
+ isOpera = check(/opera/),
+ isOpera10_5 = isOpera && check(/version\/10\.5/),
+ isChrome = check(/\bchrome\b/),
+ isWebKit = check(/webkit/),
+ isSafari = !isChrome && check(/safari/),
+ isSafari2 = isSafari && check(/applewebkit\/4/), // unique to Safari 2
+ isSafari3 = isSafari && check(/version\/3/),
+ isSafari4 = isSafari && check(/version\/4/),
+ isSafari5 = isSafari && check(/version\/5/),
+ isIE = !isOpera && check(/msie/),
+ isIE7 = isIE && (check(/msie 7/) || docMode == 7),
+ isIE8 = isIE && (check(/msie 8/) && docMode != 7 && docMode != 9 || docMode == 8),
+ isIE9 = isIE && (check(/msie 9/) && docMode != 7 && docMode != 8 || docMode == 9),
+ isIE6 = isIE && check(/msie 6/),
+ isGecko = !isWebKit && check(/gecko/),
+ isGecko3 = isGecko && check(/rv:1\.9/),
+ isGecko4 = isGecko && check(/rv:2\.0/),
+ isGecko5 = isGecko && check(/rv:5\./),
+ isFF3_0 = isGecko3 && check(/rv:1\.9\.0/),
+ isFF3_5 = isGecko3 && check(/rv:1\.9\.1/),
+ isFF3_6 = isGecko3 && check(/rv:1\.9\.2/),
+ isWindows = check(/windows|win32/),
+ isMac = check(/macintosh|mac os x/),
+ isLinux = check(/linux/),
+ scrollbarSize = null,
+ chromeVersion = version(true, /\bchrome\/(\d+\.\d+)/),
+ firefoxVersion = version(true, /\bfirefox\/(\d+\.\d+)/),
+ ieVersion = version(isIE, /msie (\d+\.\d+)/),
+ operaVersion = version(isOpera, /version\/(\d+\.\d+)/),
+ safariVersion = version(isSafari, /version\/(\d+\.\d+)/),
+ webKitVersion = version(isWebKit, /webkit\/(\d+\.\d+)/),
+ isSecure = /^https/i.test(window.location.protocol);
+
+ // remove css image flicker
+ try {
+ document.execCommand("BackgroundImageCache", false, true);
+ } catch(e) {}
+
+
+ Ext.setVersion('extjs', '4.0.7');
+ Ext.apply(Ext, {
+ /**
+ * URL to a blank file used by Ext when in secure mode for iframe src and onReady src to prevent
+ * the IE insecure content warning (<tt>'about:blank'</tt>, except for IE in secure mode, which is <tt>'javascript:""'</tt>).
+ * @type String
+ */
+ SSL_SECURE_URL : isSecure && isIE ? 'javascript:""' : 'about:blank',
+
+ /**
+ * True if the {@link Ext.fx.Anim} Class is available
+ * @type Boolean
+ * @property enableFx
+ */
+
+ /**
+ * True to scope the reset CSS to be just applied to Ext components. Note that this wraps root containers
+ * with an additional element. Also remember that when you turn on this option, you have to use ext-all-scoped {
+ * unless you use the bootstrap.js to load your javascript, in which case it will be handled for you.
+ * @type Boolean
+ */
+ scopeResetCSS : Ext.buildSettings.scopeResetCSS,
+
+ /**
+ * EXPERIMENTAL - True to cascade listener removal to child elements when an element is removed.
+ * Currently not optimized for performance.
+ * @type Boolean
+ */
+ enableNestedListenerRemoval : false,
+
+ /**
+ * Indicates whether to use native browser parsing for JSON methods.
+ * This option is ignored if the browser does not support native JSON methods.
+ * <b>Note: Native JSON methods will not work with objects that have functions.
+ * Also, property names must be quoted, otherwise the data will not parse.</b> (Defaults to false)
+ * @type Boolean
+ */
+ USE_NATIVE_JSON : false,
+
+ /**
+ * Return the dom node for the passed String (id), dom node, or Ext.Element.
+ * Optional 'strict' flag is needed for IE since it can return 'name' and
+ * 'id' elements by using getElementById.
+ * Here are some examples:
+ * <pre><code>
+// gets dom node based on id
+var elDom = Ext.getDom('elId');
+// gets dom node based on the dom node
+var elDom1 = Ext.getDom(elDom);
+
+// If we don't know if we are working with an
+// Ext.Element or a dom node use Ext.getDom
+function(el){
+ var dom = Ext.getDom(el);
+ // do something with the dom node
+}
+ * </code></pre>
+ * <b>Note</b>: the dom node to be found actually needs to exist (be rendered, etc)
+ * when this method is called to be successful.
+ * @param {String/HTMLElement/Ext.Element} el
+ * @return HTMLElement
+ */
+ getDom : function(el, strict) {
+ if (!el || !document) {
+ return null;
+ }
+ if (el.dom) {
+ return el.dom;
+ } else {
+ if (typeof el == 'string') {
+ var e = document.getElementById(el);
+ // IE returns elements with the 'name' and 'id' attribute.
+ // we do a strict check to return the element with only the id attribute
+ if (e && isIE && strict) {
+ if (el == e.getAttribute('id')) {
+ return e;
+ } else {
+ return null;
+ }
+ }
+ return e;
+ } else {
+ return el;
+ }
+ }
+ },
+
+ /**
+ * Removes a DOM node from the document.
+ * <p>Removes this element from the document, removes all DOM event listeners, and deletes the cache reference.
+ * All DOM event listeners are removed from this element. If {@link Ext#enableNestedListenerRemoval Ext.enableNestedListenerRemoval} is
+ * <code>true</code>, then DOM event listeners are also removed from all child nodes. The body node
+ * will be ignored if passed in.</p>
+ * @param {HTMLElement} node The node to remove
+ * @method
+ */
+ removeNode : isIE6 || isIE7 ? function() {
+ var d;
+ return function(n){
+ if(n && n.tagName != 'BODY'){
+ (Ext.enableNestedListenerRemoval) ? Ext.EventManager.purgeElement(n) : Ext.EventManager.removeAll(n);
+ d = d || document.createElement('div');
+ d.appendChild(n);
+ d.innerHTML = '';
+ delete Ext.cache[n.id];
+ }
+ };
+ }() : function(n) {
+ if (n && n.parentNode && n.tagName != 'BODY') {
+ (Ext.enableNestedListenerRemoval) ? Ext.EventManager.purgeElement(n) : Ext.EventManager.removeAll(n);
+ n.parentNode.removeChild(n);
+ delete Ext.cache[n.id];
+ }
+ },
+
+ isStrict: isStrict,
+
+ isIEQuirks: isIE && !isStrict,
+
+ /**
+ * True if the detected browser is Opera.
+ * @type Boolean
+ */
+ isOpera : isOpera,
+
+ /**
+ * True if the detected browser is Opera 10.5x.
+ * @type Boolean
+ */
+ isOpera10_5 : isOpera10_5,
+
+ /**
+ * True if the detected browser uses WebKit.
+ * @type Boolean
+ */
+ isWebKit : isWebKit,
+
+ /**
+ * True if the detected browser is Chrome.
+ * @type Boolean
+ */
+ isChrome : isChrome,
+
+ /**
+ * True if the detected browser is Safari.
+ * @type Boolean
+ */
+ isSafari : isSafari,
+
+ /**
+ * True if the detected browser is Safari 3.x.
+ * @type Boolean
+ */
+ isSafari3 : isSafari3,
+
+ /**
+ * True if the detected browser is Safari 4.x.
+ * @type Boolean
+ */
+ isSafari4 : isSafari4,
+
+ /**
+ * True if the detected browser is Safari 5.x.
+ * @type Boolean
+ */
+ isSafari5 : isSafari5,
+
+ /**
+ * True if the detected browser is Safari 2.x.
+ * @type Boolean
+ */
+ isSafari2 : isSafari2,
+
+ /**
+ * True if the detected browser is Internet Explorer.
+ * @type Boolean
+ */
+ isIE : isIE,
+
+ /**
+ * True if the detected browser is Internet Explorer 6.x.
+ * @type Boolean
+ */
+ isIE6 : isIE6,
+
+ /**
+ * True if the detected browser is Internet Explorer 7.x.
+ * @type Boolean
+ */
+ isIE7 : isIE7,
+
+ /**
+ * True if the detected browser is Internet Explorer 8.x.
+ * @type Boolean
+ */
+ isIE8 : isIE8,
+
+ /**
+ * True if the detected browser is Internet Explorer 9.x.
+ * @type Boolean
+ */
+ isIE9 : isIE9,
+
+ /**
+ * True if the detected browser uses the Gecko layout engine (e.g. Mozilla, Firefox).
+ * @type Boolean
+ */
+ isGecko : isGecko,
+
+ /**
+ * True if the detected browser uses a Gecko 1.9+ layout engine (e.g. Firefox 3.x).
+ * @type Boolean
+ */
+ isGecko3 : isGecko3,
+
+ /**
+ * True if the detected browser uses a Gecko 2.0+ layout engine (e.g. Firefox 4.x).
+ * @type Boolean
+ */
+ isGecko4 : isGecko4,
+
+ /**
+ * True if the detected browser uses a Gecko 5.0+ layout engine (e.g. Firefox 5.x).
+ * @type Boolean
+ */
+ isGecko5 : isGecko5,
+
+ /**
+ * True if the detected browser uses FireFox 3.0
+ * @type Boolean
+ */
+ isFF3_0 : isFF3_0,
+
+ /**
+ * True if the detected browser uses FireFox 3.5
+ * @type Boolean
+ */
+ isFF3_5 : isFF3_5,
+
+ /**
+ * True if the detected browser uses FireFox 3.6
+ * @type Boolean
+ */
+ isFF3_6 : isFF3_6,
+
+ /**
+ * True if the detected browser uses FireFox 4
+ * @type Boolean
+ */
+ isFF4 : 4 <= firefoxVersion && firefoxVersion < 5,
+
+ /**
+ * True if the detected browser uses FireFox 5
+ * @type Boolean
+ */
+ isFF5 : 5 <= firefoxVersion && firefoxVersion < 6,
+
+ /**
+ * True if the detected platform is Linux.
+ * @type Boolean
+ */
+ isLinux : isLinux,
+
+ /**
+ * True if the detected platform is Windows.
+ * @type Boolean
+ */
+ isWindows : isWindows,
+
+ /**
+ * True if the detected platform is Mac OS.
+ * @type Boolean
+ */
+ isMac : isMac,
+
+ /**
+ * The current version of Chrome (0 if the browser is not Chrome).
+ * @type Number
+ */
+ chromeVersion: chromeVersion,
+
+ /**
+ * The current version of Firefox (0 if the browser is not Firefox).
+ * @type Number
+ */
+ firefoxVersion: firefoxVersion,
+
+ /**
+ * The current version of IE (0 if the browser is not IE). This does not account
+ * for the documentMode of the current page, which is factored into {@link #isIE7},
+ * {@link #isIE8} and {@link #isIE9}. Thus this is not always true:
+ *
+ * Ext.isIE8 == (Ext.ieVersion == 8)
+ *
+ * @type Number
+ * @markdown
+ */
+ ieVersion: ieVersion,
+
+ /**
+ * The current version of Opera (0 if the browser is not Opera).
+ * @type Number
+ */
+ operaVersion: operaVersion,
+
+ /**
+ * The current version of Safari (0 if the browser is not Safari).
+ * @type Number
+ */
+ safariVersion: safariVersion,
+
+ /**
+ * The current version of WebKit (0 if the browser does not use WebKit).
+ * @type Number
+ */
+ webKitVersion: webKitVersion,
+
+ /**
+ * True if the page is running over SSL
+ * @type Boolean
+ */
+ isSecure: isSecure,
+
+ /**
+ * URL to a 1x1 transparent gif image used by Ext to create inline icons with CSS background images.
+ * In older versions of IE, this defaults to "http://sencha.com/s.gif" and you should change this to a URL on your server.
+ * For other browsers it uses an inline data URL.
+ * @type String
+ */
+ BLANK_IMAGE_URL : (isIE6 || isIE7) ? '/' + '/www.sencha.com/s.gif' : 'data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==',
+
+ /**
+ * <p>Utility method for returning a default value if the passed value is empty.</p>
+ * <p>The value is deemed to be empty if it is<div class="mdetail-params"><ul>
+ * <li>null</li>
+ * <li>undefined</li>
+ * <li>an empty array</li>
+ * <li>a zero length string (Unless the <tt>allowBlank</tt> parameter is <tt>true</tt>)</li>
+ * </ul></div>
+ * @param {Object} value The value to test
+ * @param {Object} defaultValue The value to return if the original value is empty
+ * @param {Boolean} allowBlank (optional) true to allow zero length strings to qualify as non-empty (defaults to false)
+ * @return {Object} value, if non-empty, else defaultValue
+ * @deprecated 4.0.0 Use {@link Ext#valueFrom} instead
+ */
+ value : function(v, defaultValue, allowBlank){
+ return Ext.isEmpty(v, allowBlank) ? defaultValue : v;
+ },
+
+ /**
+ * Escapes the passed string for use in a regular expression
+ * @param {String} str
+ * @return {String}
+ * @deprecated 4.0.0 Use {@link Ext.String#escapeRegex} instead
+ */
+ escapeRe : function(s) {
+ return s.replace(/([-.*+?^${}()|[\]\/\\])/g, "\\$1");
+ },
+
+ /**
+ * Applies event listeners to elements by selectors when the document is ready.
+ * The event name is specified with an <tt>@</tt> suffix.
+ * <pre><code>
+Ext.addBehaviors({
+ // add a listener for click on all anchors in element with id foo
+ '#foo a@click' : function(e, t){
+ // do something
+ },
+
+ // add the same listener to multiple selectors (separated by comma BEFORE the @)
+ '#foo a, #bar span.some-class@mouseover' : function(){
+ // do something
+ }
+});
+ * </code></pre>
+ * @param {Object} obj The list of behaviors to apply
+ */
+ addBehaviors : function(o){
+ if(!Ext.isReady){
+ Ext.onReady(function(){
+ Ext.addBehaviors(o);
+ });
+ } else {
+ var cache = {}, // simple cache for applying multiple behaviors to same selector does query multiple times
+ parts,
+ b,
+ s;
+ for (b in o) {
+ if ((parts = b.split('@'))[1]) { // for Object prototype breakers
+ s = parts[0];
+ if(!cache[s]){
+ cache[s] = Ext.select(s);
+ }
+ cache[s].on(parts[1], o[b]);
+ }
+ }
+ cache = null;
+ }
+ },
+
+ /**
+ * Returns the size of the browser scrollbars. This can differ depending on
+ * operating system settings, such as the theme or font size.
+ * @param {Boolean} force (optional) true to force a recalculation of the value.
+ * @return {Object} An object containing the width of a vertical scrollbar and the
+ * height of a horizontal scrollbar.
+ */
+ getScrollbarSize: function (force) {
+ if(!Ext.isReady){
+ return 0;
+ }
+
+ if(force === true || scrollbarSize === null){
+ // BrowserBug: IE9
+ // When IE9 positions an element offscreen via offsets, the offsetWidth is
+ // inaccurately reported. For IE9 only, we render on screen before removing.
+ var cssClass = Ext.isIE9 ? '' : Ext.baseCSSPrefix + 'hide-offsets',
+ // Append our div, do our calculation and then remove it
+ div = Ext.getBody().createChild('<div class="' + cssClass + '" style="width:100px;height:50px;overflow:hidden;"><div style="height:200px;"></div></div>'),
+ child = div.child('div', true),
+ w1 = child.offsetWidth;
+
+ div.setStyle('overflow', (Ext.isWebKit || Ext.isGecko) ? 'auto' : 'scroll');
+
+ var w2 = child.offsetWidth, width = w1 - w2;
+ div.remove();
+
+ // We assume width == height for now. TODO: is this always true?
+ scrollbarSize = { width: width, height: width };
+ }
+
+ return scrollbarSize;
+ },
+
+ /**
+ * Utility method for getting the width of the browser's vertical scrollbar. This
+ * can differ depending on operating system settings, such as the theme or font size.
+ *
+ * This method is deprected in favor of {@link #getScrollbarSize}.
+ *
+ * @param {Boolean} force (optional) true to force a recalculation of the value.
+ * @return {Number} The width of a vertical scrollbar.
+ * @deprecated
+ */
+ getScrollBarWidth: function(force){
+ var size = Ext.getScrollbarSize(force);
+ return size.width + 2; // legacy fudge factor
+ },
+
+ /**
+ * Copies a set of named properties fom the source object to the destination object.
+ *
+ * Example:
+ *
+ * ImageComponent = Ext.extend(Ext.Component, {
+ * initComponent: function() {
+ * this.autoEl = { tag: 'img' };
+ * MyComponent.superclass.initComponent.apply(this, arguments);
+ * this.initialBox = Ext.copyTo({}, this.initialConfig, 'x,y,width,height');
+ * }
+ * });
+ *
+ * Important note: To borrow class prototype methods, use {@link Ext.Base#borrow} instead.
+ *
+ * @param {Object} dest The destination object.
+ * @param {Object} source The source object.
+ * @param {String/String[]} names Either an Array of property names, or a comma-delimited list
+ * of property names to copy.
+ * @param {Boolean} usePrototypeKeys (Optional) Defaults to false. Pass true to copy keys off of the prototype as well as the instance.
+ * @return {Object} The modified object.
+ */
+ copyTo : function(dest, source, names, usePrototypeKeys){
+ if(typeof names == 'string'){
+ names = names.split(/[,;\s]/);
+ }
+ Ext.each(names, function(name){
+ if(usePrototypeKeys || source.hasOwnProperty(name)){
+ dest[name] = source[name];
+ }
+ }, this);
+ return dest;
+ },
+
+ /**
+ * Attempts to destroy and then remove a set of named properties of the passed object.
+ * @param {Object} o The object (most likely a Component) who's properties you wish to destroy.
+ * @param {String...} args One or more names of the properties to destroy and remove from the object.
+ */
+ destroyMembers : function(o){
+ for (var i = 1, a = arguments, len = a.length; i < len; i++) {
+ Ext.destroy(o[a[i]]);
+ delete o[a[i]];
+ }
+ },
+
+ /**
+ * Logs a message. If a console is present it will be used. On Opera, the method
+ * "opera.postError" is called. In other cases, the message is logged to an array
+ * "Ext.log.out". An attached debugger can watch this array and view the log. The
+ * log buffer is limited to a maximum of "Ext.log.max" entries (defaults to 250).
+ * The `Ext.log.out` array can also be written to a popup window by entering the
+ * following in the URL bar (a "bookmarklet"):
+ *
+ * javascript:void(Ext.log.show());
+ *
+ * If additional parameters are passed, they are joined and appended to the message.
+ * A technique for tracing entry and exit of a function is this:
+ *
+ * function foo () {
+ * Ext.log({ indent: 1 }, '>> foo');
+ *
+ * // log statements in here or methods called from here will be indented
+ * // by one step
+ *
+ * Ext.log({ outdent: 1 }, '<< foo');
+ * }
+ *
+ * This method does nothing in a release build.
+ *
+ * @param {String/Object} message The message to log or an options object with any
+ * of the following properties:
+ *
+ * - `msg`: The message to log (required).
+ * - `level`: One of: "error", "warn", "info" or "log" (the default is "log").
+ * - `dump`: An object to dump to the log as part of the message.
+ * - `stack`: True to include a stack trace in the log.
+ * - `indent`: Cause subsequent log statements to be indented one step.
+ * - `outdent`: Cause this and following statements to be one step less indented.
+ * @markdown
+ */
+ log :
+ Ext.emptyFn,
+
+ /**
+ * Partitions the set into two sets: a true set and a false set.
+ * Example:
+ * Example2:
+ * <pre><code>
+// Example 1:
+Ext.partition([true, false, true, true, false]); // [[true, true, true], [false, false]]
+
+// Example 2:
+Ext.partition(
+ Ext.query("p"),
+ function(val){
+ return val.className == "class1"
+ }
+);
+// true are those paragraph elements with a className of "class1",
+// false set are those that do not have that className.
+ * </code></pre>
+ * @param {Array/NodeList} arr The array to partition
+ * @param {Function} truth (optional) a function to determine truth. If this is omitted the element
+ * itself must be able to be evaluated for its truthfulness.
+ * @return {Array} [array of truish values, array of falsy values]
+ * @deprecated 4.0.0 Will be removed in the next major version
+ */
+ partition : function(arr, truth){
+ var ret = [[],[]];
+ Ext.each(arr, function(v, i, a) {
+ ret[ (truth && truth(v, i, a)) || (!truth && v) ? 0 : 1].push(v);
+ });
+ return ret;
+ },
+
+ /**
+ * Invokes a method on each item in an Array.
+ * <pre><code>
+// Example:
+Ext.invoke(Ext.query("p"), "getAttribute", "id");
+// [el1.getAttribute("id"), el2.getAttribute("id"), ..., elN.getAttribute("id")]
+ * </code></pre>
+ * @param {Array/NodeList} arr The Array of items to invoke the method on.
+ * @param {String} methodName The method name to invoke.
+ * @param {Object...} args Arguments to send into the method invocation.
+ * @return {Array} The results of invoking the method on each item in the array.
+ * @deprecated 4.0.0 Will be removed in the next major version
+ */
+ invoke : function(arr, methodName){
+ var ret = [],
+ args = Array.prototype.slice.call(arguments, 2);
+ Ext.each(arr, function(v,i) {
+ if (v && typeof v[methodName] == 'function') {
+ ret.push(v[methodName].apply(v, args));
+ } else {
+ ret.push(undefined);
+ }
+ });
+ return ret;
+ },
+
+ /**
+ * <p>Zips N sets together.</p>
+ * <pre><code>
+// Example 1:
+Ext.zip([1,2,3],[4,5,6]); // [[1,4],[2,5],[3,6]]
+// Example 2:
+Ext.zip(
+ [ "+", "-", "+"],
+ [ 12, 10, 22],
+ [ 43, 15, 96],
+ function(a, b, c){
+ return "$" + a + "" + b + "." + c
+ }
+); // ["$+12.43", "$-10.15", "$+22.96"]
+ * </code></pre>
+ * @param {Array/NodeList...} arr This argument may be repeated. Array(s) to contribute values.
+ * @param {Function} zipper (optional) The last item in the argument list. This will drive how the items are zipped together.
+ * @return {Array} The zipped set.
+ * @deprecated 4.0.0 Will be removed in the next major version
+ */
+ zip : function(){
+ var parts = Ext.partition(arguments, function( val ){ return typeof val != 'function'; }),
+ arrs = parts[0],
+ fn = parts[1][0],
+ len = Ext.max(Ext.pluck(arrs, "length")),
+ ret = [];
+
+ for (var i = 0; i < len; i++) {
+ ret[i] = [];
+ if(fn){
+ ret[i] = fn.apply(fn, Ext.pluck(arrs, i));
+ }else{
+ for (var j = 0, aLen = arrs.length; j < aLen; j++){
+ ret[i].push( arrs[j][i] );
+ }
+ }
+ }
+ return ret;
+ },
+
+ /**
+ * Turns an array into a sentence, joined by a specified connector - e.g.:
+ * Ext.toSentence(['Adama', 'Tigh', 'Roslin']); //'Adama, Tigh and Roslin'
+ * Ext.toSentence(['Adama', 'Tigh', 'Roslin'], 'or'); //'Adama, Tigh or Roslin'
+ * @param {String[]} items The array to create a sentence from
+ * @param {String} connector The string to use to connect the last two words. Usually 'and' or 'or' - defaults to 'and'.
+ * @return {String} The sentence string
+ * @deprecated 4.0.0 Will be removed in the next major version
+ */
+ toSentence: function(items, connector) {
+ var length = items.length;
+
+ if (length <= 1) {
+ return items[0];
+ } else {
+ var head = items.slice(0, length - 1),
+ tail = items[length - 1];
+
+ return Ext.util.Format.format("{0} {1} {2}", head.join(", "), connector || 'and', tail);
+ }
+ },
+
+ /**
+ * By default, Ext intelligently decides whether floating elements should be shimmed. If you are using flash,
+ * you may want to set this to true.
+ * @type Boolean
+ */
+ useShims: isIE6
+ });
+})();
+
+/**
+ * Loads Ext.app.Application class and starts it up with given configuration after the page is ready.
+ *
+ * See Ext.app.Application for details.
+ *
+ * @param {Object} config
+ */
+Ext.application = function(config) {
+ Ext.require('Ext.app.Application');
+
+ Ext.onReady(function() {
+ Ext.create('Ext.app.Application', config);
+ });
+};
+
+/**
+ * @class Ext.util.Format
+
+This class is a centralized place for formatting functions. It includes
+functions to format various different types of data, such as text, dates and numeric values.
+
+__Localization__
+This class contains several options for localization. These can be set once the library has loaded,
+all calls to the functions from that point will use the locale settings that were specified.
+Options include:
+- thousandSeparator
+- decimalSeparator
+- currenyPrecision
+- currencySign
+- currencyAtEnd
+This class also uses the default date format defined here: {@link Ext.Date#defaultFormat}.
+
+__Using with renderers__
+There are two helper functions that return a new function that can be used in conjunction with
+grid renderers:
+
+ columns: [{
+ dataIndex: 'date',
+ renderer: Ext.util.Format.dateRenderer('Y-m-d')
+ }, {
+ dataIndex: 'time',
+ renderer: Ext.util.Format.numberRenderer('0.000')
+ }]
+
+Functions that only take a single argument can also be passed directly:
+ columns: [{
+ dataIndex: 'cost',
+ renderer: Ext.util.Format.usMoney
+ }, {
+ dataIndex: 'productCode',
+ renderer: Ext.util.Format.uppercase
+ }]
+
+__Using with XTemplates__
+XTemplates can also directly use Ext.util.Format functions:
+
+ new Ext.XTemplate([
+ 'Date: {startDate:date("Y-m-d")}',
+ 'Cost: {cost:usMoney}'
+ ]);
+
+ * @markdown
+ * @singleton
+ */
+(function() {
+ Ext.ns('Ext.util');
+
+ Ext.util.Format = {};
+ var UtilFormat = Ext.util.Format,
+ stripTagsRE = /<\/?[^>]+>/gi,
+ stripScriptsRe = /(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)/ig,
+ nl2brRe = /\r?\n/g,
+
+ // A RegExp to remove from a number format string, all characters except digits and '.'
+ formatCleanRe = /[^\d\.]/g,
+
+ // A RegExp to remove from a number format string, all characters except digits and the local decimal separator.
+ // Created on first use. The local decimal separator character must be initialized for this to be created.
+ I18NFormatCleanRe;
+
+ Ext.apply(UtilFormat, {
+ /**
+ * @property {String} thousandSeparator
+ * <p>The character that the {@link #number} function uses as a thousand separator.</p>
+ * <p>This may be overridden in a locale file.</p>
+ */
+ thousandSeparator: ',',
+
+ /**
+ * @property {String} decimalSeparator
+ * <p>The character that the {@link #number} function uses as a decimal point.</p>
+ * <p>This may be overridden in a locale file.</p>
+ */
+ decimalSeparator: '.',
+
+ /**
+ * @property {Number} currencyPrecision
+ * <p>The number of decimal places that the {@link #currency} function displays.</p>
+ * <p>This may be overridden in a locale file.</p>
+ */
+ currencyPrecision: 2,
+
+ /**
+ * @property {String} currencySign
+ * <p>The currency sign that the {@link #currency} function displays.</p>
+ * <p>This may be overridden in a locale file.</p>
+ */
+ currencySign: '$',
+
+ /**
+ * @property {Boolean} currencyAtEnd
+ * <p>This may be set to <code>true</code> to make the {@link #currency} function
+ * append the currency sign to the formatted value.</p>
+ * <p>This may be overridden in a locale file.</p>
+ */
+ currencyAtEnd: false,
+
+ /**
+ * Checks a reference and converts it to empty string if it is undefined
+ * @param {Object} value Reference to check
+ * @return {Object} Empty string if converted, otherwise the original value
+ */
+ undef : function(value) {
+ return value !== undefined ? value : "";
+ },
+
+ /**
+ * Checks a reference and converts it to the default value if it's empty
+ * @param {Object} value Reference to check
+ * @param {String} defaultValue The value to insert of it's undefined (defaults to "")
+ * @return {String}
+ */
+ defaultValue : function(value, defaultValue) {
+ return value !== undefined && value !== '' ? value : defaultValue;
+ },
+
+ /**
+ * Returns a substring from within an original string
+ * @param {String} value The original text
+ * @param {Number} start The start index of the substring
+ * @param {Number} length The length of the substring
+ * @return {String} The substring
+ */
+ substr : function(value, start, length) {
+ return String(value).substr(start, length);
+ },
+
+ /**
+ * Converts a string to all lower case letters
+ * @param {String} value The text to convert
+ * @return {String} The converted text
+ */
+ lowercase : function(value) {
+ return String(value).toLowerCase();
+ },
+
+ /**
+ * Converts a string to all upper case letters
+ * @param {String} value The text to convert
+ * @return {String} The converted text
+ */
+ uppercase : function(value) {
+ return String(value).toUpperCase();
+ },
+
+ /**
+ * Format a number as US currency
+ * @param {Number/String} value The numeric value to format
+ * @return {String} The formatted currency string
+ */
+ usMoney : function(v) {
+ return UtilFormat.currency(v, '$', 2);
+ },
+
+ /**
+ * Format a number as a currency
+ * @param {Number/String} value The numeric value to format
+ * @param {String} sign The currency sign to use (defaults to {@link #currencySign})
+ * @param {Number} decimals The number of decimals to use for the currency (defaults to {@link #currencyPrecision})
+ * @param {Boolean} end True if the currency sign should be at the end of the string (defaults to {@link #currencyAtEnd})
+ * @return {String} The formatted currency string
+ */
+ currency: function(v, currencySign, decimals, end) {
+ var negativeSign = '',
+ format = ",0",
+ i = 0;
+ v = v - 0;
+ if (v < 0) {
+ v = -v;
+ negativeSign = '-';
+ }
+ decimals = decimals || UtilFormat.currencyPrecision;
+ format += format + (decimals > 0 ? '.' : '');
+ for (; i < decimals; i++) {
+ format += '0';
+ }
+ v = UtilFormat.number(v, format);
+ if ((end || UtilFormat.currencyAtEnd) === true) {
+ return Ext.String.format("{0}{1}{2}", negativeSign, v, currencySign || UtilFormat.currencySign);
+ } else {
+ return Ext.String.format("{0}{1}{2}", negativeSign, currencySign || UtilFormat.currencySign, v);
+ }
+ },
+
+ /**
+ * Formats the passed date using the specified format pattern.
+ * @param {String/Date} value The value to format. If a string is passed, it is converted to a Date by the Javascript
+ * Date object's <a href="http://www.w3schools.com/jsref/jsref_parse.asp">parse()</a> method.
+ * @param {String} format (Optional) Any valid date format string. Defaults to {@link Ext.Date#defaultFormat}.
+ * @return {String} The formatted date string.
+ */
+ date: function(v, format) {
+ if (!v) {
+ return "";
+ }
+ if (!Ext.isDate(v)) {
+ v = new Date(Date.parse(v));
+ }
+ return Ext.Date.dateFormat(v, format || Ext.Date.defaultFormat);
+ },
+
+ /**
+ * Returns a date rendering function that can be reused to apply a date format multiple times efficiently
+ * @param {String} format Any valid date format string. Defaults to {@link Ext.Date#defaultFormat}.
+ * @return {Function} The date formatting function
+ */
+ dateRenderer : function(format) {
+ return function(v) {
+ return UtilFormat.date(v, format);
+ };
+ },
+
+ /**
+ * Strips all HTML tags
+ * @param {Object} value The text from which to strip tags
+ * @return {String} The stripped text
+ */
+ stripTags : function(v) {
+ return !v ? v : String(v).replace(stripTagsRE, "");
+ },
+
+ /**
+ * Strips all script tags
+ * @param {Object} value The text from which to strip script tags
+ * @return {String} The stripped text
+ */
+ stripScripts : function(v) {
+ return !v ? v : String(v).replace(stripScriptsRe, "");
+ },
+
+ /**
+ * Simple format for a file size (xxx bytes, xxx KB, xxx MB)
+ * @param {Number/String} size The numeric value to format
+ * @return {String} The formatted file size
+ */
+ fileSize : function(size) {
+ if (size < 1024) {
+ return size + " bytes";
+ } else if (size < 1048576) {
+ return (Math.round(((size*10) / 1024))/10) + " KB";
+ } else {
+ return (Math.round(((size*10) / 1048576))/10) + " MB";
+ }
+ },
+
+ /**
+ * It does simple math for use in a template, for example:<pre><code>
+ * var tpl = new Ext.Template('{value} * 10 = {value:math("* 10")}');
+ * </code></pre>
+ * @return {Function} A function that operates on the passed value.
+ * @method
+ */
+ math : function(){
+ var fns = {};
+
+ return function(v, a){
+ if (!fns[a]) {
+ fns[a] = Ext.functionFactory('v', 'return v ' + a + ';');
+ }
+ return fns[a](v);
+ };
+ }(),
+
+ /**
+ * Rounds the passed number to the required decimal precision.
+ * @param {Number/String} value The numeric value to round.
+ * @param {Number} precision The number of decimal places to which to round the first parameter's value.
+ * @return {Number} The rounded value.
+ */
+ round : function(value, precision) {
+ var result = Number(value);
+ if (typeof precision == 'number') {
+ precision = Math.pow(10, precision);
+ result = Math.round(value * precision) / precision;
+ }
+ return result;
+ },
+
+ /**
+ * <p>Formats the passed number according to the passed format string.</p>
+ * <p>The number of digits after the decimal separator character specifies the number of
+ * decimal places in the resulting string. The <u>local-specific</u> decimal character is used in the result.</p>
+ * <p>The <i>presence</i> of a thousand separator character in the format string specifies that
+ * the <u>locale-specific</u> thousand separator (if any) is inserted separating thousand groups.</p>
+ * <p>By default, "," is expected as the thousand separator, and "." is expected as the decimal separator.</p>
+ * <p><b>New to Ext JS 4</b></p>
+ * <p>Locale-specific characters are always used in the formatted output when inserting
+ * thousand and decimal separators.</p>
+ * <p>The format string must specify separator characters according to US/UK conventions ("," as the
+ * thousand separator, and "." as the decimal separator)</p>
+ * <p>To allow specification of format strings according to local conventions for separator characters, add
+ * the string <code>/i</code> to the end of the format string.</p>
+ * <div style="margin-left:40px">examples (123456.789):
+ * <div style="margin-left:10px">
+ * 0 - (123456) show only digits, no precision<br>
+ * 0.00 - (123456.78) show only digits, 2 precision<br>
+ * 0.0000 - (123456.7890) show only digits, 4 precision<br>
+ * 0,000 - (123,456) show comma and digits, no precision<br>
+ * 0,000.00 - (123,456.78) show comma and digits, 2 precision<br>
+ * 0,0.00 - (123,456.78) shortcut method, show comma and digits, 2 precision<br>
+ * To allow specification of the formatting string using UK/US grouping characters (,) and decimal (.) for international numbers, add /i to the end.
+ * For example: 0.000,00/i
+ * </div></div>
+ * @param {Number} v The number to format.
+ * @param {String} format The way you would like to format this text.
+ * @return {String} The formatted number.
+ */
+ number: function(v, formatString) {
+ if (!formatString) {
+ return v;
+ }
+ v = Ext.Number.from(v, NaN);
+ if (isNaN(v)) {
+ return '';
+ }
+ var comma = UtilFormat.thousandSeparator,
+ dec = UtilFormat.decimalSeparator,
+ i18n = false,
+ neg = v < 0,
+ hasComma,
+ psplit;
+
+ v = Math.abs(v);
+
+ // The "/i" suffix allows caller to use a locale-specific formatting string.
+ // Clean the format string by removing all but numerals and the decimal separator.
+ // Then split the format string into pre and post decimal segments according to *what* the
+ // decimal separator is. If they are specifying "/i", they are using the local convention in the format string.
+ if (formatString.substr(formatString.length - 2) == '/i') {
+ if (!I18NFormatCleanRe) {
+ I18NFormatCleanRe = new RegExp('[^\\d\\' + UtilFormat.decimalSeparator + ']','g');
+ }
+ formatString = formatString.substr(0, formatString.length - 2);
+ i18n = true;
+ hasComma = formatString.indexOf(comma) != -1;
+ psplit = formatString.replace(I18NFormatCleanRe, '').split(dec);
+ } else {
+ hasComma = formatString.indexOf(',') != -1;
+ psplit = formatString.replace(formatCleanRe, '').split('.');
+ }
+
+ if (1 < psplit.length) {
+ v = v.toFixed(psplit[1].length);
+ } else if(2 < psplit.length) {
+ } else {
+ v = v.toFixed(0);
+ }
+
+ var fnum = v.toString();
+
+ psplit = fnum.split('.');
+
+ if (hasComma) {
+ var cnum = psplit[0],
+ parr = [],
+ j = cnum.length,
+ m = Math.floor(j / 3),
+ n = cnum.length % 3 || 3,
+ i;
+
+ for (i = 0; i < j; i += n) {
+ if (i !== 0) {
+ n = 3;
+ }
+
+ parr[parr.length] = cnum.substr(i, n);
+ m -= 1;
+ }
+ fnum = parr.join(comma);
+ if (psplit[1]) {
+ fnum += dec + psplit[1];
+ }
+ } else {
+ if (psplit[1]) {
+ fnum = psplit[0] + dec + psplit[1];
+ }
+ }
+
+ if (neg) {
+ /*
+ * Edge case. If we have a very small negative number it will get rounded to 0,
+ * however the initial check at the top will still report as negative. Replace
+ * everything but 1-9 and check if the string is empty to determine a 0 value.
+ */
+ neg = fnum.replace(/[^1-9]/g, '') !== '';
+ }
+
+ return (neg ? '-' : '') + formatString.replace(/[\d,?\.?]+/, fnum);
+ },
+
+ /**
+ * Returns a number rendering function that can be reused to apply a number format multiple times efficiently
+ * @param {String} format Any valid number format string for {@link #number}
+ * @return {Function} The number formatting function
+ */
+ numberRenderer : function(format) {
+ return function(v) {
+ return UtilFormat.number(v, format);
+ };
+ },
+
+ /**
+ * Selectively do a plural form of a word based on a numeric value. For example, in a template,
+ * {commentCount:plural("Comment")} would result in "1 Comment" if commentCount was 1 or would be "x Comments"
+ * if the value is 0 or greater than 1.
+ * @param {Number} value The value to compare against
+ * @param {String} singular The singular form of the word
+ * @param {String} plural (optional) The plural form of the word (defaults to the singular with an "s")
+ */
+ plural : function(v, s, p) {
+ return v +' ' + (v == 1 ? s : (p ? p : s+'s'));
+ },
+
+ /**
+ * Converts newline characters to the HTML tag <br/>
+ * @param {String} The string value to format.
+ * @return {String} The string with embedded <br/> tags in place of newlines.
+ */
+ nl2br : function(v) {
+ return Ext.isEmpty(v) ? '' : v.replace(nl2brRe, '<br/>');
+ },
+
+ /**
+ * Alias for {@link Ext.String#capitalize}.
+ * @method
+ * @alias Ext.String#capitalize
+ */
+ capitalize: Ext.String.capitalize,
+
+ /**
+ * Alias for {@link Ext.String#ellipsis}.
+ * @method
+ * @alias Ext.String#ellipsis
+ */
+ ellipsis: Ext.String.ellipsis,
+
+ /**
+ * Alias for {@link Ext.String#format}.
+ * @method
+ * @alias Ext.String#format
+ */
+ format: Ext.String.format,
+
+ /**
+ * Alias for {@link Ext.String#htmlDecode}.
+ * @method
+ * @alias Ext.String#htmlDecode
+ */
+ htmlDecode: Ext.String.htmlDecode,
+
+ /**
+ * Alias for {@link Ext.String#htmlEncode}.
+ * @method
+ * @alias Ext.String#htmlEncode
+ */
+ htmlEncode: Ext.String.htmlEncode,
+
+ /**
+ * Alias for {@link Ext.String#leftPad}.
+ * @method
+ * @alias Ext.String#leftPad
+ */
+ leftPad: Ext.String.leftPad,
+
+ /**
+ * Alias for {@link Ext.String#trim}.
+ * @method
+ * @alias Ext.String#trim
+ */
+ trim : Ext.String.trim,
+
+ /**
+ * Parses a number or string representing margin sizes into an object. Supports CSS-style margin declarations
+ * (e.g. 10, "10", "10 10", "10 10 10" and "10 10 10 10" are all valid options and would return the same result)
+ * @param {Number/String} v The encoded margins
+ * @return {Object} An object with margin sizes for top, right, bottom and left
+ */
+ parseBox : function(box) {
+ if (Ext.isNumber(box)) {
+ box = box.toString();
+ }
+ var parts = box.split(' '),
+ ln = parts.length;
+
+ if (ln == 1) {
+ parts[1] = parts[2] = parts[3] = parts[0];
+ }
+ else if (ln == 2) {
+ parts[2] = parts[0];
+ parts[3] = parts[1];
+ }
+ else if (ln == 3) {
+ parts[3] = parts[1];
+ }
+
+ return {
+ top :parseInt(parts[0], 10) || 0,
+ right :parseInt(parts[1], 10) || 0,
+ bottom:parseInt(parts[2], 10) || 0,
+ left :parseInt(parts[3], 10) || 0
+ };
+ },
+
+ /**
+ * Escapes the passed string for use in a regular expression
+ * @param {String} str
+ * @return {String}
+ */
+ escapeRegex : function(s) {
+ return s.replace(/([\-.*+?\^${}()|\[\]\/\\])/g, "\\$1");
+ }
+ });
+})();
+
+/**
+ * @class Ext.util.TaskRunner
+ * Provides the ability to execute one or more arbitrary tasks in a multithreaded
+ * manner. Generally, you can use the singleton {@link Ext.TaskManager} instead, but
+ * if needed, you can create separate instances of TaskRunner. Any number of
+ * separate tasks can be started at any time and will run independently of each
+ * other. Example usage:
+ * <pre><code>
+// Start a simple clock task that updates a div once per second
+var updateClock = function(){
+ Ext.fly('clock').update(new Date().format('g:i:s A'));
+}
+var task = {
+ run: updateClock,
+ interval: 1000 //1 second
+}
+var runner = new Ext.util.TaskRunner();
+runner.start(task);
+
+// equivalent using TaskManager
+Ext.TaskManager.start({
+ run: updateClock,
+ interval: 1000
+});
+
+ * </code></pre>
+ * <p>See the {@link #start} method for details about how to configure a task object.</p>
+ * Also see {@link Ext.util.DelayedTask}.
+ *
+ * @constructor
+ * @param {Number} [interval=10] The minimum precision in milliseconds supported by this TaskRunner instance
+ */
+Ext.ns('Ext.util');
+
+Ext.util.TaskRunner = function(interval) {
+ interval = interval || 10;
+ var tasks = [],
+ removeQueue = [],
+ id = 0,
+ running = false,
+
+ // private
+ stopThread = function() {
+ running = false;
+ clearInterval(id);
+ id = 0;
+ },
+
+ // private
+ startThread = function() {
+ if (!running) {
+ running = true;
+ id = setInterval(runTasks, interval);
+ }
+ },
+
+ // private
+ removeTask = function(t) {
+ removeQueue.push(t);
+ if (t.onStop) {
+ t.onStop.apply(t.scope || t);
+ }
+ },
+
+ // private
+ runTasks = function() {
+ var rqLen = removeQueue.length,
+ now = new Date().getTime(),
+ i;
+
+ if (rqLen > 0) {
+ for (i = 0; i < rqLen; i++) {
+ Ext.Array.remove(tasks, removeQueue[i]);
+ }
+ removeQueue = [];
+ if (tasks.length < 1) {
+ stopThread();
+ return;
+ }
+ }
+ i = 0;
+ var t,
+ itime,
+ rt,
+ len = tasks.length;
+ for (; i < len; ++i) {
+ t = tasks[i];
+ itime = now - t.taskRunTime;
+ if (t.interval <= itime) {
+ rt = t.run.apply(t.scope || t, t.args || [++t.taskRunCount]);
+ t.taskRunTime = now;
+ if (rt === false || t.taskRunCount === t.repeat) {
+ removeTask(t);
+ return;
+ }
+ }
+ if (t.duration && t.duration <= (now - t.taskStartTime)) {
+ removeTask(t);
+ }
+ }
+ };
+
+ /**
+ * Starts a new task.
+ * @method start
+ * @param {Object} task <p>A config object that supports the following properties:<ul>
+ * <li><code>run</code> : Function<div class="sub-desc"><p>The function to execute each time the task is invoked. The
+ * function will be called at each interval and passed the <code>args</code> argument if specified, and the
+ * current invocation count if not.</p>
+ * <p>If a particular scope (<code>this</code> reference) is required, be sure to specify it using the <code>scope</code> argument.</p>
+ * <p>Return <code>false</code> from this function to terminate the task.</p></div></li>
+ * <li><code>interval</code> : Number<div class="sub-desc">The frequency in milliseconds with which the task
+ * should be invoked.</div></li>
+ * <li><code>args</code> : Array<div class="sub-desc">(optional) An array of arguments to be passed to the function
+ * specified by <code>run</code>. If not specified, the current invocation count is passed.</div></li>
+ * <li><code>scope</code> : Object<div class="sub-desc">(optional) The scope (<tt>this</tt> reference) in which to execute the
+ * <code>run</code> function. Defaults to the task config object.</div></li>
+ * <li><code>duration</code> : Number<div class="sub-desc">(optional) The length of time in milliseconds to invoke
+ * the task before stopping automatically (defaults to indefinite).</div></li>
+ * <li><code>repeat</code> : Number<div class="sub-desc">(optional) The number of times to invoke the task before
+ * stopping automatically (defaults to indefinite).</div></li>
+ * </ul></p>
+ * <p>Before each invocation, Ext injects the property <code>taskRunCount</code> into the task object so
+ * that calculations based on the repeat count can be performed.</p>
+ * @return {Object} The task
+ */
+ this.start = function(task) {
+ tasks.push(task);
+ task.taskStartTime = new Date().getTime();
+ task.taskRunTime = 0;
+ task.taskRunCount = 0;
+ startThread();
+ return task;
+ };
+
+ /**
+ * Stops an existing running task.
+ * @method stop
+ * @param {Object} task The task to stop
+ * @return {Object} The task
+ */
+ this.stop = function(task) {
+ removeTask(task);
+ return task;
+ };
+
+ /**
+ * Stops all tasks that are currently running.
+ * @method stopAll
+ */
+ this.stopAll = function() {
+ stopThread();
+ for (var i = 0, len = tasks.length; i < len; i++) {
+ if (tasks[i].onStop) {
+ tasks[i].onStop();
+ }
+ }
+ tasks = [];
+ removeQueue = [];
+ };
+};
+
+/**
+ * @class Ext.TaskManager
+ * @extends Ext.util.TaskRunner
+ * A static {@link Ext.util.TaskRunner} instance that can be used to start and stop arbitrary tasks. See
+ * {@link Ext.util.TaskRunner} for supported methods and task config properties.
+ * <pre><code>
+// Start a simple clock task that updates a div once per second
+var task = {
+ run: function(){
+ Ext.fly('clock').update(new Date().format('g:i:s A'));
+ },
+ interval: 1000 //1 second
+}
+Ext.TaskManager.start(task);
+</code></pre>
+ * <p>See the {@link #start} method for details about how to configure a task object.</p>
+ * @singleton
+ */
+Ext.TaskManager = Ext.create('Ext.util.TaskRunner');
+/**
+ * @class Ext.is
+ *
+ * Determines information about the current platform the application is running on.
+ *
+ * @singleton
+ */
+Ext.is = {
+ init : function(navigator) {
+ var platforms = this.platforms,
+ ln = platforms.length,
+ i, platform;
+
+ navigator = navigator || window.navigator;
+
+ for (i = 0; i < ln; i++) {
+ platform = platforms[i];
+ this[platform.identity] = platform.regex.test(navigator[platform.property]);
+ }
+
+ /**
+ * @property Desktop True if the browser is running on a desktop machine
+ * @type {Boolean}
+ */
+ this.Desktop = this.Mac || this.Windows || (this.Linux && !this.Android);
+ /**
+ * @property Tablet True if the browser is running on a tablet (iPad)
+ */
+ this.Tablet = this.iPad;
+ /**
+ * @property Phone True if the browser is running on a phone.
+ * @type {Boolean}
+ */
+ this.Phone = !this.Desktop && !this.Tablet;
+ /**
+ * @property iOS True if the browser is running on iOS
+ * @type {Boolean}
+ */
+ this.iOS = this.iPhone || this.iPad || this.iPod;
+
+ /**
+ * @property Standalone Detects when application has been saved to homescreen.
+ * @type {Boolean}
+ */
+ this.Standalone = !!window.navigator.standalone;
+ },
+
+ /**
+ * @property iPhone True when the browser is running on a iPhone
+ * @type {Boolean}
+ */
+ platforms: [{
+ property: 'platform',
+ regex: /iPhone/i,
+ identity: 'iPhone'
+ },
+
+ /**
+ * @property iPod True when the browser is running on a iPod
+ * @type {Boolean}
+ */
+ {
+ property: 'platform',
+ regex: /iPod/i,
+ identity: 'iPod'
+ },
+
+ /**
+ * @property iPad True when the browser is running on a iPad
+ * @type {Boolean}
+ */
+ {
+ property: 'userAgent',
+ regex: /iPad/i,
+ identity: 'iPad'
+ },
+
+ /**
+ * @property Blackberry True when the browser is running on a Blackberry
+ * @type {Boolean}
+ */
+ {
+ property: 'userAgent',
+ regex: /Blackberry/i,
+ identity: 'Blackberry'
+ },
+
+ /**
+ * @property Android True when the browser is running on an Android device
+ * @type {Boolean}
+ */
+ {
+ property: 'userAgent',
+ regex: /Android/i,
+ identity: 'Android'
+ },
+
+ /**
+ * @property Mac True when the browser is running on a Mac
+ * @type {Boolean}
+ */
+ {
+ property: 'platform',
+ regex: /Mac/i,
+ identity: 'Mac'
+ },
+
+ /**
+ * @property Windows True when the browser is running on Windows
+ * @type {Boolean}
+ */
+ {
+ property: 'platform',
+ regex: /Win/i,
+ identity: 'Windows'
+ },
+
+ /**
+ * @property Linux True when the browser is running on Linux
+ * @type {Boolean}
+ */
+ {
+ property: 'platform',
+ regex: /Linux/i,
+ identity: 'Linux'
+ }]
+};
+
+Ext.is.init();
+
+/**
+ * @class Ext.supports
+ *
+ * Determines information about features are supported in the current environment
+ *
+ * @singleton
+ */
+Ext.supports = {
+ init : function() {
+ var doc = document,
+ div = doc.createElement('div'),
+ tests = this.tests,
+ ln = tests.length,
+ i, test;
+
+ div.innerHTML = [
+ '<div style="height:30px;width:50px;">',
+ '<div style="height:20px;width:20px;"></div>',
+ '</div>',
+ '<div style="width: 200px; height: 200px; position: relative; padding: 5px;">',
+ '<div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></div>',
+ '</div>',
+ '<div style="float:left; background-color:transparent;"></div>'
+ ].join('');
+
+ doc.body.appendChild(div);
+
+ for (i = 0; i < ln; i++) {
+ test = tests[i];
+ this[test.identity] = test.fn.call(this, doc, div);
+ }
+
+ doc.body.removeChild(div);
+ },
+
+ /**
+ * @property CSS3BoxShadow True if document environment supports the CSS3 box-shadow style.
+ * @type {Boolean}
+ */
+ CSS3BoxShadow: Ext.isDefined(document.documentElement.style.boxShadow),
+
+ /**
+ * @property ClassList True if document environment supports the HTML5 classList API.
+ * @type {Boolean}
+ */
+ ClassList: !!document.documentElement.classList,
+
+ /**
+ * @property OrientationChange True if the device supports orientation change
+ * @type {Boolean}
+ */
+ OrientationChange: ((typeof window.orientation != 'undefined') && ('onorientationchange' in window)),
+
+ /**
+ * @property DeviceMotion True if the device supports device motion (acceleration and rotation rate)
+ * @type {Boolean}
+ */
+ DeviceMotion: ('ondevicemotion' in window),
+
+ /**
+ * @property Touch True if the device supports touch
+ * @type {Boolean}
+ */
+ // is.Desktop is needed due to the bug in Chrome 5.0.375, Safari 3.1.2
+ // and Safari 4.0 (they all have 'ontouchstart' in the window object).
+ Touch: ('ontouchstart' in window) && (!Ext.is.Desktop),
+
+ tests: [
+ /**
+ * @property Transitions True if the device supports CSS3 Transitions
+ * @type {Boolean}
+ */
+ {
+ identity: 'Transitions',
+ fn: function(doc, div) {
+ var prefix = [
+ 'webkit',
+ 'Moz',
+ 'o',
+ 'ms',
+ 'khtml'
+ ],
+ TE = 'TransitionEnd',
+ transitionEndName = [
+ prefix[0] + TE,
+ 'transitionend', //Moz bucks the prefixing convention
+ prefix[2] + TE,
+ prefix[3] + TE,
+ prefix[4] + TE
+ ],
+ ln = prefix.length,
+ i = 0,
+ out = false;
+ div = Ext.get(div);
+ for (; i < ln; i++) {
+ if (div.getStyle(prefix[i] + "TransitionProperty")) {
+ Ext.supports.CSS3Prefix = prefix[i];
+ Ext.supports.CSS3TransitionEnd = transitionEndName[i];
+ out = true;
+ break;
+ }
+ }
+ return out;
+ }
+ },
+
+ /**
+ * @property RightMargin True if the device supports right margin.
+ * See https://bugs.webkit.org/show_bug.cgi?id=13343 for why this is needed.
+ * @type {Boolean}
+ */
+ {
+ identity: 'RightMargin',
+ fn: function(doc, div) {
+ var view = doc.defaultView;
+ return !(view && view.getComputedStyle(div.firstChild.firstChild, null).marginRight != '0px');
+ }
+ },
+
+ /**
+ * @property DisplayChangeInputSelectionBug True if INPUT elements lose their
+ * selection when their display style is changed. Essentially, if a text input
+ * has focus and its display style is changed, the I-beam disappears.
+ *
+ * This bug is encountered due to the work around in place for the {@link #RightMargin}
+ * bug. This has been observed in Safari 4.0.4 and older, and appears to be fixed
+ * in Safari 5. It's not clear if Safari 4.1 has the bug, but it has the same WebKit
+ * version number as Safari 5 (according to http://unixpapa.com/js/gecko.html).
+ */
+ {
+ identity: 'DisplayChangeInputSelectionBug',
+ fn: function() {
+ var webKitVersion = Ext.webKitVersion;
+ // WebKit but older than Safari 5 or Chrome 6:
+ return 0 < webKitVersion && webKitVersion < 533;
+ }
+ },
+
+ /**
+ * @property DisplayChangeTextAreaSelectionBug True if TEXTAREA elements lose their
+ * selection when their display style is changed. Essentially, if a text area has
+ * focus and its display style is changed, the I-beam disappears.
+ *
+ * This bug is encountered due to the work around in place for the {@link #RightMargin}
+ * bug. This has been observed in Chrome 10 and Safari 5 and older, and appears to
+ * be fixed in Chrome 11.
+ */
+ {
+ identity: 'DisplayChangeTextAreaSelectionBug',
+ fn: function() {
+ var webKitVersion = Ext.webKitVersion;
+
+ /*
+ Has bug w/textarea:
+
+ (Chrome) Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-US)
+ AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.127
+ Safari/534.16
+ (Safari) Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_7; en-us)
+ AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5
+ Safari/533.21.1
+
+ No bug:
+
+ (Chrome) Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_7)
+ AppleWebKit/534.24 (KHTML, like Gecko) Chrome/11.0.696.57
+ Safari/534.24
+ */
+ return 0 < webKitVersion && webKitVersion < 534.24;
+ }
+ },
+
+ /**
+ * @property TransparentColor True if the device supports transparent color
+ * @type {Boolean}
+ */
+ {
+ identity: 'TransparentColor',
+ fn: function(doc, div, view) {
+ view = doc.defaultView;
+ return !(view && view.getComputedStyle(div.lastChild, null).backgroundColor != 'transparent');
+ }
+ },
+
+ /**
+ * @property ComputedStyle True if the browser supports document.defaultView.getComputedStyle()
+ * @type {Boolean}
+ */
+ {
+ identity: 'ComputedStyle',
+ fn: function(doc, div, view) {
+ view = doc.defaultView;
+ return view && view.getComputedStyle;
+ }
+ },
+
+ /**
+ * @property SVG True if the device supports SVG
+ * @type {Boolean}
+ */
+ {
+ identity: 'Svg',
+ fn: function(doc) {
+ return !!doc.createElementNS && !!doc.createElementNS( "http:/" + "/www.w3.org/2000/svg", "svg").createSVGRect;
+ }
+ },
+
+ /**
+ * @property Canvas True if the device supports Canvas
+ * @type {Boolean}
+ */
+ {
+ identity: 'Canvas',
+ fn: function(doc) {
+ return !!doc.createElement('canvas').getContext;
+ }
+ },
+
+ /**
+ * @property VML True if the device supports VML
+ * @type {Boolean}
+ */
+ {
+ identity: 'Vml',
+ fn: function(doc) {
+ var d = doc.createElement("div");
+ d.innerHTML = "<!--[if vml]><br><br><![endif]-->";
+ return (d.childNodes.length == 2);
+ }
+ },
+
+ /**
+ * @property Float True if the device supports CSS float
+ * @type {Boolean}
+ */
+ {
+ identity: 'Float',
+ fn: function(doc, div) {
+ return !!div.lastChild.style.cssFloat;
+ }
+ },
+
+ /**
+ * @property AudioTag True if the device supports the HTML5 audio tag
+ * @type {Boolean}
+ */
+ {
+ identity: 'AudioTag',
+ fn: function(doc) {
+ return !!doc.createElement('audio').canPlayType;
+ }
+ },
+
+ /**
+ * @property History True if the device supports HTML5 history
+ * @type {Boolean}
+ */
+ {
+ identity: 'History',
+ fn: function() {
+ return !!(window.history && history.pushState);
+ }
+ },
+
+ /**
+ * @property CSS3DTransform True if the device supports CSS3DTransform
+ * @type {Boolean}
+ */
+ {
+ identity: 'CSS3DTransform',
+ fn: function() {
+ return (typeof WebKitCSSMatrix != 'undefined' && new WebKitCSSMatrix().hasOwnProperty('m41'));
+ }
+ },
+
+ /**
+ * @property CSS3LinearGradient True if the device supports CSS3 linear gradients
+ * @type {Boolean}
+ */
+ {
+ identity: 'CSS3LinearGradient',
+ fn: function(doc, div) {
+ var property = 'background-image:',
+ webkit = '-webkit-gradient(linear, left top, right bottom, from(black), to(white))',
+ w3c = 'linear-gradient(left top, black, white)',
+ moz = '-moz-' + w3c,
+ options = [property + webkit, property + w3c, property + moz];
+
+ div.style.cssText = options.join(';');
+
+ return ("" + div.style.backgroundImage).indexOf('gradient') !== -1;
+ }
+ },
+
+ /**
+ * @property CSS3BorderRadius True if the device supports CSS3 border radius
+ * @type {Boolean}
+ */
+ {
+ identity: 'CSS3BorderRadius',
+ fn: function(doc, div) {
+ var domPrefixes = ['borderRadius', 'BorderRadius', 'MozBorderRadius', 'WebkitBorderRadius', 'OBorderRadius', 'KhtmlBorderRadius'],
+ pass = false,
+ i;
+ for (i = 0; i < domPrefixes.length; i++) {
+ if (document.body.style[domPrefixes[i]] !== undefined) {
+ return true;
+ }
+ }
+ return pass;
+ }
+ },
+
+ /**
+ * @property GeoLocation True if the device supports GeoLocation
+ * @type {Boolean}
+ */
+ {
+ identity: 'GeoLocation',
+ fn: function() {
+ return (typeof navigator != 'undefined' && typeof navigator.geolocation != 'undefined') || (typeof google != 'undefined' && typeof google.gears != 'undefined');
+ }
+ },
+ /**
+ * @property MouseEnterLeave True if the browser supports mouseenter and mouseleave events
+ * @type {Boolean}
+ */
+ {
+ identity: 'MouseEnterLeave',
+ fn: function(doc, div){
+ return ('onmouseenter' in div && 'onmouseleave' in div);
+ }
+ },
+ /**
+ * @property MouseWheel True if the browser supports the mousewheel event
+ * @type {Boolean}
+ */
+ {
+ identity: 'MouseWheel',
+ fn: function(doc, div) {
+ return ('onmousewheel' in div);
+ }
+ },
+ /**
+ * @property Opacity True if the browser supports normal css opacity
+ * @type {Boolean}
+ */
+ {
+ identity: 'Opacity',
+ fn: function(doc, div){
+ // Not a strict equal comparison in case opacity can be converted to a number.
+ if (Ext.isIE6 || Ext.isIE7 || Ext.isIE8) {
+ return false;
+ }
+ div.firstChild.style.cssText = 'opacity:0.73';
+ return div.firstChild.style.opacity == '0.73';
+ }
+ },
+ /**
+ * @property Placeholder True if the browser supports the HTML5 placeholder attribute on inputs
+ * @type {Boolean}
+ */
+ {
+ identity: 'Placeholder',
+ fn: function(doc) {
+ return 'placeholder' in doc.createElement('input');
+ }
+ },
+
+ /**
+ * @property Direct2DBug True if when asking for an element's dimension via offsetWidth or offsetHeight,
+ * getBoundingClientRect, etc. the browser returns the subpixel width rounded to the nearest pixel.
+ * @type {Boolean}
+ */
+ {
+ identity: 'Direct2DBug',
+ fn: function() {
+ return Ext.isString(document.body.style.msTransformOrigin);
+ }
+ },
+ /**
+ * @property BoundingClientRect True if the browser supports the getBoundingClientRect method on elements
+ * @type {Boolean}
+ */
+ {
+ identity: 'BoundingClientRect',
+ fn: function(doc, div) {
+ return Ext.isFunction(div.getBoundingClientRect);
+ }
+ },
+ {
+ identity: 'IncludePaddingInWidthCalculation',
+ fn: function(doc, div){
+ var el = Ext.get(div.childNodes[1].firstChild);
+ return el.getWidth() == 210;
+ }
+ },
+ {
+ identity: 'IncludePaddingInHeightCalculation',
+ fn: function(doc, div){
+ var el = Ext.get(div.childNodes[1].firstChild);
+ return el.getHeight() == 210;
+ }
+ },
+
+ /**
+ * @property ArraySort True if the Array sort native method isn't bugged.
+ * @type {Boolean}
+ */
+ {
+ identity: 'ArraySort',
+ fn: function() {
+ var a = [1,2,3,4,5].sort(function(){ return 0; });
+ return a[0] === 1 && a[1] === 2 && a[2] === 3 && a[3] === 4 && a[4] === 5;
+ }
+ },
+ /**
+ * @property Range True if browser support document.createRange native method.
+ * @type {Boolean}
+ */
+ {
+ identity: 'Range',
+ fn: function() {
+ return !!document.createRange;
+ }
+ },
+ /**
+ * @property CreateContextualFragment True if browser support CreateContextualFragment range native methods.
+ * @type {Boolean}
+ */
+ {
+ identity: 'CreateContextualFragment',
+ fn: function() {
+ var range = Ext.supports.Range ? document.createRange() : false;
+
+ return range && !!range.createContextualFragment;
+ }
+ },
+
+ /**
+ * @property WindowOnError True if browser supports window.onerror.
+ * @type {Boolean}
+ */
+ {
+ identity: 'WindowOnError',
+ fn: function () {
+ // sadly, we cannot feature detect this...
+ return Ext.isIE || Ext.isGecko || Ext.webKitVersion >= 534.16; // Chrome 10+
+ }
+ }
+ ]
+};
+
+
+
+/*
+
+This file is part of Ext JS 4
+
+Copyright (c) 2011 Sencha Inc
+
+Contact: http://www.sencha.com/contact
+
+Commercial Usage
+Licensees holding valid commercial licenses may use this file in accordance with the Commercial Software License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and Sencha.
+
+If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
+
+*/
+/**
+ * @class Ext.DomHelper
+ * @alternateClassName Ext.core.DomHelper
+ *
+ * <p>The DomHelper class provides a layer of abstraction from DOM and transparently supports creating
+ * elements via DOM or using HTML fragments. It also has the ability to create HTML fragment templates
+ * from your DOM building code.</p>
+ *
+ * <p><b><u>DomHelper element specification object</u></b></p>
+ * <p>A specification object is used when creating elements. Attributes of this object
+ * are assumed to be element attributes, except for 4 special attributes:
+ * <div class="mdetail-params"><ul>
+ * <li><b><tt>tag</tt></b> : <div class="sub-desc">The tag name of the element</div></li>
+ * <li><b><tt>children</tt></b> : or <tt>cn</tt><div class="sub-desc">An array of the
+ * same kind of element definition objects to be created and appended. These can be nested
+ * as deep as you want.</div></li>
+ * <li><b><tt>cls</tt></b> : <div class="sub-desc">The class attribute of the element.
+ * This will end up being either the "class" attribute on a HTML fragment or className
+ * for a DOM node, depending on whether DomHelper is using fragments or DOM.</div></li>
+ * <li><b><tt>html</tt></b> : <div class="sub-desc">The innerHTML for the element</div></li>
+ * </ul></div></p>
+ * <p><b>NOTE:</b> For other arbitrary attributes, the value will currently <b>not</b> be automatically
+ * HTML-escaped prior to building the element's HTML string. This means that if your attribute value
+ * contains special characters that would not normally be allowed in a double-quoted attribute value,
+ * you <b>must</b> manually HTML-encode it beforehand (see {@link Ext.String#htmlEncode}) or risk
+ * malformed HTML being created. This behavior may change in a future release.</p>
+ *
+ * <p><b><u>Insertion methods</u></b></p>
+ * <p>Commonly used insertion methods:
+ * <div class="mdetail-params"><ul>
+ * <li><tt>{@link #append}</tt> : <div class="sub-desc"></div></li>
+ * <li><tt>{@link #insertBefore}</tt> : <div class="sub-desc"></div></li>
+ * <li><tt>{@link #insertAfter}</tt> : <div class="sub-desc"></div></li>
+ * <li><tt>{@link #overwrite}</tt> : <div class="sub-desc"></div></li>
+ * <li><tt>{@link #createTemplate}</tt> : <div class="sub-desc"></div></li>
+ * <li><tt>{@link #insertHtml}</tt> : <div class="sub-desc"></div></li>
+ * </ul></div></p>
+ *
+ * <p><b><u>Example</u></b></p>
+ * <p>This is an example, where an unordered list with 3 children items is appended to an existing
+ * element with id <tt>'my-div'</tt>:<br>
+ <pre><code>
+var dh = Ext.DomHelper; // create shorthand alias
+// specification object
+var spec = {
+ id: 'my-ul',
+ tag: 'ul',
+ cls: 'my-list',
+ // append children after creating
+ children: [ // may also specify 'cn' instead of 'children'
+ {tag: 'li', id: 'item0', html: 'List Item 0'},
+ {tag: 'li', id: 'item1', html: 'List Item 1'},
+ {tag: 'li', id: 'item2', html: 'List Item 2'}
+ ]
+};
+var list = dh.append(
+ 'my-div', // the context element 'my-div' can either be the id or the actual node
+ spec // the specification object
+);
+ </code></pre></p>
+ * <p>Element creation specification parameters in this class may also be passed as an Array of
+ * specification objects. This can be used to insert multiple sibling nodes into an existing
+ * container very efficiently. For example, to add more list items to the example above:<pre><code>
+dh.append('my-ul', [
+ {tag: 'li', id: 'item3', html: 'List Item 3'},
+ {tag: 'li', id: 'item4', html: 'List Item 4'}
+]);
+ * </code></pre></p>
+ *
+ * <p><b><u>Templating</u></b></p>
+ * <p>The real power is in the built-in templating. Instead of creating or appending any elements,
+ * <tt>{@link #createTemplate}</tt> returns a Template object which can be used over and over to
+ * insert new elements. Revisiting the example above, we could utilize templating this time:
+ * <pre><code>
+// create the node
+var list = dh.append('my-div', {tag: 'ul', cls: 'my-list'});
+// get template
+var tpl = dh.createTemplate({tag: 'li', id: 'item{0}', html: 'List Item {0}'});
+
+for(var i = 0; i < 5, i++){
+ tpl.append(list, [i]); // use template to append to the actual node
+}
+ * </code></pre></p>
+ * <p>An example using a template:<pre><code>
+var html = '<a id="{0}" href="{1}" class="nav">{2}</a>';
+
+var tpl = new Ext.DomHelper.createTemplate(html);
+tpl.append('blog-roll', ['link1', 'http://www.edspencer.net/', "Ed's Site"]);
+tpl.append('blog-roll', ['link2', 'http://www.dustindiaz.com/', "Dustin's Site"]);
+ * </code></pre></p>
+ *
+ * <p>The same example using named parameters:<pre><code>
+var html = '<a id="{id}" href="{url}" class="nav">{text}</a>';
+
+var tpl = new Ext.DomHelper.createTemplate(html);
+tpl.append('blog-roll', {
+ id: 'link1',
+ url: 'http://www.edspencer.net/',
+ text: "Ed's Site"
+});
+tpl.append('blog-roll', {
+ id: 'link2',
+ url: 'http://www.dustindiaz.com/',
+ text: "Dustin's Site"
+});
+ * </code></pre></p>
+ *
+ * <p><b><u>Compiling Templates</u></b></p>
+ * <p>Templates are applied using regular expressions. The performance is great, but if
+ * you are adding a bunch of DOM elements using the same template, you can increase
+ * performance even further by {@link Ext.Template#compile "compiling"} the template.
+ * The way "{@link Ext.Template#compile compile()}" works is the template is parsed and
+ * broken up at the different variable points and a dynamic function is created and eval'ed.
+ * The generated function performs string concatenation of these parts and the passed
+ * variables instead of using regular expressions.
+ * <pre><code>
+var html = '<a id="{id}" href="{url}" class="nav">{text}</a>';
+
+var tpl = new Ext.DomHelper.createTemplate(html);
+tpl.compile();
+
+//... use template like normal
+ * </code></pre></p>
+ *
+ * <p><b><u>Performance Boost</u></b></p>
+ * <p>DomHelper will transparently create HTML fragments when it can. Using HTML fragments instead
+ * of DOM can significantly boost performance.</p>
+ * <p>Element creation specification parameters may also be strings. If {@link #useDom} is <tt>false</tt>,
+ * then the string is used as innerHTML. If {@link #useDom} is <tt>true</tt>, a string specification
+ * results in the creation of a text node. Usage:</p>
+ * <pre><code>
+Ext.DomHelper.useDom = true; // force it to use DOM; reduces performance
+ * </code></pre>
+ * @singleton
+ */
+Ext.ns('Ext.core');
+Ext.core.DomHelper = Ext.DomHelper = function(){
+ var tempTableEl = null,
+ emptyTags = /^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i,
+ tableRe = /^table|tbody|tr|td$/i,
+ confRe = /tag|children|cn|html$/i,
+ tableElRe = /td|tr|tbody/i,
+ endRe = /end/i,
+ pub,
+ // kill repeat to save bytes
+ afterbegin = 'afterbegin',
+ afterend = 'afterend',
+ beforebegin = 'beforebegin',
+ beforeend = 'beforeend',
+ ts = '<table>',
+ te = '</table>',
+ tbs = ts+'<tbody>',
+ tbe = '</tbody>'+te,
+ trs = tbs + '<tr>',
+ tre = '</tr>'+tbe;
+
+ // private
+ function doInsert(el, o, returnElement, pos, sibling, append){
+ el = Ext.getDom(el);
+ var newNode;
+ if (pub.useDom) {
+ newNode = createDom(o, null);
+ if (append) {
+ el.appendChild(newNode);
+ } else {
+ (sibling == 'firstChild' ? el : el.parentNode).insertBefore(newNode, el[sibling] || el);
+ }
+ } else {
+ newNode = Ext.DomHelper.insertHtml(pos, el, Ext.DomHelper.createHtml(o));
+ }
+ return returnElement ? Ext.get(newNode, true) : newNode;
+ }
+
+ function createDom(o, parentNode){
+ var el,
+ doc = document,
+ useSet,
+ attr,
+ val,
+ cn;
+
+ if (Ext.isArray(o)) { // Allow Arrays of siblings to be inserted
+ el = doc.createDocumentFragment(); // in one shot using a DocumentFragment
+ for (var i = 0, l = o.length; i < l; i++) {
+ createDom(o[i], el);
+ }
+ } else if (typeof o == 'string') { // Allow a string as a child spec.
+ el = doc.createTextNode(o);
+ } else {
+ el = doc.createElement( o.tag || 'div' );
+ useSet = !!el.setAttribute; // In IE some elements don't have setAttribute
+ for (attr in o) {
+ if(!confRe.test(attr)){
+ val = o[attr];
+ if(attr == 'cls'){
+ el.className = val;
+ }else{
+ if(useSet){
+ el.setAttribute(attr, val);
+ }else{
+ el[attr] = val;
+ }
+ }
+ }
+ }
+ Ext.DomHelper.applyStyles(el, o.style);
+
+ if ((cn = o.children || o.cn)) {
+ createDom(cn, el);
+ } else if (o.html) {
+ el.innerHTML = o.html;
+ }
+ }
+ if(parentNode){
+ parentNode.appendChild(el);
+ }
+ return el;
+ }
+
+ // build as innerHTML where available
+ function createHtml(o){
+ var b = '',
+ attr,
+ val,
+ key,
+ cn,
+ i;
+
+ if(typeof o == "string"){
+ b = o;
+ } else if (Ext.isArray(o)) {
+ for (i=0; i < o.length; i++) {
+ if(o[i]) {
+ b += createHtml(o[i]);
+ }
+ }
+ } else {
+ b += '<' + (o.tag = o.tag || 'div');
+ for (attr in o) {
+ val = o[attr];
+ if(!confRe.test(attr)){
+ if (typeof val == "object") {
+ b += ' ' + attr + '="';
+ for (key in val) {
+ b += key + ':' + val[key] + ';';
+ }
+ b += '"';
+ }else{
+ b += ' ' + ({cls : 'class', htmlFor : 'for'}[attr] || attr) + '="' + val + '"';
+ }
+ }
+ }
+ // Now either just close the tag or try to add children and close the tag.
+ if (emptyTags.test(o.tag)) {
+ b += '/>';
+ } else {
+ b += '>';
+ if ((cn = o.children || o.cn)) {
+ b += createHtml(cn);
+ } else if(o.html){
+ b += o.html;
+ }
+ b += '</' + o.tag + '>';
+ }
+ }
+ return b;
+ }
+
+ function ieTable(depth, s, h, e){
+ tempTableEl.innerHTML = [s, h, e].join('');
+ var i = -1,
+ el = tempTableEl,
+ ns;
+ while(++i < depth){
+ el = el.firstChild;
+ }
+// If the result is multiple siblings, then encapsulate them into one fragment.
+ ns = el.nextSibling;
+ if (ns){
+ var df = document.createDocumentFragment();
+ while(el){
+ ns = el.nextSibling;
+ df.appendChild(el);
+ el = ns;
+ }
+ el = df;
+ }
+ return el;
+ }
+
+ /**
+ * @ignore
+ * Nasty code for IE's broken table implementation
+ */
+ function insertIntoTable(tag, where, el, html) {
+ var node,
+ before;
+
+ tempTableEl = tempTableEl || document.createElement('div');
+
+ if(tag == 'td' && (where == afterbegin || where == beforeend) ||
+ !tableElRe.test(tag) && (where == beforebegin || where == afterend)) {
+ return null;
+ }
+ before = where == beforebegin ? el :
+ where == afterend ? el.nextSibling :
+ where == afterbegin ? el.firstChild : null;
+
+ if (where == beforebegin || where == afterend) {
+ el = el.parentNode;
+ }
+
+ if (tag == 'td' || (tag == 'tr' && (where == beforeend || where == afterbegin))) {
+ node = ieTable(4, trs, html, tre);
+ } else if ((tag == 'tbody' && (where == beforeend || where == afterbegin)) ||
+ (tag == 'tr' && (where == beforebegin || where == afterend))) {
+ node = ieTable(3, tbs, html, tbe);
+ } else {
+ node = ieTable(2, ts, html, te);
+ }
+ el.insertBefore(node, before);
+ return node;
+ }
+
+ /**
+ * @ignore
+ * Fix for IE9 createContextualFragment missing method
+ */
+ function createContextualFragment(html){
+ var div = document.createElement("div"),
+ fragment = document.createDocumentFragment(),
+ i = 0,
+ length, childNodes;
+
+ div.innerHTML = html;
+ childNodes = div.childNodes;
+ length = childNodes.length;
+
+ for (; i < length; i++) {
+ fragment.appendChild(childNodes[i].cloneNode(true));
+ }
+
+ return fragment;
+ }
+
+ pub = {
+ /**
+ * Returns the markup for the passed Element(s) config.
+ * @param {Object} o The DOM object spec (and children)
+ * @return {String}
+ */
+ markup : function(o){
+ return createHtml(o);
+ },
+
+ /**
+ * Applies a style specification to an element.
+ * @param {String/HTMLElement} el The element to apply styles to
+ * @param {String/Object/Function} styles A style specification string e.g. 'width:100px', or object in the form {width:'100px'}, or
+ * a function which returns such a specification.
+ */
+ applyStyles : function(el, styles){
+ if (styles) {
+ el = Ext.fly(el);
+ if (typeof styles == "function") {
+ styles = styles.call();
+ }
+ if (typeof styles == "string") {
+ styles = Ext.Element.parseStyles(styles);
+ }
+ if (typeof styles == "object") {
+ el.setStyle(styles);
+ }
+ }
+ },
+
+ /**
+ * Inserts an HTML fragment into the DOM.
+ * @param {String} where Where to insert the html in relation to el - beforeBegin, afterBegin, beforeEnd, afterEnd.
+ *
+ * For example take the following HTML: `<div>Contents</div>`
+ *
+ * Using different `where` values inserts element to the following places:
+ *
+ * - beforeBegin: `<HERE><div>Contents</div>`
+ * - afterBegin: `<div><HERE>Contents</div>`
+ * - beforeEnd: `<div>Contents<HERE></div>`
+ * - afterEnd: `<div>Contents</div><HERE>`
+ *
+ * @param {HTMLElement/TextNode} el The context element
+ * @param {String} html The HTML fragment
+ * @return {HTMLElement} The new node
+ */
+ insertHtml : function(where, el, html){
+ var hash = {},
+ hashVal,
+ range,
+ rangeEl,
+ setStart,
+ frag,
+ rs;
+
+ where = where.toLowerCase();
+ // add these here because they are used in both branches of the condition.
+ hash[beforebegin] = ['BeforeBegin', 'previousSibling'];
+ hash[afterend] = ['AfterEnd', 'nextSibling'];
+
+ // if IE and context element is an HTMLElement
+ if (el.insertAdjacentHTML) {
+ if(tableRe.test(el.tagName) && (rs = insertIntoTable(el.tagName.toLowerCase(), where, el, html))){
+ return rs;
+ }
+
+ // add these two to the hash.
+ hash[afterbegin] = ['AfterBegin', 'firstChild'];
+ hash[beforeend] = ['BeforeEnd', 'lastChild'];
+ if ((hashVal = hash[where])) {
+ el.insertAdjacentHTML(hashVal[0], html);
+ return el[hashVal[1]];
+ }
+ // if (not IE and context element is an HTMLElement) or TextNode
+ } else {
+ // we cannot insert anything inside a textnode so...
+ if (Ext.isTextNode(el)) {
+ where = where === 'afterbegin' ? 'beforebegin' : where;
+ where = where === 'beforeend' ? 'afterend' : where;
+ }
+ range = Ext.supports.CreateContextualFragment ? el.ownerDocument.createRange() : undefined;
+ setStart = 'setStart' + (endRe.test(where) ? 'After' : 'Before');
+ if (hash[where]) {
+ if (range) {
+ range[setStart](el);
+ frag = range.createContextualFragment(html);
+ } else {
+ frag = createContextualFragment(html);
+ }
+ el.parentNode.insertBefore(frag, where == beforebegin ? el : el.nextSibling);
+ return el[(where == beforebegin ? 'previous' : 'next') + 'Sibling'];
+ } else {
+ rangeEl = (where == afterbegin ? 'first' : 'last') + 'Child';
+ if (el.firstChild) {
+ if (range) {
+ range[setStart](el[rangeEl]);
+ frag = range.createContextualFragment(html);
+ } else {
+ frag = createContextualFragment(html);
+ }
+
+ if(where == afterbegin){
+ el.insertBefore(frag, el.firstChild);
+ }else{
+ el.appendChild(frag);
+ }
+ } else {
+ el.innerHTML = html;
+ }
+ return el[rangeEl];
+ }
+ }
+ },
+
+ /**
+ * Creates new DOM element(s) and inserts them before el.
+ * @param {String/HTMLElement/Ext.Element} el The context element
+ * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
+ * @param {Boolean} returnElement (optional) true to return a Ext.Element
+ * @return {HTMLElement/Ext.Element} The new node
+ */
+ insertBefore : function(el, o, returnElement){
+ return doInsert(el, o, returnElement, beforebegin);
+ },
+
+ /**
+ * Creates new DOM element(s) and inserts them after el.
+ * @param {String/HTMLElement/Ext.Element} el The context element
+ * @param {Object} o The DOM object spec (and children)
+ * @param {Boolean} returnElement (optional) true to return a Ext.Element
+ * @return {HTMLElement/Ext.Element} The new node
+ */
+ insertAfter : function(el, o, returnElement){
+ return doInsert(el, o, returnElement, afterend, 'nextSibling');
+ },
+
+ /**
+ * Creates new DOM element(s) and inserts them as the first child of el.
+ * @param {String/HTMLElement/Ext.Element} el The context element
+ * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
+ * @param {Boolean} returnElement (optional) true to return a Ext.Element
+ * @return {HTMLElement/Ext.Element} The new node
+ */
+ insertFirst : function(el, o, returnElement){
+ return doInsert(el, o, returnElement, afterbegin, 'firstChild');
+ },
+
+ /**
+ * Creates new DOM element(s) and appends them to el.
+ * @param {String/HTMLElement/Ext.Element} el The context element
+ * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
+ * @param {Boolean} returnElement (optional) true to return a Ext.Element
+ * @return {HTMLElement/Ext.Element} The new node
+ */
+ append : function(el, o, returnElement){
+ return doInsert(el, o, returnElement, beforeend, '', true);
+ },
+
+ /**
+ * Creates new DOM element(s) and overwrites the contents of el with them.
+ * @param {String/HTMLElement/Ext.Element} el The context element
+ * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
+ * @param {Boolean} returnElement (optional) true to return a Ext.Element
+ * @return {HTMLElement/Ext.Element} The new node
+ */
+ overwrite : function(el, o, returnElement){
+ el = Ext.getDom(el);
+ el.innerHTML = createHtml(o);
+ return returnElement ? Ext.get(el.firstChild) : el.firstChild;
+ },
+
+ createHtml : createHtml,
+
+ /**
+ * Creates new DOM element(s) without inserting them to the document.
+ * @param {Object/String} o The DOM object spec (and children) or raw HTML blob
+ * @return {HTMLElement} The new uninserted node
+ * @method
+ */
+ createDom: createDom,
+
+ /** True to force the use of DOM instead of html fragments @type Boolean */
+ useDom : false,
+
+ /**
+ * Creates a new Ext.Template from the DOM object spec.
+ * @param {Object} o The DOM object spec (and children)
+ * @return {Ext.Template} The new template
+ */
+ createTemplate : function(o){
+ var html = Ext.DomHelper.createHtml(o);
+ return Ext.create('Ext.Template', html);
+ }
+ };
+ return pub;
+}();
+
+/*
+ * This is code is also distributed under MIT license for use
+ * with jQuery and prototype JavaScript libraries.
+ */
+/**
+ * @class Ext.DomQuery
+Provides high performance selector/xpath processing by compiling queries into reusable functions. New pseudo classes and matchers can be plugged. It works on HTML and XML documents (if a content node is passed in).
+<p>
+DomQuery supports most of the <a href="http://www.w3.org/TR/2005/WD-css3-selectors-20051215/#selectors">CSS3 selectors spec</a>, along with some custom selectors and basic XPath.</p>
+
+<p>
+All selectors, attribute filters and pseudos below can be combined infinitely in any order. For example "div.foo:nth-child(odd)[@foo=bar].bar:first" would be a perfectly valid selector. Node filters are processed in the order in which they appear, which allows you to optimize your queries for your document structure.
+</p>
+<h4>Element Selectors:</h4>
+<ul class="list">
+ <li> <b>*</b> any element</li>
+ <li> <b>E</b> an element with the tag E</li>
+ <li> <b>E F</b> All descendent elements of E that have the tag F</li>
+ <li> <b>E > F</b> or <b>E/F</b> all direct children elements of E that have the tag F</li>
+ <li> <b>E + F</b> all elements with the tag F that are immediately preceded by an element with the tag E</li>
+ <li> <b>E ~ F</b> all elements with the tag F that are preceded by a sibling element with the tag E</li>
+</ul>
+<h4>Attribute Selectors:</h4>
+<p>The use of @ and quotes are optional. For example, div[@foo='bar'] is also a valid attribute selector.</p>
+<ul class="list">
+ <li> <b>E[foo]</b> has an attribute "foo"</li>
+ <li> <b>E[foo=bar]</b> has an attribute "foo" that equals "bar"</li>
+ <li> <b>E[foo^=bar]</b> has an attribute "foo" that starts with "bar"</li>
+ <li> <b>E[foo$=bar]</b> has an attribute "foo" that ends with "bar"</li>
+ <li> <b>E[foo*=bar]</b> has an attribute "foo" that contains the substring "bar"</li>
+ <li> <b>E[foo%=2]</b> has an attribute "foo" that is evenly divisible by 2</li>
+ <li> <b>E[foo!=bar]</b> attribute "foo" does not equal "bar"</li>
+</ul>
+<h4>Pseudo Classes:</h4>
+<ul class="list">
+ <li> <b>E:first-child</b> E is the first child of its parent</li>
+ <li> <b>E:last-child</b> E is the last child of its parent</li>
+ <li> <b>E:nth-child(<i>n</i>)</b> E is the <i>n</i>th child of its parent (1 based as per the spec)</li>
+ <li> <b>E:nth-child(odd)</b> E is an odd child of its parent</li>
+ <li> <b>E:nth-child(even)</b> E is an even child of its parent</li>
+ <li> <b>E:only-child</b> E is the only child of its parent</li>
+ <li> <b>E:checked</b> E is an element that is has a checked attribute that is true (e.g. a radio or checkbox) </li>
+ <li> <b>E:first</b> the first E in the resultset</li>
+ <li> <b>E:last</b> the last E in the resultset</li>
+ <li> <b>E:nth(<i>n</i>)</b> the <i>n</i>th E in the resultset (1 based)</li>
+ <li> <b>E:odd</b> shortcut for :nth-child(odd)</li>
+ <li> <b>E:even</b> shortcut for :nth-child(even)</li>
+ <li> <b>E:contains(foo)</b> E's innerHTML contains the substring "foo"</li>
+ <li> <b>E:nodeValue(foo)</b> E contains a textNode with a nodeValue that equals "foo"</li>
+ <li> <b>E:not(S)</b> an E element that does not match simple selector S</li>
+ <li> <b>E:has(S)</b> an E element that has a descendent that matches simple selector S</li>
+ <li> <b>E:next(S)</b> an E element whose next sibling matches simple selector S</li>
+ <li> <b>E:prev(S)</b> an E element whose previous sibling matches simple selector S</li>
+ <li> <b>E:any(S1|S2|S2)</b> an E element which matches any of the simple selectors S1, S2 or S3//\\</li>
+</ul>
+<h4>CSS Value Selectors:</h4>
+<ul class="list">
+ <li> <b>E{display=none}</b> css value "display" that equals "none"</li>
+ <li> <b>E{display^=none}</b> css value "display" that starts with "none"</li>
+ <li> <b>E{display$=none}</b> css value "display" that ends with "none"</li>
+ <li> <b>E{display*=none}</b> css value "display" that contains the substring "none"</li>
+ <li> <b>E{display%=2}</b> css value "display" that is evenly divisible by 2</li>
+ <li> <b>E{display!=none}</b> css value "display" that does not equal "none"</li>
+</ul>
+ * @singleton
+ */
+Ext.ns('Ext.core');
+
+Ext.core.DomQuery = Ext.DomQuery = function(){
+ var cache = {},
+ simpleCache = {},
+ valueCache = {},
+ nonSpace = /\S/,
+ trimRe = /^\s+|\s+$/g,
+ tplRe = /\{(\d+)\}/g,
+ modeRe = /^(\s?[\/>+~]\s?|\s|$)/,
+ tagTokenRe = /^(#)?([\w-\*]+)/,
+ nthRe = /(\d*)n\+?(\d*)/,
+ nthRe2 = /\D/,
+ startIdRe = /^\s*\#/,
+ // This is for IE MSXML which does not support expandos.
+ // IE runs the same speed using setAttribute, however FF slows way down
+ // and Safari completely fails so they need to continue to use expandos.
+ isIE = window.ActiveXObject ? true : false,
+ key = 30803;
+
+ // this eval is stop the compressor from
+ // renaming the variable to something shorter
+ eval("var batch = 30803;");
+
+ // Retrieve the child node from a particular
+ // parent at the specified index.
+ function child(parent, index){
+ var i = 0,
+ n = parent.firstChild;
+ while(n){
+ if(n.nodeType == 1){
+ if(++i == index){
+ return n;
+ }
+ }
+ n = n.nextSibling;
+ }
+ return null;
+ }
+
+ // retrieve the next element node
+ function next(n){
+ while((n = n.nextSibling) && n.nodeType != 1);
+ return n;
+ }
+
+ // retrieve the previous element node
+ function prev(n){
+ while((n = n.previousSibling) && n.nodeType != 1);
+ return n;
+ }
+
+ // Mark each child node with a nodeIndex skipping and
+ // removing empty text nodes.
+ function children(parent){
+ var n = parent.firstChild,
+ nodeIndex = -1,
+ nextNode;
+ while(n){
+ nextNode = n.nextSibling;
+ // clean worthless empty nodes.
+ if(n.nodeType == 3 && !nonSpace.test(n.nodeValue)){
+ parent.removeChild(n);
+ }else{
+ // add an expando nodeIndex
+ n.nodeIndex = ++nodeIndex;
+ }
+ n = nextNode;
+ }
+ return this;
+ }
+
+
+ // nodeSet - array of nodes
+ // cls - CSS Class
+ function byClassName(nodeSet, cls){
+ if(!cls){
+ return nodeSet;
+ }
+ var result = [], ri = -1;
+ for(var i = 0, ci; ci = nodeSet[i]; i++){
+ if((' '+ci.className+' ').indexOf(cls) != -1){
+ result[++ri] = ci;
+ }
+ }
+ return result;
+ };
+
+ function attrValue(n, attr){
+ // if its an array, use the first node.
+ if(!n.tagName && typeof n.length != "undefined"){
+ n = n[0];
+ }
+ if(!n){
+ return null;
+ }
+
+ if(attr == "for"){
+ return n.htmlFor;
+ }
+ if(attr == "class" || attr == "className"){
+ return n.className;
+ }
+ return n.getAttribute(attr) || n[attr];
+
+ };
+
+
+ // ns - nodes
+ // mode - false, /, >, +, ~
+ // tagName - defaults to "*"
+ function getNodes(ns, mode, tagName){
+ var result = [], ri = -1, cs;
+ if(!ns){
+ return result;
+ }
+ tagName = tagName || "*";
+ // convert to array
+ if(typeof ns.getElementsByTagName != "undefined"){
+ ns = [ns];
+ }
+
+ // no mode specified, grab all elements by tagName
+ // at any depth
+ if(!mode){
+ for(var i = 0, ni; ni = ns[i]; i++){
+ cs = ni.getElementsByTagName(tagName);
+ for(var j = 0, ci; ci = cs[j]; j++){
+ result[++ri] = ci;
+ }
+ }
+ // Direct Child mode (/ or >)
+ // E > F or E/F all direct children elements of E that have the tag
+ } else if(mode == "/" || mode == ">"){
+ var utag = tagName.toUpperCase();
+ for(var i = 0, ni, cn; ni = ns[i]; i++){
+ cn = ni.childNodes;
+ for(var j = 0, cj; cj = cn[j]; j++){
+ if(cj.nodeName == utag || cj.nodeName == tagName || tagName == '*'){
+ result[++ri] = cj;
+ }
+ }
+ }
+ // Immediately Preceding mode (+)
+ // E + F all elements with the tag F that are immediately preceded by an element with the tag E
+ }else if(mode == "+"){
+ var utag = tagName.toUpperCase();
+ for(var i = 0, n; n = ns[i]; i++){
+ while((n = n.nextSibling) && n.nodeType != 1);
+ if(n && (n.nodeName == utag || n.nodeName == tagName || tagName == '*')){
+ result[++ri] = n;
+ }
+ }
+ // Sibling mode (~)
+ // E ~ F all elements with the tag F that are preceded by a sibling element with the tag E
+ }else if(mode == "~"){
+ var utag = tagName.toUpperCase();
+ for(var i = 0, n; n = ns[i]; i++){
+ while((n = n.nextSibling)){
+ if (n.nodeName == utag || n.nodeName == tagName || tagName == '*'){
+ result[++ri] = n;
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ function concat(a, b){
+ if(b.slice){
+ return a.concat(b);
+ }
+ for(var i = 0, l = b.length; i < l; i++){
+ a[a.length] = b[i];
+ }
+ return a;
+ }
+
+ function byTag(cs, tagName){
+ if(cs.tagName || cs == document){
+ cs = [cs];
+ }
+ if(!tagName){
+ return cs;
+ }
+ var result = [], ri = -1;
+ tagName = tagName.toLowerCase();
+ for(var i = 0, ci; ci = cs[i]; i++){
+ if(ci.nodeType == 1 && ci.tagName.toLowerCase() == tagName){
+ result[++ri] = ci;
+ }
+ }
+ return result;
+ }
+
+ function byId(cs, id){
+ if(cs.tagName || cs == document){
+ cs = [cs];
+ }
+ if(!id){
+ return cs;
+ }
+ var result = [], ri = -1;
+ for(var i = 0, ci; ci = cs[i]; i++){
+ if(ci && ci.id == id){
+ result[++ri] = ci;
+ return result;
+ }
+ }
+ return result;
+ }
+
+ // operators are =, !=, ^=, $=, *=, %=, |= and ~=
+ // custom can be "{"
+ function byAttribute(cs, attr, value, op, custom){
+ var result = [],
+ ri = -1,
+ useGetStyle = custom == "{",
+ fn = Ext.DomQuery.operators[op],
+ a,
+ xml,
+ hasXml;
+
+ for(var i = 0, ci; ci = cs[i]; i++){
+ // skip non-element nodes.
+ if(ci.nodeType != 1){
+ continue;
+ }
+ // only need to do this for the first node
+ if(!hasXml){
+ xml = Ext.DomQuery.isXml(ci);
+ hasXml = true;
+ }
+
+ // we only need to change the property names if we're dealing with html nodes, not XML
+ if(!xml){
+ if(useGetStyle){
+ a = Ext.DomQuery.getStyle(ci, attr);
+ } else if (attr == "class" || attr == "className"){
+ a = ci.className;
+ } else if (attr == "for"){
+ a = ci.htmlFor;
+ } else if (attr == "href"){
+ // getAttribute href bug
+ // http://www.glennjones.net/Post/809/getAttributehrefbug.htm
+ a = ci.getAttribute("href", 2);
+ } else{
+ a = ci.getAttribute(attr);
+ }
+ }else{
+ a = ci.getAttribute(attr);
+ }
+ if((fn && fn(a, value)) || (!fn && a)){
+ result[++ri] = ci;
+ }
+ }
+ return result;
+ }
+
+ function byPseudo(cs, name, value){
+ return Ext.DomQuery.pseudos[name](cs, value);
+ }
+
+ function nodupIEXml(cs){
+ var d = ++key,
+ r;
+ cs[0].setAttribute("_nodup", d);
+ r = [cs[0]];
+ for(var i = 1, len = cs.length; i < len; i++){
+ var c = cs[i];
+ if(!c.getAttribute("_nodup") != d){
+ c.setAttribute("_nodup", d);
+ r[r.length] = c;
+ }
+ }
+ for(var i = 0, len = cs.length; i < len; i++){
+ cs[i].removeAttribute("_nodup");
+ }
+ return r;
+ }
+
+ function nodup(cs){
+ if(!cs){
+ return [];
+ }
+ var len = cs.length, c, i, r = cs, cj, ri = -1;
+ if(!len || typeof cs.nodeType != "undefined" || len == 1){
+ return cs;
+ }
+ if(isIE && typeof cs[0].selectSingleNode != "undefined"){
+ return nodupIEXml(cs);
+ }
+ var d = ++key;
+ cs[0]._nodup = d;
+ for(i = 1; c = cs[i]; i++){
+ if(c._nodup != d){
+ c._nodup = d;
+ }else{
+ r = [];
+ for(var j = 0; j < i; j++){
+ r[++ri] = cs[j];
+ }
+ for(j = i+1; cj = cs[j]; j++){
+ if(cj._nodup != d){
+ cj._nodup = d;
+ r[++ri] = cj;
+ }
+ }
+ return r;
+ }
+ }
+ return r;
+ }
+
+ function quickDiffIEXml(c1, c2){
+ var d = ++key,
+ r = [];
+ for(var i = 0, len = c1.length; i < len; i++){
+ c1[i].setAttribute("_qdiff", d);
+ }
+ for(var i = 0, len = c2.length; i < len; i++){
+ if(c2[i].getAttribute("_qdiff") != d){
+ r[r.length] = c2[i];
+ }
+ }
+ for(var i = 0, len = c1.length; i < len; i++){
+ c1[i].removeAttribute("_qdiff");
+ }
+ return r;
+ }
+
+ function quickDiff(c1, c2){
+ var len1 = c1.length,
+ d = ++key,
+ r = [];
+ if(!len1){
+ return c2;
+ }
+ if(isIE && typeof c1[0].selectSingleNode != "undefined"){
+ return quickDiffIEXml(c1, c2);
+ }
+ for(var i = 0; i < len1; i++){
+ c1[i]._qdiff = d;
+ }
+ for(var i = 0, len = c2.length; i < len; i++){
+ if(c2[i]._qdiff != d){
+ r[r.length] = c2[i];
+ }
+ }
+ return r;
+ }
+
+ function quickId(ns, mode, root, id){
+ if(ns == root){
+ var d = root.ownerDocument || root;
+ return d.getElementById(id);
+ }
+ ns = getNodes(ns, mode, "*");
+ return byId(ns, id);
+ }
+
+ return {
+ getStyle : function(el, name){
+ return Ext.fly(el).getStyle(name);
+ },
+ /**
+ * Compiles a selector/xpath query into a reusable function. The returned function
+ * takes one parameter "root" (optional), which is the context node from where the query should start.
+ * @param {String} selector The selector/xpath query
+ * @param {String} type (optional) Either "select" (the default) or "simple" for a simple selector match
+ * @return {Function}
+ */
+ compile : function(path, type){
+ type = type || "select";
+
+ // setup fn preamble
+ var fn = ["var f = function(root){\n var mode; ++batch; var n = root || document;\n"],
+ mode,
+ lastPath,
+ matchers = Ext.DomQuery.matchers,
+ matchersLn = matchers.length,
+ modeMatch,
+ // accept leading mode switch
+ lmode = path.match(modeRe);
+
+ if(lmode && lmode[1]){
+ fn[fn.length] = 'mode="'+lmode[1].replace(trimRe, "")+'";';
+ path = path.replace(lmode[1], "");
+ }
+
+ // strip leading slashes
+ while(path.substr(0, 1)=="/"){
+ path = path.substr(1);
+ }
+
+ while(path && lastPath != path){
+ lastPath = path;
+ var tokenMatch = path.match(tagTokenRe);
+ if(type == "select"){
+ if(tokenMatch){
+ // ID Selector
+ if(tokenMatch[1] == "#"){
+ fn[fn.length] = 'n = quickId(n, mode, root, "'+tokenMatch[2]+'");';
+ }else{
+ fn[fn.length] = 'n = getNodes(n, mode, "'+tokenMatch[2]+'");';
+ }
+ path = path.replace(tokenMatch[0], "");
+ }else if(path.substr(0, 1) != '@'){
+ fn[fn.length] = 'n = getNodes(n, mode, "*");';
+ }
+ // type of "simple"
+ }else{
+ if(tokenMatch){
+ if(tokenMatch[1] == "#"){
+ fn[fn.length] = 'n = byId(n, "'+tokenMatch[2]+'");';
+ }else{
+ fn[fn.length] = 'n = byTag(n, "'+tokenMatch[2]+'");';
+ }
+ path = path.replace(tokenMatch[0], "");
+ }
+ }
+ while(!(modeMatch = path.match(modeRe))){
+ var matched = false;
+ for(var j = 0; j < matchersLn; j++){
+ var t = matchers[j];
+ var m = path.match(t.re);
+ if(m){
+ fn[fn.length] = t.select.replace(tplRe, function(x, i){
+ return m[i];
+ });
+ path = path.replace(m[0], "");
+ matched = true;
+ break;
+ }
+ }
+ // prevent infinite loop on bad selector
+ if(!matched){
+ }
+ }
+ if(modeMatch[1]){
+ fn[fn.length] = 'mode="'+modeMatch[1].replace(trimRe, "")+'";';
+ path = path.replace(modeMatch[1], "");
+ }
+ }
+ // close fn out
+ fn[fn.length] = "return nodup(n);\n}";
+
+ // eval fn and return it
+ eval(fn.join(""));
+ return f;
+ },
+
+ /**
+ * Selects an array of DOM nodes using JavaScript-only implementation.
+ *
+ * Use {@link #select} to take advantage of browsers built-in support for CSS selectors.
+ *
+ * @param {String} selector The selector/xpath query (can be a comma separated list of selectors)
+ * @param {HTMLElement/String} root (optional) The start of the query (defaults to document).
+ * @return {HTMLElement[]} An Array of DOM elements which match the selector. If there are
+ * no matches, and empty Array is returned.
+ */
+ jsSelect: function(path, root, type){
+ // set root to doc if not specified.
+ root = root || document;
+
+ if(typeof root == "string"){
+ root = document.getElementById(root);
+ }
+ var paths = path.split(","),
+ results = [];
+
+ // loop over each selector
+ for(var i = 0, len = paths.length; i < len; i++){
+ var subPath = paths[i].replace(trimRe, "");
+ // compile and place in cache
+ if(!cache[subPath]){
+ cache[subPath] = Ext.DomQuery.compile(subPath);
+ if(!cache[subPath]){
+ }
+ }
+ var result = cache[subPath](root);
+ if(result && result != document){
+ results = results.concat(result);
+ }
+ }
+
+ // if there were multiple selectors, make sure dups
+ // are eliminated
+ if(paths.length > 1){
+ return nodup(results);
+ }
+ return results;
+ },
+
+ isXml: function(el) {
+ var docEl = (el ? el.ownerDocument || el : 0).documentElement;
+ return docEl ? docEl.nodeName !== "HTML" : false;
+ },
+
+ /**
+ * Selects an array of DOM nodes by CSS/XPath selector.
+ *
+ * Uses [document.querySelectorAll][0] if browser supports that, otherwise falls back to
+ * {@link Ext.DomQuery#jsSelect} to do the work.
+ *
+ * Aliased as {@link Ext#query}.
+ *
+ * [0]: https://developer.mozilla.org/en/DOM/document.querySelectorAll
+ *
+ * @param {String} path The selector/xpath query
+ * @param {HTMLElement} root (optional) The start of the query (defaults to document).
+ * @return {HTMLElement[]} An array of DOM elements (not a NodeList as returned by `querySelectorAll`).
+ * Empty array when no matches.
+ * @method
+ */
+ select : document.querySelectorAll ? function(path, root, type) {
+ root = root || document;
+ /*
+ * Safari 3.x can't handle uppercase or unicode characters when in quirks mode.
+ */
+ if (!Ext.DomQuery.isXml(root) && !(Ext.isSafari3 && !Ext.isStrict)) {
+ try {
+ /*
+ * This checking here is to "fix" the behaviour of querySelectorAll
+ * for non root document queries. The way qsa works is intentional,
+ * however it's definitely not the expected way it should work.
+ * More info: http://ejohn.org/blog/thoughts-on-queryselectorall/
+ *
+ * We only modify the path for single selectors (ie, no multiples),
+ * without a full parser it makes it difficult to do this correctly.
+ */
+ var isDocumentRoot = root.nodeType === 9,
+ _path = path,
+ _root = root;
+
+ if (!isDocumentRoot && path.indexOf(',') === -1 && !startIdRe.test(path)) {
+ _path = '#' + Ext.id(root) + ' ' + path;
+ _root = root.parentNode;
+ }
+ return Ext.Array.toArray(_root.querySelectorAll(_path));
+ }
+ catch (e) {
+ }
+ }
+ return Ext.DomQuery.jsSelect.call(this, path, root, type);
+ } : function(path, root, type) {
+ return Ext.DomQuery.jsSelect.call(this, path, root, type);
+ },
+
+ /**
+ * Selects a single element.
+ * @param {String} selector The selector/xpath query
+ * @param {HTMLElement} root (optional) The start of the query (defaults to document).
+ * @return {HTMLElement} The DOM element which matched the selector.
+ */
+ selectNode : function(path, root){
+ return Ext.DomQuery.select(path, root)[0];
+ },
+
+ /**
+ * Selects the value of a node, optionally replacing null with the defaultValue.
+ * @param {String} selector The selector/xpath query
+ * @param {HTMLElement} root (optional) The start of the query (defaults to document).
+ * @param {String} defaultValue (optional) When specified, this is return as empty value.
+ * @return {String}
+ */
+ selectValue : function(path, root, defaultValue){
+ path = path.replace(trimRe, "");
+ if(!valueCache[path]){
+ valueCache[path] = Ext.DomQuery.compile(path, "select");
+ }
+ var n = valueCache[path](root), v;
+ n = n[0] ? n[0] : n;
+
+ // overcome a limitation of maximum textnode size
+ // Rumored to potentially crash IE6 but has not been confirmed.
+ // http://reference.sitepoint.com/javascript/Node/normalize
+ // https://developer.mozilla.org/En/DOM/Node.normalize
+ if (typeof n.normalize == 'function') n.normalize();
+
+ v = (n && n.firstChild ? n.firstChild.nodeValue : null);
+ return ((v === null||v === undefined||v==='') ? defaultValue : v);
+ },
+
+ /**
+ * Selects the value of a node, parsing integers and floats. Returns the defaultValue, or 0 if none is specified.
+ * @param {String} selector The selector/xpath query
+ * @param {HTMLElement} root (optional) The start of the query (defaults to document).
+ * @param {Number} defaultValue (optional) When specified, this is return as empty value.
+ * @return {Number}
+ */
+ selectNumber : function(path, root, defaultValue){
+ var v = Ext.DomQuery.selectValue(path, root, defaultValue || 0);
+ return parseFloat(v);
+ },
+
+ /**
+ * Returns true if the passed element(s) match the passed simple selector (e.g. div.some-class or span:first-child)
+ * @param {String/HTMLElement/HTMLElement[]} el An element id, element or array of elements
+ * @param {String} selector The simple selector to test
+ * @return {Boolean}
+ */
+ is : function(el, ss){
+ if(typeof el == "string"){
+ el = document.getElementById(el);
+ }
+ var isArray = Ext.isArray(el),
+ result = Ext.DomQuery.filter(isArray ? el : [el], ss);
+ return isArray ? (result.length == el.length) : (result.length > 0);
+ },
+
+ /**
+ * Filters an array of elements to only include matches of a simple selector (e.g. div.some-class or span:first-child)
+ * @param {HTMLElement[]} el An array of elements to filter
+ * @param {String} selector The simple selector to test
+ * @param {Boolean} nonMatches If true, it returns the elements that DON'T match
+ * the selector instead of the ones that match
+ * @return {HTMLElement[]} An Array of DOM elements which match the selector. If there are
+ * no matches, and empty Array is returned.
+ */
+ filter : function(els, ss, nonMatches){
+ ss = ss.replace(trimRe, "");
+ if(!simpleCache[ss]){
+ simpleCache[ss] = Ext.DomQuery.compile(ss, "simple");
+ }
+ var result = simpleCache[ss](els);
+ return nonMatches ? quickDiff(result, els) : result;
+ },
+
+ /**
+ * Collection of matching regular expressions and code snippets.
+ * Each capture group within () will be replace the {} in the select
+ * statement as specified by their index.
+ */
+ matchers : [{
+ re: /^\.([\w-]+)/,
+ select: 'n = byClassName(n, " {1} ");'
+ }, {
+ re: /^\:([\w-]+)(?:\(((?:[^\s>\/]*|.*?))\))?/,
+ select: 'n = byPseudo(n, "{1}", "{2}");'
+ },{
+ re: /^(?:([\[\{])(?:@)?([\w-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]\}])/,
+ select: 'n = byAttribute(n, "{2}", "{4}", "{3}", "{1}");'
+ }, {
+ re: /^#([\w-]+)/,
+ select: 'n = byId(n, "{1}");'
+ },{
+ re: /^@([\w-]+)/,
+ select: 'return {firstChild:{nodeValue:attrValue(n, "{1}")}};'
+ }
+ ],
+
+ /**
+ * Collection of operator comparison functions. The default operators are =, !=, ^=, $=, *=, %=, |= and ~=.
+ * New operators can be added as long as the match the format <i>c</i>= where <i>c</i> is any character other than space, > <.
+ */
+ operators : {
+ "=" : function(a, v){
+ return a == v;
+ },
+ "!=" : function(a, v){
+ return a != v;
+ },
+ "^=" : function(a, v){
+ return a && a.substr(0, v.length) == v;
+ },
+ "$=" : function(a, v){
+ return a && a.substr(a.length-v.length) == v;
+ },
+ "*=" : function(a, v){
+ return a && a.indexOf(v) !== -1;
+ },
+ "%=" : function(a, v){
+ return (a % v) == 0;
+ },
+ "|=" : function(a, v){
+ return a && (a == v || a.substr(0, v.length+1) == v+'-');
+ },
+ "~=" : function(a, v){
+ return a && (' '+a+' ').indexOf(' '+v+' ') != -1;
+ }
+ },
+
+ /**
+Object hash of "pseudo class" filter functions which are used when filtering selections.
+Each function is passed two parameters:
+
+- **c** : Array
+ An Array of DOM elements to filter.
+
+- **v** : String
+ The argument (if any) supplied in the selector.
+
+A filter function returns an Array of DOM elements which conform to the pseudo class.
+In addition to the provided pseudo classes listed above such as `first-child` and `nth-child`,
+developers may add additional, custom psuedo class filters to select elements according to application-specific requirements.
+
+For example, to filter `a` elements to only return links to __external__ resources:
+
+ Ext.DomQuery.pseudos.external = function(c, v){
+ var r = [], ri = -1;
+ for(var i = 0, ci; ci = c[i]; i++){
+ // Include in result set only if it's a link to an external resource
+ if(ci.hostname != location.hostname){
+ r[++ri] = ci;
+ }
+ }
+ return r;
+ };
+
+Then external links could be gathered with the following statement:
+
+ var externalLinks = Ext.select("a:external");
+
+ * @markdown
+ */
+ pseudos : {
+ "first-child" : function(c){
+ var r = [], ri = -1, n;
+ for(var i = 0, ci; ci = n = c[i]; i++){
+ while((n = n.previousSibling) && n.nodeType != 1);
+ if(!n){
+ r[++ri] = ci;
+ }
+ }
+ return r;
+ },
+
+ "last-child" : function(c){
+ var r = [], ri = -1, n;
+ for(var i = 0, ci; ci = n = c[i]; i++){
+ while((n = n.nextSibling) && n.nodeType != 1);
+ if(!n){
+ r[++ri] = ci;
+ }
+ }
+ return r;
+ },
+
+ "nth-child" : function(c, a) {
+ var r = [], ri = -1,
+ m = nthRe.exec(a == "even" && "2n" || a == "odd" && "2n+1" || !nthRe2.test(a) && "n+" + a || a),
+ f = (m[1] || 1) - 0, l = m[2] - 0;
+ for(var i = 0, n; n = c[i]; i++){
+ var pn = n.parentNode;
+ if (batch != pn._batch) {
+ var j = 0;
+ for(var cn = pn.firstChild; cn; cn = cn.nextSibling){
+ if(cn.nodeType == 1){
+ cn.nodeIndex = ++j;
+ }
+ }
+ pn._batch = batch;
+ }
+ if (f == 1) {
+ if (l == 0 || n.nodeIndex == l){
+ r[++ri] = n;
+ }
+ } else if ((n.nodeIndex + l) % f == 0){
+ r[++ri] = n;
+ }
+ }
+
+ return r;
+ },
+
+ "only-child" : function(c){
+ var r = [], ri = -1;;
+ for(var i = 0, ci; ci = c[i]; i++){
+ if(!prev(ci) && !next(ci)){
+ r[++ri] = ci;
+ }
+ }
+ return r;
+ },
+
+ "empty" : function(c){
+ var r = [], ri = -1;
+ for(var i = 0, ci; ci = c[i]; i++){
+ var cns = ci.childNodes, j = 0, cn, empty = true;
+ while(cn = cns[j]){
+ ++j;
+ if(cn.nodeType == 1 || cn.nodeType == 3){
+ empty = false;
+ break;
+ }
+ }
+ if(empty){
+ r[++ri] = ci;
+ }
+ }
+ return r;
+ },
+
+ "contains" : function(c, v){
+ var r = [], ri = -1;
+ for(var i = 0, ci; ci = c[i]; i++){
+ if((ci.textContent||ci.innerText||'').indexOf(v) != -1){
+ r[++ri] = ci;
+ }
+ }
+ return r;
+ },
+
+ "nodeValue" : function(c, v){
+ var r = [], ri = -1;
+ for(var i = 0, ci; ci = c[i]; i++){
+ if(ci.firstChild && ci.firstChild.nodeValue == v){
+ r[++ri] = ci;
+ }
+ }
+ return r;
+ },
+
+ "checked" : function(c){
+ var r = [], ri = -1;
+ for(var i = 0, ci; ci = c[i]; i++){
+ if(ci.checked == true){
+ r[++ri] = ci;
+ }
+ }
+ return r;
+ },
+
+ "not" : function(c, ss){
+ return Ext.DomQuery.filter(c, ss, true);
+ },
+
+ "any" : function(c, selectors){
+ var ss = selectors.split('|'),
+ r = [], ri = -1, s;
+ for(var i = 0, ci; ci = c[i]; i++){
+ for(var j = 0; s = ss[j]; j++){
+ if(Ext.DomQuery.is(ci, s)){
+ r[++ri] = ci;
+ break;
+ }
+ }
+ }
+ return r;
+ },
+
+ "odd" : function(c){
+ return this["nth-child"](c, "odd");
+ },
+
+ "even" : function(c){
+ return this["nth-child"](c, "even");
+ },
+
+ "nth" : function(c, a){
+ return c[a-1] || [];
+ },
+
+ "first" : function(c){
+ return c[0] || [];
+ },
+
+ "last" : function(c){
+ return c[c.length-1] || [];
+ },
+
+ "has" : function(c, ss){
+ var s = Ext.DomQuery.select,
+ r = [], ri = -1;
+ for(var i = 0, ci; ci = c[i]; i++){
+ if(s(ss, ci).length > 0){
+ r[++ri] = ci;
+ }
+ }
+ return r;
+ },
+
+ "next" : function(c, ss){
+ var is = Ext.DomQuery.is,
+ r = [], ri = -1;
+ for(var i = 0, ci; ci = c[i]; i++){
+ var n = next(ci);
+ if(n && is(n, ss)){
+ r[++ri] = ci;
+ }
+ }
+ return r;
+ },
+
+ "prev" : function(c, ss){
+ var is = Ext.DomQuery.is,
+ r = [], ri = -1;
+ for(var i = 0, ci; ci = c[i]; i++){
+ var n = prev(ci);
+ if(n && is(n, ss)){
+ r[++ri] = ci;
+ }
+ }
+ return r;
+ }
+ }
+ };
+}();
+
+/**
+ * Shorthand of {@link Ext.DomQuery#select}
+ * @member Ext
+ * @method query
+ * @alias Ext.DomQuery#select
+ */
+Ext.query = Ext.DomQuery.select;
+
+/**
+ * @class Ext.Element
+ * @alternateClassName Ext.core.Element
+ *
+ * Encapsulates a DOM element, adding simple DOM manipulation facilities, normalizing for browser differences.
+ *
+ * All instances of this class inherit the methods of {@link Ext.fx.Anim} making visual effects easily available to all
+ * DOM elements.
+ *
+ * Note that the events documented in this class are not Ext events, they encapsulate browser events. Some older browsers
+ * may not support the full range of events. Which events are supported is beyond the control of Ext JS.
+ *
+ * Usage:
+ *
+ * // by id
+ * var el = Ext.get("my-div");
+ *
+ * // by DOM element reference
+ * var el = Ext.get(myDivElement);
+ *
+ * # Animations
+ *
+ * When an element is manipulated, by default there is no animation.
+ *
+ * var el = Ext.get("my-div");
+ *
+ * // no animation
+ * el.setWidth(100);
+ *
+ * Many of the functions for manipulating an element have an optional "animate" parameter. This parameter can be
+ * specified as boolean (true) for default animation effects.
+ *
+ * // default animation
+ * el.setWidth(100, true);
+ *
+ * To configure the effects, an object literal with animation options to use as the Element animation configuration
+ * object can also be specified. Note that the supported Element animation configuration options are a subset of the
+ * {@link Ext.fx.Anim} animation options specific to Fx effects. The supported Element animation configuration options
+ * are:
+ *
+ * Option Default Description
+ * --------- -------- ---------------------------------------------
+ * {@link Ext.fx.Anim#duration duration} .35 The duration of the animation in seconds
+ * {@link Ext.fx.Anim#easing easing} easeOut The easing method
+ * {@link Ext.fx.Anim#callback callback} none A function to execute when the anim completes
+ * {@link Ext.fx.Anim#scope scope} this The scope (this) of the callback function
+ *
+ * Usage:
+ *
+ * // Element animation options object
+ * var opt = {
+ * {@link Ext.fx.Anim#duration duration}: 1,
+ * {@link Ext.fx.Anim#easing easing}: 'elasticIn',
+ * {@link Ext.fx.Anim#callback callback}: this.foo,
+ * {@link Ext.fx.Anim#scope scope}: this
+ * };
+ * // animation with some options set
+ * el.setWidth(100, opt);
+ *
+ * The Element animation object being used for the animation will be set on the options object as "anim", which allows
+ * you to stop or manipulate the animation. Here is an example:
+ *
+ * // using the "anim" property to get the Anim object
+ * if(opt.anim.isAnimated()){
+ * opt.anim.stop();
+ * }
+ *
+ * # Composite (Collections of) Elements
+ *
+ * For working with collections of Elements, see {@link Ext.CompositeElement}
+ *
+ * @constructor
+ * Creates new Element directly.
+ * @param {String/HTMLElement} element
+ * @param {Boolean} forceNew (optional) By default the constructor checks to see if there is already an instance of this
+ * element in the cache and if there is it returns the same instance. This will skip that check (useful for extending
+ * this class).
+ * @return {Object}
+ */
+ (function() {
+ var DOC = document,
+ EC = Ext.cache;
+
+ Ext.Element = Ext.core.Element = function(element, forceNew) {
+ var dom = typeof element == "string" ? DOC.getElementById(element) : element,
+ id;
+
+ if (!dom) {
+ return null;
+ }
+
+ id = dom.id;
+
+ if (!forceNew && id && EC[id]) {
+ // element object already exists
+ return EC[id].el;
+ }
+
+ /**
+ * @property {HTMLElement} dom
+ * The DOM element
+ */
+ this.dom = dom;
+
+ /**
+ * @property {String} id
+ * The DOM element ID
+ */
+ this.id = id || Ext.id(dom);
+ };
+
+ var DH = Ext.DomHelper,
+ El = Ext.Element;
+
+
+ El.prototype = {
+ /**
+ * Sets the passed attributes as attributes of this element (a style attribute can be a string, object or function)
+ * @param {Object} o The object with the attributes
+ * @param {Boolean} useSet (optional) false to override the default setAttribute to use expandos.
+ * @return {Ext.Element} this
+ */
+ set: function(o, useSet) {
+ var el = this.dom,
+ attr,
+ val;
+ useSet = (useSet !== false) && !!el.setAttribute;
+
+ for (attr in o) {
+ if (o.hasOwnProperty(attr)) {
+ val = o[attr];
+ if (attr == 'style') {
+ DH.applyStyles(el, val);
+ } else if (attr == 'cls') {
+ el.className = val;
+ } else if (useSet) {
+ el.setAttribute(attr, val);
+ } else {
+ el[attr] = val;
+ }
+ }
+ }
+ return this;
+ },
+
+ // Mouse events
+ /**
+ * @event click
+ * Fires when a mouse click is detected within the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event contextmenu
+ * Fires when a right click is detected within the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event dblclick
+ * Fires when a mouse double click is detected within the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event mousedown
+ * Fires when a mousedown is detected within the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event mouseup
+ * Fires when a mouseup is detected within the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event mouseover
+ * Fires when a mouseover is detected within the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event mousemove
+ * Fires when a mousemove is detected with the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event mouseout
+ * Fires when a mouseout is detected with the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event mouseenter
+ * Fires when the mouse enters the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event mouseleave
+ * Fires when the mouse leaves the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+
+ // Keyboard events
+ /**
+ * @event keypress
+ * Fires when a keypress is detected within the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event keydown
+ * Fires when a keydown is detected within the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event keyup
+ * Fires when a keyup is detected within the element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+
+
+ // HTML frame/object events
+ /**
+ * @event load
+ * Fires when the user agent finishes loading all content within the element. Only supported by window, frames,
+ * objects and images.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event unload
+ * Fires when the user agent removes all content from a window or frame. For elements, it fires when the target
+ * element or any of its content has been removed.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event abort
+ * Fires when an object/image is stopped from loading before completely loaded.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event error
+ * Fires when an object/image/frame cannot be loaded properly.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event resize
+ * Fires when a document view is resized.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event scroll
+ * Fires when a document view is scrolled.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+
+ // Form events
+ /**
+ * @event select
+ * Fires when a user selects some text in a text field, including input and textarea.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event change
+ * Fires when a control loses the input focus and its value has been modified since gaining focus.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event submit
+ * Fires when a form is submitted.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event reset
+ * Fires when a form is reset.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event focus
+ * Fires when an element receives focus either via the pointing device or by tab navigation.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event blur
+ * Fires when an element loses focus either via the pointing device or by tabbing navigation.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+
+ // User Interface events
+ /**
+ * @event DOMFocusIn
+ * Where supported. Similar to HTML focus event, but can be applied to any focusable element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event DOMFocusOut
+ * Where supported. Similar to HTML blur event, but can be applied to any focusable element.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event DOMActivate
+ * Where supported. Fires when an element is activated, for instance, through a mouse click or a keypress.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+
+ // DOM Mutation events
+ /**
+ * @event DOMSubtreeModified
+ * Where supported. Fires when the subtree is modified.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event DOMNodeInserted
+ * Where supported. Fires when a node has been added as a child of another node.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event DOMNodeRemoved
+ * Where supported. Fires when a descendant node of the element is removed.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event DOMNodeRemovedFromDocument
+ * Where supported. Fires when a node is being removed from a document.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event DOMNodeInsertedIntoDocument
+ * Where supported. Fires when a node is being inserted into a document.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event DOMAttrModified
+ * Where supported. Fires when an attribute has been modified.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+ /**
+ * @event DOMCharacterDataModified
+ * Where supported. Fires when the character data has been modified.
+ * @param {Ext.EventObject} e The {@link Ext.EventObject} encapsulating the DOM event.
+ * @param {HTMLElement} t The target of the event.
+ */
+
+ /**
+ * @property {String} defaultUnit
+ * The default unit to append to CSS values where a unit isn't provided.
+ */
+ defaultUnit: "px",
+
+ /**
+ * Returns true if this element matches the passed simple selector (e.g. div.some-class or span:first-child)
+ * @param {String} selector The simple selector to test
+ * @return {Boolean} True if this element matches the selector, else false
+ */
+ is: function(simpleSelector) {
+ return Ext.DomQuery.is(this.dom, simpleSelector);
+ },
+
+ /**
+ * Tries to focus the element. Any exceptions are caught and ignored.
+ * @param {Number} defer (optional) Milliseconds to defer the focus
+ * @return {Ext.Element} this
+ */
+ focus: function(defer,
+ /* private */
+ dom) {
+ var me = this;
+ dom = dom || me.dom;
+ try {
+ if (Number(defer)) {
+ Ext.defer(me.focus, defer, null, [null, dom]);
+ } else {
+ dom.focus();
+ }
+ } catch(e) {}
+ return me;
+ },
+
+ /**
+ * Tries to blur the element. Any exceptions are caught and ignored.
+ * @return {Ext.Element} this
+ */
+ blur: function() {
+ try {
+ this.dom.blur();
+ } catch(e) {}
+ return this;
+ },
+
+ /**
+ * Returns the value of the "value" attribute
+ * @param {Boolean} asNumber true to parse the value as a number
+ * @return {String/Number}
+ */
+ getValue: function(asNumber) {
+ var val = this.dom.value;
+ return asNumber ? parseInt(val, 10) : val;
+ },
+
+ /**
+ * Appends an event handler to this element.
+ *
+ * @param {String} eventName The name of event to handle.
+ *
+ * @param {Function} fn The handler function the event invokes. This function is passed the following parameters:
+ *
+ * - **evt** : EventObject
+ *
+ * The {@link Ext.EventObject EventObject} describing the event.
+ *
+ * - **el** : HtmlElement
+ *
+ * The DOM element which was the target of the event. Note that this may be filtered by using the delegate option.
+ *
+ * - **o** : Object
+ *
+ * The options object from the addListener call.
+ *
+ * @param {Object} scope (optional) The scope (**this** reference) in which the handler function is executed. **If
+ * omitted, defaults to this Element.**
+ *
+ * @param {Object} options (optional) An object containing handler configuration properties. This may contain any of
+ * the following properties:
+ *
+ * - **scope** Object :
+ *
+ * The scope (**this** reference) in which the handler function is executed. **If omitted, defaults to this
+ * Element.**
+ *
+ * - **delegate** String:
+ *
+ * A simple selector to filter the target or look for a descendant of the target. See below for additional details.
+ *
+ * - **stopEvent** Boolean:
+ *
+ * True to stop the event. That is stop propagation, and prevent the default action.
+ *
+ * - **preventDefault** Boolean:
+ *
+ * True to prevent the default action
+ *
+ * - **stopPropagation** Boolean:
+ *
+ * True to prevent event propagation
+ *
+ * - **normalized** Boolean:
+ *
+ * False to pass a browser event to the handler function instead of an Ext.EventObject
+ *
+ * - **target** Ext.Element:
+ *
+ * Only call the handler if the event was fired on the target Element, _not_ if the event was bubbled up from a
+ * child node.
+ *
+ * - **delay** Number:
+ *
+ * The number of milliseconds to delay the invocation of the handler after the event fires.
+ *
+ * - **single** Boolean:
+ *
+ * True to add a handler to handle just the next firing of the event, and then remove itself.
+ *
+ * - **buffer** Number:
+ *
+ * Causes the handler to be scheduled to run in an {@link Ext.util.DelayedTask} delayed by the specified number of
+ * milliseconds. If the event fires again within that time, the original handler is _not_ invoked, but the new
+ * handler is scheduled in its place.
+ *
+ * **Combining Options**
+ *
+ * In the following examples, the shorthand form {@link #on} is used rather than the more verbose addListener. The
+ * two are equivalent. Using the options argument, it is possible to combine different types of listeners:
+ *
+ * A delayed, one-time listener that auto stops the event and adds a custom argument (forumId) to the options
+ * object. The options object is available as the third parameter in the handler function.
+ *
+ * Code:
+ *
+ * el.on('click', this.onClick, this, {
+ * single: true,
+ * delay: 100,
+ * stopEvent : true,
+ * forumId: 4
+ * });
+ *
+ * **Attaching multiple handlers in 1 call**
+ *
+ * The method also allows for a single argument to be passed which is a config object containing properties which
+ * specify multiple handlers.
+ *
+ * Code:
+ *
+ * el.on({
+ * 'click' : {
+ * fn: this.onClick,
+ * scope: this,
+ * delay: 100
+ * },
+ * 'mouseover' : {
+ * fn: this.onMouseOver,
+ * scope: this
+ * },
+ * 'mouseout' : {
+ * fn: this.onMouseOut,
+ * scope: this
+ * }
+ * });
+ *
+ * Or a shorthand syntax:
+ *
+ * Code:
+ *
+ * el.on({
+ * 'click' : this.onClick,
+ * 'mouseover' : this.onMouseOver,
+ * 'mouseout' : this.onMouseOut,
+ * scope: this
+ * });
+ *
+ * **delegate**
+ *
+ * This is a configuration option that you can pass along when registering a handler for an event to assist with
+ * event delegation. Event delegation is a technique that is used to reduce memory consumption and prevent exposure
+ * to memory-leaks. By registering an event for a container element as opposed to each element within a container.
+ * By setting this configuration option to a simple selector, the target element will be filtered to look for a
+ * descendant of the target. For example:
+ *
+ * // using this markup:
+ * <div id='elId'>
+ * <p id='p1'>paragraph one</p>
+ * <p id='p2' class='clickable'>paragraph two</p>
+ * <p id='p3'>paragraph three</p>
+ * </div>
+ *
+ * // utilize event delegation to registering just one handler on the container element:
+ * el = Ext.get('elId');
+ * el.on(
+ * 'click',
+ * function(e,t) {
+ * // handle click
+ * console.info(t.id); // 'p2'
+ * },
+ * this,
+ * {
+ * // filter the target element to be a descendant with the class 'clickable'
+ * delegate: '.clickable'
+ * }
+ * );
+ *
+ * @return {Ext.Element} this
+ */
+ addListener: function(eventName, fn, scope, options) {
+ Ext.EventManager.on(this.dom, eventName, fn, scope || this, options);
+ return this;
+ },
+
+ /**
+ * Removes an event handler from this element.
+ *
+ * **Note**: if a *scope* was explicitly specified when {@link #addListener adding} the listener,
+ * the same scope must be specified here.
+ *
+ * Example:
+ *
+ * el.removeListener('click', this.handlerFn);
+ * // or
+ * el.un('click', this.handlerFn);
+ *
+ * @param {String} eventName The name of the event from which to remove the handler.
+ * @param {Function} fn The handler function to remove. **This must be a reference to the function passed into the
+ * {@link #addListener} call.**
+ * @param {Object} scope If a scope (**this** reference) was specified when the listener was added, then this must
+ * refer to the same object.
+ * @return {Ext.Element} this
+ */
+ removeListener: function(eventName, fn, scope) {
+ Ext.EventManager.un(this.dom, eventName, fn, scope || this);
+ return this;
+ },
+
+ /**
+ * Removes all previous added listeners from this element
+ * @return {Ext.Element} this
+ */
+ removeAllListeners: function() {
+ Ext.EventManager.removeAll(this.dom);
+ return this;
+ },
+
+ /**
+ * Recursively removes all previous added listeners from this element and its children
+ * @return {Ext.Element} this
+ */
+ purgeAllListeners: function() {
+ Ext.EventManager.purgeElement(this);
+ return this;
+ },
+
+ /**
+ * Test if size has a unit, otherwise appends the passed unit string, or the default for this Element.
+ * @param size {Mixed} The size to set
+ * @param units {String} The units to append to a numeric size value
+ * @private
+ */
+ addUnits: function(size, units) {
+
+ // Most common case first: Size is set to a number
+ if (Ext.isNumber(size)) {
+ return size + (units || this.defaultUnit || 'px');
+ }
+
+ // Size set to a value which means "auto"
+ if (size === "" || size == "auto" || size == null) {
+ return size || '';
+ }
+
+ // Otherwise, warn if it's not a valid CSS measurement
+ if (!unitPattern.test(size)) {
+ return size || '';
+ }
+ return size;
+ },
+
+ /**
+ * Tests various css rules/browsers to determine if this element uses a border box
+ * @return {Boolean}
+ */
+ isBorderBox: function() {
+ return Ext.isBorderBox || noBoxAdjust[(this.dom.tagName || "").toLowerCase()];
+ },
+
+ /**
+ * Removes this element's dom reference. Note that event and cache removal is handled at {@link Ext#removeNode
+ * Ext.removeNode}
+ */
+ remove: function() {
+ var me = this,
+ dom = me.dom;
+
+ if (dom) {
+ delete me.dom;
+ Ext.removeNode(dom);
+ }
+ },
+
+ /**
+ * Sets up event handlers to call the passed functions when the mouse is moved into and out of the Element.
+ * @param {Function} overFn The function to call when the mouse enters the Element.
+ * @param {Function} outFn The function to call when the mouse leaves the Element.
+ * @param {Object} scope (optional) The scope (`this` reference) in which the functions are executed. Defaults
+ * to the Element's DOM element.
+ * @param {Object} options (optional) Options for the listener. See {@link Ext.util.Observable#addListener the
+ * options parameter}.
+ * @return {Ext.Element} this
+ */
+ hover: function(overFn, outFn, scope, options) {
+ var me = this;
+ me.on('mouseenter', overFn, scope || me.dom, options);
+ me.on('mouseleave', outFn, scope || me.dom, options);
+ return me;
+ },
+
+ /**
+ * Returns true if this element is an ancestor of the passed element
+ * @param {HTMLElement/String} el The element to check
+ * @return {Boolean} True if this element is an ancestor of el, else false
+ */
+ contains: function(el) {
+ return ! el ? false: Ext.Element.isAncestor(this.dom, el.dom ? el.dom: el);
+ },
+
+ /**
+ * Returns the value of a namespaced attribute from the element's underlying DOM node.
+ * @param {String} namespace The namespace in which to look for the attribute
+ * @param {String} name The attribute name
+ * @return {String} The attribute value
+ */
+ getAttributeNS: function(ns, name) {
+ return this.getAttribute(name, ns);
+ },
+
+ /**
+ * Returns the value of an attribute from the element's underlying DOM node.
+ * @param {String} name The attribute name
+ * @param {String} namespace (optional) The namespace in which to look for the attribute
+ * @return {String} The attribute value
+ * @method
+ */
+ getAttribute: (Ext.isIE && !(Ext.isIE9 && document.documentMode === 9)) ?
+ function(name, ns) {
+ var d = this.dom,
+ type;
+ if(ns) {
+ type = typeof d[ns + ":" + name];
+ if (type != 'undefined' && type != 'unknown') {
+ return d[ns + ":" + name] || null;
+ }
+ return null;
+ }
+ if (name === "for") {
+ name = "htmlFor";
+ }
+ return d[name] || null;
+ }: function(name, ns) {
+ var d = this.dom;
+ if (ns) {
+ return d.getAttributeNS(ns, name) || d.getAttribute(ns + ":" + name);
+ }
+ return d.getAttribute(name) || d[name] || null;
+ },
+
+ /**
+ * Update the innerHTML of this element
+ * @param {String} html The new HTML
+ * @return {Ext.Element} this
+ */
+ update: function(html) {
+ if (this.dom) {
+ this.dom.innerHTML = html;
+ }
+ return this;
+ }
+ };
+
+ var ep = El.prototype;
+
+ El.addMethods = function(o) {
+ Ext.apply(ep, o);
+ };
+
+ /**
+ * @method
+ * @alias Ext.Element#addListener
+ * Shorthand for {@link #addListener}.
+ */
+ ep.on = ep.addListener;
+
+ /**
+ * @method
+ * @alias Ext.Element#removeListener
+ * Shorthand for {@link #removeListener}.
+ */
+ ep.un = ep.removeListener;
+
+ /**
+ * @method
+ * @alias Ext.Element#removeAllListeners
+ * Alias for {@link #removeAllListeners}.
+ */
+ ep.clearListeners = ep.removeAllListeners;
+
+ /**
+ * @method destroy
+ * @member Ext.Element
+ * Removes this element's dom reference. Note that event and cache removal is handled at {@link Ext#removeNode
+ * Ext.removeNode}. Alias to {@link #remove}.
+ */
+ ep.destroy = ep.remove;
+
+ /**
+ * @property {Boolean} autoBoxAdjust
+ * true to automatically adjust width and height settings for box-model issues (default to true)
+ */
+ ep.autoBoxAdjust = true;
+
+ // private
+ var unitPattern = /\d+(px|em|%|en|ex|pt|in|cm|mm|pc)$/i,
+ docEl;
+
+ /**
+ * Retrieves Ext.Element objects. {@link Ext#get} is an alias for {@link Ext.Element#get}.
+ *
+ * **This method does not retrieve {@link Ext.Component Component}s.** This method retrieves Ext.Element
+ * objects which encapsulate DOM elements. To retrieve a Component by its ID, use {@link Ext.ComponentManager#get}.
+ *
+ * Uses simple caching to consistently return the same object. Automatically fixes if an object was recreated with
+ * the same id via AJAX or DOM.
+ *
+ * @param {String/HTMLElement/Ext.Element} el The id of the node, a DOM Node or an existing Element.
+ * @return {Ext.Element} The Element object (or null if no matching element was found)
+ * @static
+ */
+ El.get = function(el) {
+ var ex,
+ elm,
+ id;
+ if (!el) {
+ return null;
+ }
+ if (typeof el == "string") {
+ // element id
+ if (! (elm = DOC.getElementById(el))) {
+ return null;
+ }
+ if (EC[el] && EC[el].el) {
+ ex = EC[el].el;
+ ex.dom = elm;
+ } else {
+ ex = El.addToCache(new El(elm));
+ }
+ return ex;
+ } else if (el.tagName) {
+ // dom element
+ if (! (id = el.id)) {
+ id = Ext.id(el);
+ }
+ if (EC[id] && EC[id].el) {
+ ex = EC[id].el;
+ ex.dom = el;
+ } else {
+ ex = El.addToCache(new El(el));
+ }
+ return ex;
+ } else if (el instanceof El) {
+ if (el != docEl) {
+ // refresh dom element in case no longer valid,
+ // catch case where it hasn't been appended
+ // If an el instance is passed, don't pass to getElementById without some kind of id
+ if (Ext.isIE && (el.id == undefined || el.id == '')) {
+ el.dom = el.dom;
+ } else {
+ el.dom = DOC.getElementById(el.id) || el.dom;
+ }
+ }
+ return el;
+ } else if (el.isComposite) {
+ return el;
+ } else if (Ext.isArray(el)) {
+ return El.select(el);
+ } else if (el == DOC) {
+ // create a bogus element object representing the document object
+ if (!docEl) {
+ var f = function() {};
+ f.prototype = El.prototype;
+ docEl = new f();
+ docEl.dom = DOC;
+ }
+ return docEl;
+ }
+ return null;
+ };
+
+ /**
+ * Retrieves Ext.Element objects like {@link Ext#get} but is optimized for sub-elements.
+ * This is helpful for performance, because in IE (prior to IE 9), `getElementById` uses
+ * an non-optimized search. In those browsers, starting the search for an element with a
+ * matching ID at a parent of that element will greatly speed up the process.
+ *
+ * Unlike {@link Ext#get}, this method only accepts ID's. If the ID is not a child of
+ * this element, it will still be found if it exists in the document, but will be slower
+ * than calling {@link Ext#get} directly.
+ *
+ * @param {String} id The id of the element to get.
+ * @return {Ext.Element} The Element object (or null if no matching element was found)
+ * @member Ext.Element
+ * @method getById
+ * @markdown
+ */
+ ep.getById = (!Ext.isIE6 && !Ext.isIE7 && !Ext.isIE8) ? El.get :
+ function (id) {
+ var dom = this.dom,
+ cached, el, ret;
+
+ if (dom) {
+ el = dom.all[id];
+ if (el) {
+ // calling El.get here is a real hit (2x slower) because it has to
+ // redetermine that we are giving it a dom el.
+ cached = EC[id];
+ if (cached && cached.el) {
+ ret = cached.el;
+ ret.dom = el;
+ } else {
+ ret = El.addToCache(new El(el));
+ }
+ return ret;
+ }
+ }
+
+ return El.get(id);
+ };
+
+ El.addToCache = function(el, id) {
+ if (el) {
+ id = id || el.id;
+ EC[id] = {
+ el: el,
+ data: {},
+ events: {}
+ };
+ }
+ return el;
+ };
+
+ // private method for getting and setting element data
+ El.data = function(el, key, value) {
+ el = El.get(el);
+ if (!el) {
+ return null;
+ }
+ var c = EC[el.id].data;
+ if (arguments.length == 2) {
+ return c[key];
+ } else {
+ return (c[key] = value);
+ }
+ };
+
+ // private
+ // Garbage collection - uncache elements/purge listeners on orphaned elements
+ // so we don't hold a reference and cause the browser to retain them
+ function garbageCollect() {
+ if (!Ext.enableGarbageCollector) {
+ clearInterval(El.collectorThreadId);
+ } else {
+ var eid,
+ el,
+ d,
+ o;
+
+ for (eid in EC) {
+ if (!EC.hasOwnProperty(eid)) {
+ continue;
+ }
+ o = EC[eid];
+ if (o.skipGarbageCollection) {
+ continue;
+ }
+ el = o.el;
+ d = el.dom;
+ // -------------------------------------------------------
+ // Determining what is garbage:
+ // -------------------------------------------------------
+ // !d
+ // dom node is null, definitely garbage
+ // -------------------------------------------------------
+ // !d.parentNode
+ // no parentNode == direct orphan, definitely garbage
+ // -------------------------------------------------------
+ // !d.offsetParent && !document.getElementById(eid)
+ // display none elements have no offsetParent so we will
+ // also try to look it up by it's id. However, check
+ // offsetParent first so we don't do unneeded lookups.
+ // This enables collection of elements that are not orphans
+ // directly, but somewhere up the line they have an orphan
+ // parent.
+ // -------------------------------------------------------
+ if (!d || !d.parentNode || (!d.offsetParent && !DOC.getElementById(eid))) {
+ if (d && Ext.enableListenerCollection) {
+ Ext.EventManager.removeAll(d);
+ }
+ delete EC[eid];
+ }
+ }
+ // Cleanup IE Object leaks
+ if (Ext.isIE) {
+ var t = {};
+ for (eid in EC) {
+ if (!EC.hasOwnProperty(eid)) {
+ continue;
+ }
+ t[eid] = EC[eid];
+ }
+ EC = Ext.cache = t;
+ }
+ }
+ }
+ El.collectorThreadId = setInterval(garbageCollect, 30000);
+
+ var flyFn = function() {};
+ flyFn.prototype = El.prototype;
+
+ // dom is optional
+ El.Flyweight = function(dom) {
+ this.dom = dom;
+ };
+
+ El.Flyweight.prototype = new flyFn();
+ El.Flyweight.prototype.isFlyweight = true;
+ El._flyweights = {};
+
+ /**
+ * Gets the globally shared flyweight Element, with the passed node as the active element. Do not store a reference
+ * to this element - the dom node can be overwritten by other code. {@link Ext#fly} is alias for
+ * {@link Ext.Element#fly}.
+ *
+ * Use this to make one-time references to DOM elements which are not going to be accessed again either by
+ * application code, or by Ext's classes. If accessing an element which will be processed regularly, then {@link
+ * Ext#get Ext.get} will be more appropriate to take advantage of the caching provided by the Ext.Element
+ * class.
+ *
+ * @param {String/HTMLElement} el The dom node or id
+ * @param {String} named (optional) Allows for creation of named reusable flyweights to prevent conflicts (e.g.
+ * internally Ext uses "_global")
+ * @return {Ext.Element} The shared Element object (or null if no matching element was found)
+ * @static
+ */
+ El.fly = function(el, named) {
+ var ret = null;
+ named = named || '_global';
+ el = Ext.getDom(el);
+ if (el) {
+ (El._flyweights[named] = El._flyweights[named] || new El.Flyweight()).dom = el;
+ ret = El._flyweights[named];
+ }
+ return ret;
+ };
+
+ /**
+ * @member Ext
+ * @method get
+ * @alias Ext.Element#get
+ */
+ Ext.get = El.get;
+
+ /**
+ * @member Ext
+ * @method fly
+ * @alias Ext.Element#fly
+ */
+ Ext.fly = El.fly;
+
+ // speedy lookup for elements never to box adjust
+ var noBoxAdjust = Ext.isStrict ? {
+ select: 1
+ }: {
+ input: 1,
+ select: 1,
+ textarea: 1
+ };
+ if (Ext.isIE || Ext.isGecko) {
+ noBoxAdjust['button'] = 1;
+ }
+})();
+
+/**
+ * @class Ext.Element
+ */
+Ext.Element.addMethods({
+ /**
+ * Looks at this node and then at parent nodes for a match of the passed simple selector (e.g. div.some-class or span:first-child)
+ * @param {String} selector The simple selector to test
+ * @param {Number/String/HTMLElement/Ext.Element} maxDepth (optional)
+ * The max depth to search as a number or element (defaults to 50 || document.body)
+ * @param {Boolean} returnEl (optional) True to return a Ext.Element object instead of DOM node
+ * @return {HTMLElement} The matching DOM node (or null if no match was found)
+ */
+ findParent : function(simpleSelector, maxDepth, returnEl) {
+ var p = this.dom,
+ b = document.body,
+ depth = 0,
+ stopEl;
+
+ maxDepth = maxDepth || 50;
+ if (isNaN(maxDepth)) {
+ stopEl = Ext.getDom(maxDepth);
+ maxDepth = Number.MAX_VALUE;
+ }
+ while (p && p.nodeType == 1 && depth < maxDepth && p != b && p != stopEl) {
+ if (Ext.DomQuery.is(p, simpleSelector)) {
+ return returnEl ? Ext.get(p) : p;
+ }
+ depth++;
+ p = p.parentNode;
+ }
+ return null;
+ },
+
+ /**
+ * Looks at parent nodes for a match of the passed simple selector (e.g. div.some-class or span:first-child)
+ * @param {String} selector The simple selector to test
+ * @param {Number/String/HTMLElement/Ext.Element} maxDepth (optional)
+ * The max depth to search as a number or element (defaults to 10 || document.body)
+ * @param {Boolean} returnEl (optional) True to return a Ext.Element object instead of DOM node
+ * @return {HTMLElement} The matching DOM node (or null if no match was found)
+ */
+ findParentNode : function(simpleSelector, maxDepth, returnEl) {
+ var p = Ext.fly(this.dom.parentNode, '_internal');
+ return p ? p.findParent(simpleSelector, maxDepth, returnEl) : null;
+ },
+
+ /**
+ * Walks up the dom looking for a parent node that matches the passed simple selector (e.g. div.some-class or span:first-child).
+ * This is a shortcut for findParentNode() that always returns an Ext.Element.
+ * @param {String} selector The simple selector to test
+ * @param {Number/String/HTMLElement/Ext.Element} maxDepth (optional)
+ * The max depth to search as a number or element (defaults to 10 || document.body)
+ * @return {Ext.Element} The matching DOM node (or null if no match was found)
+ */
+ up : function(simpleSelector, maxDepth) {
+ return this.findParentNode(simpleSelector, maxDepth, true);
+ },
+
+ /**
+ * Creates a {@link Ext.CompositeElement} for child nodes based on the passed CSS selector (the selector should not contain an id).
+ * @param {String} selector The CSS selector
+ * @return {Ext.CompositeElement/Ext.CompositeElement} The composite element
+ */
+ select : function(selector) {
+ return Ext.Element.select(selector, false, this.dom);
+ },
+
+ /**
+ * Selects child nodes based on the passed CSS selector (the selector should not contain an id).
+ * @param {String} selector The CSS selector
+ * @return {HTMLElement[]} An array of the matched nodes
+ */
+ query : function(selector) {
+ return Ext.DomQuery.select(selector, this.dom);
+ },
+
+ /**
+ * Selects a single child at any depth below this element based on the passed CSS selector (the selector should not contain an id).
+ * @param {String} selector The CSS selector
+ * @param {Boolean} returnDom (optional) True to return the DOM node instead of Ext.Element (defaults to false)
+ * @return {HTMLElement/Ext.Element} The child Ext.Element (or DOM node if returnDom = true)
+ */
+ down : function(selector, returnDom) {
+ var n = Ext.DomQuery.selectNode(selector, this.dom);
+ return returnDom ? n : Ext.get(n);
+ },
+
+ /**
+ * Selects a single *direct* child based on the passed CSS selector (the selector should not contain an id).
+ * @param {String} selector The CSS selector
+ * @param {Boolean} returnDom (optional) True to return the DOM node instead of Ext.Element (defaults to false)
+ * @return {HTMLElement/Ext.Element} The child Ext.Element (or DOM node if returnDom = true)
+ */
+ child : function(selector, returnDom) {
+ var node,
+ me = this,
+ id;
+ id = Ext.get(me).id;
+ // Escape . or :
+ id = id.replace(/[\.:]/g, "\\$0");
+ node = Ext.DomQuery.selectNode('#' + id + " > " + selector, me.dom);
+ return returnDom ? node : Ext.get(node);
+ },
+
+ /**
+ * Gets the parent node for this element, optionally chaining up trying to match a selector
+ * @param {String} selector (optional) Find a parent node that matches the passed simple selector
+ * @param {Boolean} returnDom (optional) True to return a raw dom node instead of an Ext.Element
+ * @return {Ext.Element/HTMLElement} The parent node or null
+ */
+ parent : function(selector, returnDom) {
+ return this.matchNode('parentNode', 'parentNode', selector, returnDom);
+ },
+
+ /**
+ * Gets the next sibling, skipping text nodes
+ * @param {String} selector (optional) Find the next sibling that matches the passed simple selector
+ * @param {Boolean} returnDom (optional) True to return a raw dom node instead of an Ext.Element
+ * @return {Ext.Element/HTMLElement} The next sibling or null
+ */
+ next : function(selector, returnDom) {
+ return this.matchNode('nextSibling', 'nextSibling', selector, returnDom);
+ },
+
+ /**
+ * Gets the previous sibling, skipping text nodes
+ * @param {String} selector (optional) Find the previous sibling that matches the passed simple selector
+ * @param {Boolean} returnDom (optional) True to return a raw dom node instead of an Ext.Element
+ * @return {Ext.Element/HTMLElement} The previous sibling or null
+ */
+ prev : function(selector, returnDom) {
+ return this.matchNode('previousSibling', 'previousSibling', selector, returnDom);
+ },
+
+
+ /**
+ * Gets the first child, skipping text nodes
+ * @param {String} selector (optional) Find the next sibling that matches the passed simple selector
+ * @param {Boolean} returnDom (optional) True to return a raw dom node instead of an Ext.Element
+ * @return {Ext.Element/HTMLElement} The first child or null
+ */
+ first : function(selector, returnDom) {
+ return this.matchNode('nextSibling', 'firstChild', selector, returnDom);
+ },
+
+ /**
+ * Gets the last child, skipping text nodes
+ * @param {String} selector (optional) Find the previous sibling that matches the passed simple selector
+ * @param {Boolean} returnDom (optional) True to return a raw dom node instead of an Ext.Element
+ * @return {Ext.Element/HTMLElement} The last child or null
+ */
+ last : function(selector, returnDom) {
+ return this.matchNode('previousSibling', 'lastChild', selector, returnDom);
+ },
+
+ matchNode : function(dir, start, selector, returnDom) {
+ if (!this.dom) {
+ return null;
+ }
+
+ var n = this.dom[start];
+ while (n) {
+ if (n.nodeType == 1 && (!selector || Ext.DomQuery.is(n, selector))) {
+ return !returnDom ? Ext.get(n) : n;
+ }
+ n = n[dir];
+ }
+ return null;
+ }
+});
+
+/**
+ * @class Ext.Element
+ */
+Ext.Element.addMethods({
+ /**
+ * Appends the passed element(s) to this element
+ * @param {String/HTMLElement/Ext.Element} el
+ * The id of the node, a DOM Node or an existing Element.
+ * @return {Ext.Element} this
+ */
+ appendChild : function(el) {
+ return Ext.get(el).appendTo(this);
+ },
+
+ /**
+ * Appends this element to the passed element
+ * @param {String/HTMLElement/Ext.Element} el The new parent element.
+ * The id of the node, a DOM Node or an existing Element.
+ * @return {Ext.Element} this
+ */
+ appendTo : function(el) {
+ Ext.getDom(el).appendChild(this.dom);
+ return this;
+ },
+
+ /**
+ * Inserts this element before the passed element in the DOM
+ * @param {String/HTMLElement/Ext.Element} el The element before which this element will be inserted.
+ * The id of the node, a DOM Node or an existing Element.
+ * @return {Ext.Element} this
+ */
+ insertBefore : function(el) {
+ el = Ext.getDom(el);
+ el.parentNode.insertBefore(this.dom, el);
+ return this;
+ },
+
+ /**
+ * Inserts this element after the passed element in the DOM
+ * @param {String/HTMLElement/Ext.Element} el The element to insert after.
+ * The id of the node, a DOM Node or an existing Element.
+ * @return {Ext.Element} this
+ */
+ insertAfter : function(el) {
+ el = Ext.getDom(el);
+ el.parentNode.insertBefore(this.dom, el.nextSibling);
+ return this;
+ },
+
+ /**
+ * Inserts (or creates) an element (or DomHelper config) as the first child of this element
+ * @param {String/HTMLElement/Ext.Element/Object} el The id or element to insert or a DomHelper config
+ * to create and insert
+ * @return {Ext.Element} The new child
+ */
+ insertFirst : function(el, returnDom) {
+ el = el || {};
+ if (el.nodeType || el.dom || typeof el == 'string') { // element
+ el = Ext.getDom(el);
+ this.dom.insertBefore(el, this.dom.firstChild);
+ return !returnDom ? Ext.get(el) : el;
+ }
+ else { // dh config
+ return this.createChild(el, this.dom.firstChild, returnDom);
+ }
+ },
+
+ /**
+ * Inserts (or creates) the passed element (or DomHelper config) as a sibling of this element
+ * @param {String/HTMLElement/Ext.Element/Object/Array} el The id, element to insert or a DomHelper config
+ * to create and insert *or* an array of any of those.
+ * @param {String} where (optional) 'before' or 'after' defaults to before
+ * @param {Boolean} returnDom (optional) True to return the .;ll;l,raw DOM element instead of Ext.Element
+ * @return {Ext.Element} The inserted Element. If an array is passed, the last inserted element is returned.
+ */
+ insertSibling: function(el, where, returnDom){
+ var me = this, rt,
+ isAfter = (where || 'before').toLowerCase() == 'after',
+ insertEl;
+
+ if(Ext.isArray(el)){
+ insertEl = me;
+ Ext.each(el, function(e) {
+ rt = Ext.fly(insertEl, '_internal').insertSibling(e, where, returnDom);
+ if(isAfter){
+ insertEl = rt;
+ }
+ });
+ return rt;
+ }
+
+ el = el || {};
+
+ if(el.nodeType || el.dom){
+ rt = me.dom.parentNode.insertBefore(Ext.getDom(el), isAfter ? me.dom.nextSibling : me.dom);
+ if (!returnDom) {
+ rt = Ext.get(rt);
+ }
+ }else{
+ if (isAfter && !me.dom.nextSibling) {
+ rt = Ext.DomHelper.append(me.dom.parentNode, el, !returnDom);
+ } else {
+ rt = Ext.DomHelper[isAfter ? 'insertAfter' : 'insertBefore'](me.dom, el, !returnDom);
+ }
+ }
+ return rt;
+ },
+
+ /**
+ * Replaces the passed element with this element
+ * @param {String/HTMLElement/Ext.Element} el The element to replace.
+ * The id of the node, a DOM Node or an existing Element.
+ * @return {Ext.Element} this
+ */
+ replace : function(el) {
+ el = Ext.get(el);
+ this.insertBefore(el);
+ el.remove();
+ return this;
+ },
+
+ /**
+ * Replaces this element with the passed element
+ * @param {String/HTMLElement/Ext.Element/Object} el The new element (id of the node, a DOM Node
+ * or an existing Element) or a DomHelper config of an element to create
+ * @return {Ext.Element} this
+ */
+ replaceWith: function(el){
+ var me = this;
+
+ if(el.nodeType || el.dom || typeof el == 'string'){
+ el = Ext.get(el);
+ me.dom.parentNode.insertBefore(el, me.dom);
+ }else{
+ el = Ext.DomHelper.insertBefore(me.dom, el);
+ }
+
+ delete Ext.cache[me.id];
+ Ext.removeNode(me.dom);
+ me.id = Ext.id(me.dom = el);
+ Ext.Element.addToCache(me.isFlyweight ? new Ext.Element(me.dom) : me);
+ return me;
+ },
+
+ /**
+ * Creates the passed DomHelper config and appends it to this element or optionally inserts it before the passed child element.
+ * @param {Object} config DomHelper element config object. If no tag is specified (e.g., {tag:'input'}) then a div will be
+ * automatically generated with the specified attributes.
+ * @param {HTMLElement} insertBefore (optional) a child element of this element
+ * @param {Boolean} returnDom (optional) true to return the dom node instead of creating an Element
+ * @return {Ext.Element} The new child element
+ */
+ createChild : function(config, insertBefore, returnDom) {
+ config = config || {tag:'div'};
+ if (insertBefore) {
+ return Ext.DomHelper.insertBefore(insertBefore, config, returnDom !== true);
+ }
+ else {
+ return Ext.DomHelper[!this.dom.firstChild ? 'insertFirst' : 'append'](this.dom, config, returnDom !== true);
+ }
+ },
+
+ /**
+ * Creates and wraps this element with another element
+ * @param {Object} config (optional) DomHelper element config object for the wrapper element or null for an empty div
+ * @param {Boolean} returnDom (optional) True to return the raw DOM element instead of Ext.Element
+ * @return {HTMLElement/Ext.Element} The newly created wrapper element
+ */
+ wrap : function(config, returnDom) {
+ var newEl = Ext.DomHelper.insertBefore(this.dom, config || {tag: "div"}, !returnDom),
+ d = newEl.dom || newEl;
+
+ d.appendChild(this.dom);
+ return newEl;
+ },
+
+ /**
+ * Inserts an html fragment into this element
+ * @param {String} where Where to insert the html in relation to this element - beforeBegin, afterBegin, beforeEnd, afterEnd.
+ * See {@link Ext.DomHelper#insertHtml} for details.
+ * @param {String} html The HTML fragment
+ * @param {Boolean} returnEl (optional) True to return an Ext.Element (defaults to false)
+ * @return {HTMLElement/Ext.Element} The inserted node (or nearest related if more than 1 inserted)
+ */
+ insertHtml : function(where, html, returnEl) {
+ var el = Ext.DomHelper.insertHtml(where, this.dom, html);
+ return returnEl ? Ext.get(el) : el;
+ }
+});
+
+/**
+ * @class Ext.Element
+ */
+(function(){
+ // local style camelizing for speed
+ var ELEMENT = Ext.Element,
+ supports = Ext.supports,
+ view = document.defaultView,
+ opacityRe = /alpha\(opacity=(.*)\)/i,
+ trimRe = /^\s+|\s+$/g,
+ spacesRe = /\s+/,
+ wordsRe = /\w/g,
+ adjustDirect2DTableRe = /table-row|table-.*-group/,
+ INTERNAL = '_internal',
+ PADDING = 'padding',
+ MARGIN = 'margin',
+ BORDER = 'border',
+ LEFT = '-left',
+ RIGHT = '-right',
+ TOP = '-top',
+ BOTTOM = '-bottom',
+ WIDTH = '-width',
+ MATH = Math,
+ HIDDEN = 'hidden',
+ ISCLIPPED = 'isClipped',
+ OVERFLOW = 'overflow',
+ OVERFLOWX = 'overflow-x',
+ OVERFLOWY = 'overflow-y',
+ ORIGINALCLIP = 'originalClip',
+ // special markup used throughout Ext when box wrapping elements
+ borders = {l: BORDER + LEFT + WIDTH, r: BORDER + RIGHT + WIDTH, t: BORDER + TOP + WIDTH, b: BORDER + BOTTOM + WIDTH},
+ paddings = {l: PADDING + LEFT, r: PADDING + RIGHT, t: PADDING + TOP, b: PADDING + BOTTOM},
+ margins = {l: MARGIN + LEFT, r: MARGIN + RIGHT, t: MARGIN + TOP, b: MARGIN + BOTTOM},
+ data = ELEMENT.data;
+
+ ELEMENT.boxMarkup = '<div class="{0}-tl"><div class="{0}-tr"><div class="{0}-tc"></div></div></div><div class="{0}-ml"><div class="{0}-mr"><div class="{0}-mc"></div></div></div><div class="{0}-bl"><div class="{0}-br"><div class="{0}-bc"></div></div></div>';
+
+ // These property values are read from the parentNode if they cannot be read
+ // from the child:
+ ELEMENT.inheritedProps = {
+ fontSize: 1,
+ fontStyle: 1,
+ opacity: 1
+ };
+
+ Ext.override(ELEMENT, {
+
+ /**
+ * TODO: Look at this
+ */
+ // private ==> used by Fx
+ adjustWidth : function(width) {
+ var me = this,
+ isNum = (typeof width == 'number');
+
+ if(isNum && me.autoBoxAdjust && !me.isBorderBox()){
+ width -= (me.getBorderWidth("lr") + me.getPadding("lr"));
+ }
+ return (isNum && width < 0) ? 0 : width;
+ },
+
+ // private ==> used by Fx
+ adjustHeight : function(height) {
+ var me = this,
+ isNum = (typeof height == "number");
+
+ if(isNum && me.autoBoxAdjust && !me.isBorderBox()){
+ height -= (me.getBorderWidth("tb") + me.getPadding("tb"));
+ }
+ return (isNum && height < 0) ? 0 : height;
+ },
+
+
+ /**
+ * Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out.
+ * @param {String/String[]} className The CSS classes to add separated by space, or an array of classes
+ * @return {Ext.Element} this
+ */
+ addCls : function(className){
+ var me = this,
+ cls = [],
+ space = ((me.dom.className.replace(trimRe, '') == '') ? "" : " "),
+ i, len, v;
+ if (className === undefined) {
+ return me;
+ }
+ // Separate case is for speed
+ if (Object.prototype.toString.call(className) !== '[object Array]') {
+ if (typeof className === 'string') {
+ className = className.replace(trimRe, '').split(spacesRe);
+ if (className.length === 1) {
+ className = className[0];
+ if (!me.hasCls(className)) {
+ me.dom.className += space + className;
+ }
+ } else {
+ this.addCls(className);
+ }
+ }
+ } else {
+ for (i = 0, len = className.length; i < len; i++) {
+ v = className[i];
+ if (typeof v == 'string' && (' ' + me.dom.className + ' ').indexOf(' ' + v + ' ') == -1) {
+ cls.push(v);
+ }
+ }
+ if (cls.length) {
+ me.dom.className += space + cls.join(" ");
+ }
+ }
+ return me;
+ },
+
+ /**
+ * Removes one or more CSS classes from the element.
+ * @param {String/String[]} className The CSS classes to remove separated by space, or an array of classes
+ * @return {Ext.Element} this
+ */
+ removeCls : function(className){
+ var me = this,
+ i, idx, len, cls, elClasses;
+ if (className === undefined) {
+ return me;
+ }
+ if (Object.prototype.toString.call(className) !== '[object Array]') {
+ className = className.replace(trimRe, '').split(spacesRe);
+ }
+ if (me.dom && me.dom.className) {
+ elClasses = me.dom.className.replace(trimRe, '').split(spacesRe);
+ for (i = 0, len = className.length; i < len; i++) {
+ cls = className[i];
+ if (typeof cls == 'string') {
+ cls = cls.replace(trimRe, '');
+ idx = Ext.Array.indexOf(elClasses, cls);
+ if (idx != -1) {
+ Ext.Array.erase(elClasses, idx, 1);
+ }
+ }
+ }
+ me.dom.className = elClasses.join(" ");
+ }
+ return me;
+ },
+
+ /**
+ * Adds one or more CSS classes to this element and removes the same class(es) from all siblings.
+ * @param {String/String[]} className The CSS class to add, or an array of classes
+ * @return {Ext.Element} this
+ */
+ radioCls : function(className){
+ var cn = this.dom.parentNode.childNodes,
+ v, i, len;
+ className = Ext.isArray(className) ? className : [className];
+ for (i = 0, len = cn.length; i < len; i++) {
+ v = cn[i];
+ if (v && v.nodeType == 1) {
+ Ext.fly(v, '_internal').removeCls(className);
+ }
+ }
+ return this.addCls(className);
+ },
+
+ /**
+ * Toggles the specified CSS class on this element (removes it if it already exists, otherwise adds it).
+ * @param {String} className The CSS class to toggle
+ * @return {Ext.Element} this
+ * @method
+ */
+ toggleCls : Ext.supports.ClassList ?
+ function(className) {
+ this.dom.classList.toggle(Ext.String.trim(className));
+ return this;
+ } :
+ function(className) {
+ return this.hasCls(className) ? this.removeCls(className) : this.addCls(className);
+ },
+
+ /**
+ * Checks if the specified CSS class exists on this element's DOM node.
+ * @param {String} className The CSS class to check for
+ * @return {Boolean} True if the class exists, else false
+ * @method
+ */
+ hasCls : Ext.supports.ClassList ?
+ function(className) {
+ if (!className) {
+ return false;
+ }
+ className = className.split(spacesRe);
+ var ln = className.length,
+ i = 0;
+ for (; i < ln; i++) {
+ if (className[i] && this.dom.classList.contains(className[i])) {
+ return true;
+ }
+ }
+ return false;
+ } :
+ function(className){
+ return className && (' ' + this.dom.className + ' ').indexOf(' ' + className + ' ') != -1;
+ },
+
+ /**
+ * Replaces a CSS class on the element with another. If the old name does not exist, the new name will simply be added.
+ * @param {String} oldClassName The CSS class to replace
+ * @param {String} newClassName The replacement CSS class
+ * @return {Ext.Element} this
+ */
+ replaceCls : function(oldClassName, newClassName){
+ return this.removeCls(oldClassName).addCls(newClassName);
+ },
+
+ isStyle : function(style, val) {
+ return this.getStyle(style) == val;
+ },
+
+ /**
+ * Normalizes currentStyle and computedStyle.
+ * @param {String} property The style property whose value is returned.
+ * @return {String} The current value of the style property for this element.
+ * @method
+ */
+ getStyle : function() {
+ return view && view.getComputedStyle ?
+ function(prop){
+ var el = this.dom,
+ v, cs, out, display, cleaner;
+
+ if(el == document){
+ return null;
+ }
+ prop = ELEMENT.normalize(prop);
+ out = (v = el.style[prop]) ? v :
+ (cs = view.getComputedStyle(el, "")) ? cs[prop] : null;
+
+ // Ignore cases when the margin is correctly reported as 0, the bug only shows
+ // numbers larger.
+ if(prop == 'marginRight' && out != '0px' && !supports.RightMargin){
+ cleaner = ELEMENT.getRightMarginFixCleaner(el);
+ display = this.getStyle('display');
+ el.style.display = 'inline-block';
+ out = view.getComputedStyle(el, '').marginRight;
+ el.style.display = display;
+ cleaner();
+ }
+
+ if(prop == 'backgroundColor' && out == 'rgba(0, 0, 0, 0)' && !supports.TransparentColor){
+ out = 'transparent';
+ }
+ return out;
+ } :
+ function (prop) {
+ var el = this.dom,
+ m, cs;
+
+ if (el == document) {
+ return null;
+ }
+ prop = ELEMENT.normalize(prop);
+
+ do {
+ if (prop == 'opacity') {
+ if (el.style.filter.match) {
+ m = el.style.filter.match(opacityRe);
+ if(m){
+ var fv = parseFloat(m[1]);
+ if(!isNaN(fv)){
+ return fv ? fv / 100 : 0;
+ }
+ }
+ }
+ return 1;
+ }
+
+ // the try statement does have a cost, so we avoid it unless we are
+ // on IE6
+ if (!Ext.isIE6) {
+ return el.style[prop] || ((cs = el.currentStyle) ? cs[prop] : null);
+ }
+
+ try {
+ return el.style[prop] || ((cs = el.currentStyle) ? cs[prop] : null);
+ } catch (e) {
+ // in some cases, IE6 will throw Invalid Argument for properties
+ // like fontSize (see in /examples/tabs/tabs.html).
+ }
+
+ if (!ELEMENT.inheritedProps[prop]) {
+ break;
+ }
+
+ el = el.parentNode;
+ // this is _not_ perfect, but we can only hope that the style we
+ // need is inherited from a parentNode. If not and since IE won't
+ // give us the info we need, we are never going to be 100% right.
+ } while (el);
+
+ return null;
+ }
+ }(),
+
+ /**
+ * Return the CSS color for the specified CSS attribute. rgb, 3 digit (like #fff) and valid values
+ * are convert to standard 6 digit hex color.
+ * @param {String} attr The css attribute
+ * @param {String} defaultValue The default value to use when a valid color isn't found
+ * @param {String} prefix (optional) defaults to #. Use an empty string when working with
+ * color anims.
+ */
+ getColor : function(attr, defaultValue, prefix){
+ var v = this.getStyle(attr),
+ color = prefix || prefix === '' ? prefix : '#',
+ h;
+
+ if(!v || (/transparent|inherit/.test(v))) {
+ return defaultValue;
+ }
+ if(/^r/.test(v)){
+ Ext.each(v.slice(4, v.length -1).split(','), function(s){
+ h = parseInt(s, 10);
+ color += (h < 16 ? '0' : '') + h.toString(16);
+ });
+ }else{
+ v = v.replace('#', '');
+ color += v.length == 3 ? v.replace(/^(\w)(\w)(\w)$/, '$1$1$2$2$3$3') : v;
+ }
+ return(color.length > 5 ? color.toLowerCase() : defaultValue);
+ },
+
+ /**
+ * Wrapper for setting style properties, also takes single object parameter of multiple styles.
+ * @param {String/Object} property The style property to be set, or an object of multiple styles.
+ * @param {String} value (optional) The value to apply to the given property, or null if an object was passed.
+ * @return {Ext.Element} this
+ */
+ setStyle : function(prop, value){
+ var me = this,
+ tmp, style;
+
+ if (!me.dom) {
+ return me;
+ }
+ if (typeof prop === 'string') {
+ tmp = {};
+ tmp[prop] = value;
+ prop = tmp;
+ }
+ for (style in prop) {
+ if (prop.hasOwnProperty(style)) {
+ value = Ext.value(prop[style], '');
+ if (style == 'opacity') {
+ me.setOpacity(value);
+ }
+ else {
+ me.dom.style[ELEMENT.normalize(style)] = value;
+ }
+ }
+ }
+ return me;
+ },
+
+ /**
+ * Set the opacity of the element
+ * @param {Number} opacity The new opacity. 0 = transparent, .5 = 50% visibile, 1 = fully visible, etc
+ * @param {Boolean/Object} animate (optional) a standard Element animation config object or <tt>true</tt> for
+ * the default animation (<tt>{duration: .35, easing: 'easeIn'}</tt>)
+ * @return {Ext.Element} this
+ */
+ setOpacity: function(opacity, animate) {
+ var me = this,
+ dom = me.dom,
+ val,
+ style;
+
+ if (!me.dom) {
+ return me;
+ }
+
+ style = me.dom.style;
+
+ if (!animate || !me.anim) {
+ if (!Ext.supports.Opacity) {
+ opacity = opacity < 1 ? 'alpha(opacity=' + opacity * 100 + ')': '';
+ val = style.filter.replace(opacityRe, '').replace(trimRe, '');
+
+ style.zoom = 1;
+ style.filter = val + (val.length > 0 ? ' ': '') + opacity;
+ }
+ else {
+ style.opacity = opacity;
+ }
+ }
+ else {
+ if (!Ext.isObject(animate)) {
+ animate = {
+ duration: 350,
+ easing: 'ease-in'
+ };
+ }
+ me.animate(Ext.applyIf({
+ to: {
+ opacity: opacity
+ }
+ },
+ animate));
+ }
+ return me;
+ },
+
+
+ /**
+ * Clears any opacity settings from this element. Required in some cases for IE.
+ * @return {Ext.Element} this
+ */
+ clearOpacity : function(){
+ var style = this.dom.style;
+ if(!Ext.supports.Opacity){
+ if(!Ext.isEmpty(style.filter)){
+ style.filter = style.filter.replace(opacityRe, '').replace(trimRe, '');
+ }
+ }else{
+ style.opacity = style['-moz-opacity'] = style['-khtml-opacity'] = '';
+ }
+ return this;
+ },
+
+ /**
+ * @private
+ * Returns 1 if the browser returns the subpixel dimension rounded to the lowest pixel.
+ * @return {Number} 0 or 1
+ */
+ adjustDirect2DDimension: function(dimension) {
+ var me = this,
+ dom = me.dom,
+ display = me.getStyle('display'),
+ inlineDisplay = dom.style['display'],
+ inlinePosition = dom.style['position'],
+ originIndex = dimension === 'width' ? 0 : 1,
+ floating;
+
+ if (display === 'inline') {
+ dom.style['display'] = 'inline-block';
+ }
+
+ dom.style['position'] = display.match(adjustDirect2DTableRe) ? 'absolute' : 'static';
+
+ // floating will contain digits that appears after the decimal point
+ // if height or width are set to auto we fallback to msTransformOrigin calculation
+ floating = (parseFloat(me.getStyle(dimension)) || parseFloat(dom.currentStyle.msTransformOrigin.split(' ')[originIndex]) * 2) % 1;
+
+ dom.style['position'] = inlinePosition;
+
+ if (display === 'inline') {
+ dom.style['display'] = inlineDisplay;
+ }
+
+ return floating;
+ },
+
+ /**
+ * Returns the offset height of the element
+ * @param {Boolean} contentHeight (optional) true to get the height minus borders and padding
+ * @return {Number} The element's height
+ */
+ getHeight: function(contentHeight, preciseHeight) {
+ var me = this,
+ dom = me.dom,
+ hidden = Ext.isIE && me.isStyle('display', 'none'),
+ height, overflow, style, floating;
+
+ // IE Quirks mode acts more like a max-size measurement unless overflow is hidden during measurement.
+ // We will put the overflow back to it's original value when we are done measuring.
+ if (Ext.isIEQuirks) {
+ style = dom.style;
+ overflow = style.overflow;
+ me.setStyle({ overflow: 'hidden'});
+ }
+
+ height = dom.offsetHeight;
+
+ height = MATH.max(height, hidden ? 0 : dom.clientHeight) || 0;
+
+ // IE9 Direct2D dimension rounding bug
+ if (!hidden && Ext.supports.Direct2DBug) {
+ floating = me.adjustDirect2DDimension('height');
+ if (preciseHeight) {
+ height += floating;
+ }
+ else if (floating > 0 && floating < 0.5) {
+ height++;
+ }
+ }
+
+ if (contentHeight) {
+ height -= (me.getBorderWidth("tb") + me.getPadding("tb"));
+ }
+
+ if (Ext.isIEQuirks) {
+ me.setStyle({ overflow: overflow});
+ }
+
+ if (height < 0) {
+ height = 0;
+ }
+ return height;
+ },
+
+ /**
+ * Returns the offset width of the element
+ * @param {Boolean} contentWidth (optional) true to get the width minus borders and padding
+ * @return {Number} The element's width
+ */
+ getWidth: function(contentWidth, preciseWidth) {
+ var me = this,
+ dom = me.dom,
+ hidden = Ext.isIE && me.isStyle('display', 'none'),
+ rect, width, overflow, style, floating, parentPosition;
+
+ // IE Quirks mode acts more like a max-size measurement unless overflow is hidden during measurement.
+ // We will put the overflow back to it's original value when we are done measuring.
+ if (Ext.isIEQuirks) {
+ style = dom.style;
+ overflow = style.overflow;
+ me.setStyle({overflow: 'hidden'});
+ }
+
+ // Fix Opera 10.5x width calculation issues
+ if (Ext.isOpera10_5) {
+ if (dom.parentNode.currentStyle.position === 'relative') {
+ parentPosition = dom.parentNode.style.position;
+ dom.parentNode.style.position = 'static';
+ width = dom.offsetWidth;
+ dom.parentNode.style.position = parentPosition;
+ }
+ width = Math.max(width || 0, dom.offsetWidth);
+
+ // Gecko will in some cases report an offsetWidth that is actually less than the width of the
+ // text contents, because it measures fonts with sub-pixel precision but rounds the calculated
+ // value down. Using getBoundingClientRect instead of offsetWidth allows us to get the precise
+ // subpixel measurements so we can force them to always be rounded up. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=458617
+ } else if (Ext.supports.BoundingClientRect) {
+ rect = dom.getBoundingClientRect();
+ width = rect.right - rect.left;
+ width = preciseWidth ? width : Math.ceil(width);
+ } else {
+ width = dom.offsetWidth;
+ }
+
+ width = MATH.max(width, hidden ? 0 : dom.clientWidth) || 0;
+
+ // IE9 Direct2D dimension rounding bug
+ if (!hidden && Ext.supports.Direct2DBug) {
+ floating = me.adjustDirect2DDimension('width');
+ if (preciseWidth) {
+ width += floating;
+ }
+ else if (floating > 0 && floating < 0.5) {
+ width++;
+ }
+ }
+
+ if (contentWidth) {
+ width -= (me.getBorderWidth("lr") + me.getPadding("lr"));
+ }
+
+ if (Ext.isIEQuirks) {
+ me.setStyle({ overflow: overflow});
+ }
+
+ if (width < 0) {
+ width = 0;
+ }
+ return width;
+ },
+
+ /**
+ * Set the width of this Element.
+ * @param {Number/String} width The new width. This may be one of:<div class="mdetail-params"><ul>
+ * <li>A Number specifying the new width in this Element's {@link #defaultUnit}s (by default, pixels).</li>
+ * <li>A String used to set the CSS width style. Animation may <b>not</b> be used.
+ * </ul></div>
+ * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ setWidth : function(width, animate){
+ var me = this;
+ width = me.adjustWidth(width);
+ if (!animate || !me.anim) {
+ me.dom.style.width = me.addUnits(width);
+ }
+ else {
+ if (!Ext.isObject(animate)) {
+ animate = {};
+ }
+ me.animate(Ext.applyIf({
+ to: {
+ width: width
+ }
+ }, animate));
+ }
+ return me;
+ },
+
+ /**
+ * Set the height of this Element.
+ * <pre><code>
+// change the height to 200px and animate with default configuration
+Ext.fly('elementId').setHeight(200, true);
+
+// change the height to 150px and animate with a custom configuration
+Ext.fly('elId').setHeight(150, {
+ duration : .5, // animation will have a duration of .5 seconds
+ // will change the content to "finished"
+ callback: function(){ this.{@link #update}("finished"); }
+});
+ * </code></pre>
+ * @param {Number/String} height The new height. This may be one of:<div class="mdetail-params"><ul>
+ * <li>A Number specifying the new height in this Element's {@link #defaultUnit}s (by default, pixels.)</li>
+ * <li>A String used to set the CSS height style. Animation may <b>not</b> be used.</li>
+ * </ul></div>
+ * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ setHeight : function(height, animate){
+ var me = this;
+ height = me.adjustHeight(height);
+ if (!animate || !me.anim) {
+ me.dom.style.height = me.addUnits(height);
+ }
+ else {
+ if (!Ext.isObject(animate)) {
+ animate = {};
+ }
+ me.animate(Ext.applyIf({
+ to: {
+ height: height
+ }
+ }, animate));
+ }
+ return me;
+ },
+
+ /**
+ * Gets the width of the border(s) for the specified side(s)
+ * @param {String} side Can be t, l, r, b or any combination of those to add multiple values. For example,
+ * passing <tt>'lr'</tt> would get the border <b><u>l</u></b>eft width + the border <b><u>r</u></b>ight width.
+ * @return {Number} The width of the sides passed added together
+ */
+ getBorderWidth : function(side){
+ return this.addStyles(side, borders);
+ },
+
+ /**
+ * Gets the width of the padding(s) for the specified side(s)
+ * @param {String} side Can be t, l, r, b or any combination of those to add multiple values. For example,
+ * passing <tt>'lr'</tt> would get the padding <b><u>l</u></b>eft + the padding <b><u>r</u></b>ight.
+ * @return {Number} The padding of the sides passed added together
+ */
+ getPadding : function(side){
+ return this.addStyles(side, paddings);
+ },
+
+ /**
+ * Store the current overflow setting and clip overflow on the element - use <tt>{@link #unclip}</tt> to remove
+ * @return {Ext.Element} this
+ */
+ clip : function(){
+ var me = this,
+ dom = me.dom;
+
+ if(!data(dom, ISCLIPPED)){
+ data(dom, ISCLIPPED, true);
+ data(dom, ORIGINALCLIP, {
+ o: me.getStyle(OVERFLOW),
+ x: me.getStyle(OVERFLOWX),
+ y: me.getStyle(OVERFLOWY)
+ });
+ me.setStyle(OVERFLOW, HIDDEN);
+ me.setStyle(OVERFLOWX, HIDDEN);
+ me.setStyle(OVERFLOWY, HIDDEN);
+ }
+ return me;
+ },
+
+ /**
+ * Return clipping (overflow) to original clipping before <tt>{@link #clip}</tt> was called
+ * @return {Ext.Element} this
+ */
+ unclip : function(){
+ var me = this,
+ dom = me.dom,
+ clip;
+
+ if(data(dom, ISCLIPPED)){
+ data(dom, ISCLIPPED, false);
+ clip = data(dom, ORIGINALCLIP);
+ if(clip.o){
+ me.setStyle(OVERFLOW, clip.o);
+ }
+ if(clip.x){
+ me.setStyle(OVERFLOWX, clip.x);
+ }
+ if(clip.y){
+ me.setStyle(OVERFLOWY, clip.y);
+ }
+ }
+ return me;
+ },
+
+ // private
+ addStyles : function(sides, styles){
+ var totalSize = 0,
+ sidesArr = sides.match(wordsRe),
+ i = 0,
+ len = sidesArr.length,
+ side, size;
+ for (; i < len; i++) {
+ side = sidesArr[i];
+ size = side && parseInt(this.getStyle(styles[side]), 10);
+ if (size) {
+ totalSize += MATH.abs(size);
+ }
+ }
+ return totalSize;
+ },
+
+ margins : margins,
+
+ /**
+ * More flexible version of {@link #setStyle} for setting style properties.
+ * @param {String/Object/Function} styles A style specification string, e.g. "width:100px", or object in the form {width:"100px"}, or
+ * a function which returns such a specification.
+ * @return {Ext.Element} this
+ */
+ applyStyles : function(style){
+ Ext.DomHelper.applyStyles(this.dom, style);
+ return this;
+ },
+
+ /**
+ * Returns an object with properties matching the styles requested.
+ * For example, el.getStyles('color', 'font-size', 'width') might return
+ * {'color': '#FFFFFF', 'font-size': '13px', 'width': '100px'}.
+ * @param {String} style1 A style name
+ * @param {String} style2 A style name
+ * @param {String} etc.
+ * @return {Object} The style object
+ */
+ getStyles : function(){
+ var styles = {},
+ len = arguments.length,
+ i = 0, style;
+
+ for(; i < len; ++i) {
+ style = arguments[i];
+ styles[style] = this.getStyle(style);
+ }
+ return styles;
+ },
+
+ /**
+ * <p>Wraps the specified element with a special 9 element markup/CSS block that renders by default as
+ * a gray container with a gradient background, rounded corners and a 4-way shadow.</p>
+ * <p>This special markup is used throughout Ext when box wrapping elements ({@link Ext.button.Button},
+ * {@link Ext.panel.Panel} when <tt>{@link Ext.panel.Panel#frame frame=true}</tt>, {@link Ext.window.Window}). The markup
+ * is of this form:</p>
+ * <pre><code>
+ Ext.Element.boxMarkup =
+ '<div class="{0}-tl"><div class="{0}-tr"><div class="{0}-tc"></div></div></div>
+ <div class="{0}-ml"><div class="{0}-mr"><div class="{0}-mc"></div></div></div>
+ <div class="{0}-bl"><div class="{0}-br"><div class="{0}-bc"></div></div></div>';
+ * </code></pre>
+ * <p>Example usage:</p>
+ * <pre><code>
+ // Basic box wrap
+ Ext.get("foo").boxWrap();
+
+ // You can also add a custom class and use CSS inheritance rules to customize the box look.
+ // 'x-box-blue' is a built-in alternative -- look at the related CSS definitions as an example
+ // for how to create a custom box wrap style.
+ Ext.get("foo").boxWrap().addCls("x-box-blue");
+ * </code></pre>
+ * @param {String} class (optional) A base CSS class to apply to the containing wrapper element
+ * (defaults to <tt>'x-box'</tt>). Note that there are a number of CSS rules that are dependent on
+ * this name to make the overall effect work, so if you supply an alternate base class, make sure you
+ * also supply all of the necessary rules.
+ * @return {Ext.Element} The outermost wrapping element of the created box structure.
+ */
+ boxWrap : function(cls){
+ cls = cls || Ext.baseCSSPrefix + 'box';
+ var el = Ext.get(this.insertHtml("beforeBegin", "<div class='" + cls + "'>" + Ext.String.format(ELEMENT.boxMarkup, cls) + "</div>"));
+ Ext.DomQuery.selectNode('.' + cls + '-mc', el.dom).appendChild(this.dom);
+ return el;
+ },
+
+ /**
+ * Set the size of this Element. If animation is true, both width and height will be animated concurrently.
+ * @param {Number/String} width The new width. This may be one of:<div class="mdetail-params"><ul>
+ * <li>A Number specifying the new width in this Element's {@link #defaultUnit}s (by default, pixels).</li>
+ * <li>A String used to set the CSS width style. Animation may <b>not</b> be used.
+ * <li>A size object in the format <code>{width: widthValue, height: heightValue}</code>.</li>
+ * </ul></div>
+ * @param {Number/String} height The new height. This may be one of:<div class="mdetail-params"><ul>
+ * <li>A Number specifying the new height in this Element's {@link #defaultUnit}s (by default, pixels).</li>
+ * <li>A String used to set the CSS height style. Animation may <b>not</b> be used.</li>
+ * </ul></div>
+ * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ setSize : function(width, height, animate){
+ var me = this;
+ if (Ext.isObject(width)) { // in case of object from getSize()
+ animate = height;
+ height = width.height;
+ width = width.width;
+ }
+ width = me.adjustWidth(width);
+ height = me.adjustHeight(height);
+ if(!animate || !me.anim){
+ // Must touch some property before setting style.width/height on non-quirk IE6,7, or the
+ // properties will not reflect the changes on the style immediately
+ if (!Ext.isIEQuirks && (Ext.isIE6 || Ext.isIE7)) {
+ me.dom.offsetTop;
+ }
+ me.dom.style.width = me.addUnits(width);
+ me.dom.style.height = me.addUnits(height);
+ }
+ else {
+ if (animate === true) {
+ animate = {};
+ }
+ me.animate(Ext.applyIf({
+ to: {
+ width: width,
+ height: height
+ }
+ }, animate));
+ }
+ return me;
+ },
+
+ /**
+ * Returns either the offsetHeight or the height of this element based on CSS height adjusted by padding or borders
+ * when needed to simulate offsetHeight when offsets aren't available. This may not work on display:none elements
+ * if a height has not been set using CSS.
+ * @return {Number}
+ */
+ getComputedHeight : function(){
+ var me = this,
+ h = Math.max(me.dom.offsetHeight, me.dom.clientHeight);
+ if(!h){
+ h = parseFloat(me.getStyle('height')) || 0;
+ if(!me.isBorderBox()){
+ h += me.getFrameWidth('tb');
+ }
+ }
+ return h;
+ },
+
+ /**
+ * Returns either the offsetWidth or the width of this element based on CSS width adjusted by padding or borders
+ * when needed to simulate offsetWidth when offsets aren't available. This may not work on display:none elements
+ * if a width has not been set using CSS.
+ * @return {Number}
+ */
+ getComputedWidth : function(){
+ var me = this,
+ w = Math.max(me.dom.offsetWidth, me.dom.clientWidth);
+
+ if(!w){
+ w = parseFloat(me.getStyle('width')) || 0;
+ if(!me.isBorderBox()){
+ w += me.getFrameWidth('lr');
+ }
+ }
+ return w;
+ },
+
+ /**
+ * Returns the sum width of the padding and borders for the passed "sides". See getBorderWidth()
+ for more information about the sides.
+ * @param {String} sides
+ * @return {Number}
+ */
+ getFrameWidth : function(sides, onlyContentBox){
+ return onlyContentBox && this.isBorderBox() ? 0 : (this.getPadding(sides) + this.getBorderWidth(sides));
+ },
+
+ /**
+ * Sets up event handlers to add and remove a css class when the mouse is over this element
+ * @param {String} className
+ * @return {Ext.Element} this
+ */
+ addClsOnOver : function(className){
+ var dom = this.dom;
+ this.hover(
+ function(){
+ Ext.fly(dom, INTERNAL).addCls(className);
+ },
+ function(){
+ Ext.fly(dom, INTERNAL).removeCls(className);
+ }
+ );
+ return this;
+ },
+
+ /**
+ * Sets up event handlers to add and remove a css class when this element has the focus
+ * @param {String} className
+ * @return {Ext.Element} this
+ */
+ addClsOnFocus : function(className){
+ var me = this,
+ dom = me.dom;
+ me.on("focus", function(){
+ Ext.fly(dom, INTERNAL).addCls(className);
+ });
+ me.on("blur", function(){
+ Ext.fly(dom, INTERNAL).removeCls(className);
+ });
+ return me;
+ },
+
+ /**
+ * Sets up event handlers to add and remove a css class when the mouse is down and then up on this element (a click effect)
+ * @param {String} className
+ * @return {Ext.Element} this
+ */
+ addClsOnClick : function(className){
+ var dom = this.dom;
+ this.on("mousedown", function(){
+ Ext.fly(dom, INTERNAL).addCls(className);
+ var d = Ext.getDoc(),
+ fn = function(){
+ Ext.fly(dom, INTERNAL).removeCls(className);
+ d.removeListener("mouseup", fn);
+ };
+ d.on("mouseup", fn);
+ });
+ return this;
+ },
+
+ /**
+ * <p>Returns the dimensions of the element available to lay content out in.<p>
+ * <p>If the element (or any ancestor element) has CSS style <code>display : none</code>, the dimensions will be zero.</p>
+ * example:<pre><code>
+ var vpSize = Ext.getBody().getViewSize();
+
+ // all Windows created afterwards will have a default value of 90% height and 95% width
+ Ext.Window.override({
+ width: vpSize.width * 0.9,
+ height: vpSize.height * 0.95
+ });
+ // To handle window resizing you would have to hook onto onWindowResize.
+ * </code></pre>
+ *
+ * getViewSize utilizes clientHeight/clientWidth which excludes sizing of scrollbars.
+ * To obtain the size including scrollbars, use getStyleSize
+ *
+ * Sizing of the document body is handled at the adapter level which handles special cases for IE and strict modes, etc.
+ */
+
+ getViewSize : function(){
+ var me = this,
+ dom = me.dom,
+ isDoc = (dom == Ext.getDoc().dom || dom == Ext.getBody().dom),
+ style, overflow, ret;
+
+ // If the body, use static methods
+ if (isDoc) {
+ ret = {
+ width : ELEMENT.getViewWidth(),
+ height : ELEMENT.getViewHeight()
+ };
+
+ // Else use clientHeight/clientWidth
+ }
+ else {
+ // IE 6 & IE Quirks mode acts more like a max-size measurement unless overflow is hidden during measurement.
+ // We will put the overflow back to it's original value when we are done measuring.
+ if (Ext.isIE6 || Ext.isIEQuirks) {
+ style = dom.style;
+ overflow = style.overflow;
+ me.setStyle({ overflow: 'hidden'});
+ }
+ ret = {
+ width : dom.clientWidth,
+ height : dom.clientHeight
+ };
+ if (Ext.isIE6 || Ext.isIEQuirks) {
+ me.setStyle({ overflow: overflow });
+ }
+ }
+ return ret;
+ },
+
+ /**
+ * <p>Returns the dimensions of the element available to lay content out in.<p>
+ *
+ * getStyleSize utilizes prefers style sizing if present, otherwise it chooses the larger of offsetHeight/clientHeight and offsetWidth/clientWidth.
+ * To obtain the size excluding scrollbars, use getViewSize
+ *
+ * Sizing of the document body is handled at the adapter level which handles special cases for IE and strict modes, etc.
+ */
+
+ getStyleSize : function(){
+ var me = this,
+ doc = document,
+ d = this.dom,
+ isDoc = (d == doc || d == doc.body),
+ s = d.style,
+ w, h;
+
+ // If the body, use static methods
+ if (isDoc) {
+ return {
+ width : ELEMENT.getViewWidth(),
+ height : ELEMENT.getViewHeight()
+ };
+ }
+ // Use Styles if they are set
+ if(s.width && s.width != 'auto'){
+ w = parseFloat(s.width);
+ if(me.isBorderBox()){
+ w -= me.getFrameWidth('lr');
+ }
+ }
+ // Use Styles if they are set
+ if(s.height && s.height != 'auto'){
+ h = parseFloat(s.height);
+ if(me.isBorderBox()){
+ h -= me.getFrameWidth('tb');
+ }
+ }
+ // Use getWidth/getHeight if style not set.
+ return {width: w || me.getWidth(true), height: h || me.getHeight(true)};
+ },
+
+ /**
+ * Returns the size of the element.
+ * @param {Boolean} contentSize (optional) true to get the width/size minus borders and padding
+ * @return {Object} An object containing the element's size {width: (element width), height: (element height)}
+ */
+ getSize : function(contentSize){
+ return {width: this.getWidth(contentSize), height: this.getHeight(contentSize)};
+ },
+
+ /**
+ * Forces the browser to repaint this element
+ * @return {Ext.Element} this
+ */
+ repaint : function(){
+ var dom = this.dom;
+ this.addCls(Ext.baseCSSPrefix + 'repaint');
+ setTimeout(function(){
+ Ext.fly(dom).removeCls(Ext.baseCSSPrefix + 'repaint');
+ }, 1);
+ return this;
+ },
+
+ /**
+ * Enable text selection for this element (normalized across browsers)
+ * @return {Ext.Element} this
+ */
+ selectable : function() {
+ var me = this;
+ me.dom.unselectable = "off";
+ // Prevent it from bubles up and enables it to be selectable
+ me.on('selectstart', function (e) {
+ e.stopPropagation();
+ return true;
+ });
+ me.applyStyles("-moz-user-select: text; -khtml-user-select: text;");
+ me.removeCls(Ext.baseCSSPrefix + 'unselectable');
+ return me;
+ },
+
+ /**
+ * Disables text selection for this element (normalized across browsers)
+ * @return {Ext.Element} this
+ */
+ unselectable : function(){
+ var me = this;
+ me.dom.unselectable = "on";
+
+ me.swallowEvent("selectstart", true);
+ me.applyStyles("-moz-user-select:-moz-none;-khtml-user-select:none;");
+ me.addCls(Ext.baseCSSPrefix + 'unselectable');
+
+ return me;
+ },
+
+ /**
+ * Returns an object with properties top, left, right and bottom representing the margins of this element unless sides is passed,
+ * then it returns the calculated width of the sides (see getPadding)
+ * @param {String} sides (optional) Any combination of l, r, t, b to get the sum of those sides
+ * @return {Object/Number}
+ */
+ getMargin : function(side){
+ var me = this,
+ hash = {t:"top", l:"left", r:"right", b: "bottom"},
+ o = {},
+ key;
+
+ if (!side) {
+ for (key in me.margins){
+ o[hash[key]] = parseFloat(me.getStyle(me.margins[key])) || 0;
+ }
+ return o;
+ } else {
+ return me.addStyles.call(me, side, me.margins);
+ }
+ }
+ });
+})();
+/**
+ * @class Ext.Element
+ */
+/**
+ * Visibility mode constant for use with {@link #setVisibilityMode}. Use visibility to hide element
+ * @static
+ * @type Number
+ */
+Ext.Element.VISIBILITY = 1;
+/**
+ * Visibility mode constant for use with {@link #setVisibilityMode}. Use display to hide element
+ * @static
+ * @type Number
+ */
+Ext.Element.DISPLAY = 2;
+
+/**
+ * Visibility mode constant for use with {@link #setVisibilityMode}. Use offsets (x and y positioning offscreen)
+ * to hide element.
+ * @static
+ * @type Number
+ */
+Ext.Element.OFFSETS = 3;
+
+
+Ext.Element.ASCLASS = 4;
+
+/**
+ * Defaults to 'x-hide-nosize'
+ * @static
+ * @type String
+ */
+Ext.Element.visibilityCls = Ext.baseCSSPrefix + 'hide-nosize';
+
+Ext.Element.addMethods(function(){
+ var El = Ext.Element,
+ OPACITY = "opacity",
+ VISIBILITY = "visibility",
+ DISPLAY = "display",
+ HIDDEN = "hidden",
+ OFFSETS = "offsets",
+ ASCLASS = "asclass",
+ NONE = "none",
+ NOSIZE = 'nosize',
+ ORIGINALDISPLAY = 'originalDisplay',
+ VISMODE = 'visibilityMode',
+ ISVISIBLE = 'isVisible',
+ data = El.data,
+ getDisplay = function(dom){
+ var d = data(dom, ORIGINALDISPLAY);
+ if(d === undefined){
+ data(dom, ORIGINALDISPLAY, d = '');
+ }
+ return d;
+ },
+ getVisMode = function(dom){
+ var m = data(dom, VISMODE);
+ if(m === undefined){
+ data(dom, VISMODE, m = 1);
+ }
+ return m;
+ };
+
+ return {
+ /**
+ * @property {String} originalDisplay
+ * The element's default display mode
+ */
+ originalDisplay : "",
+ visibilityMode : 1,
+
+ /**
+ * Sets the element's visibility mode. When setVisible() is called it
+ * will use this to determine whether to set the visibility or the display property.
+ * @param {Number} visMode Ext.Element.VISIBILITY or Ext.Element.DISPLAY
+ * @return {Ext.Element} this
+ */
+ setVisibilityMode : function(visMode){
+ data(this.dom, VISMODE, visMode);
+ return this;
+ },
+
+ /**
+ * Checks whether the element is currently visible using both visibility and display properties.
+ * @return {Boolean} True if the element is currently visible, else false
+ */
+ isVisible : function() {
+ var me = this,
+ dom = me.dom,
+ visible = data(dom, ISVISIBLE);
+
+ if(typeof visible == 'boolean'){ //return the cached value if registered
+ return visible;
+ }
+ //Determine the current state based on display states
+ visible = !me.isStyle(VISIBILITY, HIDDEN) &&
+ !me.isStyle(DISPLAY, NONE) &&
+ !((getVisMode(dom) == El.ASCLASS) && me.hasCls(me.visibilityCls || El.visibilityCls));
+
+ data(dom, ISVISIBLE, visible);
+ return visible;
+ },
+
+ /**
+ * Sets the visibility of the element (see details). If the visibilityMode is set to Element.DISPLAY, it will use
+ * the display property to hide the element, otherwise it uses visibility. The default is to hide and show using the visibility property.
+ * @param {Boolean} visible Whether the element is visible
+ * @param {Boolean/Object} animate (optional) True for the default animation, or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ setVisible : function(visible, animate){
+ var me = this, isDisplay, isVisibility, isOffsets, isNosize,
+ dom = me.dom,
+ visMode = getVisMode(dom);
+
+
+ // hideMode string override
+ if (typeof animate == 'string'){
+ switch (animate) {
+ case DISPLAY:
+ visMode = El.DISPLAY;
+ break;
+ case VISIBILITY:
+ visMode = El.VISIBILITY;
+ break;
+ case OFFSETS:
+ visMode = El.OFFSETS;
+ break;
+ case NOSIZE:
+ case ASCLASS:
+ visMode = El.ASCLASS;
+ break;
+ }
+ me.setVisibilityMode(visMode);
+ animate = false;
+ }
+
+ if (!animate || !me.anim) {
+ if(visMode == El.ASCLASS ){
+
+ me[visible?'removeCls':'addCls'](me.visibilityCls || El.visibilityCls);
+
+ } else if (visMode == El.DISPLAY){
+
+ return me.setDisplayed(visible);
+
+ } else if (visMode == El.OFFSETS){
+
+ if (!visible){
+ // Remember position for restoring, if we are not already hidden by offsets.
+ if (!me.hideModeStyles) {
+ me.hideModeStyles = {
+ position: me.getStyle('position'),
+ top: me.getStyle('top'),
+ left: me.getStyle('left')
+ };
+ }
+ me.applyStyles({position: 'absolute', top: '-10000px', left: '-10000px'});
+ }
+
+ // Only "restore" as position if we have actually been hidden using offsets.
+ // Calling setVisible(true) on a positioned element should not reposition it.
+ else if (me.hideModeStyles) {
+ me.applyStyles(me.hideModeStyles || {position: '', top: '', left: ''});
+ delete me.hideModeStyles;
+ }
+
+ }else{
+ me.fixDisplay();
+ // Show by clearing visibility style. Explicitly setting to "visible" overrides parent visibility setting.
+ dom.style.visibility = visible ? '' : HIDDEN;
+ }
+ }else{
+ // closure for composites
+ if(visible){
+ me.setOpacity(0.01);
+ me.setVisible(true);
+ }
+ if (!Ext.isObject(animate)) {
+ animate = {
+ duration: 350,
+ easing: 'ease-in'
+ };
+ }
+ me.animate(Ext.applyIf({
+ callback: function() {
+ visible || me.setVisible(false).setOpacity(1);
+ },
+ to: {
+ opacity: (visible) ? 1 : 0
+ }
+ }, animate));
+ }
+ data(dom, ISVISIBLE, visible); //set logical visibility state
+ return me;
+ },
+
+
+ /**
+ * @private
+ * Determine if the Element has a relevant height and width available based
+ * upon current logical visibility state
+ */
+ hasMetrics : function(){
+ var dom = this.dom;
+ return this.isVisible() || (getVisMode(dom) == El.OFFSETS) || (getVisMode(dom) == El.VISIBILITY);
+ },
+
+ /**
+ * Toggles the element's visibility or display, depending on visibility mode.
+ * @param {Boolean/Object} animate (optional) True for the default animation, or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ toggle : function(animate){
+ var me = this;
+ me.setVisible(!me.isVisible(), me.anim(animate));
+ return me;
+ },
+
+ /**
+ * Sets the CSS display property. Uses originalDisplay if the specified value is a boolean true.
+ * @param {Boolean/String} value Boolean value to display the element using its default display, or a string to set the display directly.
+ * @return {Ext.Element} this
+ */
+ setDisplayed : function(value) {
+ if(typeof value == "boolean"){
+ value = value ? getDisplay(this.dom) : NONE;
+ }
+ this.setStyle(DISPLAY, value);
+ return this;
+ },
+
+ // private
+ fixDisplay : function(){
+ var me = this;
+ if (me.isStyle(DISPLAY, NONE)) {
+ me.setStyle(VISIBILITY, HIDDEN);
+ me.setStyle(DISPLAY, getDisplay(this.dom)); // first try reverting to default
+ if (me.isStyle(DISPLAY, NONE)) { // if that fails, default to block
+ me.setStyle(DISPLAY, "block");
+ }
+ }
+ },
+
+ /**
+ * Hide this element - Uses display mode to determine whether to use "display" or "visibility". See {@link #setVisible}.
+ * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ hide : function(animate){
+ // hideMode override
+ if (typeof animate == 'string'){
+ this.setVisible(false, animate);
+ return this;
+ }
+ this.setVisible(false, this.anim(animate));
+ return this;
+ },
+
+ /**
+ * Show this element - Uses display mode to determine whether to use "display" or "visibility". See {@link #setVisible}.
+ * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ show : function(animate){
+ // hideMode override
+ if (typeof animate == 'string'){
+ this.setVisible(true, animate);
+ return this;
+ }
+ this.setVisible(true, this.anim(animate));
+ return this;
+ }
+ };
+}());
+/**
+ * @class Ext.Element
+ */
+Ext.applyIf(Ext.Element.prototype, {
+ // @private override base Ext.util.Animate mixin for animate for backwards compatibility
+ animate: function(config) {
+ var me = this;
+ if (!me.id) {
+ me = Ext.get(me.dom);
+ }
+ if (Ext.fx.Manager.hasFxBlock(me.id)) {
+ return me;
+ }
+ Ext.fx.Manager.queueFx(Ext.create('Ext.fx.Anim', me.anim(config)));
+ return this;
+ },
+
+ // @private override base Ext.util.Animate mixin for animate for backwards compatibility
+ anim: function(config) {
+ if (!Ext.isObject(config)) {
+ return (config) ? {} : false;
+ }
+
+ var me = this,
+ duration = config.duration || Ext.fx.Anim.prototype.duration,
+ easing = config.easing || 'ease',
+ animConfig;
+
+ if (config.stopAnimation) {
+ me.stopAnimation();
+ }
+
+ Ext.applyIf(config, Ext.fx.Manager.getFxDefaults(me.id));
+
+ // Clear any 'paused' defaults.
+ Ext.fx.Manager.setFxDefaults(me.id, {
+ delay: 0
+ });
+
+ animConfig = {
+ target: me,
+ remove: config.remove,
+ alternate: config.alternate || false,
+ duration: duration,
+ easing: easing,
+ callback: config.callback,
+ listeners: config.listeners,
+ iterations: config.iterations || 1,
+ scope: config.scope,
+ block: config.block,
+ concurrent: config.concurrent,
+ delay: config.delay || 0,
+ paused: true,
+ keyframes: config.keyframes,
+ from: config.from || {},
+ to: Ext.apply({}, config)
+ };
+ Ext.apply(animConfig.to, config.to);
+
+ // Anim API properties - backward compat
+ delete animConfig.to.to;
+ delete animConfig.to.from;
+ delete animConfig.to.remove;
+ delete animConfig.to.alternate;
+ delete animConfig.to.keyframes;
+ delete animConfig.to.iterations;
+ delete animConfig.to.listeners;
+ delete animConfig.to.target;
+ delete animConfig.to.paused;
+ delete animConfig.to.callback;
+ delete animConfig.to.scope;
+ delete animConfig.to.duration;
+ delete animConfig.to.easing;
+ delete animConfig.to.concurrent;
+ delete animConfig.to.block;
+ delete animConfig.to.stopAnimation;
+ delete animConfig.to.delay;
+ return animConfig;
+ },
+
+ /**
+ * Slides the element into view. An anchor point can be optionally passed to set the point of origin for the slide
+ * effect. This function automatically handles wrapping the element with a fixed-size container if needed. See the
+ * Fx class overview for valid anchor point options. Usage:
+ *
+ * // default: slide the element in from the top
+ * el.slideIn();
+ *
+ * // custom: slide the element in from the right with a 2-second duration
+ * el.slideIn('r', { duration: 2000 });
+ *
+ * // common config options shown with default values
+ * el.slideIn('t', {
+ * easing: 'easeOut',
+ * duration: 500
+ * });
+ *
+ * @param {String} [anchor='t'] One of the valid Fx anchor positions
+ * @param {Object} [options] Object literal with any of the Fx config options
+ * @return {Ext.Element} The Element
+ */
+ slideIn: function(anchor, obj, slideOut) {
+ var me = this,
+ elStyle = me.dom.style,
+ beforeAnim, wrapAnim;
+
+ anchor = anchor || "t";
+ obj = obj || {};
+
+ beforeAnim = function() {
+ var animScope = this,
+ listeners = obj.listeners,
+ box, position, restoreSize, wrap, anim;
+
+ if (!slideOut) {
+ me.fixDisplay();
+ }
+
+ box = me.getBox();
+ if ((anchor == 't' || anchor == 'b') && box.height === 0) {
+ box.height = me.dom.scrollHeight;
+ }
+ else if ((anchor == 'l' || anchor == 'r') && box.width === 0) {
+ box.width = me.dom.scrollWidth;
+ }
+
+ position = me.getPositioning();
+ me.setSize(box.width, box.height);
+
+ wrap = me.wrap({
+ style: {
+ visibility: slideOut ? 'visible' : 'hidden'
+ }
+ });
+ wrap.setPositioning(position);
+ if (wrap.isStyle('position', 'static')) {
+ wrap.position('relative');
+ }
+ me.clearPositioning('auto');
+ wrap.clip();
+
+ // This element is temporarily positioned absolute within its wrapper.
+ // Restore to its default, CSS-inherited visibility setting.
+ // We cannot explicitly poke visibility:visible into its style because that overrides the visibility of the wrap.
+ me.setStyle({
+ visibility: '',
+ position: 'absolute'
+ });
+ if (slideOut) {
+ wrap.setSize(box.width, box.height);
+ }
+
+ switch (anchor) {
+ case 't':
+ anim = {
+ from: {
+ width: box.width + 'px',
+ height: '0px'
+ },
+ to: {
+ width: box.width + 'px',
+ height: box.height + 'px'
+ }
+ };
+ elStyle.bottom = '0px';
+ break;
+ case 'l':
+ anim = {
+ from: {
+ width: '0px',
+ height: box.height + 'px'
+ },
+ to: {
+ width: box.width + 'px',
+ height: box.height + 'px'
+ }
+ };
+ elStyle.right = '0px';
+ break;
+ case 'r':
+ anim = {
+ from: {
+ x: box.x + box.width,
+ width: '0px',
+ height: box.height + 'px'
+ },
+ to: {
+ x: box.x,
+ width: box.width + 'px',
+ height: box.height + 'px'
+ }
+ };
+ break;
+ case 'b':
+ anim = {
+ from: {
+ y: box.y + box.height,
+ width: box.width + 'px',
+ height: '0px'
+ },
+ to: {
+ y: box.y,
+ width: box.width + 'px',
+ height: box.height + 'px'
+ }
+ };
+ break;
+ case 'tl':
+ anim = {
+ from: {
+ x: box.x,
+ y: box.y,
+ width: '0px',
+ height: '0px'
+ },
+ to: {
+ width: box.width + 'px',
+ height: box.height + 'px'
+ }
+ };
+ elStyle.bottom = '0px';
+ elStyle.right = '0px';
+ break;
+ case 'bl':
+ anim = {
+ from: {
+ x: box.x + box.width,
+ width: '0px',
+ height: '0px'
+ },
+ to: {
+ x: box.x,
+ width: box.width + 'px',
+ height: box.height + 'px'
+ }
+ };
+ elStyle.right = '0px';
+ break;
+ case 'br':
+ anim = {
+ from: {
+ x: box.x + box.width,
+ y: box.y + box.height,
+ width: '0px',
+ height: '0px'
+ },
+ to: {
+ x: box.x,
+ y: box.y,
+ width: box.width + 'px',
+ height: box.height + 'px'
+ }
+ };
+ break;
+ case 'tr':
+ anim = {
+ from: {
+ y: box.y + box.height,
+ width: '0px',
+ height: '0px'
+ },
+ to: {
+ y: box.y,
+ width: box.width + 'px',
+ height: box.height + 'px'
+ }
+ };
+ elStyle.bottom = '0px';
+ break;
+ }
+
+ wrap.show();
+ wrapAnim = Ext.apply({}, obj);
+ delete wrapAnim.listeners;
+ wrapAnim = Ext.create('Ext.fx.Anim', Ext.applyIf(wrapAnim, {
+ target: wrap,
+ duration: 500,
+ easing: 'ease-out',
+ from: slideOut ? anim.to : anim.from,
+ to: slideOut ? anim.from : anim.to
+ }));
+
+ // In the absence of a callback, this listener MUST be added first
+ wrapAnim.on('afteranimate', function() {
+ if (slideOut) {
+ me.setPositioning(position);
+ if (obj.useDisplay) {
+ me.setDisplayed(false);
+ } else {
+ me.hide();
+ }
+ }
+ else {
+ me.clearPositioning();
+ me.setPositioning(position);
+ }
+ if (wrap.dom) {
+ wrap.dom.parentNode.insertBefore(me.dom, wrap.dom);
+ wrap.remove();
+ }
+ me.setSize(box.width, box.height);
+ animScope.end();
+ });
+ // Add configured listeners after
+ if (listeners) {
+ wrapAnim.on(listeners);
+ }
+ };
+
+ me.animate({
+ duration: obj.duration ? obj.duration * 2 : 1000,
+ listeners: {
+ beforeanimate: {
+ fn: beforeAnim
+ },
+ afteranimate: {
+ fn: function() {
+ if (wrapAnim && wrapAnim.running) {
+ wrapAnim.end();
+ }
+ }
+ }
+ }
+ });
+ return me;
+ },
+
+
+ /**
+ * Slides the element out of view. An anchor point can be optionally passed to set the end point for the slide
+ * effect. When the effect is completed, the element will be hidden (visibility = 'hidden') but block elements will
+ * still take up space in the document. The element must be removed from the DOM using the 'remove' config option if
+ * desired. This function automatically handles wrapping the element with a fixed-size container if needed. See the
+ * Fx class overview for valid anchor point options. Usage:
+ *
+ * // default: slide the element out to the top
+ * el.slideOut();
+ *
+ * // custom: slide the element out to the right with a 2-second duration
+ * el.slideOut('r', { duration: 2000 });
+ *
+ * // common config options shown with default values
+ * el.slideOut('t', {
+ * easing: 'easeOut',
+ * duration: 500,
+ * remove: false,
+ * useDisplay: false
+ * });
+ *
+ * @param {String} [anchor='t'] One of the valid Fx anchor positions
+ * @param {Object} [options] Object literal with any of the Fx config options
+ * @return {Ext.Element} The Element
+ */
+ slideOut: function(anchor, o) {
+ return this.slideIn(anchor, o, true);
+ },
+
+ /**
+ * Fades the element out while slowly expanding it in all directions. When the effect is completed, the element will
+ * be hidden (visibility = 'hidden') but block elements will still take up space in the document. Usage:
+ *
+ * // default
+ * el.puff();
+ *
+ * // common config options shown with default values
+ * el.puff({
+ * easing: 'easeOut',
+ * duration: 500,
+ * useDisplay: false
+ * });
+ *
+ * @param {Object} options (optional) Object literal with any of the Fx config options
+ * @return {Ext.Element} The Element
+ */
+ puff: function(obj) {
+ var me = this,
+ beforeAnim;
+ obj = Ext.applyIf(obj || {}, {
+ easing: 'ease-out',
+ duration: 500,
+ useDisplay: false
+ });
+
+ beforeAnim = function() {
+ me.clearOpacity();
+ me.show();
+
+ var box = me.getBox(),
+ fontSize = me.getStyle('fontSize'),
+ position = me.getPositioning();
+ this.to = {
+ width: box.width * 2,
+ height: box.height * 2,
+ x: box.x - (box.width / 2),
+ y: box.y - (box.height /2),
+ opacity: 0,
+ fontSize: '200%'
+ };
+ this.on('afteranimate',function() {
+ if (me.dom) {
+ if (obj.useDisplay) {
+ me.setDisplayed(false);
+ } else {
+ me.hide();
+ }
+ me.clearOpacity();
+ me.setPositioning(position);
+ me.setStyle({fontSize: fontSize});
+ }
+ });
+ };
+
+ me.animate({
+ duration: obj.duration,
+ easing: obj.easing,
+ listeners: {
+ beforeanimate: {
+ fn: beforeAnim
+ }
+ }
+ });
+ return me;
+ },
+
+ /**
+ * Blinks the element as if it was clicked and then collapses on its center (similar to switching off a television).
+ * When the effect is completed, the element will be hidden (visibility = 'hidden') but block elements will still
+ * take up space in the document. The element must be removed from the DOM using the 'remove' config option if
+ * desired. Usage:
+ *
+ * // default
+ * el.switchOff();
+ *
+ * // all config options shown with default values
+ * el.switchOff({
+ * easing: 'easeIn',
+ * duration: .3,
+ * remove: false,
+ * useDisplay: false
+ * });
+ *
+ * @param {Object} options (optional) Object literal with any of the Fx config options
+ * @return {Ext.Element} The Element
+ */
+ switchOff: function(obj) {
+ var me = this,
+ beforeAnim;
+
+ obj = Ext.applyIf(obj || {}, {
+ easing: 'ease-in',
+ duration: 500,
+ remove: false,
+ useDisplay: false
+ });
+
+ beforeAnim = function() {
+ var animScope = this,
+ size = me.getSize(),
+ xy = me.getXY(),
+ keyframe, position;
+ me.clearOpacity();
+ me.clip();
+ position = me.getPositioning();
+
+ keyframe = Ext.create('Ext.fx.Animator', {
+ target: me,
+ duration: obj.duration,
+ easing: obj.easing,
+ keyframes: {
+ 33: {
+ opacity: 0.3
+ },
+ 66: {
+ height: 1,
+ y: xy[1] + size.height / 2
+ },
+ 100: {
+ width: 1,
+ x: xy[0] + size.width / 2
+ }
+ }
+ });
+ keyframe.on('afteranimate', function() {
+ if (obj.useDisplay) {
+ me.setDisplayed(false);
+ } else {
+ me.hide();
+ }
+ me.clearOpacity();
+ me.setPositioning(position);
+ me.setSize(size);
+ animScope.end();
+ });
+ };
+ me.animate({
+ duration: (obj.duration * 2),
+ listeners: {
+ beforeanimate: {
+ fn: beforeAnim
+ }
+ }
+ });
+ return me;
+ },
+
+ /**
+ * Shows a ripple of exploding, attenuating borders to draw attention to an Element. Usage:
+ *
+ * // default: a single light blue ripple
+ * el.frame();
+ *
+ * // custom: 3 red ripples lasting 3 seconds total
+ * el.frame("#ff0000", 3, { duration: 3 });
+ *
+ * // common config options shown with default values
+ * el.frame("#C3DAF9", 1, {
+ * duration: 1 //duration of each individual ripple.
+ * // Note: Easing is not configurable and will be ignored if included
+ * });
+ *
+ * @param {String} [color='C3DAF9'] The color of the border. Should be a 6 char hex color without the leading #
+ * (defaults to light blue).
+ * @param {Number} [count=1] The number of ripples to display
+ * @param {Object} [options] Object literal with any of the Fx config options
+ * @return {Ext.Element} The Element
+ */
+ frame : function(color, count, obj){
+ var me = this,
+ beforeAnim;
+
+ color = color || '#C3DAF9';
+ count = count || 1;
+ obj = obj || {};
+
+ beforeAnim = function() {
+ me.show();
+ var animScope = this,
+ box = me.getBox(),
+ proxy = Ext.getBody().createChild({
+ style: {
+ position : 'absolute',
+ 'pointer-events': 'none',
+ 'z-index': 35000,
+ border : '0px solid ' + color
+ }
+ }),
+ proxyAnim;
+ proxyAnim = Ext.create('Ext.fx.Anim', {
+ target: proxy,
+ duration: obj.duration || 1000,
+ iterations: count,
+ from: {
+ top: box.y,
+ left: box.x,
+ borderWidth: 0,
+ opacity: 1,
+ height: box.height,
+ width: box.width
+ },
+ to: {
+ top: box.y - 20,
+ left: box.x - 20,
+ borderWidth: 10,
+ opacity: 0,
+ height: box.height + 40,
+ width: box.width + 40
+ }
+ });
+ proxyAnim.on('afteranimate', function() {
+ proxy.remove();
+ animScope.end();
+ });
+ };
+
+ me.animate({
+ duration: (obj.duration * 2) || 2000,
+ listeners: {
+ beforeanimate: {
+ fn: beforeAnim
+ }
+ }
+ });
+ return me;
+ },
+
+ /**
+ * Slides the element while fading it out of view. An anchor point can be optionally passed to set the ending point
+ * of the effect. Usage:
+ *
+ * // default: slide the element downward while fading out
+ * el.ghost();
+ *
+ * // custom: slide the element out to the right with a 2-second duration
+ * el.ghost('r', { duration: 2000 });
+ *
+ * // common config options shown with default values
+ * el.ghost('b', {
+ * easing: 'easeOut',
+ * duration: 500
+ * });
+ *
+ * @param {String} [anchor='b'] One of the valid Fx anchor positions
+ * @param {Object} [options] Object literal with any of the Fx config options
+ * @return {Ext.Element} The Element
+ */
+ ghost: function(anchor, obj) {
+ var me = this,
+ beforeAnim;
+
+ anchor = anchor || "b";
+ beforeAnim = function() {
+ var width = me.getWidth(),
+ height = me.getHeight(),
+ xy = me.getXY(),
+ position = me.getPositioning(),
+ to = {
+ opacity: 0
+ };
+ switch (anchor) {
+ case 't':
+ to.y = xy[1] - height;
+ break;
+ case 'l':
+ to.x = xy[0] - width;
+ break;
+ case 'r':
+ to.x = xy[0] + width;
+ break;
+ case 'b':
+ to.y = xy[1] + height;
+ break;
+ case 'tl':
+ to.x = xy[0] - width;
+ to.y = xy[1] - height;
+ break;
+ case 'bl':
+ to.x = xy[0] - width;
+ to.y = xy[1] + height;
+ break;
+ case 'br':
+ to.x = xy[0] + width;
+ to.y = xy[1] + height;
+ break;
+ case 'tr':
+ to.x = xy[0] + width;
+ to.y = xy[1] - height;
+ break;
+ }
+ this.to = to;
+ this.on('afteranimate', function () {
+ if (me.dom) {
+ me.hide();
+ me.clearOpacity();
+ me.setPositioning(position);
+ }
+ });
+ };
+
+ me.animate(Ext.applyIf(obj || {}, {
+ duration: 500,
+ easing: 'ease-out',
+ listeners: {
+ beforeanimate: {
+ fn: beforeAnim
+ }
+ }
+ }));
+ return me;
+ },
+
+ /**
+ * Highlights the Element by setting a color (applies to the background-color by default, but can be changed using
+ * the "attr" config option) and then fading back to the original color. If no original color is available, you
+ * should provide the "endColor" config option which will be cleared after the animation. Usage:
+ *
+ * // default: highlight background to yellow
+ * el.highlight();
+ *
+ * // custom: highlight foreground text to blue for 2 seconds
+ * el.highlight("0000ff", { attr: 'color', duration: 2000 });
+ *
+ * // common config options shown with default values
+ * el.highlight("ffff9c", {
+ * attr: "backgroundColor", //can be any valid CSS property (attribute) that supports a color value
+ * endColor: (current color) or "ffffff",
+ * easing: 'easeIn',
+ * duration: 1000
+ * });
+ *
+ * @param {String} [color='ffff9c'] The highlight color. Should be a 6 char hex color without the leading #
+ * @param {Object} [options] Object literal with any of the Fx config options
+ * @return {Ext.Element} The Element
+ */
+ highlight: function(color, o) {
+ var me = this,
+ dom = me.dom,
+ from = {},
+ restore, to, attr, lns, event, fn;
+
+ o = o || {};
+ lns = o.listeners || {};
+ attr = o.attr || 'backgroundColor';
+ from[attr] = color || 'ffff9c';
+
+ if (!o.to) {
+ to = {};
+ to[attr] = o.endColor || me.getColor(attr, 'ffffff', '');
+ }
+ else {
+ to = o.to;
+ }
+
+ // Don't apply directly on lns, since we reference it in our own callbacks below
+ o.listeners = Ext.apply(Ext.apply({}, lns), {
+ beforeanimate: function() {
+ restore = dom.style[attr];
+ me.clearOpacity();
+ me.show();
+
+ event = lns.beforeanimate;
+ if (event) {
+ fn = event.fn || event;
+ return fn.apply(event.scope || lns.scope || window, arguments);
+ }
+ },
+ afteranimate: function() {
+ if (dom) {
+ dom.style[attr] = restore;
+ }
+
+ event = lns.afteranimate;
+ if (event) {
+ fn = event.fn || event;
+ fn.apply(event.scope || lns.scope || window, arguments);
+ }
+ }
+ });
+
+ me.animate(Ext.apply({}, o, {
+ duration: 1000,
+ easing: 'ease-in',
+ from: from,
+ to: to
+ }));
+ return me;
+ },
+
+ /**
+ * @deprecated 4.0
+ * Creates a pause before any subsequent queued effects begin. If there are no effects queued after the pause it will
+ * have no effect. Usage:
+ *
+ * el.pause(1);
+ *
+ * @param {Number} seconds The length of time to pause (in seconds)
+ * @return {Ext.Element} The Element
+ */
+ pause: function(ms) {
+ var me = this;
+ Ext.fx.Manager.setFxDefaults(me.id, {
+ delay: ms
+ });
+ return me;
+ },
+
+ /**
+ * Fade an element in (from transparent to opaque). The ending opacity can be specified using the `opacity`
+ * config option. Usage:
+ *
+ * // default: fade in from opacity 0 to 100%
+ * el.fadeIn();
+ *
+ * // custom: fade in from opacity 0 to 75% over 2 seconds
+ * el.fadeIn({ opacity: .75, duration: 2000});
+ *
+ * // common config options shown with default values
+ * el.fadeIn({
+ * opacity: 1, //can be any value between 0 and 1 (e.g. .5)
+ * easing: 'easeOut',
+ * duration: 500
+ * });
+ *
+ * @param {Object} options (optional) Object literal with any of the Fx config options
+ * @return {Ext.Element} The Element
+ */
+ fadeIn: function(o) {
+ this.animate(Ext.apply({}, o, {
+ opacity: 1
+ }));
+ return this;
+ },
+
+ /**
+ * Fade an element out (from opaque to transparent). The ending opacity can be specified using the `opacity`
+ * config option. Note that IE may require `useDisplay:true` in order to redisplay correctly.
+ * Usage:
+ *
+ * // default: fade out from the element's current opacity to 0
+ * el.fadeOut();
+ *
+ * // custom: fade out from the element's current opacity to 25% over 2 seconds
+ * el.fadeOut({ opacity: .25, duration: 2000});
+ *
+ * // common config options shown with default values
+ * el.fadeOut({
+ * opacity: 0, //can be any value between 0 and 1 (e.g. .5)
+ * easing: 'easeOut',
+ * duration: 500,
+ * remove: false,
+ * useDisplay: false
+ * });
+ *
+ * @param {Object} options (optional) Object literal with any of the Fx config options
+ * @return {Ext.Element} The Element
+ */
+ fadeOut: function(o) {
+ this.animate(Ext.apply({}, o, {
+ opacity: 0
+ }));
+ return this;
+ },
+
+ /**
+ * @deprecated 4.0
+ * Animates the transition of an element's dimensions from a starting height/width to an ending height/width. This
+ * method is a convenience implementation of {@link #shift}. Usage:
+ *
+ * // change height and width to 100x100 pixels
+ * el.scale(100, 100);
+ *
+ * // common config options shown with default values. The height and width will default to
+ * // the element's existing values if passed as null.
+ * el.scale(
+ * [element's width],
+ * [element's height], {
+ * easing: 'easeOut',
+ * duration: .35
+ * }
+ * );
+ *
+ * @param {Number} width The new width (pass undefined to keep the original width)
+ * @param {Number} height The new height (pass undefined to keep the original height)
+ * @param {Object} options (optional) Object literal with any of the Fx config options
+ * @return {Ext.Element} The Element
+ */
+ scale: function(w, h, o) {
+ this.animate(Ext.apply({}, o, {
+ width: w,
+ height: h
+ }));
+ return this;
+ },
+
+ /**
+ * @deprecated 4.0
+ * Animates the transition of any combination of an element's dimensions, xy position and/or opacity. Any of these
+ * properties not specified in the config object will not be changed. This effect requires that at least one new
+ * dimension, position or opacity setting must be passed in on the config object in order for the function to have
+ * any effect. Usage:
+ *
+ * // slide the element horizontally to x position 200 while changing the height and opacity
+ * el.shift({ x: 200, height: 50, opacity: .8 });
+ *
+ * // common config options shown with default values.
+ * el.shift({
+ * width: [element's width],
+ * height: [element's height],
+ * x: [element's x position],
+ * y: [element's y position],
+ * opacity: [element's opacity],
+ * easing: 'easeOut',
+ * duration: .35
+ * });
+ *
+ * @param {Object} options Object literal with any of the Fx config options
+ * @return {Ext.Element} The Element
+ */
+ shift: function(config) {
+ this.animate(config);
+ return this;
+ }
+});
+
+/**
+ * @class Ext.Element
+ */
+Ext.applyIf(Ext.Element, {
+ unitRe: /\d+(px|em|%|en|ex|pt|in|cm|mm|pc)$/i,
+ camelRe: /(-[a-z])/gi,
+ opacityRe: /alpha\(opacity=(.*)\)/i,
+ cssRe: /([a-z0-9-]+)\s*:\s*([^;\s]+(?:\s*[^;\s]+)*);?/gi,
+ propertyCache: {},
+ defaultUnit : "px",
+ borders: {l: 'border-left-width', r: 'border-right-width', t: 'border-top-width', b: 'border-bottom-width'},
+ paddings: {l: 'padding-left', r: 'padding-right', t: 'padding-top', b: 'padding-bottom'},
+ margins: {l: 'margin-left', r: 'margin-right', t: 'margin-top', b: 'margin-bottom'},
+
+ // Reference the prototype's version of the method. Signatures are identical.
+ addUnits : Ext.Element.prototype.addUnits,
+
+ /**
+ * Parses a number or string representing margin sizes into an object. Supports CSS-style margin declarations
+ * (e.g. 10, "10", "10 10", "10 10 10" and "10 10 10 10" are all valid options and would return the same result)
+ * @static
+ * @param {Number/String} box The encoded margins
+ * @return {Object} An object with margin sizes for top, right, bottom and left
+ */
+ parseBox : function(box) {
+ if (Ext.isObject(box)) {
+ return {
+ top: box.top || 0,
+ right: box.right || 0,
+ bottom: box.bottom || 0,
+ left: box.left || 0
+ };
+ } else {
+ if (typeof box != 'string') {
+ box = box.toString();
+ }
+ var parts = box.split(' '),
+ ln = parts.length;
+
+ if (ln == 1) {
+ parts[1] = parts[2] = parts[3] = parts[0];
+ }
+ else if (ln == 2) {
+ parts[2] = parts[0];
+ parts[3] = parts[1];
+ }
+ else if (ln == 3) {
+ parts[3] = parts[1];
+ }
+
+ return {
+ top :parseFloat(parts[0]) || 0,
+ right :parseFloat(parts[1]) || 0,
+ bottom:parseFloat(parts[2]) || 0,
+ left :parseFloat(parts[3]) || 0
+ };
+ }
+
+ },
+
+ /**
+ * Parses a number or string representing margin sizes into an object. Supports CSS-style margin declarations
+ * (e.g. 10, "10", "10 10", "10 10 10" and "10 10 10 10" are all valid options and would return the same result)
+ * @static
+ * @param {Number/String} box The encoded margins
+ * @param {String} units The type of units to add
+ * @return {String} An string with unitized (px if units is not specified) metrics for top, right, bottom and left
+ */
+ unitizeBox : function(box, units) {
+ var A = this.addUnits,
+ B = this.parseBox(box);
+
+ return A(B.top, units) + ' ' +
+ A(B.right, units) + ' ' +
+ A(B.bottom, units) + ' ' +
+ A(B.left, units);
+
+ },
+
+ // private
+ camelReplaceFn : function(m, a) {
+ return a.charAt(1).toUpperCase();
+ },
+
+ /**
+ * Normalizes CSS property keys from dash delimited to camel case JavaScript Syntax.
+ * For example:
+ * <ul>
+ * <li>border-width -> borderWidth</li>
+ * <li>padding-top -> paddingTop</li>
+ * </ul>
+ * @static
+ * @param {String} prop The property to normalize
+ * @return {String} The normalized string
+ */
+ normalize : function(prop) {
+ if (prop == 'float') {
+ prop = Ext.supports.Float ? 'cssFloat' : 'styleFloat';
+ }
+ return this.propertyCache[prop] || (this.propertyCache[prop] = prop.replace(this.camelRe, this.camelReplaceFn));
+ },
+
+ /**
+ * Retrieves the document height
+ * @static
+ * @return {Number} documentHeight
+ */
+ getDocumentHeight: function() {
+ return Math.max(!Ext.isStrict ? document.body.scrollHeight : document.documentElement.scrollHeight, this.getViewportHeight());
+ },
+
+ /**
+ * Retrieves the document width
+ * @static
+ * @return {Number} documentWidth
+ */
+ getDocumentWidth: function() {
+ return Math.max(!Ext.isStrict ? document.body.scrollWidth : document.documentElement.scrollWidth, this.getViewportWidth());
+ },
+
+ /**
+ * Retrieves the viewport height of the window.
+ * @static
+ * @return {Number} viewportHeight
+ */
+ getViewportHeight: function(){
+ return window.innerHeight;
+ },
+
+ /**
+ * Retrieves the viewport width of the window.
+ * @static
+ * @return {Number} viewportWidth
+ */
+ getViewportWidth : function() {
+ return window.innerWidth;
+ },
+
+ /**
+ * Retrieves the viewport size of the window.
+ * @static
+ * @return {Object} object containing width and height properties
+ */
+ getViewSize : function() {
+ return {
+ width: window.innerWidth,
+ height: window.innerHeight
+ };
+ },
+
+ /**
+ * Retrieves the current orientation of the window. This is calculated by
+ * determing if the height is greater than the width.
+ * @static
+ * @return {String} Orientation of window: 'portrait' or 'landscape'
+ */
+ getOrientation : function() {
+ if (Ext.supports.OrientationChange) {
+ return (window.orientation == 0) ? 'portrait' : 'landscape';
+ }
+
+ return (window.innerHeight > window.innerWidth) ? 'portrait' : 'landscape';
+ },
+
+ /**
+ * Returns the top Element that is located at the passed coordinates
+ * @static
+ * @param {Number} x The x coordinate
+ * @param {Number} y The y coordinate
+ * @return {String} The found Element
+ */
+ fromPoint: function(x, y) {
+ return Ext.get(document.elementFromPoint(x, y));
+ },
+
+ /**
+ * Converts a CSS string into an object with a property for each style.
+ * <p>
+ * The sample code below would return an object with 2 properties, one
+ * for background-color and one for color.</p>
+ * <pre><code>
+var css = 'background-color: red;color: blue; ';
+console.log(Ext.Element.parseStyles(css));
+ * </code></pre>
+ * @static
+ * @param {String} styles A CSS string
+ * @return {Object} styles
+ */
+ parseStyles: function(styles){
+ var out = {},
+ cssRe = this.cssRe,
+ matches;
+
+ if (styles) {
+ // Since we're using the g flag on the regex, we need to set the lastIndex.
+ // This automatically happens on some implementations, but not others, see:
+ // http://stackoverflow.com/questions/2645273/javascript-regular-expression-literal-persists-between-function-calls
+ // http://blog.stevenlevithan.com/archives/fixing-javascript-regexp
+ cssRe.lastIndex = 0;
+ while ((matches = cssRe.exec(styles))) {
+ out[matches[1]] = matches[2];
+ }
+ }
+ return out;
+ }
+});
+
+/**
+ * @class Ext.CompositeElementLite
+ * <p>This class encapsulates a <i>collection</i> of DOM elements, providing methods to filter
+ * members, or to perform collective actions upon the whole set.</p>
+ * <p>Although they are not listed, this class supports all of the methods of {@link Ext.Element} and
+ * {@link Ext.fx.Anim}. The methods from these classes will be performed on all the elements in this collection.</p>
+ * Example:<pre><code>
+var els = Ext.select("#some-el div.some-class");
+// or select directly from an existing element
+var el = Ext.get('some-el');
+el.select('div.some-class');
+
+els.setWidth(100); // all elements become 100 width
+els.hide(true); // all elements fade out and hide
+// or
+els.setWidth(100).hide(true);
+</code></pre>
+ */
+Ext.CompositeElementLite = function(els, root){
+ /**
+ * <p>The Array of DOM elements which this CompositeElement encapsulates. Read-only.</p>
+ * <p>This will not <i>usually</i> be accessed in developers' code, but developers wishing
+ * to augment the capabilities of the CompositeElementLite class may use it when adding
+ * methods to the class.</p>
+ * <p>For example to add the <code>nextAll</code> method to the class to <b>add</b> all
+ * following siblings of selected elements, the code would be</p><code><pre>
+Ext.override(Ext.CompositeElementLite, {
+ nextAll: function() {
+ var els = this.elements, i, l = els.length, n, r = [], ri = -1;
+
+// Loop through all elements in this Composite, accumulating
+// an Array of all siblings.
+ for (i = 0; i < l; i++) {
+ for (n = els[i].nextSibling; n; n = n.nextSibling) {
+ r[++ri] = n;
+ }
+ }
+
+// Add all found siblings to this Composite
+ return this.add(r);
+ }
+});</pre></code>
+ * @property {HTMLElement} elements
+ */
+ this.elements = [];
+ this.add(els, root);
+ this.el = new Ext.Element.Flyweight();
+};
+
+Ext.CompositeElementLite.prototype = {
+ isComposite: true,
+
+ // private
+ getElement : function(el){
+ // Set the shared flyweight dom property to the current element
+ var e = this.el;
+ e.dom = el;
+ e.id = el.id;
+ return e;
+ },
+
+ // private
+ transformElement : function(el){
+ return Ext.getDom(el);
+ },
+
+ /**
+ * Returns the number of elements in this Composite.
+ * @return Number
+ */
+ getCount : function(){
+ return this.elements.length;
+ },
+ /**
+ * Adds elements to this Composite object.
+ * @param {HTMLElement[]/Ext.CompositeElement} els Either an Array of DOM elements to add, or another Composite object who's elements should be added.
+ * @return {Ext.CompositeElement} This Composite object.
+ */
+ add : function(els, root){
+ var me = this,
+ elements = me.elements;
+ if(!els){
+ return this;
+ }
+ if(typeof els == "string"){
+ els = Ext.Element.selectorFunction(els, root);
+ }else if(els.isComposite){
+ els = els.elements;
+ }else if(!Ext.isIterable(els)){
+ els = [els];
+ }
+
+ for(var i = 0, len = els.length; i < len; ++i){
+ elements.push(me.transformElement(els[i]));
+ }
+ return me;
+ },
+
+ invoke : function(fn, args){
+ var me = this,
+ els = me.elements,
+ len = els.length,
+ e,
+ i;
+
+ for(i = 0; i < len; i++) {
+ e = els[i];
+ if(e){
+ Ext.Element.prototype[fn].apply(me.getElement(e), args);
+ }
+ }
+ return me;
+ },
+ /**
+ * Returns a flyweight Element of the dom element object at the specified index
+ * @param {Number} index
+ * @return {Ext.Element}
+ */
+ item : function(index){
+ var me = this,
+ el = me.elements[index],
+ out = null;
+
+ if(el){
+ out = me.getElement(el);
+ }
+ return out;
+ },
+
+ // fixes scope with flyweight
+ addListener : function(eventName, handler, scope, opt){
+ var els = this.elements,
+ len = els.length,
+ i, e;
+
+ for(i = 0; i<len; i++) {
+ e = els[i];
+ if(e) {
+ Ext.EventManager.on(e, eventName, handler, scope || e, opt);
+ }
+ }
+ return this;
+ },
+ /**
+ * <p>Calls the passed function for each element in this composite.</p>
+ * @param {Function} fn The function to call. The function is passed the following parameters:<ul>
+ * <li><b>el</b> : Element<div class="sub-desc">The current Element in the iteration.
+ * <b>This is the flyweight (shared) Ext.Element instance, so if you require a
+ * a reference to the dom node, use el.dom.</b></div></li>
+ * <li><b>c</b> : Composite<div class="sub-desc">This Composite object.</div></li>
+ * <li><b>idx</b> : Number<div class="sub-desc">The zero-based index in the iteration.</div></li>
+ * </ul>
+ * @param {Object} [scope] The scope (<i>this</i> reference) in which the function is executed. (defaults to the Element)
+ * @return {Ext.CompositeElement} this
+ */
+ each : function(fn, scope){
+ var me = this,
+ els = me.elements,
+ len = els.length,
+ i, e;
+
+ for(i = 0; i<len; i++) {
+ e = els[i];
+ if(e){
+ e = this.getElement(e);
+ if(fn.call(scope || e, e, me, i) === false){
+ break;
+ }
+ }
+ }
+ return me;
+ },
+
+ /**
+ * Clears this Composite and adds the elements passed.
+ * @param {HTMLElement[]/Ext.CompositeElement} els Either an array of DOM elements, or another Composite from which to fill this Composite.
+ * @return {Ext.CompositeElement} this
+ */
+ fill : function(els){
+ var me = this;
+ me.elements = [];
+ me.add(els);
+ return me;
+ },
+
+ /**
+ * Filters this composite to only elements that match the passed selector.
+ * @param {String/Function} selector A string CSS selector or a comparison function.
+ * The comparison function will be called with the following arguments:<ul>
+ * <li><code>el</code> : Ext.Element<div class="sub-desc">The current DOM element.</div></li>
+ * <li><code>index</code> : Number<div class="sub-desc">The current index within the collection.</div></li>
+ * </ul>
+ * @return {Ext.CompositeElement} this
+ */
+ filter : function(selector){
+ var els = [],
+ me = this,
+ fn = Ext.isFunction(selector) ? selector
+ : function(el){
+ return el.is(selector);
+ };
+
+ me.each(function(el, self, i) {
+ if (fn(el, i) !== false) {
+ els[els.length] = me.transformElement(el);
+ }
+ });
+
+ me.elements = els;
+ return me;
+ },
+
+ /**
+ * Find the index of the passed element within the composite collection.
+ * @param el {Mixed} The id of an element, or an Ext.Element, or an HtmlElement to find within the composite collection.
+ * @return Number The index of the passed Ext.Element in the composite collection, or -1 if not found.
+ */
+ indexOf : function(el){
+ return Ext.Array.indexOf(this.elements, this.transformElement(el));
+ },
+
+ /**
+ * Replaces the specified element with the passed element.
+ * @param {String/HTMLElement/Ext.Element/Number} el The id of an element, the Element itself, the index of the element in this composite
+ * to replace.
+ * @param {String/Ext.Element} replacement The id of an element or the Element itself.
+ * @param {Boolean} domReplace (Optional) True to remove and replace the element in the document too.
+ * @return {Ext.CompositeElement} this
+ */
+ replaceElement : function(el, replacement, domReplace){
+ var index = !isNaN(el) ? el : this.indexOf(el),
+ d;
+ if(index > -1){
+ replacement = Ext.getDom(replacement);
+ if(domReplace){
+ d = this.elements[index];
+ d.parentNode.insertBefore(replacement, d);
+ Ext.removeNode(d);
+ }
+ Ext.Array.splice(this.elements, index, 1, replacement);
+ }
+ return this;
+ },
+
+ /**
+ * Removes all elements.
+ */
+ clear : function(){
+ this.elements = [];
+ }
+};
+
+Ext.CompositeElementLite.prototype.on = Ext.CompositeElementLite.prototype.addListener;
+
+/**
+ * @private
+ * Copies all of the functions from Ext.Element's prototype onto CompositeElementLite's prototype.
+ * This is called twice - once immediately below, and once again after additional Ext.Element
+ * are added in Ext JS
+ */
+Ext.CompositeElementLite.importElementMethods = function() {
+ var fnName,
+ ElProto = Ext.Element.prototype,
+ CelProto = Ext.CompositeElementLite.prototype;
+
+ for (fnName in ElProto) {
+ if (typeof ElProto[fnName] == 'function'){
+ (function(fnName) {
+ CelProto[fnName] = CelProto[fnName] || function() {
+ return this.invoke(fnName, arguments);
+ };
+ }).call(CelProto, fnName);
+
+ }
+ }
+};
+
+Ext.CompositeElementLite.importElementMethods();
+
+if(Ext.DomQuery){
+ Ext.Element.selectorFunction = Ext.DomQuery.select;
+}
+
+/**
+ * Selects elements based on the passed CSS selector to enable {@link Ext.Element Element} methods
+ * to be applied to many related elements in one statement through the returned {@link Ext.CompositeElement CompositeElement} or
+ * {@link Ext.CompositeElementLite CompositeElementLite} object.
+ * @param {String/HTMLElement[]} selector The CSS selector or an array of elements
+ * @param {HTMLElement/String} root (optional) The root element of the query or id of the root
+ * @return {Ext.CompositeElementLite/Ext.CompositeElement}
+ * @member Ext.Element
+ * @method select
+ */
+Ext.Element.select = function(selector, root){
+ var els;
+ if(typeof selector == "string"){
+ els = Ext.Element.selectorFunction(selector, root);
+ }else if(selector.length !== undefined){
+ els = selector;
+ }else{
+ }
+ return new Ext.CompositeElementLite(els);
+};
+/**
+ * Selects elements based on the passed CSS selector to enable {@link Ext.Element Element} methods
+ * to be applied to many related elements in one statement through the returned {@link Ext.CompositeElement CompositeElement} or
+ * {@link Ext.CompositeElementLite CompositeElementLite} object.
+ * @param {String/HTMLElement[]} selector The CSS selector or an array of elements
+ * @param {HTMLElement/String} root (optional) The root element of the query or id of the root
+ * @return {Ext.CompositeElementLite/Ext.CompositeElement}
+ * @member Ext
+ * @method select
+ */
+Ext.select = Ext.Element.select;
+
+/**
+ * @class Ext.util.DelayedTask
+ *
+ * The DelayedTask class provides a convenient way to "buffer" the execution of a method,
+ * performing setTimeout where a new timeout cancels the old timeout. When called, the
+ * task will wait the specified time period before executing. If durng that time period,
+ * the task is called again, the original call will be cancelled. This continues so that
+ * the function is only called a single time for each iteration.
+ *
+ * This method is especially useful for things like detecting whether a user has finished
+ * typing in a text field. An example would be performing validation on a keypress. You can
+ * use this class to buffer the keypress events for a certain number of milliseconds, and
+ * perform only if they stop for that amount of time.
+ *
+ * ## Usage
+ *
+ * var task = new Ext.util.DelayedTask(function(){
+ * alert(Ext.getDom('myInputField').value.length);
+ * });
+ *
+ * // Wait 500ms before calling our function. If the user presses another key
+ * // during that 500ms, it will be cancelled and we'll wait another 500ms.
+ * Ext.get('myInputField').on('keypress', function(){
+ * task.{@link #delay}(500);
+ * });
+ *
+ * Note that we are using a DelayedTask here to illustrate a point. The configuration
+ * option `buffer` for {@link Ext.util.Observable#addListener addListener/on} will
+ * also setup a delayed task for you to buffer events.
+ *
+ * @constructor The parameters to this constructor serve as defaults and are not required.
+ * @param {Function} fn (optional) The default function to call. If not specified here, it must be specified during the {@link #delay} call.
+ * @param {Object} scope (optional) The default scope (The <code><b>this</b></code> reference) in which the
+ * function is called. If not specified, <code>this</code> will refer to the browser window.
+ * @param {Array} args (optional) The default Array of arguments.
+ */
+Ext.util.DelayedTask = function(fn, scope, args) {
+ var me = this,
+ id,
+ call = function() {
+ clearInterval(id);
+ id = null;
+ fn.apply(scope, args || []);
+ };
+
+ /**
+ * Cancels any pending timeout and queues a new one
+ * @param {Number} delay The milliseconds to delay
+ * @param {Function} newFn (optional) Overrides function passed to constructor
+ * @param {Object} newScope (optional) Overrides scope passed to constructor. Remember that if no scope
+ * is specified, <code>this</code> will refer to the browser window.
+ * @param {Array} newArgs (optional) Overrides args passed to constructor
+ */
+ this.delay = function(delay, newFn, newScope, newArgs) {
+ me.cancel();
+ fn = newFn || fn;
+ scope = newScope || scope;
+ args = newArgs || args;
+ id = setInterval(call, delay);
+ };
+
+ /**
+ * Cancel the last queued timeout
+ */
+ this.cancel = function(){
+ if (id) {
+ clearInterval(id);
+ id = null;
+ }
+ };
+};
+Ext.require('Ext.util.DelayedTask', function() {
+
+ Ext.util.Event = Ext.extend(Object, (function() {
+ function createBuffered(handler, listener, o, scope) {
+ listener.task = new Ext.util.DelayedTask();
+ return function() {
+ listener.task.delay(o.buffer, handler, scope, Ext.Array.toArray(arguments));
+ };
+ }
+
+ function createDelayed(handler, listener, o, scope) {
+ return function() {
+ var task = new Ext.util.DelayedTask();
+ if (!listener.tasks) {
+ listener.tasks = [];
+ }
+ listener.tasks.push(task);
+ task.delay(o.delay || 10, handler, scope, Ext.Array.toArray(arguments));
+ };
+ }
+
+ function createSingle(handler, listener, o, scope) {
+ return function() {
+ listener.ev.removeListener(listener.fn, scope);
+ return handler.apply(scope, arguments);
+ };
+ }
+
+ return {
+ isEvent: true,
+
+ constructor: function(observable, name) {
+ this.name = name;
+ this.observable = observable;
+ this.listeners = [];
+ },
+
+ addListener: function(fn, scope, options) {
+ var me = this,
+ listener;
+ scope = scope || me.observable;
+
+
+ if (!me.isListening(fn, scope)) {
+ listener = me.createListener(fn, scope, options);
+ if (me.firing) {
+ // if we are currently firing this event, don't disturb the listener loop
+ me.listeners = me.listeners.slice(0);
+ }
+ me.listeners.push(listener);
+ }
+ },
+
+ createListener: function(fn, scope, o) {
+ o = o || {};
+ scope = scope || this.observable;
+
+ var listener = {
+ fn: fn,
+ scope: scope,
+ o: o,
+ ev: this
+ },
+ handler = fn;
+
+ // The order is important. The 'single' wrapper must be wrapped by the 'buffer' and 'delayed' wrapper
+ // because the event removal that the single listener does destroys the listener's DelayedTask(s)
+ if (o.single) {
+ handler = createSingle(handler, listener, o, scope);
+ }
+ if (o.delay) {
+ handler = createDelayed(handler, listener, o, scope);
+ }
+ if (o.buffer) {
+ handler = createBuffered(handler, listener, o, scope);
+ }
+
+ listener.fireFn = handler;
+ return listener;
+ },
+
+ findListener: function(fn, scope) {
+ var listeners = this.listeners,
+ i = listeners.length,
+ listener,
+ s;
+
+ while (i--) {
+ listener = listeners[i];
+ if (listener) {
+ s = listener.scope;
+ if (listener.fn == fn && (s == scope || s == this.observable)) {
+ return i;
+ }
+ }
+ }
+
+ return - 1;
+ },
+
+ isListening: function(fn, scope) {
+ return this.findListener(fn, scope) !== -1;
+ },
+
+ removeListener: function(fn, scope) {
+ var me = this,
+ index,
+ listener,
+ k;
+ index = me.findListener(fn, scope);
+ if (index != -1) {
+ listener = me.listeners[index];
+
+ if (me.firing) {
+ me.listeners = me.listeners.slice(0);
+ }
+
+ // cancel and remove a buffered handler that hasn't fired yet
+ if (listener.task) {
+ listener.task.cancel();
+ delete listener.task;
+ }
+
+ // cancel and remove all delayed handlers that haven't fired yet
+ k = listener.tasks && listener.tasks.length;
+ if (k) {
+ while (k--) {
+ listener.tasks[k].cancel();
+ }
+ delete listener.tasks;
+ }
+
+ // remove this listener from the listeners array
+ Ext.Array.erase(me.listeners, index, 1);
+ return true;
+ }
+
+ return false;
+ },
+
+ // Iterate to stop any buffered/delayed events
+ clearListeners: function() {
+ var listeners = this.listeners,
+ i = listeners.length;
+
+ while (i--) {
+ this.removeListener(listeners[i].fn, listeners[i].scope);
+ }
+ },
+
+ fire: function() {
+ var me = this,
+ listeners = me.listeners,
+ count = listeners.length,
+ i,
+ args,
+ listener;
+
+ if (count > 0) {
+ me.firing = true;
+ for (i = 0; i < count; i++) {
+ listener = listeners[i];
+ args = arguments.length ? Array.prototype.slice.call(arguments, 0) : [];
+ if (listener.o) {
+ args.push(listener.o);
+ }
+ if (listener && listener.fireFn.apply(listener.scope || me.observable, args) === false) {
+ return (me.firing = false);
+ }
+ }
+ }
+ me.firing = false;
+ return true;
+ }
+ };
+ })());
+});
+
+/**
+ * @class Ext.EventManager
+ * Registers event handlers that want to receive a normalized EventObject instead of the standard browser event and provides
+ * several useful events directly.
+ * See {@link Ext.EventObject} for more details on normalized event objects.
+ * @singleton
+ */
+Ext.EventManager = {
+
+ // --------------------- onReady ---------------------
+
+ /**
+ * Check if we have bound our global onReady listener
+ * @private
+ */
+ hasBoundOnReady: false,
+
+ /**
+ * Check if fireDocReady has been called
+ * @private
+ */
+ hasFiredReady: false,
+
+ /**
+ * Timer for the document ready event in old IE versions
+ * @private
+ */
+ readyTimeout: null,
+
+ /**
+ * Checks if we have bound an onreadystatechange event
+ * @private
+ */
+ hasOnReadyStateChange: false,
+
+ /**
+ * Holds references to any onReady functions
+ * @private
+ */
+ readyEvent: new Ext.util.Event(),
+
+ /**
+ * Check the ready state for old IE versions
+ * @private
+ * @return {Boolean} True if the document is ready
+ */
+ checkReadyState: function(){
+ var me = Ext.EventManager;
+
+ if(window.attachEvent){
+ // See here for reference: http://javascript.nwbox.com/IEContentLoaded/
+ // licensed courtesy of http://developer.yahoo.com/yui/license.html
+ if (window != top) {
+ return false;
+ }
+ try{
+ document.documentElement.doScroll('left');
+ }catch(e){
+ return false;
+ }
+ me.fireDocReady();
+ return true;
+ }
+ if (document.readyState == 'complete') {
+ me.fireDocReady();
+ return true;
+ }
+ me.readyTimeout = setTimeout(arguments.callee, 2);
+ return false;
+ },
+
+ /**
+ * Binds the appropriate browser event for checking if the DOM has loaded.
+ * @private
+ */
+ bindReadyEvent: function(){
+ var me = Ext.EventManager;
+ if (me.hasBoundOnReady) {
+ return;
+ }
+
+ if (document.addEventListener) {
+ document.addEventListener('DOMContentLoaded', me.fireDocReady, false);
+ // fallback, load will ~always~ fire
+ window.addEventListener('load', me.fireDocReady, false);
+ } else {
+ // check if the document is ready, this will also kick off the scroll checking timer
+ if (!me.checkReadyState()) {
+ document.attachEvent('onreadystatechange', me.checkReadyState);
+ me.hasOnReadyStateChange = true;
+ }
+ // fallback, onload will ~always~ fire
+ window.attachEvent('onload', me.fireDocReady, false);
+ }
+ me.hasBoundOnReady = true;
+ },
+
+ /**
+ * We know the document is loaded, so trigger any onReady events.
+ * @private
+ */
+ fireDocReady: function(){
+ var me = Ext.EventManager;
+
+ // only unbind these events once
+ if (!me.hasFiredReady) {
+ me.hasFiredReady = true;
+
+ if (document.addEventListener) {
+ document.removeEventListener('DOMContentLoaded', me.fireDocReady, false);
+ window.removeEventListener('load', me.fireDocReady, false);
+ } else {
+ if (me.readyTimeout !== null) {
+ clearTimeout(me.readyTimeout);
+ }
+ if (me.hasOnReadyStateChange) {
+ document.detachEvent('onreadystatechange', me.checkReadyState);
+ }
+ window.detachEvent('onload', me.fireDocReady);
+ }
+ Ext.supports.init();
+ }
+ if (!Ext.isReady) {
+ Ext.isReady = true;
+ me.onWindowUnload();
+ me.readyEvent.fire();
+ }
+ },
+
+ /**
+ * Adds a listener to be notified when the document is ready (before onload and before images are loaded). Can be
+ * accessed shorthanded as Ext.onReady().
+ * @param {Function} fn The method the event invokes.
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the handler function executes. Defaults to the browser window.
+ * @param {Boolean} options (optional) Options object as passed to {@link Ext.Element#addListener}.
+ */
+ onDocumentReady: function(fn, scope, options){
+ options = options || {};
+ var me = Ext.EventManager,
+ readyEvent = me.readyEvent;
+
+ // force single to be true so our event is only ever fired once.
+ options.single = true;
+
+ // Document already loaded, let's just fire it
+ if (Ext.isReady) {
+ readyEvent.addListener(fn, scope, options);
+ readyEvent.fire();
+ } else {
+ options.delay = options.delay || 1;
+ readyEvent.addListener(fn, scope, options);
+ me.bindReadyEvent();
+ }
+ },
+
+
+ // --------------------- event binding ---------------------
+
+ /**
+ * Contains a list of all document mouse downs, so we can ensure they fire even when stopEvent is called.
+ * @private
+ */
+ stoppedMouseDownEvent: new Ext.util.Event(),
+
+ /**
+ * Options to parse for the 4th argument to addListener.
+ * @private
+ */
+ propRe: /^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate|freezeEvent)$/,
+
+ /**
+ * Get the id of the element. If one has not been assigned, automatically assign it.
+ * @param {HTMLElement/Ext.Element} element The element to get the id for.
+ * @return {String} id
+ */
+ getId : function(element) {
+ var skipGarbageCollection = false,
+ id;
+
+ element = Ext.getDom(element);
+
+ if (element === document || element === window) {
+ id = element === document ? Ext.documentId : Ext.windowId;
+ }
+ else {
+ id = Ext.id(element);
+ }
+ // skip garbage collection for special elements (window, document, iframes)
+ if (element && (element.getElementById || element.navigator)) {
+ skipGarbageCollection = true;
+ }
+
+ if (!Ext.cache[id]){
+ Ext.Element.addToCache(new Ext.Element(element), id);
+ if (skipGarbageCollection) {
+ Ext.cache[id].skipGarbageCollection = true;
+ }
+ }
+ return id;
+ },
+
+ /**
+ * Convert a "config style" listener into a set of flat arguments so they can be passed to addListener
+ * @private
+ * @param {Object} element The element the event is for
+ * @param {Object} event The event configuration
+ * @param {Object} isRemove True if a removal should be performed, otherwise an add will be done.
+ */
+ prepareListenerConfig: function(element, config, isRemove){
+ var me = this,
+ propRe = me.propRe,
+ key, value, args;
+
+ // loop over all the keys in the object
+ for (key in config) {
+ if (config.hasOwnProperty(key)) {
+ // if the key is something else then an event option
+ if (!propRe.test(key)) {
+ value = config[key];
+ // if the value is a function it must be something like click: function(){}, scope: this
+ // which means that there might be multiple event listeners with shared options
+ if (Ext.isFunction(value)) {
+ // shared options
+ args = [element, key, value, config.scope, config];
+ } else {
+ // if its not a function, it must be an object like click: {fn: function(){}, scope: this}
+ args = [element, key, value.fn, value.scope, value];
+ }
+
+ if (isRemove === true) {
+ me.removeListener.apply(this, args);
+ } else {
+ me.addListener.apply(me, args);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Normalize cross browser event differences
+ * @private
+ * @param {Object} eventName The event name
+ * @param {Object} fn The function to execute
+ * @return {Object} The new event name/function
+ */
+ normalizeEvent: function(eventName, fn){
+ if (/mouseenter|mouseleave/.test(eventName) && !Ext.supports.MouseEnterLeave) {
+ if (fn) {
+ fn = Ext.Function.createInterceptor(fn, this.contains, this);
+ }
+ eventName = eventName == 'mouseenter' ? 'mouseover' : 'mouseout';
+ } else if (eventName == 'mousewheel' && !Ext.supports.MouseWheel && !Ext.isOpera){
+ eventName = 'DOMMouseScroll';
+ }
+ return {
+ eventName: eventName,
+ fn: fn
+ };
+ },
+
+ /**
+ * Checks whether the event's relatedTarget is contained inside (or <b>is</b>) the element.
+ * @private
+ * @param {Object} event
+ */
+ contains: function(event){
+ var parent = event.browserEvent.currentTarget,
+ child = this.getRelatedTarget(event);
+
+ if (parent && parent.firstChild) {
+ while (child) {
+ if (child === parent) {
+ return false;
+ }
+ child = child.parentNode;
+ if (child && (child.nodeType != 1)) {
+ child = null;
+ }
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Appends an event handler to an element. The shorthand version {@link #on} is equivalent. Typically you will
+ * use {@link Ext.Element#addListener} directly on an Element in favor of calling this version.
+ * @param {String/HTMLElement} el The html element or id to assign the event handler to.
+ * @param {String} eventName The name of the event to listen for.
+ * @param {Function} handler The handler function the event invokes. This function is passed
+ * the following parameters:<ul>
+ * <li>evt : EventObject<div class="sub-desc">The {@link Ext.EventObject EventObject} describing the event.</div></li>
+ * <li>t : Element<div class="sub-desc">The {@link Ext.Element Element} which was the target of the event.
+ * Note that this may be filtered by using the <tt>delegate</tt> option.</div></li>
+ * <li>o : Object<div class="sub-desc">The options object from the addListener call.</div></li>
+ * </ul>
+ * @param {Object} scope (optional) The scope (<b><code>this</code></b> reference) in which the handler function is executed. <b>Defaults to the Element</b>.
+ * @param {Object} options (optional) An object containing handler configuration properties.
+ * This may contain any of the following properties:<ul>
+ * <li>scope : Object<div class="sub-desc">The scope (<b><code>this</code></b> reference) in which the handler function is executed. <b>Defaults to the Element</b>.</div></li>
+ * <li>delegate : String<div class="sub-desc">A simple selector to filter the target or look for a descendant of the target</div></li>
+ * <li>stopEvent : Boolean<div class="sub-desc">True to stop the event. That is stop propagation, and prevent the default action.</div></li>
+ * <li>preventDefault : Boolean<div class="sub-desc">True to prevent the default action</div></li>
+ * <li>stopPropagation : Boolean<div class="sub-desc">True to prevent event propagation</div></li>
+ * <li>normalized : Boolean<div class="sub-desc">False to pass a browser event to the handler function instead of an Ext.EventObject</div></li>
+ * <li>delay : Number<div class="sub-desc">The number of milliseconds to delay the invocation of the handler after te event fires.</div></li>
+ * <li>single : Boolean<div class="sub-desc">True to add a handler to handle just the next firing of the event, and then remove itself.</div></li>
+ * <li>buffer : Number<div class="sub-desc">Causes the handler to be scheduled to run in an {@link Ext.util.DelayedTask} delayed
+ * by the specified number of milliseconds. If the event fires again within that time, the original
+ * handler is <em>not</em> invoked, but the new handler is scheduled in its place.</div></li>
+ * <li>target : Element<div class="sub-desc">Only call the handler if the event was fired on the target Element, <i>not</i> if the event was bubbled up from a child node.</div></li>
+ * </ul><br>
+ * <p>See {@link Ext.Element#addListener} for examples of how to use these options.</p>
+ */
+ addListener: function(element, eventName, fn, scope, options){
+ // Check if we've been passed a "config style" event.
+ if (typeof eventName !== 'string') {
+ this.prepareListenerConfig(element, eventName);
+ return;
+ }
+
+ var dom = Ext.getDom(element),
+ bind,
+ wrap;
+
+
+ // create the wrapper function
+ options = options || {};
+
+ bind = this.normalizeEvent(eventName, fn);
+ wrap = this.createListenerWrap(dom, eventName, bind.fn, scope, options);
+
+
+ if (dom.attachEvent) {
+ dom.attachEvent('on' + bind.eventName, wrap);
+ } else {
+ dom.addEventListener(bind.eventName, wrap, options.capture || false);
+ }
+
+ if (dom == document && eventName == 'mousedown') {
+ this.stoppedMouseDownEvent.addListener(wrap);
+ }
+
+ // add all required data into the event cache
+ this.getEventListenerCache(dom, eventName).push({
+ fn: fn,
+ wrap: wrap,
+ scope: scope
+ });
+ },
+
+ /**
+ * Removes an event handler from an element. The shorthand version {@link #un} is equivalent. Typically
+ * you will use {@link Ext.Element#removeListener} directly on an Element in favor of calling this version.
+ * @param {String/HTMLElement} el The id or html element from which to remove the listener.
+ * @param {String} eventName The name of the event.
+ * @param {Function} fn The handler function to remove. <b>This must be a reference to the function passed into the {@link #addListener} call.</b>
+ * @param {Object} scope If a scope (<b><code>this</code></b> reference) was specified when the listener was added,
+ * then this must refer to the same object.
+ */
+ removeListener : function(element, eventName, fn, scope) {
+ // handle our listener config object syntax
+ if (typeof eventName !== 'string') {
+ this.prepareListenerConfig(element, eventName, true);
+ return;
+ }
+
+ var dom = Ext.getDom(element),
+ cache = this.getEventListenerCache(dom, eventName),
+ bindName = this.normalizeEvent(eventName).eventName,
+ i = cache.length, j,
+ listener, wrap, tasks;
+
+
+ while (i--) {
+ listener = cache[i];
+
+ if (listener && (!fn || listener.fn == fn) && (!scope || listener.scope === scope)) {
+ wrap = listener.wrap;
+
+ // clear buffered calls
+ if (wrap.task) {
+ clearTimeout(wrap.task);
+ delete wrap.task;
+ }
+
+ // clear delayed calls
+ j = wrap.tasks && wrap.tasks.length;
+ if (j) {
+ while (j--) {
+ clearTimeout(wrap.tasks[j]);
+ }
+ delete wrap.tasks;
+ }
+
+ if (dom.detachEvent) {
+ dom.detachEvent('on' + bindName, wrap);
+ } else {
+ dom.removeEventListener(bindName, wrap, false);
+ }
+
+ if (wrap && dom == document && eventName == 'mousedown') {
+ this.stoppedMouseDownEvent.removeListener(wrap);
+ }
+
+ // remove listener from cache
+ Ext.Array.erase(cache, i, 1);
+ }
+ }
+ },
+
+ /**
+ * Removes all event handers from an element. Typically you will use {@link Ext.Element#removeAllListeners}
+ * directly on an Element in favor of calling this version.
+ * @param {String/HTMLElement} el The id or html element from which to remove all event handlers.
+ */
+ removeAll : function(element){
+ var dom = Ext.getDom(element),
+ cache, ev;
+ if (!dom) {
+ return;
+ }
+ cache = this.getElementEventCache(dom);
+
+ for (ev in cache) {
+ if (cache.hasOwnProperty(ev)) {
+ this.removeListener(dom, ev);
+ }
+ }
+ Ext.cache[dom.id].events = {};
+ },
+
+ /**
+ * Recursively removes all previous added listeners from an element and its children. Typically you will use {@link Ext.Element#purgeAllListeners}
+ * directly on an Element in favor of calling this version.
+ * @param {String/HTMLElement} el The id or html element from which to remove all event handlers.
+ * @param {String} eventName (optional) The name of the event.
+ */
+ purgeElement : function(element, eventName) {
+ var dom = Ext.getDom(element),
+ i = 0, len;
+
+ if(eventName) {
+ this.removeListener(dom, eventName);
+ }
+ else {
+ this.removeAll(dom);
+ }
+
+ if(dom && dom.childNodes) {
+ for(len = element.childNodes.length; i < len; i++) {
+ this.purgeElement(element.childNodes[i], eventName);
+ }
+ }
+ },
+
+ /**
+ * Create the wrapper function for the event
+ * @private
+ * @param {HTMLElement} dom The dom element
+ * @param {String} ename The event name
+ * @param {Function} fn The function to execute
+ * @param {Object} scope The scope to execute callback in
+ * @param {Object} options The options
+ * @return {Function} the wrapper function
+ */
+ createListenerWrap : function(dom, ename, fn, scope, options) {
+ options = options || {};
+
+ var f, gen;
+
+ return function wrap(e, args) {
+ // Compile the implementation upon first firing
+ if (!gen) {
+ f = ['if(!Ext) {return;}'];
+
+ if(options.buffer || options.delay || options.freezeEvent) {
+ f.push('e = new Ext.EventObjectImpl(e, ' + (options.freezeEvent ? 'true' : 'false' ) + ');');
+ } else {
+ f.push('e = Ext.EventObject.setEvent(e);');
+ }
+
+ if (options.delegate) {
+ f.push('var t = e.getTarget("' + options.delegate + '", this);');
+ f.push('if(!t) {return;}');
+ } else {
+ f.push('var t = e.target;');
+ }
+
+ if (options.target) {
+ f.push('if(e.target !== options.target) {return;}');
+ }
+
+ if(options.stopEvent) {
+ f.push('e.stopEvent();');
+ } else {
+ if(options.preventDefault) {
+ f.push('e.preventDefault();');
+ }
+ if(options.stopPropagation) {
+ f.push('e.stopPropagation();');
+ }
+ }
+
+ if(options.normalized === false) {
+ f.push('e = e.browserEvent;');
+ }
+
+ if(options.buffer) {
+ f.push('(wrap.task && clearTimeout(wrap.task));');
+ f.push('wrap.task = setTimeout(function(){');
+ }
+
+ if(options.delay) {
+ f.push('wrap.tasks = wrap.tasks || [];');
+ f.push('wrap.tasks.push(setTimeout(function(){');
+ }
+
+ // finally call the actual handler fn
+ f.push('fn.call(scope || dom, e, t, options);');
+
+ if(options.single) {
+ f.push('Ext.EventManager.removeListener(dom, ename, fn, scope);');
+ }
+
+ if(options.delay) {
+ f.push('}, ' + options.delay + '));');
+ }
+
+ if(options.buffer) {
+ f.push('}, ' + options.buffer + ');');
+ }
+
+ gen = Ext.functionFactory('e', 'options', 'fn', 'scope', 'ename', 'dom', 'wrap', 'args', f.join('\n'));
+ }
+
+ gen.call(dom, e, options, fn, scope, ename, dom, wrap, args);
+ };
+ },
+
+ /**
+ * Get the event cache for a particular element for a particular event
+ * @private
+ * @param {HTMLElement} element The element
+ * @param {Object} eventName The event name
+ * @return {Array} The events for the element
+ */
+ getEventListenerCache : function(element, eventName) {
+ if (!element) {
+ return [];
+ }
+
+ var eventCache = this.getElementEventCache(element);
+ return eventCache[eventName] || (eventCache[eventName] = []);
+ },
+
+ /**
+ * Gets the event cache for the object
+ * @private
+ * @param {HTMLElement} element The element
+ * @return {Object} The event cache for the object
+ */
+ getElementEventCache : function(element) {
+ if (!element) {
+ return {};
+ }
+ var elementCache = Ext.cache[this.getId(element)];
+ return elementCache.events || (elementCache.events = {});
+ },
+
+ // --------------------- utility methods ---------------------
+ mouseLeaveRe: /(mouseout|mouseleave)/,
+ mouseEnterRe: /(mouseover|mouseenter)/,
+
+ /**
+ * Stop the event (preventDefault and stopPropagation)
+ * @param {Event} The event to stop
+ */
+ stopEvent: function(event) {
+ this.stopPropagation(event);
+ this.preventDefault(event);
+ },
+
+ /**
+ * Cancels bubbling of the event.
+ * @param {Event} The event to stop bubbling.
+ */
+ stopPropagation: function(event) {
+ event = event.browserEvent || event;
+ if (event.stopPropagation) {
+ event.stopPropagation();
+ } else {
+ event.cancelBubble = true;
+ }
+ },
+
+ /**
+ * Prevents the browsers default handling of the event.
+ * @param {Event} The event to prevent the default
+ */
+ preventDefault: function(event) {
+ event = event.browserEvent || event;
+ if (event.preventDefault) {
+ event.preventDefault();
+ } else {
+ event.returnValue = false;
+ // Some keys events require setting the keyCode to -1 to be prevented
+ try {
+ // all ctrl + X and F1 -> F12
+ if (event.ctrlKey || event.keyCode > 111 && event.keyCode < 124) {
+ event.keyCode = -1;
+ }
+ } catch (e) {
+ // see this outdated document http://support.microsoft.com/kb/934364/en-us for more info
+ }
+ }
+ },
+
+ /**
+ * Gets the related target from the event.
+ * @param {Object} event The event
+ * @return {HTMLElement} The related target.
+ */
+ getRelatedTarget: function(event) {
+ event = event.browserEvent || event;
+ var target = event.relatedTarget;
+ if (!target) {
+ if (this.mouseLeaveRe.test(event.type)) {
+ target = event.toElement;
+ } else if (this.mouseEnterRe.test(event.type)) {
+ target = event.fromElement;
+ }
+ }
+ return this.resolveTextNode(target);
+ },
+
+ /**
+ * Gets the x coordinate from the event
+ * @param {Object} event The event
+ * @return {Number} The x coordinate
+ */
+ getPageX: function(event) {
+ return this.getXY(event)[0];
+ },
+
+ /**
+ * Gets the y coordinate from the event
+ * @param {Object} event The event
+ * @return {Number} The y coordinate
+ */
+ getPageY: function(event) {
+ return this.getXY(event)[1];
+ },
+
+ /**
+ * Gets the x & y coordinate from the event
+ * @param {Object} event The event
+ * @return {Number[]} The x/y coordinate
+ */
+ getPageXY: function(event) {
+ event = event.browserEvent || event;
+ var x = event.pageX,
+ y = event.pageY,
+ doc = document.documentElement,
+ body = document.body;
+
+ // pageX/pageY not available (undefined, not null), use clientX/clientY instead
+ if (!x && x !== 0) {
+ x = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
+ y = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
+ }
+ return [x, y];
+ },
+
+ /**
+ * Gets the target of the event.
+ * @param {Object} event The event
+ * @return {HTMLElement} target
+ */
+ getTarget: function(event) {
+ event = event.browserEvent || event;
+ return this.resolveTextNode(event.target || event.srcElement);
+ },
+
+ /**
+ * Resolve any text nodes accounting for browser differences.
+ * @private
+ * @param {HTMLElement} node The node
+ * @return {HTMLElement} The resolved node
+ */
+ // technically no need to browser sniff this, however it makes no sense to check this every time, for every event, whether the string is equal.
+ resolveTextNode: Ext.isGecko ?
+ function(node) {
+ if (!node) {
+ return;
+ }
+ // work around firefox bug, https://bugzilla.mozilla.org/show_bug.cgi?id=101197
+ var s = HTMLElement.prototype.toString.call(node);
+ if (s == '[xpconnect wrapped native prototype]' || s == '[object XULElement]') {
+ return;
+ }
+ return node.nodeType == 3 ? node.parentNode: node;
+ }: function(node) {
+ return node && node.nodeType == 3 ? node.parentNode: node;
+ },
+
+ // --------------------- custom event binding ---------------------
+
+ // Keep track of the current width/height
+ curWidth: 0,
+ curHeight: 0,
+
+ /**
+ * Adds a listener to be notified when the browser window is resized and provides resize event buffering (100 milliseconds),
+ * passes new viewport width and height to handlers.
+ * @param {Function} fn The handler function the window resize event invokes.
+ * @param {Object} scope The scope (<code>this</code> reference) in which the handler function executes. Defaults to the browser window.
+ * @param {Boolean} options Options object as passed to {@link Ext.Element#addListener}
+ */
+ onWindowResize: function(fn, scope, options){
+ var resize = this.resizeEvent;
+ if(!resize){
+ this.resizeEvent = resize = new Ext.util.Event();
+ this.on(window, 'resize', this.fireResize, this, {buffer: 100});
+ }
+ resize.addListener(fn, scope, options);
+ },
+
+ /**
+ * Fire the resize event.
+ * @private
+ */
+ fireResize: function(){
+ var me = this,
+ w = Ext.Element.getViewWidth(),
+ h = Ext.Element.getViewHeight();
+
+ //whacky problem in IE where the resize event will sometimes fire even though the w/h are the same.
+ if(me.curHeight != h || me.curWidth != w){
+ me.curHeight = h;
+ me.curWidth = w;
+ me.resizeEvent.fire(w, h);
+ }
+ },
+
+ /**
+ * Removes the passed window resize listener.
+ * @param {Function} fn The method the event invokes
+ * @param {Object} scope The scope of handler
+ */
+ removeResizeListener: function(fn, scope){
+ if (this.resizeEvent) {
+ this.resizeEvent.removeListener(fn, scope);
+ }
+ },
+
+ onWindowUnload: function() {
+ var unload = this.unloadEvent;
+ if (!unload) {
+ this.unloadEvent = unload = new Ext.util.Event();
+ this.addListener(window, 'unload', this.fireUnload, this);
+ }
+ },
+
+ /**
+ * Fires the unload event for items bound with onWindowUnload
+ * @private
+ */
+ fireUnload: function() {
+ // wrap in a try catch, could have some problems during unload
+ try {
+ this.removeUnloadListener();
+ // Work around FF3 remembering the last scroll position when refreshing the grid and then losing grid view
+ if (Ext.isGecko3) {
+ var gridviews = Ext.ComponentQuery.query('gridview'),
+ i = 0,
+ ln = gridviews.length;
+ for (; i < ln; i++) {
+ gridviews[i].scrollToTop();
+ }
+ }
+ // Purge all elements in the cache
+ var el,
+ cache = Ext.cache;
+ for (el in cache) {
+ if (cache.hasOwnProperty(el)) {
+ Ext.EventManager.removeAll(el);
+ }
+ }
+ } catch(e) {
+ }
+ },
+
+ /**
+ * Removes the passed window unload listener.
+ * @param {Function} fn The method the event invokes
+ * @param {Object} scope The scope of handler
+ */
+ removeUnloadListener: function(){
+ if (this.unloadEvent) {
+ this.removeListener(window, 'unload', this.fireUnload);
+ }
+ },
+
+ /**
+ * note 1: IE fires ONLY the keydown event on specialkey autorepeat
+ * note 2: Safari < 3.1, Gecko (Mac/Linux) & Opera fire only the keypress event on specialkey autorepeat
+ * (research done by Jan Wolter at http://unixpapa.com/js/key.html)
+ * @private
+ */
+ useKeyDown: Ext.isWebKit ?
+ parseInt(navigator.userAgent.match(/AppleWebKit\/(\d+)/)[1], 10) >= 525 :
+ !((Ext.isGecko && !Ext.isWindows) || Ext.isOpera),
+
+ /**
+ * Indicates which event to use for getting key presses.
+ * @return {String} The appropriate event name.
+ */
+ getKeyEvent: function(){
+ return this.useKeyDown ? 'keydown' : 'keypress';
+ }
+};
+
+/**
+ * Alias for {@link Ext.Loader#onReady Ext.Loader.onReady} with withDomReady set to true
+ * @member Ext
+ * @method onReady
+ */
+Ext.onReady = function(fn, scope, options) {
+ Ext.Loader.onReady(fn, scope, true, options);
+};
+
+/**
+ * Alias for {@link Ext.EventManager#onDocumentReady Ext.EventManager.onDocumentReady}
+ * @member Ext
+ * @method onDocumentReady
+ */
+Ext.onDocumentReady = Ext.EventManager.onDocumentReady;
+
+/**
+ * Alias for {@link Ext.EventManager#addListener Ext.EventManager.addListener}
+ * @member Ext.EventManager
+ * @method on
+ */
+Ext.EventManager.on = Ext.EventManager.addListener;
+
+/**
+ * Alias for {@link Ext.EventManager#removeListener Ext.EventManager.removeListener}
+ * @member Ext.EventManager
+ * @method un
+ */
+Ext.EventManager.un = Ext.EventManager.removeListener;
+
+(function(){
+ var initExtCss = function() {
+ // find the body element
+ var bd = document.body || document.getElementsByTagName('body')[0],
+ baseCSSPrefix = Ext.baseCSSPrefix,
+ cls = [baseCSSPrefix + 'body'],
+ htmlCls = [],
+ html;
+
+ if (!bd) {
+ return false;
+ }
+
+ html = bd.parentNode;
+
+ function add (c) {
+ cls.push(baseCSSPrefix + c);
+ }
+
+ //Let's keep this human readable!
+ if (Ext.isIE) {
+ add('ie');
+
+ // very often CSS needs to do checks like "IE7+" or "IE6 or 7". To help
+ // reduce the clutter (since CSS/SCSS cannot do these tests), we add some
+ // additional classes:
+ //
+ // x-ie7p : IE7+ : 7 <= ieVer
+ // x-ie7m : IE7- : ieVer <= 7
+ // x-ie8p : IE8+ : 8 <= ieVer
+ // x-ie8m : IE8- : ieVer <= 8
+ // x-ie9p : IE9+ : 9 <= ieVer
+ // x-ie78 : IE7 or 8 : 7 <= ieVer <= 8
+ //
+ if (Ext.isIE6) {
+ add('ie6');
+ } else { // ignore pre-IE6 :)
+ add('ie7p');
+
+ if (Ext.isIE7) {
+ add('ie7');
+ } else {
+ add('ie8p');
+
+ if (Ext.isIE8) {
+ add('ie8');
+ } else {
+ add('ie9p');
+
+ if (Ext.isIE9) {
+ add('ie9');
+ }
+ }
+ }
+ }
+
+ if (Ext.isIE6 || Ext.isIE7) {
+ add('ie7m');
+ }
+ if (Ext.isIE6 || Ext.isIE7 || Ext.isIE8) {
+ add('ie8m');
+ }
+ if (Ext.isIE7 || Ext.isIE8) {
+ add('ie78');
+ }
+ }
+ if (Ext.isGecko) {
+ add('gecko');
+ if (Ext.isGecko3) {
+ add('gecko3');
+ }
+ if (Ext.isGecko4) {
+ add('gecko4');
+ }
+ if (Ext.isGecko5) {
+ add('gecko5');
+ }
+ }
+ if (Ext.isOpera) {
+ add('opera');
+ }
+ if (Ext.isWebKit) {
+ add('webkit');
+ }
+ if (Ext.isSafari) {
+ add('safari');
+ if (Ext.isSafari2) {
+ add('safari2');
+ }
+ if (Ext.isSafari3) {
+ add('safari3');
+ }
+ if (Ext.isSafari4) {
+ add('safari4');
+ }
+ if (Ext.isSafari5) {
+ add('safari5');
+ }
+ }
+ if (Ext.isChrome) {
+ add('chrome');
+ }
+ if (Ext.isMac) {
+ add('mac');
+ }
+ if (Ext.isLinux) {
+ add('linux');
+ }
+ if (!Ext.supports.CSS3BorderRadius) {
+ add('nbr');
+ }
+ if (!Ext.supports.CSS3LinearGradient) {
+ add('nlg');
+ }
+ if (!Ext.scopeResetCSS) {
+ add('reset');
+ }
+
+ // add to the parent to allow for selectors x-strict x-border-box, also set the isBorderBox property correctly
+ if (html) {
+ if (Ext.isStrict && (Ext.isIE6 || Ext.isIE7)) {
+ Ext.isBorderBox = false;
+ }
+ else {
+ Ext.isBorderBox = true;
+ }
+
+ htmlCls.push(baseCSSPrefix + (Ext.isBorderBox ? 'border-box' : 'strict'));
+ if (!Ext.isStrict) {
+ htmlCls.push(baseCSSPrefix + 'quirks');
+ }
+ Ext.fly(html, '_internal').addCls(htmlCls);
+ }
+
+ Ext.fly(bd, '_internal').addCls(cls);
+ return true;
+ };
+
+ Ext.onReady(initExtCss);
+})();
+
+/**
+ * @class Ext.EventObject
+
+Just as {@link Ext.Element} wraps around a native DOM node, Ext.EventObject
+wraps the browser's native event-object normalizing cross-browser differences,
+such as which mouse button is clicked, keys pressed, mechanisms to stop
+event-propagation along with a method to prevent default actions from taking place.
+
+For example:
+
+ function handleClick(e, t){ // e is not a standard event object, it is a Ext.EventObject
+ e.preventDefault();
+ var target = e.getTarget(); // same as t (the target HTMLElement)
+ ...
+ }
+
+ var myDiv = {@link Ext#get Ext.get}("myDiv"); // get reference to an {@link Ext.Element}
+ myDiv.on( // 'on' is shorthand for addListener
+ "click", // perform an action on click of myDiv
+ handleClick // reference to the action handler
+ );
+
+ // other methods to do the same:
+ Ext.EventManager.on("myDiv", 'click', handleClick);
+ Ext.EventManager.addListener("myDiv", 'click', handleClick);
+
+ * @singleton
+ * @markdown
+ */
+Ext.define('Ext.EventObjectImpl', {
+ uses: ['Ext.util.Point'],
+
+ /** Key constant @type Number */
+ BACKSPACE: 8,
+ /** Key constant @type Number */
+ TAB: 9,
+ /** Key constant @type Number */
+ NUM_CENTER: 12,
+ /** Key constant @type Number */
+ ENTER: 13,
+ /** Key constant @type Number */
+ RETURN: 13,
+ /** Key constant @type Number */
+ SHIFT: 16,
+ /** Key constant @type Number */
+ CTRL: 17,
+ /** Key constant @type Number */
+ ALT: 18,
+ /** Key constant @type Number */
+ PAUSE: 19,
+ /** Key constant @type Number */
+ CAPS_LOCK: 20,
+ /** Key constant @type Number */
+ ESC: 27,
+ /** Key constant @type Number */
+ SPACE: 32,
+ /** Key constant @type Number */
+ PAGE_UP: 33,
+ /** Key constant @type Number */
+ PAGE_DOWN: 34,
+ /** Key constant @type Number */
+ END: 35,
+ /** Key constant @type Number */
+ HOME: 36,
+ /** Key constant @type Number */
+ LEFT: 37,
+ /** Key constant @type Number */
+ UP: 38,
+ /** Key constant @type Number */
+ RIGHT: 39,
+ /** Key constant @type Number */
+ DOWN: 40,
+ /** Key constant @type Number */
+ PRINT_SCREEN: 44,
+ /** Key constant @type Number */
+ INSERT: 45,
+ /** Key constant @type Number */
+ DELETE: 46,
+ /** Key constant @type Number */
+ ZERO: 48,
+ /** Key constant @type Number */
+ ONE: 49,
+ /** Key constant @type Number */
+ TWO: 50,
+ /** Key constant @type Number */
+ THREE: 51,
+ /** Key constant @type Number */
+ FOUR: 52,
+ /** Key constant @type Number */
+ FIVE: 53,
+ /** Key constant @type Number */
+ SIX: 54,
+ /** Key constant @type Number */
+ SEVEN: 55,
+ /** Key constant @type Number */
+ EIGHT: 56,
+ /** Key constant @type Number */
+ NINE: 57,
+ /** Key constant @type Number */
+ A: 65,
+ /** Key constant @type Number */
+ B: 66,
+ /** Key constant @type Number */
+ C: 67,
+ /** Key constant @type Number */
+ D: 68,
+ /** Key constant @type Number */
+ E: 69,
+ /** Key constant @type Number */
+ F: 70,
+ /** Key constant @type Number */
+ G: 71,
+ /** Key constant @type Number */
+ H: 72,
+ /** Key constant @type Number */
+ I: 73,
+ /** Key constant @type Number */
+ J: 74,
+ /** Key constant @type Number */
+ K: 75,
+ /** Key constant @type Number */
+ L: 76,
+ /** Key constant @type Number */
+ M: 77,
+ /** Key constant @type Number */
+ N: 78,
+ /** Key constant @type Number */
+ O: 79,
+ /** Key constant @type Number */
+ P: 80,
+ /** Key constant @type Number */
+ Q: 81,
+ /** Key constant @type Number */
+ R: 82,
+ /** Key constant @type Number */
+ S: 83,
+ /** Key constant @type Number */
+ T: 84,
+ /** Key constant @type Number */
+ U: 85,
+ /** Key constant @type Number */
+ V: 86,
+ /** Key constant @type Number */
+ W: 87,
+ /** Key constant @type Number */
+ X: 88,
+ /** Key constant @type Number */
+ Y: 89,
+ /** Key constant @type Number */
+ Z: 90,
+ /** Key constant @type Number */
+ CONTEXT_MENU: 93,
+ /** Key constant @type Number */
+ NUM_ZERO: 96,
+ /** Key constant @type Number */
+ NUM_ONE: 97,
+ /** Key constant @type Number */
+ NUM_TWO: 98,
+ /** Key constant @type Number */
+ NUM_THREE: 99,
+ /** Key constant @type Number */
+ NUM_FOUR: 100,
+ /** Key constant @type Number */
+ NUM_FIVE: 101,
+ /** Key constant @type Number */
+ NUM_SIX: 102,
+ /** Key constant @type Number */
+ NUM_SEVEN: 103,
+ /** Key constant @type Number */
+ NUM_EIGHT: 104,
+ /** Key constant @type Number */
+ NUM_NINE: 105,
+ /** Key constant @type Number */
+ NUM_MULTIPLY: 106,
+ /** Key constant @type Number */
+ NUM_PLUS: 107,
+ /** Key constant @type Number */
+ NUM_MINUS: 109,
+ /** Key constant @type Number */
+ NUM_PERIOD: 110,
+ /** Key constant @type Number */
+ NUM_DIVISION: 111,
+ /** Key constant @type Number */
+ F1: 112,
+ /** Key constant @type Number */
+ F2: 113,
+ /** Key constant @type Number */
+ F3: 114,
+ /** Key constant @type Number */
+ F4: 115,
+ /** Key constant @type Number */
+ F5: 116,
+ /** Key constant @type Number */
+ F6: 117,
+ /** Key constant @type Number */
+ F7: 118,
+ /** Key constant @type Number */
+ F8: 119,
+ /** Key constant @type Number */
+ F9: 120,
+ /** Key constant @type Number */
+ F10: 121,
+ /** Key constant @type Number */
+ F11: 122,
+ /** Key constant @type Number */
+ F12: 123,
+ /**
+ * The mouse wheel delta scaling factor. This value depends on browser version and OS and
+ * attempts to produce a similar scrolling experience across all platforms and browsers.
+ *
+ * To change this value:
+ *
+ * Ext.EventObjectImpl.prototype.WHEEL_SCALE = 72;
+ *
+ * @type Number
+ * @markdown
+ */
+ WHEEL_SCALE: (function () {
+ var scale;
+
+ if (Ext.isGecko) {
+ // Firefox uses 3 on all platforms
+ scale = 3;
+ } else if (Ext.isMac) {
+ // Continuous scrolling devices have momentum and produce much more scroll than
+ // discrete devices on the same OS and browser. To make things exciting, Safari
+ // (and not Chrome) changed from small values to 120 (like IE).
+
+ if (Ext.isSafari && Ext.webKitVersion >= 532.0) {
+ // Safari changed the scrolling factor to match IE (for details see
+ // https://bugs.webkit.org/show_bug.cgi?id=24368). The WebKit version where this
+ // change was introduced was 532.0
+ // Detailed discussion:
+ // https://bugs.webkit.org/show_bug.cgi?id=29601
+ // http://trac.webkit.org/browser/trunk/WebKit/chromium/src/mac/WebInputEventFactory.mm#L1063
+ scale = 120;
+ } else {
+ // MS optical wheel mouse produces multiples of 12 which is close enough
+ // to help tame the speed of the continuous mice...
+ scale = 12;
+ }
+
+ // Momentum scrolling produces very fast scrolling, so increase the scale factor
+ // to help produce similar results cross platform. This could be even larger and
+ // it would help those mice, but other mice would become almost unusable as a
+ // result (since we cannot tell which device type is in use).
+ scale *= 3;
+ } else {
+ // IE, Opera and other Windows browsers use 120.
+ scale = 120;
+ }
+
+ return scale;
+ })(),
+
+ /**
+ * Simple click regex
+ * @private
+ */
+ clickRe: /(dbl)?click/,
+ // safari keypress events for special keys return bad keycodes
+ safariKeys: {
+ 3: 13, // enter
+ 63234: 37, // left
+ 63235: 39, // right
+ 63232: 38, // up
+ 63233: 40, // down
+ 63276: 33, // page up
+ 63277: 34, // page down
+ 63272: 46, // delete
+ 63273: 36, // home
+ 63275: 35 // end
+ },
+ // normalize button clicks, don't see any way to feature detect this.
+ btnMap: Ext.isIE ? {
+ 1: 0,
+ 4: 1,
+ 2: 2
+ } : {
+ 0: 0,
+ 1: 1,
+ 2: 2
+ },
+
+ constructor: function(event, freezeEvent){
+ if (event) {
+ this.setEvent(event.browserEvent || event, freezeEvent);
+ }
+ },
+
+ setEvent: function(event, freezeEvent){
+ var me = this, button, options;
+
+ if (event == me || (event && event.browserEvent)) { // already wrapped
+ return event;
+ }
+ me.browserEvent = event;
+ if (event) {
+ // normalize buttons
+ button = event.button ? me.btnMap[event.button] : (event.which ? event.which - 1 : -1);
+ if (me.clickRe.test(event.type) && button == -1) {
+ button = 0;
+ }
+ options = {
+ type: event.type,
+ button: button,
+ shiftKey: event.shiftKey,
+ // mac metaKey behaves like ctrlKey
+ ctrlKey: event.ctrlKey || event.metaKey || false,
+ altKey: event.altKey,
+ // in getKey these will be normalized for the mac
+ keyCode: event.keyCode,
+ charCode: event.charCode,
+ // cache the targets for the delayed and or buffered events
+ target: Ext.EventManager.getTarget(event),
+ relatedTarget: Ext.EventManager.getRelatedTarget(event),
+ currentTarget: event.currentTarget,
+ xy: (freezeEvent ? me.getXY() : null)
+ };
+ } else {
+ options = {
+ button: -1,
+ shiftKey: false,
+ ctrlKey: false,
+ altKey: false,
+ keyCode: 0,
+ charCode: 0,
+ target: null,
+ xy: [0, 0]
+ };
+ }
+ Ext.apply(me, options);
+ return me;
+ },
+
+ /**
+ * Stop the event (preventDefault and stopPropagation)
+ */
+ stopEvent: function(){
+ this.stopPropagation();
+ this.preventDefault();
+ },
+
+ /**
+ * Prevents the browsers default handling of the event.
+ */
+ preventDefault: function(){
+ if (this.browserEvent) {
+ Ext.EventManager.preventDefault(this.browserEvent);
+ }
+ },
+
+ /**
+ * Cancels bubbling of the event.
+ */
+ stopPropagation: function(){
+ var browserEvent = this.browserEvent;
+
+ if (browserEvent) {
+ if (browserEvent.type == 'mousedown') {
+ Ext.EventManager.stoppedMouseDownEvent.fire(this);
+ }
+ Ext.EventManager.stopPropagation(browserEvent);
+ }
+ },
+
+ /**
+ * Gets the character code for the event.
+ * @return {Number}
+ */
+ getCharCode: function(){
+ return this.charCode || this.keyCode;
+ },
+
+ /**
+ * Returns a normalized keyCode for the event.
+ * @return {Number} The key code
+ */
+ getKey: function(){
+ return this.normalizeKey(this.keyCode || this.charCode);
+ },
+
+ /**
+ * Normalize key codes across browsers
+ * @private
+ * @param {Number} key The key code
+ * @return {Number} The normalized code
+ */
+ normalizeKey: function(key){
+ // can't feature detect this
+ return Ext.isWebKit ? (this.safariKeys[key] || key) : key;
+ },
+
+ /**
+ * Gets the x coordinate of the event.
+ * @return {Number}
+ * @deprecated 4.0 Replaced by {@link #getX}
+ */
+ getPageX: function(){
+ return this.getX();
+ },
+
+ /**
+ * Gets the y coordinate of the event.
+ * @return {Number}
+ * @deprecated 4.0 Replaced by {@link #getY}
+ */
+ getPageY: function(){
+ return this.getY();
+ },
+
+ /**
+ * Gets the x coordinate of the event.
+ * @return {Number}
+ */
+ getX: function() {
+ return this.getXY()[0];
+ },
+
+ /**
+ * Gets the y coordinate of the event.
+ * @return {Number}
+ */
+ getY: function() {
+ return this.getXY()[1];
+ },
+
+ /**
+ * Gets the page coordinates of the event.
+ * @return {Number[]} The xy values like [x, y]
+ */
+ getXY: function() {
+ if (!this.xy) {
+ // same for XY
+ this.xy = Ext.EventManager.getPageXY(this.browserEvent);
+ }
+ return this.xy;
+ },
+
+ /**
+ * Gets the target for the event.
+ * @param {String} selector (optional) A simple selector to filter the target or look for an ancestor of the target
+ * @param {Number/HTMLElement} maxDepth (optional) The max depth to search as a number or element (defaults to 10 || document.body)
+ * @param {Boolean} returnEl (optional) True to return a Ext.Element object instead of DOM node
+ * @return {HTMLElement}
+ */
+ getTarget : function(selector, maxDepth, returnEl){
+ if (selector) {
+ return Ext.fly(this.target).findParent(selector, maxDepth, returnEl);
+ }
+ return returnEl ? Ext.get(this.target) : this.target;
+ },
+
+ /**
+ * Gets the related target.
+ * @param {String} selector (optional) A simple selector to filter the target or look for an ancestor of the target
+ * @param {Number/HTMLElement} maxDepth (optional) The max depth to search as a number or element (defaults to 10 || document.body)
+ * @param {Boolean} returnEl (optional) True to return a Ext.Element object instead of DOM node
+ * @return {HTMLElement}
+ */
+ getRelatedTarget : function(selector, maxDepth, returnEl){
+ if (selector) {
+ return Ext.fly(this.relatedTarget).findParent(selector, maxDepth, returnEl);
+ }
+ return returnEl ? Ext.get(this.relatedTarget) : this.relatedTarget;
+ },
+
+ /**
+ * Correctly scales a given wheel delta.
+ * @param {Number} delta The delta value.
+ */
+ correctWheelDelta : function (delta) {
+ var scale = this.WHEEL_SCALE,
+ ret = Math.round(delta / scale);
+
+ if (!ret && delta) {
+ ret = (delta < 0) ? -1 : 1; // don't allow non-zero deltas to go to zero!
+ }
+
+ return ret;
+ },
+
+ /**
+ * Returns the mouse wheel deltas for this event.
+ * @return {Object} An object with "x" and "y" properties holding the mouse wheel deltas.
+ */
+ getWheelDeltas : function () {
+ var me = this,
+ event = me.browserEvent,
+ dx = 0, dy = 0; // the deltas
+
+ if (Ext.isDefined(event.wheelDeltaX)) { // WebKit has both dimensions
+ dx = event.wheelDeltaX;
+ dy = event.wheelDeltaY;
+ } else if (event.wheelDelta) { // old WebKit and IE
+ dy = event.wheelDelta;
+ } else if (event.detail) { // Gecko
+ dy = -event.detail; // gecko is backwards
+
+ // Gecko sometimes returns really big values if the user changes settings to
+ // scroll a whole page per scroll
+ if (dy > 100) {
+ dy = 3;
+ } else if (dy < -100) {
+ dy = -3;
+ }
+
+ // Firefox 3.1 adds an axis field to the event to indicate direction of
+ // scroll. See https://developer.mozilla.org/en/Gecko-Specific_DOM_Events
+ if (Ext.isDefined(event.axis) && event.axis === event.HORIZONTAL_AXIS) {
+ dx = dy;
+ dy = 0;
+ }
+ }
+
+ return {
+ x: me.correctWheelDelta(dx),
+ y: me.correctWheelDelta(dy)
+ };
+ },
+
+ /**
+ * Normalizes mouse wheel y-delta across browsers. To get x-delta information, use
+ * {@link #getWheelDeltas} instead.
+ * @return {Number} The mouse wheel y-delta
+ */
+ getWheelDelta : function(){
+ var deltas = this.getWheelDeltas();
+
+ return deltas.y;
+ },
+
+ /**
+ * Returns true if the target of this event is a child of el. Unless the allowEl parameter is set, it will return false if if the target is el.
+ * Example usage:<pre><code>
+// Handle click on any child of an element
+Ext.getBody().on('click', function(e){
+ if(e.within('some-el')){
+ alert('Clicked on a child of some-el!');
+ }
+});
+
+// Handle click directly on an element, ignoring clicks on child nodes
+Ext.getBody().on('click', function(e,t){
+ if((t.id == 'some-el') && !e.within(t, true)){
+ alert('Clicked directly on some-el!');
+ }
+});
+</code></pre>
+ * @param {String/HTMLElement/Ext.Element} el The id, DOM element or Ext.Element to check
+ * @param {Boolean} related (optional) true to test if the related target is within el instead of the target
+ * @param {Boolean} allowEl (optional) true to also check if the passed element is the target or related target
+ * @return {Boolean}
+ */
+ within : function(el, related, allowEl){
+ if(el){
+ var t = related ? this.getRelatedTarget() : this.getTarget(),
+ result;
+
+ if (t) {
+ result = Ext.fly(el).contains(t);
+ if (!result && allowEl) {
+ result = t == Ext.getDom(el);
+ }
+ return result;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Checks if the key pressed was a "navigation" key
+ * @return {Boolean} True if the press is a navigation keypress
+ */
+ isNavKeyPress : function(){
+ var me = this,
+ k = this.normalizeKey(me.keyCode);
+
+ return (k >= 33 && k <= 40) || // Page Up/Down, End, Home, Left, Up, Right, Down
+ k == me.RETURN ||
+ k == me.TAB ||
+ k == me.ESC;
+ },
+
+ /**
+ * Checks if the key pressed was a "special" key
+ * @return {Boolean} True if the press is a special keypress
+ */
+ isSpecialKey : function(){
+ var k = this.normalizeKey(this.keyCode);
+ return (this.type == 'keypress' && this.ctrlKey) ||
+ this.isNavKeyPress() ||
+ (k == this.BACKSPACE) || // Backspace
+ (k >= 16 && k <= 20) || // Shift, Ctrl, Alt, Pause, Caps Lock
+ (k >= 44 && k <= 46); // Print Screen, Insert, Delete
+ },
+
+ /**
+ * Returns a point object that consists of the object coordinates.
+ * @return {Ext.util.Point} point
+ */
+ getPoint : function(){
+ var xy = this.getXY();
+ return Ext.create('Ext.util.Point', xy[0], xy[1]);
+ },
+
+ /**
+ * Returns true if the control, meta, shift or alt key was pressed during this event.
+ * @return {Boolean}
+ */
+ hasModifier : function(){
+ return this.ctrlKey || this.altKey || this.shiftKey || this.metaKey;
+ },
+
+ /**
+ * Injects a DOM event using the data in this object and (optionally) a new target.
+ * This is a low-level technique and not likely to be used by application code. The
+ * currently supported event types are:
+ * <p><b>HTMLEvents</b></p>
+ * <ul>
+ * <li>load</li>
+ * <li>unload</li>
+ * <li>select</li>
+ * <li>change</li>
+ * <li>submit</li>
+ * <li>reset</li>
+ * <li>resize</li>
+ * <li>scroll</li>
+ * </ul>
+ * <p><b>MouseEvents</b></p>
+ * <ul>
+ * <li>click</li>
+ * <li>dblclick</li>
+ * <li>mousedown</li>
+ * <li>mouseup</li>
+ * <li>mouseover</li>
+ * <li>mousemove</li>
+ * <li>mouseout</li>
+ * </ul>
+ * <p><b>UIEvents</b></p>
+ * <ul>
+ * <li>focusin</li>
+ * <li>focusout</li>
+ * <li>activate</li>
+ * <li>focus</li>
+ * <li>blur</li>
+ * </ul>
+ * @param {Ext.Element/HTMLElement} target (optional) If specified, the target for the event. This
+ * is likely to be used when relaying a DOM event. If not specified, {@link #getTarget}
+ * is used to determine the target.
+ */
+ injectEvent: function () {
+ var API,
+ dispatchers = {}; // keyed by event type (e.g., 'mousedown')
+
+ // Good reference: http://developer.yahoo.com/yui/docs/UserAction.js.html
+
+ // IE9 has createEvent, but this code causes major problems with htmleditor (it
+ // blocks all mouse events and maybe more). TODO
+
+ if (!Ext.isIE && document.createEvent) { // if (DOM compliant)
+ API = {
+ createHtmlEvent: function (doc, type, bubbles, cancelable) {
+ var event = doc.createEvent('HTMLEvents');
+
+ event.initEvent(type, bubbles, cancelable);
+ return event;
+ },
+
+ createMouseEvent: function (doc, type, bubbles, cancelable, detail,
+ clientX, clientY, ctrlKey, altKey, shiftKey, metaKey,
+ button, relatedTarget) {
+ var event = doc.createEvent('MouseEvents'),
+ view = doc.defaultView || window;
+
+ if (event.initMouseEvent) {
+ event.initMouseEvent(type, bubbles, cancelable, view, detail,
+ clientX, clientY, clientX, clientY, ctrlKey, altKey,
+ shiftKey, metaKey, button, relatedTarget);
+ } else { // old Safari
+ event = doc.createEvent('UIEvents');
+ event.initEvent(type, bubbles, cancelable);
+ event.view = view;
+ event.detail = detail;
+ event.screenX = clientX;
+ event.screenY = clientY;
+ event.clientX = clientX;
+ event.clientY = clientY;
+ event.ctrlKey = ctrlKey;
+ event.altKey = altKey;
+ event.metaKey = metaKey;
+ event.shiftKey = shiftKey;
+ event.button = button;
+ event.relatedTarget = relatedTarget;
+ }
+
+ return event;
+ },
+
+ createUIEvent: function (doc, type, bubbles, cancelable, detail) {
+ var event = doc.createEvent('UIEvents'),
+ view = doc.defaultView || window;
+
+ event.initUIEvent(type, bubbles, cancelable, view, detail);
+ return event;
+ },
+
+ fireEvent: function (target, type, event) {
+ target.dispatchEvent(event);
+ },
+
+ fixTarget: function (target) {
+ // Safari3 doesn't have window.dispatchEvent()
+ if (target == window && !target.dispatchEvent) {
+ return document;
+ }
+
+ return target;
+ }
+ };
+ } else if (document.createEventObject) { // else if (IE)
+ var crazyIEButtons = { 0: 1, 1: 4, 2: 2 };
+
+ API = {
+ createHtmlEvent: function (doc, type, bubbles, cancelable) {
+ var event = doc.createEventObject();
+ event.bubbles = bubbles;
+ event.cancelable = cancelable;
+ return event;
+ },
+
+ createMouseEvent: function (doc, type, bubbles, cancelable, detail,
+ clientX, clientY, ctrlKey, altKey, shiftKey, metaKey,
+ button, relatedTarget) {
+ var event = doc.createEventObject();
+ event.bubbles = bubbles;
+ event.cancelable = cancelable;
+ event.detail = detail;
+ event.screenX = clientX;
+ event.screenY = clientY;
+ event.clientX = clientX;
+ event.clientY = clientY;
+ event.ctrlKey = ctrlKey;
+ event.altKey = altKey;
+ event.shiftKey = shiftKey;
+ event.metaKey = metaKey;
+ event.button = crazyIEButtons[button] || button;
+ event.relatedTarget = relatedTarget; // cannot assign to/fromElement
+ return event;
+ },
+
+ createUIEvent: function (doc, type, bubbles, cancelable, detail) {
+ var event = doc.createEventObject();
+ event.bubbles = bubbles;
+ event.cancelable = cancelable;
+ return event;
+ },
+
+ fireEvent: function (target, type, event) {
+ target.fireEvent('on' + type, event);
+ },
+
+ fixTarget: function (target) {
+ if (target == document) {
+ // IE6,IE7 thinks window==document and doesn't have window.fireEvent()
+ // IE6,IE7 cannot properly call document.fireEvent()
+ return document.documentElement;
+ }
+
+ return target;
+ }
+ };
+ }
+
+ //----------------
+ // HTMLEvents
+
+ Ext.Object.each({
+ load: [false, false],
+ unload: [false, false],
+ select: [true, false],
+ change: [true, false],
+ submit: [true, true],
+ reset: [true, false],
+ resize: [true, false],
+ scroll: [true, false]
+ },
+ function (name, value) {
+ var bubbles = value[0], cancelable = value[1];
+ dispatchers[name] = function (targetEl, srcEvent) {
+ var e = API.createHtmlEvent(name, bubbles, cancelable);
+ API.fireEvent(targetEl, name, e);
+ };
+ });
+
+ //----------------
+ // MouseEvents
+
+ function createMouseEventDispatcher (type, detail) {
+ var cancelable = (type != 'mousemove');
+ return function (targetEl, srcEvent) {
+ var xy = srcEvent.getXY(),
+ e = API.createMouseEvent(targetEl.ownerDocument, type, true, cancelable,
+ detail, xy[0], xy[1], srcEvent.ctrlKey, srcEvent.altKey,
+ srcEvent.shiftKey, srcEvent.metaKey, srcEvent.button,
+ srcEvent.relatedTarget);
+ API.fireEvent(targetEl, type, e);
+ };
+ }
+
+ Ext.each(['click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mousemove', 'mouseout'],
+ function (eventName) {
+ dispatchers[eventName] = createMouseEventDispatcher(eventName, 1);
+ });
+
+ //----------------
+ // UIEvents
+
+ Ext.Object.each({
+ focusin: [true, false],
+ focusout: [true, false],
+ activate: [true, true],
+ focus: [false, false],
+ blur: [false, false]
+ },
+ function (name, value) {
+ var bubbles = value[0], cancelable = value[1];
+ dispatchers[name] = function (targetEl, srcEvent) {
+ var e = API.createUIEvent(targetEl.ownerDocument, name, bubbles, cancelable, 1);
+ API.fireEvent(targetEl, name, e);
+ };
+ });
+
+ //---------
+ if (!API) {
+ // not even sure what ancient browsers fall into this category...
+
+ dispatchers = {}; // never mind all those we just built :P
+
+ API = {
+ fixTarget: function (t) {
+ return t;
+ }
+ };
+ }
+
+ function cannotInject (target, srcEvent) {
+ }
+
+ return function (target) {
+ var me = this,
+ dispatcher = dispatchers[me.type] || cannotInject,
+ t = target ? (target.dom || target) : me.getTarget();
+
+ t = API.fixTarget(t);
+ dispatcher(t, me);
+ };
+ }() // call to produce method
+
+}, function() {
+
+Ext.EventObject = new Ext.EventObjectImpl();
+
+});
+
+
+/**
+ * @class Ext.Element
+ */
+(function(){
+ var doc = document,
+ activeElement = null,
+ isCSS1 = doc.compatMode == "CSS1Compat",
+ ELEMENT = Ext.Element,
+ fly = function(el){
+ if (!_fly) {
+ _fly = new Ext.Element.Flyweight();
+ }
+ _fly.dom = el;
+ return _fly;
+ }, _fly;
+
+ // If the browser does not support document.activeElement we need some assistance.
+ // This covers old Safari 3.2 (4.0 added activeElement along with just about all
+ // other browsers). We need this support to handle issues with old Safari.
+ if (!('activeElement' in doc) && doc.addEventListener) {
+ doc.addEventListener('focus',
+ function (ev) {
+ if (ev && ev.target) {
+ activeElement = (ev.target == doc) ? null : ev.target;
+ }
+ }, true);
+ }
+
+ /*
+ * Helper function to create the function that will restore the selection.
+ */
+ function makeSelectionRestoreFn (activeEl, start, end) {
+ return function () {
+ activeEl.selectionStart = start;
+ activeEl.selectionEnd = end;
+ };
+ }
+
+ Ext.apply(ELEMENT, {
+ isAncestor : function(p, c) {
+ var ret = false;
+
+ p = Ext.getDom(p);
+ c = Ext.getDom(c);
+ if (p && c) {
+ if (p.contains) {
+ return p.contains(c);
+ } else if (p.compareDocumentPosition) {
+ return !!(p.compareDocumentPosition(c) & 16);
+ } else {
+ while ((c = c.parentNode)) {
+ ret = c == p || ret;
+ }
+ }
+ }
+ return ret;
+ },
+
+ /**
+ * Returns the active element in the DOM. If the browser supports activeElement
+ * on the document, this is returned. If not, the focus is tracked and the active
+ * element is maintained internally.
+ * @return {HTMLElement} The active (focused) element in the document.
+ */
+ getActiveElement: function () {
+ return doc.activeElement || activeElement;
+ },
+
+ /**
+ * Creates a function to call to clean up problems with the work-around for the
+ * WebKit RightMargin bug. The work-around is to add "display: 'inline-block'" to
+ * the element before calling getComputedStyle and then to restore its original
+ * display value. The problem with this is that it corrupts the selection of an
+ * INPUT or TEXTAREA element (as in the "I-beam" goes away but ths focus remains).
+ * To cleanup after this, we need to capture the selection of any such element and
+ * then restore it after we have restored the display style.
+ *
+ * @param target {Element} The top-most element being adjusted.
+ * @private
+ */
+ getRightMarginFixCleaner: function (target) {
+ var supports = Ext.supports,
+ hasInputBug = supports.DisplayChangeInputSelectionBug,
+ hasTextAreaBug = supports.DisplayChangeTextAreaSelectionBug;
+
+ if (hasInputBug || hasTextAreaBug) {
+ var activeEl = doc.activeElement || activeElement, // save a call
+ tag = activeEl && activeEl.tagName,
+ start,
+ end;
+
+ if ((hasTextAreaBug && tag == 'TEXTAREA') ||
+ (hasInputBug && tag == 'INPUT' && activeEl.type == 'text')) {
+ if (ELEMENT.isAncestor(target, activeEl)) {
+ start = activeEl.selectionStart;
+ end = activeEl.selectionEnd;
+
+ if (Ext.isNumber(start) && Ext.isNumber(end)) { // to be safe...
+ // We don't create the raw closure here inline because that
+ // will be costly even if we don't want to return it (nested
+ // function decls and exprs are often instantiated on entry
+ // regardless of whether execution ever reaches them):
+ return makeSelectionRestoreFn(activeEl, start, end);
+ }
+ }
+ }
+ }
+
+ return Ext.emptyFn; // avoid special cases, just return a nop
+ },
+
+ getViewWidth : function(full) {
+ return full ? ELEMENT.getDocumentWidth() : ELEMENT.getViewportWidth();
+ },
+
+ getViewHeight : function(full) {
+ return full ? ELEMENT.getDocumentHeight() : ELEMENT.getViewportHeight();
+ },
+
+ getDocumentHeight: function() {
+ return Math.max(!isCSS1 ? doc.body.scrollHeight : doc.documentElement.scrollHeight, ELEMENT.getViewportHeight());
+ },
+
+ getDocumentWidth: function() {
+ return Math.max(!isCSS1 ? doc.body.scrollWidth : doc.documentElement.scrollWidth, ELEMENT.getViewportWidth());
+ },
+
+ getViewportHeight: function(){
+ return Ext.isIE ?
+ (Ext.isStrict ? doc.documentElement.clientHeight : doc.body.clientHeight) :
+ self.innerHeight;
+ },
+
+ getViewportWidth : function() {
+ return (!Ext.isStrict && !Ext.isOpera) ? doc.body.clientWidth :
+ Ext.isIE ? doc.documentElement.clientWidth : self.innerWidth;
+ },
+
+ getY : function(el) {
+ return ELEMENT.getXY(el)[1];
+ },
+
+ getX : function(el) {
+ return ELEMENT.getXY(el)[0];
+ },
+
+ getOffsetParent: function (el) {
+ el = Ext.getDom(el);
+ try {
+ // accessing offsetParent can throw "Unspecified Error" in IE6-8 (not 9)
+ return el.offsetParent;
+ } catch (e) {
+ var body = document.body; // safe bet, unless...
+ return (el == body) ? null : body;
+ }
+ },
+
+ getXY : function(el) {
+ var p,
+ pe,
+ b,
+ bt,
+ bl,
+ dbd,
+ x = 0,
+ y = 0,
+ scroll,
+ hasAbsolute,
+ bd = (doc.body || doc.documentElement),
+ ret;
+
+ el = Ext.getDom(el);
+
+ if(el != bd){
+ hasAbsolute = fly(el).isStyle("position", "absolute");
+
+ if (el.getBoundingClientRect) {
+ try {
+ b = el.getBoundingClientRect();
+ scroll = fly(document).getScroll();
+ ret = [ Math.round(b.left + scroll.left), Math.round(b.top + scroll.top) ];
+ } catch (e) {
+ // IE6-8 can also throw from getBoundingClientRect...
+ }
+ }
+
+ if (!ret) {
+ for (p = el; p; p = ELEMENT.getOffsetParent(p)) {
+ pe = fly(p);
+ x += p.offsetLeft;
+ y += p.offsetTop;
+
+ hasAbsolute = hasAbsolute || pe.isStyle("position", "absolute");
+
+ if (Ext.isGecko) {
+ y += bt = parseInt(pe.getStyle("borderTopWidth"), 10) || 0;
+ x += bl = parseInt(pe.getStyle("borderLeftWidth"), 10) || 0;
+
+ if (p != el && !pe.isStyle('overflow','visible')) {
+ x += bl;
+ y += bt;
+ }
+ }
+ }
+
+ if (Ext.isSafari && hasAbsolute) {
+ x -= bd.offsetLeft;
+ y -= bd.offsetTop;
+ }
+
+ if (Ext.isGecko && !hasAbsolute) {
+ dbd = fly(bd);
+ x += parseInt(dbd.getStyle("borderLeftWidth"), 10) || 0;
+ y += parseInt(dbd.getStyle("borderTopWidth"), 10) || 0;
+ }
+
+ p = el.parentNode;
+ while (p && p != bd) {
+ if (!Ext.isOpera || (p.tagName != 'TR' && !fly(p).isStyle("display", "inline"))) {
+ x -= p.scrollLeft;
+ y -= p.scrollTop;
+ }
+ p = p.parentNode;
+ }
+ ret = [x,y];
+ }
+ }
+ return ret || [0,0];
+ },
+
+ setXY : function(el, xy) {
+ (el = Ext.fly(el, '_setXY')).position();
+
+ var pts = el.translatePoints(xy),
+ style = el.dom.style,
+ pos;
+
+ for (pos in pts) {
+ if (!isNaN(pts[pos])) {
+ style[pos] = pts[pos] + "px";
+ }
+ }
+ },
+
+ setX : function(el, x) {
+ ELEMENT.setXY(el, [x, false]);
+ },
+
+ setY : function(el, y) {
+ ELEMENT.setXY(el, [false, y]);
+ },
+
+ /**
+ * Serializes a DOM form into a url encoded string
+ * @param {Object} form The form
+ * @return {String} The url encoded form
+ */
+ serializeForm: function(form) {
+ var fElements = form.elements || (document.forms[form] || Ext.getDom(form)).elements,
+ hasSubmit = false,
+ encoder = encodeURIComponent,
+ name,
+ data = '',
+ type,
+ hasValue;
+
+ Ext.each(fElements, function(element){
+ name = element.name;
+ type = element.type;
+
+ if (!element.disabled && name) {
+ if (/select-(one|multiple)/i.test(type)) {
+ Ext.each(element.options, function(opt){
+ if (opt.selected) {
+ hasValue = opt.hasAttribute ? opt.hasAttribute('value') : opt.getAttributeNode('value').specified;
+ data += Ext.String.format("{0}={1}&", encoder(name), encoder(hasValue ? opt.value : opt.text));
+ }
+ });
+ } else if (!(/file|undefined|reset|button/i.test(type))) {
+ if (!(/radio|checkbox/i.test(type) && !element.checked) && !(type == 'submit' && hasSubmit)) {
+ data += encoder(name) + '=' + encoder(element.value) + '&';
+ hasSubmit = /submit/i.test(type);
+ }
+ }
+ }
+ });
+ return data.substr(0, data.length - 1);
+ }
+ });
+})();
+
+/**
+ * @class Ext.Element
+ */
+
+Ext.Element.addMethods((function(){
+ var focusRe = /button|input|textarea|select|object/;
+ return {
+ /**
+ * Monitors this Element for the mouse leaving. Calls the function after the specified delay only if
+ * the mouse was not moved back into the Element within the delay. If the mouse <i>was</i> moved
+ * back in, the function is not called.
+ * @param {Number} delay The delay <b>in milliseconds</b> to wait for possible mouse re-entry before calling the handler function.
+ * @param {Function} handler The function to call if the mouse remains outside of this Element for the specified time.
+ * @param {Object} scope The scope (<code>this</code> reference) in which the handler function executes. Defaults to this Element.
+ * @return {Object} The listeners object which was added to this element so that monitoring can be stopped. Example usage:<pre><code>
+// Hide the menu if the mouse moves out for 250ms or more
+this.mouseLeaveMonitor = this.menuEl.monitorMouseLeave(250, this.hideMenu, this);
+
+...
+// Remove mouseleave monitor on menu destroy
+this.menuEl.un(this.mouseLeaveMonitor);
+ </code></pre>
+ */
+ monitorMouseLeave: function(delay, handler, scope) {
+ var me = this,
+ timer,
+ listeners = {
+ mouseleave: function(e) {
+ timer = setTimeout(Ext.Function.bind(handler, scope||me, [e]), delay);
+ },
+ mouseenter: function() {
+ clearTimeout(timer);
+ },
+ freezeEvent: true
+ };
+
+ me.on(listeners);
+ return listeners;
+ },
+
+ /**
+ * Stops the specified event(s) from bubbling and optionally prevents the default action
+ * @param {String/String[]} eventName an event / array of events to stop from bubbling
+ * @param {Boolean} preventDefault (optional) true to prevent the default action too
+ * @return {Ext.Element} this
+ */
+ swallowEvent : function(eventName, preventDefault) {
+ var me = this;
+ function fn(e) {
+ e.stopPropagation();
+ if (preventDefault) {
+ e.preventDefault();
+ }
+ }
+
+ if (Ext.isArray(eventName)) {
+ Ext.each(eventName, function(e) {
+ me.on(e, fn);
+ });
+ return me;
+ }
+ me.on(eventName, fn);
+ return me;
+ },
+
+ /**
+ * Create an event handler on this element such that when the event fires and is handled by this element,
+ * it will be relayed to another object (i.e., fired again as if it originated from that object instead).
+ * @param {String} eventName The type of event to relay
+ * @param {Object} object Any object that extends {@link Ext.util.Observable} that will provide the context
+ * for firing the relayed event
+ */
+ relayEvent : function(eventName, observable) {
+ this.on(eventName, function(e) {
+ observable.fireEvent(eventName, e);
+ });
+ },
+
+ /**
+ * Removes Empty, or whitespace filled text nodes. Combines adjacent text nodes.
+ * @param {Boolean} forceReclean (optional) By default the element
+ * keeps track if it has been cleaned already so
+ * you can call this over and over. However, if you update the element and
+ * need to force a reclean, you can pass true.
+ */
+ clean : function(forceReclean) {
+ var me = this,
+ dom = me.dom,
+ n = dom.firstChild,
+ nx,
+ ni = -1;
+
+ if (Ext.Element.data(dom, 'isCleaned') && forceReclean !== true) {
+ return me;
+ }
+
+ while (n) {
+ nx = n.nextSibling;
+ if (n.nodeType == 3) {
+ // Remove empty/whitespace text nodes
+ if (!(/\S/.test(n.nodeValue))) {
+ dom.removeChild(n);
+ // Combine adjacent text nodes
+ } else if (nx && nx.nodeType == 3) {
+ n.appendData(Ext.String.trim(nx.data));
+ dom.removeChild(nx);
+ nx = n.nextSibling;
+ n.nodeIndex = ++ni;
+ }
+ } else {
+ // Recursively clean
+ Ext.fly(n).clean();
+ n.nodeIndex = ++ni;
+ }
+ n = nx;
+ }
+
+ Ext.Element.data(dom, 'isCleaned', true);
+ return me;
+ },
+
+ /**
+ * Direct access to the Ext.ElementLoader {@link Ext.ElementLoader#load} method. The method takes the same object
+ * parameter as {@link Ext.ElementLoader#load}
+ * @return {Ext.Element} this
+ */
+ load : function(options) {
+ this.getLoader().load(options);
+ return this;
+ },
+
+ /**
+ * Gets this element's {@link Ext.ElementLoader ElementLoader}
+ * @return {Ext.ElementLoader} The loader
+ */
+ getLoader : function() {
+ var dom = this.dom,
+ data = Ext.Element.data,
+ loader = data(dom, 'loader');
+
+ if (!loader) {
+ loader = Ext.create('Ext.ElementLoader', {
+ target: this
+ });
+ data(dom, 'loader', loader);
+ }
+ return loader;
+ },
+
+ /**
+ * Update the innerHTML of this element, optionally searching for and processing scripts
+ * @param {String} html The new HTML
+ * @param {Boolean} [loadScripts=false] True to look for and process scripts
+ * @param {Function} [callback] For async script loading you can be notified when the update completes
+ * @return {Ext.Element} this
+ */
+ update : function(html, loadScripts, callback) {
+ var me = this,
+ id,
+ dom,
+ interval;
+
+ if (!me.dom) {
+ return me;
+ }
+ html = html || '';
+ dom = me.dom;
+
+ if (loadScripts !== true) {
+ dom.innerHTML = html;
+ Ext.callback(callback, me);
+ return me;
+ }
+
+ id = Ext.id();
+ html += '<span id="' + id + '"></span>';
+
+ interval = setInterval(function(){
+ if (!document.getElementById(id)) {
+ return false;
+ }
+ clearInterval(interval);
+ var DOC = document,
+ hd = DOC.getElementsByTagName("head")[0],
+ re = /(?:<script([^>]*)?>)((\n|\r|.)*?)(?:<\/script>)/ig,
+ srcRe = /\ssrc=([\'\"])(.*?)\1/i,
+ typeRe = /\stype=([\'\"])(.*?)\1/i,
+ match,
+ attrs,
+ srcMatch,
+ typeMatch,
+ el,
+ s;
+
+ while ((match = re.exec(html))) {
+ attrs = match[1];
+ srcMatch = attrs ? attrs.match(srcRe) : false;
+ if (srcMatch && srcMatch[2]) {
+ s = DOC.createElement("script");
+ s.src = srcMatch[2];
+ typeMatch = attrs.match(typeRe);
+ if (typeMatch && typeMatch[2]) {
+ s.type = typeMatch[2];
+ }
+ hd.appendChild(s);
+ } else if (match[2] && match[2].length > 0) {
+ if (window.execScript) {
+ window.execScript(match[2]);
+ } else {
+ window.eval(match[2]);
+ }
+ }
+ }
+
+ el = DOC.getElementById(id);
+ if (el) {
+ Ext.removeNode(el);
+ }
+ Ext.callback(callback, me);
+ }, 20);
+ dom.innerHTML = html.replace(/(?:<script.*?>)((\n|\r|.)*?)(?:<\/script>)/ig, '');
+ return me;
+ },
+
+ // inherit docs, overridden so we can add removeAnchor
+ removeAllListeners : function() {
+ this.removeAnchor();
+ Ext.EventManager.removeAll(this.dom);
+ return this;
+ },
+
+ /**
+ * Gets the parent node of the current element taking into account Ext.scopeResetCSS
+ * @protected
+ * @return {HTMLElement} The parent element
+ */
+ getScopeParent: function(){
+ var parent = this.dom.parentNode;
+ return Ext.scopeResetCSS ? parent.parentNode : parent;
+ },
+
+ /**
+ * Creates a proxy element of this element
+ * @param {String/Object} config The class name of the proxy element or a DomHelper config object
+ * @param {String/HTMLElement} [renderTo] The element or element id to render the proxy to (defaults to document.body)
+ * @param {Boolean} [matchBox=false] True to align and size the proxy to this element now.
+ * @return {Ext.Element} The new proxy element
+ */
+ createProxy : function(config, renderTo, matchBox) {
+ config = (typeof config == 'object') ? config : {tag : "div", cls: config};
+
+ var me = this,
+ proxy = renderTo ? Ext.DomHelper.append(renderTo, config, true) :
+ Ext.DomHelper.insertBefore(me.dom, config, true);
+
+ proxy.setVisibilityMode(Ext.Element.DISPLAY);
+ proxy.hide();
+ if (matchBox && me.setBox && me.getBox) { // check to make sure Element.position.js is loaded
+ proxy.setBox(me.getBox());
+ }
+ return proxy;
+ },
+
+ /**
+ * Checks whether this element can be focused.
+ * @return {Boolean} True if the element is focusable
+ */
+ focusable: function(){
+ var dom = this.dom,
+ nodeName = dom.nodeName.toLowerCase(),
+ canFocus = false,
+ hasTabIndex = !isNaN(dom.tabIndex);
+
+ if (!dom.disabled) {
+ if (focusRe.test(nodeName)) {
+ canFocus = true;
+ } else {
+ canFocus = nodeName == 'a' ? dom.href || hasTabIndex : hasTabIndex;
+ }
+ }
+ return canFocus && this.isVisible(true);
+ }
+ };
+})());
+Ext.Element.prototype.clearListeners = Ext.Element.prototype.removeAllListeners;
+
+/**
+ * @class Ext.Element
+ */
+Ext.Element.addMethods({
+ /**
+ * Gets the x,y coordinates specified by the anchor position on the element.
+ * @param {String} [anchor='c'] The specified anchor position. See {@link #alignTo}
+ * for details on supported anchor positions.
+ * @param {Boolean} [local] True to get the local (element top/left-relative) anchor position instead
+ * of page coordinates
+ * @param {Object} [size] An object containing the size to use for calculating anchor position
+ * {width: (target width), height: (target height)} (defaults to the element's current size)
+ * @return {Number[]} [x, y] An array containing the element's x and y coordinates
+ */
+ getAnchorXY : function(anchor, local, s){
+ //Passing a different size is useful for pre-calculating anchors,
+ //especially for anchored animations that change the el size.
+ anchor = (anchor || "tl").toLowerCase();
+ s = s || {};
+
+ var me = this,
+ vp = me.dom == document.body || me.dom == document,
+ w = s.width || vp ? Ext.Element.getViewWidth() : me.getWidth(),
+ h = s.height || vp ? Ext.Element.getViewHeight() : me.getHeight(),
+ xy,
+ r = Math.round,
+ o = me.getXY(),
+ scroll = me.getScroll(),
+ extraX = vp ? scroll.left : !local ? o[0] : 0,
+ extraY = vp ? scroll.top : !local ? o[1] : 0,
+ hash = {
+ c : [r(w * 0.5), r(h * 0.5)],
+ t : [r(w * 0.5), 0],
+ l : [0, r(h * 0.5)],
+ r : [w, r(h * 0.5)],
+ b : [r(w * 0.5), h],
+ tl : [0, 0],
+ bl : [0, h],
+ br : [w, h],
+ tr : [w, 0]
+ };
+
+ xy = hash[anchor];
+ return [xy[0] + extraX, xy[1] + extraY];
+ },
+
+ /**
+ * Anchors an element to another element and realigns it when the window is resized.
+ * @param {String/HTMLElement/Ext.Element} element The element to align to.
+ * @param {String} position The position to align to.
+ * @param {Number[]} [offsets] Offset the positioning by [x, y]
+ * @param {Boolean/Object} [animate] True for the default animation or a standard Element animation config object
+ * @param {Boolean/Number} [monitorScroll] True to monitor body scroll and reposition. If this parameter
+ * is a number, it is used as the buffer delay (defaults to 50ms).
+ * @param {Function} [callback] The function to call after the animation finishes
+ * @return {Ext.Element} this
+ */
+ anchorTo : function(el, alignment, offsets, animate, monitorScroll, callback){
+ var me = this,
+ dom = me.dom,
+ scroll = !Ext.isEmpty(monitorScroll),
+ action = function(){
+ Ext.fly(dom).alignTo(el, alignment, offsets, animate);
+ Ext.callback(callback, Ext.fly(dom));
+ },
+ anchor = this.getAnchor();
+
+ // previous listener anchor, remove it
+ this.removeAnchor();
+ Ext.apply(anchor, {
+ fn: action,
+ scroll: scroll
+ });
+
+ Ext.EventManager.onWindowResize(action, null);
+
+ if(scroll){
+ Ext.EventManager.on(window, 'scroll', action, null,
+ {buffer: !isNaN(monitorScroll) ? monitorScroll : 50});
+ }
+ action.call(me); // align immediately
+ return me;
+ },
+
+ /**
+ * Remove any anchor to this element. See {@link #anchorTo}.
+ * @return {Ext.Element} this
+ */
+ removeAnchor : function(){
+ var me = this,
+ anchor = this.getAnchor();
+
+ if(anchor && anchor.fn){
+ Ext.EventManager.removeResizeListener(anchor.fn);
+ if(anchor.scroll){
+ Ext.EventManager.un(window, 'scroll', anchor.fn);
+ }
+ delete anchor.fn;
+ }
+ return me;
+ },
+
+ // private
+ getAnchor : function(){
+ var data = Ext.Element.data,
+ dom = this.dom;
+ if (!dom) {
+ return;
+ }
+ var anchor = data(dom, '_anchor');
+
+ if(!anchor){
+ anchor = data(dom, '_anchor', {});
+ }
+ return anchor;
+ },
+
+ getAlignVector: function(el, spec, offset) {
+ var me = this,
+ side = {t:"top", l:"left", r:"right", b: "bottom"},
+ thisRegion = me.getRegion(),
+ elRegion;
+
+ el = Ext.get(el);
+ if(!el || !el.dom){
+ }
+
+ elRegion = el.getRegion();
+ },
+
+ /**
+ * Gets the x,y coordinates to align this element with another element. See {@link #alignTo} for more info on the
+ * supported position values.
+ * @param {String/HTMLElement/Ext.Element} element The element to align to.
+ * @param {String} [position="tl-bl?"] The position to align to (defaults to )
+ * @param {Number[]} [offsets] Offset the positioning by [x, y]
+ * @return {Number[]} [x, y]
+ */
+ getAlignToXY : function(el, p, o){
+ el = Ext.get(el);
+
+ if(!el || !el.dom){
+ }
+
+ o = o || [0,0];
+ p = (!p || p == "?" ? "tl-bl?" : (!(/-/).test(p) && p !== "" ? "tl-" + p : p || "tl-bl")).toLowerCase();
+
+ var me = this,
+ d = me.dom,
+ a1,
+ a2,
+ x,
+ y,
+ //constrain the aligned el to viewport if necessary
+ w,
+ h,
+ r,
+ dw = Ext.Element.getViewWidth() -10, // 10px of margin for ie
+ dh = Ext.Element.getViewHeight()-10, // 10px of margin for ie
+ p1y,
+ p1x,
+ p2y,
+ p2x,
+ swapY,
+ swapX,
+ doc = document,
+ docElement = doc.documentElement,
+ docBody = doc.body,
+ scrollX = (docElement.scrollLeft || docBody.scrollLeft || 0)+5,
+ scrollY = (docElement.scrollTop || docBody.scrollTop || 0)+5,
+ c = false, //constrain to viewport
+ p1 = "",
+ p2 = "",
+ m = p.match(/^([a-z]+)-([a-z]+)(\?)?$/);
+
+ if(!m){
+ }
+
+ p1 = m[1];
+ p2 = m[2];
+ c = !!m[3];
+
+ //Subtract the aligned el's internal xy from the target's offset xy
+ //plus custom offset to get the aligned el's new offset xy
+ a1 = me.getAnchorXY(p1, true);
+ a2 = el.getAnchorXY(p2, false);
+
+ x = a2[0] - a1[0] + o[0];
+ y = a2[1] - a1[1] + o[1];
+
+ if(c){
+ w = me.getWidth();
+ h = me.getHeight();
+ r = el.getRegion();
+ //If we are at a viewport boundary and the aligned el is anchored on a target border that is
+ //perpendicular to the vp border, allow the aligned el to slide on that border,
+ //otherwise swap the aligned el to the opposite border of the target.
+ p1y = p1.charAt(0);
+ p1x = p1.charAt(p1.length-1);
+ p2y = p2.charAt(0);
+ p2x = p2.charAt(p2.length-1);
+ swapY = ((p1y=="t" && p2y=="b") || (p1y=="b" && p2y=="t"));
+ swapX = ((p1x=="r" && p2x=="l") || (p1x=="l" && p2x=="r"));
+
+
+ if (x + w > dw + scrollX) {
+ x = swapX ? r.left-w : dw+scrollX-w;
+ }
+ if (x < scrollX) {
+ x = swapX ? r.right : scrollX;
+ }
+ if (y + h > dh + scrollY) {
+ y = swapY ? r.top-h : dh+scrollY-h;
+ }
+ if (y < scrollY){
+ y = swapY ? r.bottom : scrollY;
+ }
+ }
+ return [x,y];
+ },
+
+ /**
+ * Aligns this element with another element relative to the specified anchor points. If the other element is the
+ * document it aligns it to the viewport.
+ * The position parameter is optional, and can be specified in any one of the following formats:
+ * <ul>
+ * <li><b>Blank</b>: Defaults to aligning the element's top-left corner to the target's bottom-left corner ("tl-bl").</li>
+ * <li><b>One anchor (deprecated)</b>: The passed anchor position is used as the target element's anchor point.
+ * The element being aligned will position its top-left corner (tl) to that point. <i>This method has been
+ * deprecated in favor of the newer two anchor syntax below</i>.</li>
+ * <li><b>Two anchors</b>: If two values from the table below are passed separated by a dash, the first value is used as the
+ * element's anchor point, and the second value is used as the target's anchor point.</li>
+ * </ul>
+ * In addition to the anchor points, the position parameter also supports the "?" character. If "?" is passed at the end of
+ * the position string, the element will attempt to align as specified, but the position will be adjusted to constrain to
+ * the viewport if necessary. Note that the element being aligned might be swapped to align to a different position than
+ * that specified in order to enforce the viewport constraints.
+ * Following are all of the supported anchor positions:
+<pre>
+Value Description
+----- -----------------------------
+tl The top left corner (default)
+t The center of the top edge
+tr The top right corner
+l The center of the left edge
+c In the center of the element
+r The center of the right edge
+bl The bottom left corner
+b The center of the bottom edge
+br The bottom right corner
+</pre>
+Example Usage:
+<pre><code>
+// align el to other-el using the default positioning ("tl-bl", non-constrained)
+el.alignTo("other-el");
+
+// align the top left corner of el with the top right corner of other-el (constrained to viewport)
+el.alignTo("other-el", "tr?");
+
+// align the bottom right corner of el with the center left edge of other-el
+el.alignTo("other-el", "br-l?");
+
+// align the center of el with the bottom left corner of other-el and
+// adjust the x position by -6 pixels (and the y position by 0)
+el.alignTo("other-el", "c-bl", [-6, 0]);
+</code></pre>
+ * @param {String/HTMLElement/Ext.Element} element The element to align to.
+ * @param {String} [position="tl-bl?"] The position to align to
+ * @param {Number[]} [offsets] Offset the positioning by [x, y]
+ * @param {Boolean/Object} [animate] true for the default animation or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ alignTo : function(element, position, offsets, animate){
+ var me = this;
+ return me.setXY(me.getAlignToXY(element, position, offsets),
+ me.anim && !!animate ? me.anim(animate) : false);
+ },
+
+ // private ==> used outside of core
+ adjustForConstraints : function(xy, parent) {
+ var vector = this.getConstrainVector(parent, xy);
+ if (vector) {
+ xy[0] += vector[0];
+ xy[1] += vector[1];
+ }
+ return xy;
+ },
+
+ /**
+ * <p>Returns the <code>[X, Y]</code> vector by which this element must be translated to make a best attempt
+ * to constrain within the passed constraint. Returns <code>false</code> is this element does not need to be moved.</p>
+ * <p>Priority is given to constraining the top and left within the constraint.</p>
+ * <p>The constraint may either be an existing element into which this element is to be constrained, or
+ * an {@link Ext.util.Region Region} into which this element is to be constrained.</p>
+ * @param constrainTo {Mixed} The Element or {@link Ext.util.Region Region} into which this element is to be constrained.
+ * @param proposedPosition {Array} A proposed <code>[X, Y]</code> position to test for validity and to produce a vector for instead
+ * of using this Element's current position;
+ * @returns {Number[]/Boolean} <b>If</b> this element <i>needs</i> to be translated, an <code>[X, Y]</code>
+ * vector by which this element must be translated. Otherwise, <code>false</code>.
+ */
+ getConstrainVector: function(constrainTo, proposedPosition) {
+ if (!(constrainTo instanceof Ext.util.Region)) {
+ constrainTo = Ext.get(constrainTo).getViewRegion();
+ }
+ var thisRegion = this.getRegion(),
+ vector = [0, 0],
+ shadowSize = this.shadow && this.shadow.offset,
+ overflowed = false;
+
+ // Shift this region to occupy the proposed position
+ if (proposedPosition) {
+ thisRegion.translateBy(proposedPosition[0] - thisRegion.x, proposedPosition[1] - thisRegion.y);
+ }
+
+ // Reduce the constrain region to allow for shadow
+ // TODO: Rewrite the Shadow class. When that's done, get the extra for each side from the Shadow.
+ if (shadowSize) {
+ constrainTo.adjust(0, -shadowSize, -shadowSize, shadowSize);
+ }
+
+ // Constrain the X coordinate by however much this Element overflows
+ if (thisRegion.right > constrainTo.right) {
+ overflowed = true;
+ vector[0] = (constrainTo.right - thisRegion.right); // overflowed the right
+ }
+ if (thisRegion.left + vector[0] < constrainTo.left) {
+ overflowed = true;
+ vector[0] = (constrainTo.left - thisRegion.left); // overflowed the left
+ }
+
+ // Constrain the Y coordinate by however much this Element overflows
+ if (thisRegion.bottom > constrainTo.bottom) {
+ overflowed = true;
+ vector[1] = (constrainTo.bottom - thisRegion.bottom); // overflowed the bottom
+ }
+ if (thisRegion.top + vector[1] < constrainTo.top) {
+ overflowed = true;
+ vector[1] = (constrainTo.top - thisRegion.top); // overflowed the top
+ }
+ return overflowed ? vector : false;
+ },
+
+ /**
+ * Calculates the x, y to center this element on the screen
+ * @return {Number[]} The x, y values [x, y]
+ */
+ getCenterXY : function(){
+ return this.getAlignToXY(document, 'c-c');
+ },
+
+ /**
+ * Centers the Element in either the viewport, or another Element.
+ * @param {String/HTMLElement/Ext.Element} centerIn (optional) The element in which to center the element.
+ */
+ center : function(centerIn){
+ return this.alignTo(centerIn || document, 'c-c');
+ }
+});
+
+/**
+ * @class Ext.Element
+ */
+(function(){
+
+var ELEMENT = Ext.Element,
+ LEFT = "left",
+ RIGHT = "right",
+ TOP = "top",
+ BOTTOM = "bottom",
+ POSITION = "position",
+ STATIC = "static",
+ RELATIVE = "relative",
+ AUTO = "auto",
+ ZINDEX = "z-index";
+
+Ext.override(Ext.Element, {
+ /**
+ * Gets the current X position of the element based on page coordinates. Element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
+ * @return {Number} The X position of the element
+ */
+ getX : function(){
+ return ELEMENT.getX(this.dom);
+ },
+
+ /**
+ * Gets the current Y position of the element based on page coordinates. Element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
+ * @return {Number} The Y position of the element
+ */
+ getY : function(){
+ return ELEMENT.getY(this.dom);
+ },
+
+ /**
+ * Gets the current position of the element based on page coordinates. Element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
+ * @return {Number[]} The XY position of the element
+ */
+ getXY : function(){
+ return ELEMENT.getXY(this.dom);
+ },
+
+ /**
+ * Returns the offsets of this element from the passed element. Both element must be part of the DOM tree and not have display:none to have page coordinates.
+ * @param {String/HTMLElement/Ext.Element} element The element to get the offsets from.
+ * @return {Number[]} The XY page offsets (e.g. [100, -200])
+ */
+ getOffsetsTo : function(el){
+ var o = this.getXY(),
+ e = Ext.fly(el, '_internal').getXY();
+ return [o[0]-e[0],o[1]-e[1]];
+ },
+
+ /**
+ * Sets the X position of the element based on page coordinates. Element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
+ * @param {Number} The X position of the element
+ * @param {Boolean/Object} animate (optional) True for the default animation, or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ setX : function(x, animate){
+ return this.setXY([x, this.getY()], animate);
+ },
+
+ /**
+ * Sets the Y position of the element based on page coordinates. Element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
+ * @param {Number} The Y position of the element
+ * @param {Boolean/Object} animate (optional) True for the default animation, or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ setY : function(y, animate){
+ return this.setXY([this.getX(), y], animate);
+ },
+
+ /**
+ * Sets the element's left position directly using CSS style (instead of {@link #setX}).
+ * @param {String} left The left CSS property value
+ * @return {Ext.Element} this
+ */
+ setLeft : function(left){
+ this.setStyle(LEFT, this.addUnits(left));
+ return this;
+ },
+
+ /**
+ * Sets the element's top position directly using CSS style (instead of {@link #setY}).
+ * @param {String} top The top CSS property value
+ * @return {Ext.Element} this
+ */
+ setTop : function(top){
+ this.setStyle(TOP, this.addUnits(top));
+ return this;
+ },
+
+ /**
+ * Sets the element's CSS right style.
+ * @param {String} right The right CSS property value
+ * @return {Ext.Element} this
+ */
+ setRight : function(right){
+ this.setStyle(RIGHT, this.addUnits(right));
+ return this;
+ },
+
+ /**
+ * Sets the element's CSS bottom style.
+ * @param {String} bottom The bottom CSS property value
+ * @return {Ext.Element} this
+ */
+ setBottom : function(bottom){
+ this.setStyle(BOTTOM, this.addUnits(bottom));
+ return this;
+ },
+
+ /**
+ * Sets the position of the element in page coordinates, regardless of how the element is positioned.
+ * The element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
+ * @param {Number[]} pos Contains X & Y [x, y] values for new position (coordinates are page-based)
+ * @param {Boolean/Object} animate (optional) True for the default animation, or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ setXY: function(pos, animate) {
+ var me = this;
+ if (!animate || !me.anim) {
+ ELEMENT.setXY(me.dom, pos);
+ }
+ else {
+ if (!Ext.isObject(animate)) {
+ animate = {};
+ }
+ me.animate(Ext.applyIf({ to: { x: pos[0], y: pos[1] } }, animate));
+ }
+ return me;
+ },
+
+ /**
+ * Sets the position of the element in page coordinates, regardless of how the element is positioned.
+ * The element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
+ * @param {Number} x X value for new position (coordinates are page-based)
+ * @param {Number} y Y value for new position (coordinates are page-based)
+ * @param {Boolean/Object} animate (optional) True for the default animation, or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ setLocation : function(x, y, animate){
+ return this.setXY([x, y], animate);
+ },
+
+ /**
+ * Sets the position of the element in page coordinates, regardless of how the element is positioned.
+ * The element must be part of the DOM tree to have page coordinates (display:none or elements not appended return false).
+ * @param {Number} x X value for new position (coordinates are page-based)
+ * @param {Number} y Y value for new position (coordinates are page-based)
+ * @param {Boolean/Object} animate (optional) True for the default animation, or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ moveTo : function(x, y, animate){
+ return this.setXY([x, y], animate);
+ },
+
+ /**
+ * Gets the left X coordinate
+ * @param {Boolean} local True to get the local css position instead of page coordinate
+ * @return {Number}
+ */
+ getLeft : function(local){
+ return !local ? this.getX() : parseInt(this.getStyle(LEFT), 10) || 0;
+ },
+
+ /**
+ * Gets the right X coordinate of the element (element X position + element width)
+ * @param {Boolean} local True to get the local css position instead of page coordinate
+ * @return {Number}
+ */
+ getRight : function(local){
+ var me = this;
+ return !local ? me.getX() + me.getWidth() : (me.getLeft(true) + me.getWidth()) || 0;
+ },
+
+ /**
+ * Gets the top Y coordinate
+ * @param {Boolean} local True to get the local css position instead of page coordinate
+ * @return {Number}
+ */
+ getTop : function(local) {
+ return !local ? this.getY() : parseInt(this.getStyle(TOP), 10) || 0;
+ },
+
+ /**
+ * Gets the bottom Y coordinate of the element (element Y position + element height)
+ * @param {Boolean} local True to get the local css position instead of page coordinate
+ * @return {Number}
+ */
+ getBottom : function(local){
+ var me = this;
+ return !local ? me.getY() + me.getHeight() : (me.getTop(true) + me.getHeight()) || 0;
+ },
+
+ /**
+ * Initializes positioning on this element. If a desired position is not passed, it will make the
+ * the element positioned relative IF it is not already positioned.
+ * @param {String} pos (optional) Positioning to use "relative", "absolute" or "fixed"
+ * @param {Number} zIndex (optional) The zIndex to apply
+ * @param {Number} x (optional) Set the page X position
+ * @param {Number} y (optional) Set the page Y position
+ */
+ position : function(pos, zIndex, x, y) {
+ var me = this;
+
+ if (!pos && me.isStyle(POSITION, STATIC)){
+ me.setStyle(POSITION, RELATIVE);
+ } else if(pos) {
+ me.setStyle(POSITION, pos);
+ }
+ if (zIndex){
+ me.setStyle(ZINDEX, zIndex);
+ }
+ if (x || y) {
+ me.setXY([x || false, y || false]);
+ }
+ },
+
+ /**
+ * Clear positioning back to the default when the document was loaded
+ * @param {String} value (optional) The value to use for the left,right,top,bottom, defaults to '' (empty string). You could use 'auto'.
+ * @return {Ext.Element} this
+ */
+ clearPositioning : function(value){
+ value = value || '';
+ this.setStyle({
+ left : value,
+ right : value,
+ top : value,
+ bottom : value,
+ "z-index" : "",
+ position : STATIC
+ });
+ return this;
+ },
+
+ /**
+ * Gets an object with all CSS positioning properties. Useful along with setPostioning to get
+ * snapshot before performing an update and then restoring the element.
+ * @return {Object}
+ */
+ getPositioning : function(){
+ var l = this.getStyle(LEFT);
+ var t = this.getStyle(TOP);
+ return {
+ "position" : this.getStyle(POSITION),
+ "left" : l,
+ "right" : l ? "" : this.getStyle(RIGHT),
+ "top" : t,
+ "bottom" : t ? "" : this.getStyle(BOTTOM),
+ "z-index" : this.getStyle(ZINDEX)
+ };
+ },
+
+ /**
+ * Set positioning with an object returned by getPositioning().
+ * @param {Object} posCfg
+ * @return {Ext.Element} this
+ */
+ setPositioning : function(pc){
+ var me = this,
+ style = me.dom.style;
+
+ me.setStyle(pc);
+
+ if(pc.right == AUTO){
+ style.right = "";
+ }
+ if(pc.bottom == AUTO){
+ style.bottom = "";
+ }
+
+ return me;
+ },
+
+ /**
+ * Translates the passed page coordinates into left/top css values for this element
+ * @param {Number/Number[]} x The page x or an array containing [x, y]
+ * @param {Number} y (optional) The page y, required if x is not an array
+ * @return {Object} An object with left and top properties. e.g. {left: (value), top: (value)}
+ */
+ translatePoints: function(x, y) {
+ if (Ext.isArray(x)) {
+ y = x[1];
+ x = x[0];
+ }
+ var me = this,
+ relative = me.isStyle(POSITION, RELATIVE),
+ o = me.getXY(),
+ left = parseInt(me.getStyle(LEFT), 10),
+ top = parseInt(me.getStyle(TOP), 10);
+
+ if (!Ext.isNumber(left)) {
+ left = relative ? 0 : me.dom.offsetLeft;
+ }
+ if (!Ext.isNumber(top)) {
+ top = relative ? 0 : me.dom.offsetTop;
+ }
+ left = (Ext.isNumber(x)) ? x - o[0] + left : undefined;
+ top = (Ext.isNumber(y)) ? y - o[1] + top : undefined;
+ return {
+ left: left,
+ top: top
+ };
+ },
+
+ /**
+ * Sets the element's box. Use getBox() on another element to get a box obj. If animate is true then width, height, x and y will be animated concurrently.
+ * @param {Object} box The box to fill {x, y, width, height}
+ * @param {Boolean} adjust (optional) Whether to adjust for box-model issues automatically
+ * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ setBox: function(box, adjust, animate) {
+ var me = this,
+ w = box.width,
+ h = box.height;
+ if ((adjust && !me.autoBoxAdjust) && !me.isBorderBox()) {
+ w -= (me.getBorderWidth("lr") + me.getPadding("lr"));
+ h -= (me.getBorderWidth("tb") + me.getPadding("tb"));
+ }
+ me.setBounds(box.x, box.y, w, h, animate);
+ return me;
+ },
+
+ /**
+ * Return an object defining the area of this Element which can be passed to {@link #setBox} to
+ * set another Element's size/location to match this element.
+ * @param {Boolean} contentBox (optional) If true a box for the content of the element is returned.
+ * @param {Boolean} local (optional) If true the element's left and top are returned instead of page x/y.
+ * @return {Object} box An object in the format<pre><code>
+{
+ x: <Element's X position>,
+ y: <Element's Y position>,
+ width: <Element's width>,
+ height: <Element's height>,
+ bottom: <Element's lower bound>,
+ right: <Element's rightmost bound>
+}
+</code></pre>
+ * The returned object may also be addressed as an Array where index 0 contains the X position
+ * and index 1 contains the Y position. So the result may also be used for {@link #setXY}
+ */
+ getBox: function(contentBox, local) {
+ var me = this,
+ xy,
+ left,
+ top,
+ getBorderWidth = me.getBorderWidth,
+ getPadding = me.getPadding,
+ l, r, t, b, w, h, bx;
+ if (!local) {
+ xy = me.getXY();
+ } else {
+ left = parseInt(me.getStyle("left"), 10) || 0;
+ top = parseInt(me.getStyle("top"), 10) || 0;
+ xy = [left, top];
+ }
+ w = me.getWidth();
+ h = me.getHeight();
+ if (!contentBox) {
+ bx = {
+ x: xy[0],
+ y: xy[1],
+ 0: xy[0],
+ 1: xy[1],
+ width: w,
+ height: h
+ };
+ } else {
+ l = getBorderWidth.call(me, "l") + getPadding.call(me, "l");
+ r = getBorderWidth.call(me, "r") + getPadding.call(me, "r");
+ t = getBorderWidth.call(me, "t") + getPadding.call(me, "t");
+ b = getBorderWidth.call(me, "b") + getPadding.call(me, "b");
+ bx = {
+ x: xy[0] + l,
+ y: xy[1] + t,
+ 0: xy[0] + l,
+ 1: xy[1] + t,
+ width: w - (l + r),
+ height: h - (t + b)
+ };
+ }
+ bx.right = bx.x + bx.width;
+ bx.bottom = bx.y + bx.height;
+ return bx;
+ },
+
+ /**
+ * Move this element relative to its current position.
+ * @param {String} direction Possible values are: "l" (or "left"), "r" (or "right"), "t" (or "top", or "up"), "b" (or "bottom", or "down").
+ * @param {Number} distance How far to move the element in pixels
+ * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
+ */
+ move: function(direction, distance, animate) {
+ var me = this,
+ xy = me.getXY(),
+ x = xy[0],
+ y = xy[1],
+ left = [x - distance, y],
+ right = [x + distance, y],
+ top = [x, y - distance],
+ bottom = [x, y + distance],
+ hash = {
+ l: left,
+ left: left,
+ r: right,
+ right: right,
+ t: top,
+ top: top,
+ up: top,
+ b: bottom,
+ bottom: bottom,
+ down: bottom
+ };
+
+ direction = direction.toLowerCase();
+ me.moveTo(hash[direction][0], hash[direction][1], animate);
+ },
+
+ /**
+ * Quick set left and top adding default units
+ * @param {String} left The left CSS property value
+ * @param {String} top The top CSS property value
+ * @return {Ext.Element} this
+ */
+ setLeftTop: function(left, top) {
+ var me = this,
+ style = me.dom.style;
+ style.left = me.addUnits(left);
+ style.top = me.addUnits(top);
+ return me;
+ },
+
+ /**
+ * Returns the region of this element.
+ * The element must be part of the DOM tree to have a region (display:none or elements not appended return false).
+ * @return {Ext.util.Region} A Region containing "top, left, bottom, right" member data.
+ */
+ getRegion: function() {
+ return this.getPageBox(true);
+ },
+
+ /**
+ * Returns the <b>content</b> region of this element. That is the region within the borders and padding.
+ * @return {Ext.util.Region} A Region containing "top, left, bottom, right" member data.
+ */
+ getViewRegion: function() {
+ var me = this,
+ isBody = me.dom === document.body,
+ scroll, pos, top, left, width, height;
+
+ // For the body we want to do some special logic
+ if (isBody) {
+ scroll = me.getScroll();
+ left = scroll.left;
+ top = scroll.top;
+ width = Ext.Element.getViewportWidth();
+ height = Ext.Element.getViewportHeight();
+ }
+ else {
+ pos = me.getXY();
+ left = pos[0] + me.getBorderWidth('l') + me.getPadding('l');
+ top = pos[1] + me.getBorderWidth('t') + me.getPadding('t');
+ width = me.getWidth(true);
+ height = me.getHeight(true);
+ }
+
+ return Ext.create('Ext.util.Region', top, left + width, top + height, left);
+ },
+
+ /**
+ * Return an object defining the area of this Element which can be passed to {@link #setBox} to
+ * set another Element's size/location to match this element.
+ * @param {Boolean} asRegion(optional) If true an Ext.util.Region will be returned
+ * @return {Object} box An object in the format<pre><code>
+{
+ x: <Element's X position>,
+ y: <Element's Y position>,
+ width: <Element's width>,
+ height: <Element's height>,
+ bottom: <Element's lower bound>,
+ right: <Element's rightmost bound>
+}
+</code></pre>
+ * The returned object may also be addressed as an Array where index 0 contains the X position
+ * and index 1 contains the Y position. So the result may also be used for {@link #setXY}
+ */
+ getPageBox : function(getRegion) {
+ var me = this,
+ el = me.dom,
+ isDoc = el === document.body,
+ w = isDoc ? Ext.Element.getViewWidth() : el.offsetWidth,
+ h = isDoc ? Ext.Element.getViewHeight() : el.offsetHeight,
+ xy = me.getXY(),
+ t = xy[1],
+ r = xy[0] + w,
+ b = xy[1] + h,
+ l = xy[0];
+
+ if (getRegion) {
+ return Ext.create('Ext.util.Region', t, r, b, l);
+ }
+ else {
+ return {
+ left: l,
+ top: t,
+ width: w,
+ height: h,
+ right: r,
+ bottom: b
+ };
+ }
+ },
+
+ /**
+ * Sets the element's position and size in one shot. If animation is true then width, height, x and y will be animated concurrently.
+ * @param {Number} x X value for new position (coordinates are page-based)
+ * @param {Number} y Y value for new position (coordinates are page-based)
+ * @param {Number/String} width The new width. This may be one of:<div class="mdetail-params"><ul>
+ * <li>A Number specifying the new width in this Element's {@link #defaultUnit}s (by default, pixels)</li>
+ * <li>A String used to set the CSS width style. Animation may <b>not</b> be used.
+ * </ul></div>
+ * @param {Number/String} height The new height. This may be one of:<div class="mdetail-params"><ul>
+ * <li>A Number specifying the new height in this Element's {@link #defaultUnit}s (by default, pixels)</li>
+ * <li>A String used to set the CSS height style. Animation may <b>not</b> be used.</li>
+ * </ul></div>
+ * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ setBounds: function(x, y, width, height, animate) {
+ var me = this;
+ if (!animate || !me.anim) {
+ me.setSize(width, height);
+ me.setLocation(x, y);
+ } else {
+ if (!Ext.isObject(animate)) {
+ animate = {};
+ }
+ me.animate(Ext.applyIf({
+ to: {
+ x: x,
+ y: y,
+ width: me.adjustWidth(width),
+ height: me.adjustHeight(height)
+ }
+ }, animate));
+ }
+ return me;
+ },
+
+ /**
+ * Sets the element's position and size the specified region. If animation is true then width, height, x and y will be animated concurrently.
+ * @param {Ext.util.Region} region The region to fill
+ * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ setRegion: function(region, animate) {
+ return this.setBounds(region.left, region.top, region.right - region.left, region.bottom - region.top, animate);
+ }
+});
+})();
+
+/**
+ * @class Ext.Element
+ */
+Ext.override(Ext.Element, {
+ /**
+ * Returns true if this element is scrollable.
+ * @return {Boolean}
+ */
+ isScrollable : function(){
+ var dom = this.dom;
+ return dom.scrollHeight > dom.clientHeight || dom.scrollWidth > dom.clientWidth;
+ },
+
+ /**
+ * Returns the current scroll position of the element.
+ * @return {Object} An object containing the scroll position in the format {left: (scrollLeft), top: (scrollTop)}
+ */
+ getScroll : function() {
+ var d = this.dom,
+ doc = document,
+ body = doc.body,
+ docElement = doc.documentElement,
+ l,
+ t,
+ ret;
+
+ if (d == doc || d == body) {
+ if (Ext.isIE && Ext.isStrict) {
+ l = docElement.scrollLeft;
+ t = docElement.scrollTop;
+ } else {
+ l = window.pageXOffset;
+ t = window.pageYOffset;
+ }
+ ret = {
+ left: l || (body ? body.scrollLeft : 0),
+ top : t || (body ? body.scrollTop : 0)
+ };
+ } else {
+ ret = {
+ left: d.scrollLeft,
+ top : d.scrollTop
+ };
+ }
+
+ return ret;
+ },
+
+ /**
+ * Scrolls this element the specified scroll point. It does NOT do bounds checking so if you scroll to a weird value it will try to do it. For auto bounds checking, use scroll().
+ * @param {String} side Either "left" for scrollLeft values or "top" for scrollTop values.
+ * @param {Number} value The new scroll value
+ * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
+ * @return {Ext.Element} this
+ */
+ scrollTo : function(side, value, animate) {
+ //check if we're scrolling top or left
+ var top = /top/i.test(side),
+ me = this,
+ dom = me.dom,
+ obj = {},
+ prop;
+ if (!animate || !me.anim) {
+ // just setting the value, so grab the direction
+ prop = 'scroll' + (top ? 'Top' : 'Left');
+ dom[prop] = value;
+ }
+ else {
+ if (!Ext.isObject(animate)) {
+ animate = {};
+ }
+ obj['scroll' + (top ? 'Top' : 'Left')] = value;
+ me.animate(Ext.applyIf({
+ to: obj
+ }, animate));
+ }
+ return me;
+ },
+
+ /**
+ * Scrolls this element into view within the passed container.
+ * @param {String/HTMLElement/Ext.Element} container (optional) The container element to scroll (defaults to document.body). Should be a
+ * string (id), dom node, or Ext.Element.
+ * @param {Boolean} hscroll (optional) False to disable horizontal scroll (defaults to true)
+ * @return {Ext.Element} this
+ */
+ scrollIntoView : function(container, hscroll) {
+ container = Ext.getDom(container) || Ext.getBody().dom;
+ var el = this.dom,
+ offsets = this.getOffsetsTo(container),
+ // el's box
+ left = offsets[0] + container.scrollLeft,
+ top = offsets[1] + container.scrollTop,
+ bottom = top + el.offsetHeight,
+ right = left + el.offsetWidth,
+ // ct's box
+ ctClientHeight = container.clientHeight,
+ ctScrollTop = parseInt(container.scrollTop, 10),
+ ctScrollLeft = parseInt(container.scrollLeft, 10),
+ ctBottom = ctScrollTop + ctClientHeight,
+ ctRight = ctScrollLeft + container.clientWidth;
+
+ if (el.offsetHeight > ctClientHeight || top < ctScrollTop) {
+ container.scrollTop = top;
+ } else if (bottom > ctBottom) {
+ container.scrollTop = bottom - ctClientHeight;
+ }
+ // corrects IE, other browsers will ignore
+ container.scrollTop = container.scrollTop;
+
+ if (hscroll !== false) {
+ if (el.offsetWidth > container.clientWidth || left < ctScrollLeft) {
+ container.scrollLeft = left;
+ }
+ else if (right > ctRight) {
+ container.scrollLeft = right - container.clientWidth;
+ }
+ container.scrollLeft = container.scrollLeft;
+ }
+ return this;
+ },
+
+ // private
+ scrollChildIntoView : function(child, hscroll) {
+ Ext.fly(child, '_scrollChildIntoView').scrollIntoView(this, hscroll);
+ },
+
+ /**
+ * Scrolls this element the specified direction. Does bounds checking to make sure the scroll is
+ * within this element's scrollable range.
+ * @param {String} direction Possible values are: "l" (or "left"), "r" (or "right"), "t" (or "top", or "up"), "b" (or "bottom", or "down").
+ * @param {Number} distance How far to scroll the element in pixels
+ * @param {Boolean/Object} animate (optional) true for the default animation or a standard Element animation config object
+ * @return {Boolean} Returns true if a scroll was triggered or false if the element
+ * was scrolled as far as it could go.
+ */
+ scroll : function(direction, distance, animate) {
+ if (!this.isScrollable()) {
+ return false;
+ }
+ var el = this.dom,
+ l = el.scrollLeft, t = el.scrollTop,
+ w = el.scrollWidth, h = el.scrollHeight,
+ cw = el.clientWidth, ch = el.clientHeight,
+ scrolled = false, v,
+ hash = {
+ l: Math.min(l + distance, w-cw),
+ r: v = Math.max(l - distance, 0),
+ t: Math.max(t - distance, 0),
+ b: Math.min(t + distance, h-ch)
+ };
+ hash.d = hash.b;
+ hash.u = hash.t;
+
+ direction = direction.substr(0, 1);
+ if ((v = hash[direction]) > -1) {
+ scrolled = true;
+ this.scrollTo(direction == 'l' || direction == 'r' ? 'left' : 'top', v, this.anim(animate));
+ }
+ return scrolled;
+ }
+});
+/**
+ * @class Ext.Element
+ */
+Ext.Element.addMethods(
+ function() {
+ var VISIBILITY = "visibility",
+ DISPLAY = "display",
+ HIDDEN = "hidden",
+ NONE = "none",
+ XMASKED = Ext.baseCSSPrefix + "masked",
+ XMASKEDRELATIVE = Ext.baseCSSPrefix + "masked-relative",
+ data = Ext.Element.data;
+
+ return {
+ /**
+ * Checks whether the element is currently visible using both visibility and display properties.
+ * @param {Boolean} [deep=false] True to walk the dom and see if parent elements are hidden
+ * @return {Boolean} True if the element is currently visible, else false
+ */
+ isVisible : function(deep) {
+ var vis = !this.isStyle(VISIBILITY, HIDDEN) && !this.isStyle(DISPLAY, NONE),
+ p = this.dom.parentNode;
+
+ if (deep !== true || !vis) {
+ return vis;
+ }
+
+ while (p && !(/^body/i.test(p.tagName))) {
+ if (!Ext.fly(p, '_isVisible').isVisible()) {
+ return false;
+ }
+ p = p.parentNode;
+ }
+ return true;
+ },
+
+ /**
+ * Returns true if display is not "none"
+ * @return {Boolean}
+ */
+ isDisplayed : function() {
+ return !this.isStyle(DISPLAY, NONE);
+ },
+
+ /**
+ * Convenience method for setVisibilityMode(Element.DISPLAY)
+ * @param {String} display (optional) What to set display to when visible
+ * @return {Ext.Element} this
+ */
+ enableDisplayMode : function(display) {
+ this.setVisibilityMode(Ext.Element.DISPLAY);
+
+ if (!Ext.isEmpty(display)) {
+ data(this.dom, 'originalDisplay', display);
+ }
+
+ return this;
+ },
+
+ /**
+ * Puts a mask over this element to disable user interaction. Requires core.css.
+ * This method can only be applied to elements which accept child nodes.
+ * @param {String} msg (optional) A message to display in the mask
+ * @param {String} msgCls (optional) A css class to apply to the msg element
+ * @return {Ext.Element} The mask element
+ */
+ mask : function(msg, msgCls) {
+ var me = this,
+ dom = me.dom,
+ setExpression = dom.style.setExpression,
+ dh = Ext.DomHelper,
+ EXTELMASKMSG = Ext.baseCSSPrefix + "mask-msg",
+ el,
+ mask;
+
+ if (!(/^body/i.test(dom.tagName) && me.getStyle('position') == 'static')) {
+ me.addCls(XMASKEDRELATIVE);
+ }
+ el = data(dom, 'maskMsg');
+ if (el) {
+ el.remove();
+ }
+ el = data(dom, 'mask');
+ if (el) {
+ el.remove();
+ }
+
+ mask = dh.append(dom, {cls : Ext.baseCSSPrefix + "mask"}, true);
+ data(dom, 'mask', mask);
+
+ me.addCls(XMASKED);
+ mask.setDisplayed(true);
+
+ if (typeof msg == 'string') {
+ var mm = dh.append(dom, {cls : EXTELMASKMSG, cn:{tag:'div'}}, true);
+ data(dom, 'maskMsg', mm);
+ mm.dom.className = msgCls ? EXTELMASKMSG + " " + msgCls : EXTELMASKMSG;
+ mm.dom.firstChild.innerHTML = msg;
+ mm.setDisplayed(true);
+ mm.center(me);
+ }
+ // NOTE: CSS expressions are resource intensive and to be used only as a last resort
+ // These expressions are removed as soon as they are no longer necessary - in the unmask method.
+ // In normal use cases an element will be masked for a limited period of time.
+ // Fix for https://sencha.jira.com/browse/EXTJSIV-19.
+ // IE6 strict mode and IE6-9 quirks mode takes off left+right padding when calculating width!
+ if (!Ext.supports.IncludePaddingInWidthCalculation && setExpression) {
+ mask.dom.style.setExpression('width', 'this.parentNode.offsetWidth + "px"');
+ }
+
+ // Some versions and modes of IE subtract top+bottom padding when calculating height.
+ // Different versions from those which make the same error for width!
+ if (!Ext.supports.IncludePaddingInHeightCalculation && setExpression) {
+ mask.dom.style.setExpression('height', 'this.parentNode.offsetHeight + "px"');
+ }
+ // ie will not expand full height automatically
+ else if (Ext.isIE && !(Ext.isIE7 && Ext.isStrict) && me.getStyle('height') == 'auto') {
+ mask.setSize(undefined, me.getHeight());
+ }
+ return mask;
+ },
+
+ /**
+ * Removes a previously applied mask.
+ */
+ unmask : function() {
+ var me = this,
+ dom = me.dom,
+ mask = data(dom, 'mask'),
+ maskMsg = data(dom, 'maskMsg');
+
+ if (mask) {
+ // Remove resource-intensive CSS expressions as soon as they are not required.
+ if (mask.dom.style.clearExpression) {
+ mask.dom.style.clearExpression('width');
+ mask.dom.style.clearExpression('height');
+ }
+ if (maskMsg) {
+ maskMsg.remove();
+ data(dom, 'maskMsg', undefined);
+ }
+
+ mask.remove();
+ data(dom, 'mask', undefined);
+ me.removeCls([XMASKED, XMASKEDRELATIVE]);
+ }
+ },
+ /**
+ * Returns true if this element is masked. Also re-centers any displayed message within the mask.
+ * @return {Boolean}
+ */
+ isMasked : function() {
+ var me = this,
+ mask = data(me.dom, 'mask'),
+ maskMsg = data(me.dom, 'maskMsg');
+
+ if (mask && mask.isVisible()) {
+ if (maskMsg) {
+ maskMsg.center(me);
+ }
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Creates an iframe shim for this element to keep selects and other windowed objects from
+ * showing through.
+ * @return {Ext.Element} The new shim element
+ */
+ createShim : function() {
+ var el = document.createElement('iframe'),
+ shim;
+
+ el.frameBorder = '0';
+ el.className = Ext.baseCSSPrefix + 'shim';
+ el.src = Ext.SSL_SECURE_URL;
+ shim = Ext.get(this.dom.parentNode.insertBefore(el, this.dom));
+ shim.autoBoxAdjust = false;
+ return shim;
+ }
+ };
+ }()
+);
+/**
+ * @class Ext.Element
+ */
+Ext.Element.addMethods({
+ /**
+ * Convenience method for constructing a KeyMap
+ * @param {String/Number/Number[]/Object} key Either a string with the keys to listen for, the numeric key code, array of key codes or an object with the following options:
+ * <code>{key: (number or array), shift: (true/false), ctrl: (true/false), alt: (true/false)}</code>
+ * @param {Function} fn The function to call
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the specified function is executed. Defaults to this Element.
+ * @return {Ext.util.KeyMap} The KeyMap created
+ */
+ addKeyListener : function(key, fn, scope){
+ var config;
+ if(typeof key != 'object' || Ext.isArray(key)){
+ config = {
+ key: key,
+ fn: fn,
+ scope: scope
+ };
+ }else{
+ config = {
+ key : key.key,
+ shift : key.shift,
+ ctrl : key.ctrl,
+ alt : key.alt,
+ fn: fn,
+ scope: scope
+ };
+ }
+ return Ext.create('Ext.util.KeyMap', this, config);
+ },
+
+ /**
+ * Creates a KeyMap for this element
+ * @param {Object} config The KeyMap config. See {@link Ext.util.KeyMap} for more details
+ * @return {Ext.util.KeyMap} The KeyMap created
+ */
+ addKeyMap : function(config){
+ return Ext.create('Ext.util.KeyMap', this, config);
+ }
+});
+
+//Import the newly-added Ext.Element functions into CompositeElementLite. We call this here because
+//Element.keys.js is the last extra Ext.Element include in the ext-all.js build
+Ext.CompositeElementLite.importElementMethods();
+
+/**
+ * @class Ext.CompositeElementLite
+ */
+Ext.apply(Ext.CompositeElementLite.prototype, {
+ addElements : function(els, root){
+ if(!els){
+ return this;
+ }
+ if(typeof els == "string"){
+ els = Ext.Element.selectorFunction(els, root);
+ }
+ var yels = this.elements;
+ Ext.each(els, function(e) {
+ yels.push(Ext.get(e));
+ });
+ return this;
+ },
+
+ /**
+ * Returns the first Element
+ * @return {Ext.Element}
+ */
+ first : function(){
+ return this.item(0);
+ },
+
+ /**
+ * Returns the last Element
+ * @return {Ext.Element}
+ */
+ last : function(){
+ return this.item(this.getCount()-1);
+ },
+
+ /**
+ * Returns true if this composite contains the passed element
+ * @param el {String/HTMLElement/Ext.Element/Number} The id of an element, or an Ext.Element, or an HtmlElement to find within the composite collection.
+ * @return Boolean
+ */
+ contains : function(el){
+ return this.indexOf(el) != -1;
+ },
+
+ /**
+ * Removes the specified element(s).
+ * @param {String/HTMLElement/Ext.Element/Number} el The id of an element, the Element itself, the index of the element in this composite
+ * or an array of any of those.
+ * @param {Boolean} removeDom (optional) True to also remove the element from the document
+ * @return {Ext.CompositeElement} this
+ */
+ removeElement : function(keys, removeDom){
+ var me = this,
+ els = this.elements,
+ el;
+ Ext.each(keys, function(val){
+ if ((el = (els[val] || els[val = me.indexOf(val)]))) {
+ if(removeDom){
+ if(el.dom){
+ el.remove();
+ }else{
+ Ext.removeNode(el);
+ }
+ }
+ Ext.Array.erase(els, val, 1);
+ }
+ });
+ return this;
+ }
+});
+
+/**
+ * @class Ext.CompositeElement
+ * @extends Ext.CompositeElementLite
+ * <p>This class encapsulates a <i>collection</i> of DOM elements, providing methods to filter
+ * members, or to perform collective actions upon the whole set.</p>
+ * <p>Although they are not listed, this class supports all of the methods of {@link Ext.Element} and
+ * {@link Ext.fx.Anim}. The methods from these classes will be performed on all the elements in this collection.</p>
+ * <p>All methods return <i>this</i> and can be chained.</p>
+ * Usage:
+<pre><code>
+var els = Ext.select("#some-el div.some-class", true);
+// or select directly from an existing element
+var el = Ext.get('some-el');
+el.select('div.some-class', true);
+
+els.setWidth(100); // all elements become 100 width
+els.hide(true); // all elements fade out and hide
+// or
+els.setWidth(100).hide(true);
+</code></pre>
+ */
+Ext.CompositeElement = Ext.extend(Ext.CompositeElementLite, {
+
+ constructor : function(els, root){
+ this.elements = [];
+ this.add(els, root);
+ },
+
+ // private
+ getElement : function(el){
+ // In this case just return it, since we already have a reference to it
+ return el;
+ },
+
+ // private
+ transformElement : function(el){
+ return Ext.get(el);
+ }
+});
+
+/**
+ * Selects elements based on the passed CSS selector to enable {@link Ext.Element Element} methods
+ * to be applied to many related elements in one statement through the returned {@link Ext.CompositeElement CompositeElement} or
+ * {@link Ext.CompositeElementLite CompositeElementLite} object.
+ * @param {String/HTMLElement[]} selector The CSS selector or an array of elements
+ * @param {Boolean} [unique] true to create a unique Ext.Element for each element (defaults to a shared flyweight object)
+ * @param {HTMLElement/String} [root] The root element of the query or id of the root
+ * @return {Ext.CompositeElementLite/Ext.CompositeElement}
+ * @member Ext.Element
+ * @method select
+ */
+Ext.Element.select = function(selector, unique, root){
+ var els;
+ if(typeof selector == "string"){
+ els = Ext.Element.selectorFunction(selector, root);
+ }else if(selector.length !== undefined){
+ els = selector;
+ }else{
+ }
+ return (unique === true) ? new Ext.CompositeElement(els) : new Ext.CompositeElementLite(els);
+};
+
+/**
+ * Shorthand of {@link Ext.Element#select}.
+ * @member Ext
+ * @method select
+ * @alias Ext.Element#select
+ */
+Ext.select = Ext.Element.select;
+
+
+/*
+
+This file is part of Ext JS 4
+
+Copyright (c) 2011 Sencha Inc
+
+Contact: http://www.sencha.com/contact
+
+Commercial Usage
+Licensees holding valid commercial licenses may use this file in accordance with the Commercial Software License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and Sencha.
+
+If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
+
+*/
+/**
+ * Base class that provides a common interface for publishing events. Subclasses are expected to to have a property
+ * "events" with all the events defined, and, optionally, a property "listeners" with configured listeners defined.
+ *
+ * For example:
+ *
+ * Ext.define('Employee', {
+ * extend: 'Ext.util.Observable',
+ * constructor: function(config){
+ * this.name = config.name;
+ * this.addEvents({
+ * "fired" : true,
+ * "quit" : true
+ * });
+ *
+ * // Copy configured listeners into *this* object so that the base class's
+ * // constructor will add them.
+ * this.listeners = config.listeners;
+ *
+ * // Call our superclass constructor to complete construction process.
+ * this.callParent(arguments)
+ * }
+ * });
+ *
+ * This could then be used like this:
+ *
+ * var newEmployee = new Employee({
+ * name: employeeName,
+ * listeners: {
+ * quit: function() {
+ * // By default, "this" will be the object that fired the event.
+ * alert(this.name + " has quit!");
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.util.Observable', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.util.Event'],
+
+ statics: {
+ /**
+ * Removes **all** added captures from the Observable.
+ *
+ * @param {Ext.util.Observable} o The Observable to release
+ * @static
+ */
+ releaseCapture: function(o) {
+ o.fireEvent = this.prototype.fireEvent;
+ },
+
+ /**
+ * Starts capture on the specified Observable. All events will be passed to the supplied function with the event
+ * name + standard signature of the event **before** the event is fired. If the supplied function returns false,
+ * the event will not fire.
+ *
+ * @param {Ext.util.Observable} o The Observable to capture events from.
+ * @param {Function} fn The function to call when an event is fired.
+ * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to
+ * the Observable firing the event.
+ * @static
+ */
+ capture: function(o, fn, scope) {
+ o.fireEvent = Ext.Function.createInterceptor(o.fireEvent, fn, scope);
+ },
+
+ /**
+ * Sets observability on the passed class constructor.
+ *
+ * This makes any event fired on any instance of the passed class also fire a single event through
+ * the **class** allowing for central handling of events on many instances at once.
+ *
+ * Usage:
+ *
+ * Ext.util.Observable.observe(Ext.data.Connection);
+ * Ext.data.Connection.on('beforerequest', function(con, options) {
+ * console.log('Ajax request made to ' + options.url);
+ * });
+ *
+ * @param {Function} c The class constructor to make observable.
+ * @param {Object} listeners An object containing a series of listeners to add. See {@link #addListener}.
+ * @static
+ */
+ observe: function(cls, listeners) {
+ if (cls) {
+ if (!cls.isObservable) {
+ Ext.applyIf(cls, new this());
+ this.capture(cls.prototype, cls.fireEvent, cls);
+ }
+ if (Ext.isObject(listeners)) {
+ cls.on(listeners);
+ }
+ return cls;
+ }
+ }
+ },
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Object} listeners
+ *
+ * A config object containing one or more event handlers to be added to this object during initialization. This
+ * should be a valid listeners config object as specified in the {@link #addListener} example for attaching multiple
+ * handlers at once.
+ *
+ * **DOM events from Ext JS {@link Ext.Component Components}**
+ *
+ * While _some_ Ext JS Component classes export selected DOM events (e.g. "click", "mouseover" etc), this is usually
+ * only done when extra value can be added. For example the {@link Ext.view.View DataView}'s **`{@link
+ * Ext.view.View#itemclick itemclick}`** event passing the node clicked on. To access DOM events directly from a
+ * child element of a Component, we need to specify the `element` option to identify the Component property to add a
+ * DOM listener to:
+ *
+ * new Ext.panel.Panel({
+ * width: 400,
+ * height: 200,
+ * dockedItems: [{
+ * xtype: 'toolbar'
+ * }],
+ * listeners: {
+ * click: {
+ * element: 'el', //bind to the underlying el property on the panel
+ * fn: function(){ console.log('click el'); }
+ * },
+ * dblclick: {
+ * element: 'body', //bind to the underlying body property on the panel
+ * fn: function(){ console.log('dblclick body'); }
+ * }
+ * }
+ * });
+ */
+ // @private
+ isObservable: true,
+
+ constructor: function(config) {
+ var me = this;
+
+ Ext.apply(me, config);
+ if (me.listeners) {
+ me.on(me.listeners);
+ delete me.listeners;
+ }
+ me.events = me.events || {};
+
+ if (me.bubbleEvents) {
+ me.enableBubble(me.bubbleEvents);
+ }
+ },
+
+ // @private
+ eventOptionsRe : /^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate|element|vertical|horizontal|freezeEvent)$/,
+
+ /**
+ * Adds listeners to any Observable object (or Ext.Element) which are automatically removed when this Component is
+ * destroyed.
+ *
+ * @param {Ext.util.Observable/Ext.Element} item The item to which to add a listener/listeners.
+ * @param {Object/String} ename The event name, or an object containing event name properties.
+ * @param {Function} fn (optional) If the `ename` parameter was an event name, this is the handler function.
+ * @param {Object} scope (optional) If the `ename` parameter was an event name, this is the scope (`this` reference)
+ * in which the handler function is executed.
+ * @param {Object} opt (optional) If the `ename` parameter was an event name, this is the
+ * {@link Ext.util.Observable#addListener addListener} options.
+ */
+ addManagedListener : function(item, ename, fn, scope, options) {
+ var me = this,
+ managedListeners = me.managedListeners = me.managedListeners || [],
+ config;
+
+ if (typeof ename !== 'string') {
+ options = ename;
+ for (ename in options) {
+ if (options.hasOwnProperty(ename)) {
+ config = options[ename];
+ if (!me.eventOptionsRe.test(ename)) {
+ me.addManagedListener(item, ename, config.fn || config, config.scope || options.scope, config.fn ? config : options);
+ }
+ }
+ }
+ }
+ else {
+ managedListeners.push({
+ item: item,
+ ename: ename,
+ fn: fn,
+ scope: scope,
+ options: options
+ });
+
+ item.on(ename, fn, scope, options);
+ }
+ },
+
+ /**
+ * Removes listeners that were added by the {@link #mon} method.
+ *
+ * @param {Ext.util.Observable/Ext.Element} item The item from which to remove a listener/listeners.
+ * @param {Object/String} ename The event name, or an object containing event name properties.
+ * @param {Function} fn (optional) If the `ename` parameter was an event name, this is the handler function.
+ * @param {Object} scope (optional) If the `ename` parameter was an event name, this is the scope (`this` reference)
+ * in which the handler function is executed.
+ */
+ removeManagedListener : function(item, ename, fn, scope) {
+ var me = this,
+ options,
+ config,
+ managedListeners,
+ length,
+ i;
+
+ if (typeof ename !== 'string') {
+ options = ename;
+ for (ename in options) {
+ if (options.hasOwnProperty(ename)) {
+ config = options[ename];
+ if (!me.eventOptionsRe.test(ename)) {
+ me.removeManagedListener(item, ename, config.fn || config, config.scope || options.scope);
+ }
+ }
+ }
+ }
+
+ managedListeners = me.managedListeners ? me.managedListeners.slice() : [];
+
+ for (i = 0, length = managedListeners.length; i < length; i++) {
+ me.removeManagedListenerItem(false, managedListeners[i], item, ename, fn, scope);
+ }
+ },
+
+ /**
+ * Fires the specified event with the passed parameters (minus the event name, plus the `options` object passed
+ * to {@link #addListener}).
+ *
+ * An event may be set to bubble up an Observable parent hierarchy (See {@link Ext.Component#getBubbleTarget}) by
+ * calling {@link #enableBubble}.
+ *
+ * @param {String} eventName The name of the event to fire.
+ * @param {Object...} args Variable number of parameters are passed to handlers.
+ * @return {Boolean} returns false if any of the handlers return false otherwise it returns true.
+ */
+ fireEvent: function(eventName) {
+ var name = eventName.toLowerCase(),
+ events = this.events,
+ event = events && events[name],
+ bubbles = event && event.bubble;
+
+ return this.continueFireEvent(name, Ext.Array.slice(arguments, 1), bubbles);
+ },
+
+ /**
+ * Continue to fire event.
+ * @private
+ *
+ * @param {String} eventName
+ * @param {Array} args
+ * @param {Boolean} bubbles
+ */
+ continueFireEvent: function(eventName, args, bubbles) {
+ var target = this,
+ queue, event,
+ ret = true;
+
+ do {
+ if (target.eventsSuspended === true) {
+ if ((queue = target.eventQueue)) {
+ queue.push([eventName, args, bubbles]);
+ }
+ return ret;
+ } else {
+ event = target.events[eventName];
+ // Continue bubbling if event exists and it is `true` or the handler didn't returns false and it
+ // configure to bubble.
+ if (event && event != true) {
+ if ((ret = event.fire.apply(event, args)) === false) {
+ break;
+ }
+ }
+ }
+ } while (bubbles && (target = target.getBubbleParent()));
+ return ret;
+ },
+
+ /**
+ * Gets the bubbling parent for an Observable
+ * @private
+ * @return {Ext.util.Observable} The bubble parent. null is returned if no bubble target exists
+ */
+ getBubbleParent: function(){
+ var me = this, parent = me.getBubbleTarget && me.getBubbleTarget();
+ if (parent && parent.isObservable) {
+ return parent;
+ }
+ return null;
+ },
+
+ /**
+ * Appends an event handler to this object.
+ *
+ * @param {String} eventName The name of the event to listen for. May also be an object who's property names are
+ * event names.
+ * @param {Function} fn The method the event invokes. Will be called with arguments given to
+ * {@link #fireEvent} plus the `options` parameter described below.
+ * @param {Object} [scope] The scope (`this` reference) in which the handler function is executed. **If
+ * omitted, defaults to the object which fired the event.**
+ * @param {Object} [options] An object containing handler configuration.
+ *
+ * **Note:** Unlike in ExtJS 3.x, the options object will also be passed as the last argument to every event handler.
+ *
+ * This object may contain any of the following properties:
+ *
+ * - **scope** : Object
+ *
+ * The scope (`this` reference) in which the handler function is executed. **If omitted, defaults to the object
+ * which fired the event.**
+ *
+ * - **delay** : Number
+ *
+ * The number of milliseconds to delay the invocation of the handler after the event fires.
+ *
+ * - **single** : Boolean
+ *
+ * True to add a handler to handle just the next firing of the event, and then remove itself.
+ *
+ * - **buffer** : Number
+ *
+ * Causes the handler to be scheduled to run in an {@link Ext.util.DelayedTask} delayed by the specified number of
+ * milliseconds. If the event fires again within that time, the original handler is _not_ invoked, but the new
+ * handler is scheduled in its place.
+ *
+ * - **target** : Observable
+ *
+ * Only call the handler if the event was fired on the target Observable, _not_ if the event was bubbled up from a
+ * child Observable.
+ *
+ * - **element** : String
+ *
+ * **This option is only valid for listeners bound to {@link Ext.Component Components}.** The name of a Component
+ * property which references an element to add a listener to.
+ *
+ * This option is useful during Component construction to add DOM event listeners to elements of
+ * {@link Ext.Component Components} which will exist only after the Component is rendered.
+ * For example, to add a click listener to a Panel's body:
+ *
+ * new Ext.panel.Panel({
+ * title: 'The title',
+ * listeners: {
+ * click: this.handlePanelClick,
+ * element: 'body'
+ * }
+ * });
+ *
+ * **Combining Options**
+ *
+ * Using the options argument, it is possible to combine different types of listeners:
+ *
+ * A delayed, one-time listener.
+ *
+ * myPanel.on('hide', this.handleClick, this, {
+ * single: true,
+ * delay: 100
+ * });
+ *
+ * **Attaching multiple handlers in 1 call**
+ *
+ * The method also allows for a single argument to be passed which is a config object containing properties which
+ * specify multiple events. For example:
+ *
+ * myGridPanel.on({
+ * cellClick: this.onCellClick,
+ * mouseover: this.onMouseOver,
+ * mouseout: this.onMouseOut,
+ * scope: this // Important. Ensure "this" is correct during handler execution
+ * });
+ *
+ * One can also specify options for each event handler separately:
+ *
+ * myGridPanel.on({
+ * cellClick: {fn: this.onCellClick, scope: this, single: true},
+ * mouseover: {fn: panel.onMouseOver, scope: panel}
+ * });
+ *
+ */
+ addListener: function(ename, fn, scope, options) {
+ var me = this,
+ config,
+ event;
+
+ if (typeof ename !== 'string') {
+ options = ename;
+ for (ename in options) {
+ if (options.hasOwnProperty(ename)) {
+ config = options[ename];
+ if (!me.eventOptionsRe.test(ename)) {
+ me.addListener(ename, config.fn || config, config.scope || options.scope, config.fn ? config : options);
+ }
+ }
+ }
+ }
+ else {
+ ename = ename.toLowerCase();
+ me.events[ename] = me.events[ename] || true;
+ event = me.events[ename] || true;
+ if (Ext.isBoolean(event)) {
+ me.events[ename] = event = new Ext.util.Event(me, ename);
+ }
+ event.addListener(fn, scope, Ext.isObject(options) ? options : {});
+ }
+ },
+
+ /**
+ * Removes an event handler.
+ *
+ * @param {String} eventName The type of event the handler was associated with.
+ * @param {Function} fn The handler to remove. **This must be a reference to the function passed into the
+ * {@link #addListener} call.**
+ * @param {Object} scope (optional) The scope originally specified for the handler. It must be the same as the
+ * scope argument specified in the original call to {@link #addListener} or the listener will not be removed.
+ */
+ removeListener: function(ename, fn, scope) {
+ var me = this,
+ config,
+ event,
+ options;
+
+ if (typeof ename !== 'string') {
+ options = ename;
+ for (ename in options) {
+ if (options.hasOwnProperty(ename)) {
+ config = options[ename];
+ if (!me.eventOptionsRe.test(ename)) {
+ me.removeListener(ename, config.fn || config, config.scope || options.scope);
+ }
+ }
+ }
+ } else {
+ ename = ename.toLowerCase();
+ event = me.events[ename];
+ if (event && event.isEvent) {
+ event.removeListener(fn, scope);
+ }
+ }
+ },
+
+ /**
+ * Removes all listeners for this object including the managed listeners
+ */
+ clearListeners: function() {
+ var events = this.events,
+ event,
+ key;
+
+ for (key in events) {
+ if (events.hasOwnProperty(key)) {
+ event = events[key];
+ if (event.isEvent) {
+ event.clearListeners();
+ }
+ }
+ }
+
+ this.clearManagedListeners();
+ },
+
+
+ /**
+ * Removes all managed listeners for this object.
+ */
+ clearManagedListeners : function() {
+ var managedListeners = this.managedListeners || [],
+ i = 0,
+ len = managedListeners.length;
+
+ for (; i < len; i++) {
+ this.removeManagedListenerItem(true, managedListeners[i]);
+ }
+
+ this.managedListeners = [];
+ },
+
+ /**
+ * Remove a single managed listener item
+ * @private
+ * @param {Boolean} isClear True if this is being called during a clear
+ * @param {Object} managedListener The managed listener item
+ * See removeManagedListener for other args
+ */
+ removeManagedListenerItem: function(isClear, managedListener, item, ename, fn, scope){
+ if (isClear || (managedListener.item === item && managedListener.ename === ename && (!fn || managedListener.fn === fn) && (!scope || managedListener.scope === scope))) {
+ managedListener.item.un(managedListener.ename, managedListener.fn, managedListener.scope);
+ if (!isClear) {
+ Ext.Array.remove(this.managedListeners, managedListener);
+ }
+ }
+ },
+
+
+ /**
+ * Adds the specified events to the list of events which this Observable may fire.
+ *
+ * @param {Object/String} o Either an object with event names as properties with a value of `true` or the first
+ * event name string if multiple event names are being passed as separate parameters. Usage:
+ *
+ * this.addEvents({
+ * storeloaded: true,
+ * storecleared: true
+ * });
+ *
+ * @param {String...} more (optional) Additional event names if multiple event names are being passed as separate
+ * parameters. Usage:
+ *
+ * this.addEvents('storeloaded', 'storecleared');
+ *
+ */
+ addEvents: function(o) {
+ var me = this,
+ args,
+ len,
+ i;
+
+ me.events = me.events || {};
+ if (Ext.isString(o)) {
+ args = arguments;
+ i = args.length;
+
+ while (i--) {
+ me.events[args[i]] = me.events[args[i]] || true;
+ }
+ } else {
+ Ext.applyIf(me.events, o);
+ }
+ },
+
+ /**
+ * Checks to see if this object has any listeners for a specified event
+ *
+ * @param {String} eventName The name of the event to check for
+ * @return {Boolean} True if the event is being listened for, else false
+ */
+ hasListener: function(ename) {
+ var event = this.events[ename.toLowerCase()];
+ return event && event.isEvent === true && event.listeners.length > 0;
+ },
+
+ /**
+ * Suspends the firing of all events. (see {@link #resumeEvents})
+ *
+ * @param {Boolean} queueSuspended Pass as true to queue up suspended events to be fired
+ * after the {@link #resumeEvents} call instead of discarding all suspended events.
+ */
+ suspendEvents: function(queueSuspended) {
+ this.eventsSuspended = true;
+ if (queueSuspended && !this.eventQueue) {
+ this.eventQueue = [];
+ }
+ },
+
+ /**
+ * Resumes firing events (see {@link #suspendEvents}).
+ *
+ * If events were suspended using the `queueSuspended` parameter, then all events fired
+ * during event suspension will be sent to any listeners now.
+ */
+ resumeEvents: function() {
+ var me = this,
+ queued = me.eventQueue;
+
+ me.eventsSuspended = false;
+ delete me.eventQueue;
+
+ if (queued) {
+ Ext.each(queued, function(e) {
+ me.continueFireEvent.apply(me, e);
+ });
+ }
+ },
+
+ /**
+ * Relays selected events from the specified Observable as if the events were fired by `this`.
+ *
+ * @param {Object} origin The Observable whose events this object is to relay.
+ * @param {String[]} events Array of event names to relay.
+ * @param {String} prefix
+ */
+ relayEvents : function(origin, events, prefix) {
+ prefix = prefix || '';
+ var me = this,
+ len = events.length,
+ i = 0,
+ oldName,
+ newName;
+
+ for (; i < len; i++) {
+ oldName = events[i].substr(prefix.length);
+ newName = prefix + oldName;
+ me.events[newName] = me.events[newName] || true;
+ origin.on(oldName, me.createRelayer(newName));
+ }
+ },
+
+ /**
+ * @private
+ * Creates an event handling function which refires the event from this object as the passed event name.
+ * @param newName
+ * @returns {Function}
+ */
+ createRelayer: function(newName){
+ var me = this;
+ return function(){
+ return me.fireEvent.apply(me, [newName].concat(Array.prototype.slice.call(arguments, 0, -1)));
+ };
+ },
+
+ /**
+ * Enables events fired by this Observable to bubble up an owner hierarchy by calling `this.getBubbleTarget()` if
+ * present. There is no implementation in the Observable base class.
+ *
+ * This is commonly used by Ext.Components to bubble events to owner Containers.
+ * See {@link Ext.Component#getBubbleTarget}. The default implementation in Ext.Component returns the
+ * Component's immediate owner. But if a known target is required, this can be overridden to access the
+ * required target more quickly.
+ *
+ * Example:
+ *
+ * Ext.override(Ext.form.field.Base, {
+ * // Add functionality to Field's initComponent to enable the change event to bubble
+ * initComponent : Ext.Function.createSequence(Ext.form.field.Base.prototype.initComponent, function() {
+ * this.enableBubble('change');
+ * }),
+ *
+ * // We know that we want Field's events to bubble directly to the FormPanel.
+ * getBubbleTarget : function() {
+ * if (!this.formPanel) {
+ * this.formPanel = this.findParentByType('form');
+ * }
+ * return this.formPanel;
+ * }
+ * });
+ *
+ * var myForm = new Ext.formPanel({
+ * title: 'User Details',
+ * items: [{
+ * ...
+ * }],
+ * listeners: {
+ * change: function() {
+ * // Title goes red if form has been modified.
+ * myForm.header.setStyle('color', 'red');
+ * }
+ * }
+ * });
+ *
+ * @param {String/String[]} events The event name to bubble, or an Array of event names.
+ */
+ enableBubble: function(events) {
+ var me = this;
+ if (!Ext.isEmpty(events)) {
+ events = Ext.isArray(events) ? events: Ext.Array.toArray(arguments);
+ Ext.each(events,
+ function(ename) {
+ ename = ename.toLowerCase();
+ var ce = me.events[ename] || true;
+ if (Ext.isBoolean(ce)) {
+ ce = new Ext.util.Event(me, ename);
+ me.events[ename] = ce;
+ }
+ ce.bubble = true;
+ });
+ }
+ }
+}, function() {
+
+ this.createAlias({
+ /**
+ * @method
+ * Shorthand for {@link #addListener}.
+ * @alias Ext.util.Observable#addListener
+ */
+ on: 'addListener',
+ /**
+ * @method
+ * Shorthand for {@link #removeListener}.
+ * @alias Ext.util.Observable#removeListener
+ */
+ un: 'removeListener',
+ /**
+ * @method
+ * Shorthand for {@link #addManagedListener}.
+ * @alias Ext.util.Observable#addManagedListener
+ */
+ mon: 'addManagedListener',
+ /**
+ * @method
+ * Shorthand for {@link #removeManagedListener}.
+ * @alias Ext.util.Observable#removeManagedListener
+ */
+ mun: 'removeManagedListener'
+ });
+
+ //deprecated, will be removed in 5.0
+ this.observeClass = this.observe;
+
+ Ext.apply(Ext.util.Observable.prototype, function(){
+ // this is considered experimental (along with beforeMethod, afterMethod, removeMethodListener?)
+ // allows for easier interceptor and sequences, including cancelling and overwriting the return value of the call
+ // private
+ function getMethodEvent(method){
+ var e = (this.methodEvents = this.methodEvents || {})[method],
+ returnValue,
+ v,
+ cancel,
+ obj = this;
+
+ if (!e) {
+ this.methodEvents[method] = e = {};
+ e.originalFn = this[method];
+ e.methodName = method;
+ e.before = [];
+ e.after = [];
+
+ var makeCall = function(fn, scope, args){
+ if((v = fn.apply(scope || obj, args)) !== undefined){
+ if (typeof v == 'object') {
+ if(v.returnValue !== undefined){
+ returnValue = v.returnValue;
+ }else{
+ returnValue = v;
+ }
+ cancel = !!v.cancel;
+ }
+ else
+ if (v === false) {
+ cancel = true;
+ }
+ else {
+ returnValue = v;
+ }
+ }
+ };
+
+ this[method] = function(){
+ var args = Array.prototype.slice.call(arguments, 0),
+ b, i, len;
+ returnValue = v = undefined;
+ cancel = false;
+
+ for(i = 0, len = e.before.length; i < len; i++){
+ b = e.before[i];
+ makeCall(b.fn, b.scope, args);
+ if (cancel) {
+ return returnValue;
+ }
+ }
+
+ if((v = e.originalFn.apply(obj, args)) !== undefined){
+ returnValue = v;
+ }
+
+ for(i = 0, len = e.after.length; i < len; i++){
+ b = e.after[i];
+ makeCall(b.fn, b.scope, args);
+ if (cancel) {
+ return returnValue;
+ }
+ }
+ return returnValue;
+ };
+ }
+ return e;
+ }
+
+ return {
+ // these are considered experimental
+ // allows for easier interceptor and sequences, including cancelling and overwriting the return value of the call
+ // adds an 'interceptor' called before the original method
+ beforeMethod : function(method, fn, scope){
+ getMethodEvent.call(this, method).before.push({
+ fn: fn,
+ scope: scope
+ });
+ },
+
+ // adds a 'sequence' called after the original method
+ afterMethod : function(method, fn, scope){
+ getMethodEvent.call(this, method).after.push({
+ fn: fn,
+ scope: scope
+ });
+ },
+
+ removeMethodListener: function(method, fn, scope){
+ var e = this.getMethodEvent(method),
+ i, len;
+ for(i = 0, len = e.before.length; i < len; i++){
+ if(e.before[i].fn == fn && e.before[i].scope == scope){
+ Ext.Array.erase(e.before, i, 1);
+ return;
+ }
+ }
+ for(i = 0, len = e.after.length; i < len; i++){
+ if(e.after[i].fn == fn && e.after[i].scope == scope){
+ Ext.Array.erase(e.after, i, 1);
+ return;
+ }
+ }
+ },
+
+ toggleEventLogging: function(toggle) {
+ Ext.util.Observable[toggle ? 'capture' : 'releaseCapture'](this, function(en) {
+ if (Ext.isDefined(Ext.global.console)) {
+ Ext.global.console.log(en, arguments);
+ }
+ });
+ }
+ };
+ }());
+});
+
+/**
+ * @class Ext.util.Animate
+ * This animation class is a mixin.
+ *
+ * Ext.util.Animate provides an API for the creation of animated transitions of properties and styles.
+ * This class is used as a mixin and currently applied to {@link Ext.Element}, {@link Ext.CompositeElement},
+ * {@link Ext.draw.Sprite}, {@link Ext.draw.CompositeSprite}, and {@link Ext.Component}. Note that Components
+ * have a limited subset of what attributes can be animated such as top, left, x, y, height, width, and
+ * opacity (color, paddings, and margins can not be animated).
+ *
+ * ## Animation Basics
+ *
+ * All animations require three things - `easing`, `duration`, and `to` (the final end value for each property)
+ * you wish to animate. Easing and duration are defaulted values specified below.
+ * Easing describes how the intermediate values used during a transition will be calculated.
+ * {@link Ext.fx.Anim#easing Easing} allows for a transition to change speed over its duration.
+ * You may use the defaults for easing and duration, but you must always set a
+ * {@link Ext.fx.Anim#to to} property which is the end value for all animations.
+ *
+ * Popular element 'to' configurations are:
+ *
+ * - opacity
+ * - x
+ * - y
+ * - color
+ * - height
+ * - width
+ *
+ * Popular sprite 'to' configurations are:
+ *
+ * - translation
+ * - path
+ * - scale
+ * - stroke
+ * - rotation
+ *
+ * The default duration for animations is 250 (which is a 1/4 of a second). Duration is denoted in
+ * milliseconds. Therefore 1 second is 1000, 1 minute would be 60000, and so on. The default easing curve
+ * used for all animations is 'ease'. Popular easing functions are included and can be found in {@link Ext.fx.Anim#easing Easing}.
+ *
+ * For example, a simple animation to fade out an element with a default easing and duration:
+ *
+ * var p1 = Ext.get('myElementId');
+ *
+ * p1.animate({
+ * to: {
+ * opacity: 0
+ * }
+ * });
+ *
+ * To make this animation fade out in a tenth of a second:
+ *
+ * var p1 = Ext.get('myElementId');
+ *
+ * p1.animate({
+ * duration: 100,
+ * to: {
+ * opacity: 0
+ * }
+ * });
+ *
+ * ## Animation Queues
+ *
+ * By default all animations are added to a queue which allows for animation via a chain-style API.
+ * For example, the following code will queue 4 animations which occur sequentially (one right after the other):
+ *
+ * p1.animate({
+ * to: {
+ * x: 500
+ * }
+ * }).animate({
+ * to: {
+ * y: 150
+ * }
+ * }).animate({
+ * to: {
+ * backgroundColor: '#f00' //red
+ * }
+ * }).animate({
+ * to: {
+ * opacity: 0
+ * }
+ * });
+ *
+ * You can change this behavior by calling the {@link Ext.util.Animate#syncFx syncFx} method and all
+ * subsequent animations for the specified target will be run concurrently (at the same time).
+ *
+ * p1.syncFx(); //this will make all animations run at the same time
+ *
+ * p1.animate({
+ * to: {
+ * x: 500
+ * }
+ * }).animate({
+ * to: {
+ * y: 150
+ * }
+ * }).animate({
+ * to: {
+ * backgroundColor: '#f00' //red
+ * }
+ * }).animate({
+ * to: {
+ * opacity: 0
+ * }
+ * });
+ *
+ * This works the same as:
+ *
+ * p1.animate({
+ * to: {
+ * x: 500,
+ * y: 150,
+ * backgroundColor: '#f00' //red
+ * opacity: 0
+ * }
+ * });
+ *
+ * The {@link Ext.util.Animate#stopAnimation stopAnimation} method can be used to stop any
+ * currently running animations and clear any queued animations.
+ *
+ * ## Animation Keyframes
+ *
+ * You can also set up complex animations with {@link Ext.fx.Anim#keyframes keyframes} which follow the
+ * CSS3 Animation configuration pattern. Note rotation, translation, and scaling can only be done for sprites.
+ * The previous example can be written with the following syntax:
+ *
+ * p1.animate({
+ * duration: 1000, //one second total
+ * keyframes: {
+ * 25: { //from 0 to 250ms (25%)
+ * x: 0
+ * },
+ * 50: { //from 250ms to 500ms (50%)
+ * y: 0
+ * },
+ * 75: { //from 500ms to 750ms (75%)
+ * backgroundColor: '#f00' //red
+ * },
+ * 100: { //from 750ms to 1sec
+ * opacity: 0
+ * }
+ * }
+ * });
+ *
+ * ## Animation Events
+ *
+ * Each animation you create has events for {@link Ext.fx.Anim#beforeanimate beforeanimate},
+ * {@link Ext.fx.Anim#afteranimate afteranimate}, and {@link Ext.fx.Anim#lastframe lastframe}.
+ * Keyframed animations adds an additional {@link Ext.fx.Animator#keyframe keyframe} event which
+ * fires for each keyframe in your animation.
+ *
+ * All animations support the {@link Ext.util.Observable#listeners listeners} configuration to attact functions to these events.
+ *
+ * startAnimate: function() {
+ * var p1 = Ext.get('myElementId');
+ * p1.animate({
+ * duration: 100,
+ * to: {
+ * opacity: 0
+ * },
+ * listeners: {
+ * beforeanimate: function() {
+ * // Execute my custom method before the animation
+ * this.myBeforeAnimateFn();
+ * },
+ * afteranimate: function() {
+ * // Execute my custom method after the animation
+ * this.myAfterAnimateFn();
+ * },
+ * scope: this
+ * });
+ * },
+ * myBeforeAnimateFn: function() {
+ * // My custom logic
+ * },
+ * myAfterAnimateFn: function() {
+ * // My custom logic
+ * }
+ *
+ * Due to the fact that animations run asynchronously, you can determine if an animation is currently
+ * running on any target by using the {@link Ext.util.Animate#getActiveAnimation getActiveAnimation}
+ * method. This method will return false if there are no active animations or return the currently
+ * running {@link Ext.fx.Anim} instance.
+ *
+ * In this example, we're going to wait for the current animation to finish, then stop any other
+ * queued animations before we fade our element's opacity to 0:
+ *
+ * var curAnim = p1.getActiveAnimation();
+ * if (curAnim) {
+ * curAnim.on('afteranimate', function() {
+ * p1.stopAnimation();
+ * p1.animate({
+ * to: {
+ * opacity: 0
+ * }
+ * });
+ * });
+ * }
+ *
+ * @docauthor Jamie Avins <jamie@sencha.com>
+ */
+Ext.define('Ext.util.Animate', {
+
+ uses: ['Ext.fx.Manager', 'Ext.fx.Anim'],
+
+ /**
+ * <p>Perform custom animation on this object.<p>
+ * <p>This method is applicable to both the {@link Ext.Component Component} class and the {@link Ext.Element Element} class.
+ * It performs animated transitions of certain properties of this object over a specified timeline.</p>
+ * <p>The sole parameter is an object which specifies start property values, end property values, and properties which
+ * describe the timeline. Of the properties listed below, only <b><code>to</code></b> is mandatory.</p>
+ * <p>Properties include<ul>
+ * <li><code>from</code> <div class="sub-desc">An object which specifies start values for the properties being animated.
+ * If not supplied, properties are animated from current settings. The actual properties which may be animated depend upon
+ * ths object being animated. See the sections below on Element and Component animation.<div></li>
+ * <li><code>to</code> <div class="sub-desc">An object which specifies end values for the properties being animated.</div></li>
+ * <li><code>duration</code><div class="sub-desc">The duration <b>in milliseconds</b> for which the animation will run.</div></li>
+ * <li><code>easing</code> <div class="sub-desc">A string value describing an easing type to modify the rate of change from the default linear to non-linear. Values may be one of:<code><ul>
+ * <li>ease</li>
+ * <li>easeIn</li>
+ * <li>easeOut</li>
+ * <li>easeInOut</li>
+ * <li>backIn</li>
+ * <li>backOut</li>
+ * <li>elasticIn</li>
+ * <li>elasticOut</li>
+ * <li>bounceIn</li>
+ * <li>bounceOut</li>
+ * </ul></code></div></li>
+ * <li><code>keyframes</code> <div class="sub-desc">This is an object which describes the state of animated properties at certain points along the timeline.
+ * it is an object containing properties who's names are the percentage along the timeline being described and who's values specify the animation state at that point.</div></li>
+ * <li><code>listeners</code> <div class="sub-desc">This is a standard {@link Ext.util.Observable#listeners listeners} configuration object which may be used
+ * to inject behaviour at either the <code>beforeanimate</code> event or the <code>afteranimate</code> event.</div></li>
+ * </ul></p>
+ * <h3>Animating an {@link Ext.Element Element}</h3>
+ * When animating an Element, the following properties may be specified in <code>from</code>, <code>to</code>, and <code>keyframe</code> objects:<ul>
+ * <li><code>x</code> <div class="sub-desc">The page X position in pixels.</div></li>
+ * <li><code>y</code> <div class="sub-desc">The page Y position in pixels</div></li>
+ * <li><code>left</code> <div class="sub-desc">The element's CSS <code>left</code> value. Units must be supplied.</div></li>
+ * <li><code>top</code> <div class="sub-desc">The element's CSS <code>top</code> value. Units must be supplied.</div></li>
+ * <li><code>width</code> <div class="sub-desc">The element's CSS <code>width</code> value. Units must be supplied.</div></li>
+ * <li><code>height</code> <div class="sub-desc">The element's CSS <code>height</code> value. Units must be supplied.</div></li>
+ * <li><code>scrollLeft</code> <div class="sub-desc">The element's <code>scrollLeft</code> value.</div></li>
+ * <li><code>scrollTop</code> <div class="sub-desc">The element's <code>scrollLeft</code> value.</div></li>
+ * <li><code>opacity</code> <div class="sub-desc">The element's <code>opacity</code> value. This must be a value between <code>0</code> and <code>1</code>.</div></li>
+ * </ul>
+ * <p><b>Be aware than animating an Element which is being used by an Ext Component without in some way informing the Component about the changed element state
+ * will result in incorrect Component behaviour. This is because the Component will be using the old state of the element. To avoid this problem, it is now possible to
+ * directly animate certain properties of Components.</b></p>
+ * <h3>Animating a {@link Ext.Component Component}</h3>
+ * When animating an Element, the following properties may be specified in <code>from</code>, <code>to</code>, and <code>keyframe</code> objects:<ul>
+ * <li><code>x</code> <div class="sub-desc">The Component's page X position in pixels.</div></li>
+ * <li><code>y</code> <div class="sub-desc">The Component's page Y position in pixels</div></li>
+ * <li><code>left</code> <div class="sub-desc">The Component's <code>left</code> value in pixels.</div></li>
+ * <li><code>top</code> <div class="sub-desc">The Component's <code>top</code> value in pixels.</div></li>
+ * <li><code>width</code> <div class="sub-desc">The Component's <code>width</code> value in pixels.</div></li>
+ * <li><code>width</code> <div class="sub-desc">The Component's <code>width</code> value in pixels.</div></li>
+ * <li><code>dynamic</code> <div class="sub-desc">Specify as true to update the Component's layout (if it is a Container) at every frame
+ * of the animation. <i>Use sparingly as laying out on every intermediate size change is an expensive operation</i>.</div></li>
+ * </ul>
+ * <p>For example, to animate a Window to a new size, ensuring that its internal layout, and any shadow is correct:</p>
+ * <pre><code>
+myWindow = Ext.create('Ext.window.Window', {
+ title: 'Test Component animation',
+ width: 500,
+ height: 300,
+ layout: {
+ type: 'hbox',
+ align: 'stretch'
+ },
+ items: [{
+ title: 'Left: 33%',
+ margins: '5 0 5 5',
+ flex: 1
+ }, {
+ title: 'Left: 66%',
+ margins: '5 5 5 5',
+ flex: 2
+ }]
+});
+myWindow.show();
+myWindow.header.el.on('click', function() {
+ myWindow.animate({
+ to: {
+ width: (myWindow.getWidth() == 500) ? 700 : 500,
+ height: (myWindow.getHeight() == 300) ? 400 : 300,
+ }
+ });
+});
+</code></pre>
+ * <p>For performance reasons, by default, the internal layout is only updated when the Window reaches its final <code>"to"</code> size. If dynamic updating of the Window's child
+ * Components is required, then configure the animation with <code>dynamic: true</code> and the two child items will maintain their proportions during the animation.</p>
+ * @param {Object} config An object containing properties which describe the animation's start and end states, and the timeline of the animation.
+ * @return {Object} this
+ */
+ animate: function(animObj) {
+ var me = this;
+ if (Ext.fx.Manager.hasFxBlock(me.id)) {
+ return me;
+ }
+ Ext.fx.Manager.queueFx(Ext.create('Ext.fx.Anim', me.anim(animObj)));
+ return this;
+ },
+
+ // @private - process the passed fx configuration.
+ anim: function(config) {
+ if (!Ext.isObject(config)) {
+ return (config) ? {} : false;
+ }
+
+ var me = this;
+
+ if (config.stopAnimation) {
+ me.stopAnimation();
+ }
+
+ Ext.applyIf(config, Ext.fx.Manager.getFxDefaults(me.id));
+
+ return Ext.apply({
+ target: me,
+ paused: true
+ }, config);
+ },
+
+ /**
+ * @deprecated 4.0 Replaced by {@link #stopAnimation}
+ * Stops any running effects and clears this object's internal effects queue if it contains
+ * any additional effects that haven't started yet.
+ * @return {Ext.Element} The Element
+ * @method
+ */
+ stopFx: Ext.Function.alias(Ext.util.Animate, 'stopAnimation'),
+
+ /**
+ * Stops any running effects and clears this object's internal effects queue if it contains
+ * any additional effects that haven't started yet.
+ * @return {Ext.Element} The Element
+ */
+ stopAnimation: function() {
+ Ext.fx.Manager.stopAnimation(this.id);
+ return this;
+ },
+
+ /**
+ * Ensures that all effects queued after syncFx is called on this object are
+ * run concurrently. This is the opposite of {@link #sequenceFx}.
+ * @return {Object} this
+ */
+ syncFx: function() {
+ Ext.fx.Manager.setFxDefaults(this.id, {
+ concurrent: true
+ });
+ return this;
+ },
+
+ /**
+ * Ensures that all effects queued after sequenceFx is called on this object are
+ * run in sequence. This is the opposite of {@link #syncFx}.
+ * @return {Object} this
+ */
+ sequenceFx: function() {
+ Ext.fx.Manager.setFxDefaults(this.id, {
+ concurrent: false
+ });
+ return this;
+ },
+
+ /**
+ * @deprecated 4.0 Replaced by {@link #getActiveAnimation}
+ * @alias Ext.util.Animate#getActiveAnimation
+ * @method
+ */
+ hasActiveFx: Ext.Function.alias(Ext.util.Animate, 'getActiveAnimation'),
+
+ /**
+ * Returns the current animation if this object has any effects actively running or queued, else returns false.
+ * @return {Ext.fx.Anim/Boolean} Anim if element has active effects, else false
+ */
+ getActiveAnimation: function() {
+ return Ext.fx.Manager.getActiveAnimation(this.id);
+ }
+}, function(){
+ // Apply Animate mixin manually until Element is defined in the proper 4.x way
+ Ext.applyIf(Ext.Element.prototype, this.prototype);
+ // We need to call this again so the animation methods get copied over to CE
+ Ext.CompositeElementLite.importElementMethods();
+});
+/**
+ * @class Ext.state.Provider
+ * <p>Abstract base class for state provider implementations. The provider is responsible
+ * for setting values and extracting values to/from the underlying storage source. The
+ * storage source can vary and the details should be implemented in a subclass. For example
+ * a provider could use a server side database or the browser localstorage where supported.</p>
+ *
+ * <p>This class provides methods for encoding and decoding <b>typed</b> variables including
+ * dates and defines the Provider interface. By default these methods put the value and the
+ * type information into a delimited string that can be stored. These should be overridden in
+ * a subclass if you want to change the format of the encoded value and subsequent decoding.</p>
+ */
+Ext.define('Ext.state.Provider', {
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ /**
+ * @cfg {String} prefix A string to prefix to items stored in the underlying state store.
+ * Defaults to <tt>'ext-'</tt>
+ */
+ prefix: 'ext-',
+
+ constructor : function(config){
+ config = config || {};
+ var me = this;
+ Ext.apply(me, config);
+ /**
+ * @event statechange
+ * Fires when a state change occurs.
+ * @param {Ext.state.Provider} this This state provider
+ * @param {String} key The state key which was changed
+ * @param {String} value The encoded value for the state
+ */
+ me.addEvents("statechange");
+ me.state = {};
+ me.mixins.observable.constructor.call(me);
+ },
+
+ /**
+ * Returns the current value for a key
+ * @param {String} name The key name
+ * @param {Object} defaultValue A default value to return if the key's value is not found
+ * @return {Object} The state data
+ */
+ get : function(name, defaultValue){
+ return typeof this.state[name] == "undefined" ?
+ defaultValue : this.state[name];
+ },
+
+ /**
+ * Clears a value from the state
+ * @param {String} name The key name
+ */
+ clear : function(name){
+ var me = this;
+ delete me.state[name];
+ me.fireEvent("statechange", me, name, null);
+ },
+
+ /**
+ * Sets the value for a key
+ * @param {String} name The key name
+ * @param {Object} value The value to set
+ */
+ set : function(name, value){
+ var me = this;
+ me.state[name] = value;
+ me.fireEvent("statechange", me, name, value);
+ },
+
+ /**
+ * Decodes a string previously encoded with {@link #encodeValue}.
+ * @param {String} value The value to decode
+ * @return {Object} The decoded value
+ */
+ decodeValue : function(value){
+
+ // a -> Array
+ // n -> Number
+ // d -> Date
+ // b -> Boolean
+ // s -> String
+ // o -> Object
+ // -> Empty (null)
+
+ var me = this,
+ re = /^(a|n|d|b|s|o|e)\:(.*)$/,
+ matches = re.exec(unescape(value)),
+ all,
+ type,
+ value,
+ keyValue;
+
+ if(!matches || !matches[1]){
+ return; // non state
+ }
+
+ type = matches[1];
+ value = matches[2];
+ switch (type) {
+ case 'e':
+ return null;
+ case 'n':
+ return parseFloat(value);
+ case 'd':
+ return new Date(Date.parse(value));
+ case 'b':
+ return (value == '1');
+ case 'a':
+ all = [];
+ if(value != ''){
+ Ext.each(value.split('^'), function(val){
+ all.push(me.decodeValue(val));
+ }, me);
+ }
+ return all;
+ case 'o':
+ all = {};
+ if(value != ''){
+ Ext.each(value.split('^'), function(val){
+ keyValue = val.split('=');
+ all[keyValue[0]] = me.decodeValue(keyValue[1]);
+ }, me);
+ }
+ return all;
+ default:
+ return value;
+ }
+ },
+
+ /**
+ * Encodes a value including type information. Decode with {@link #decodeValue}.
+ * @param {Object} value The value to encode
+ * @return {String} The encoded value
+ */
+ encodeValue : function(value){
+ var flat = '',
+ i = 0,
+ enc,
+ len,
+ key;
+
+ if (value == null) {
+ return 'e:1';
+ } else if(typeof value == 'number') {
+ enc = 'n:' + value;
+ } else if(typeof value == 'boolean') {
+ enc = 'b:' + (value ? '1' : '0');
+ } else if(Ext.isDate(value)) {
+ enc = 'd:' + value.toGMTString();
+ } else if(Ext.isArray(value)) {
+ for (len = value.length; i < len; i++) {
+ flat += this.encodeValue(value[i]);
+ if (i != len - 1) {
+ flat += '^';
+ }
+ }
+ enc = 'a:' + flat;
+ } else if (typeof value == 'object') {
+ for (key in value) {
+ if (typeof value[key] != 'function' && value[key] !== undefined) {
+ flat += key + '=' + this.encodeValue(value[key]) + '^';
+ }
+ }
+ enc = 'o:' + flat.substring(0, flat.length-1);
+ } else {
+ enc = 's:' + value;
+ }
+ return escape(enc);
+ }
+});
+/**
+ * Provides searching of Components within Ext.ComponentManager (globally) or a specific
+ * Ext.container.Container on the document with a similar syntax to a CSS selector.
+ *
+ * Components can be retrieved by using their {@link Ext.Component xtype} with an optional . prefix
+ *
+ * - `component` or `.component`
+ * - `gridpanel` or `.gridpanel`
+ *
+ * An itemId or id must be prefixed with a #
+ *
+ * - `#myContainer`
+ *
+ * Attributes must be wrapped in brackets
+ *
+ * - `component[autoScroll]`
+ * - `panel[title="Test"]`
+ *
+ * Member expressions from candidate Components may be tested. If the expression returns a *truthy* value,
+ * the candidate Component will be included in the query:
+ *
+ * var disabledFields = myFormPanel.query("{isDisabled()}");
+ *
+ * Pseudo classes may be used to filter results in the same way as in {@link Ext.DomQuery DomQuery}:
+ *
+ * // Function receives array and returns a filtered array.
+ * Ext.ComponentQuery.pseudos.invalid = function(items) {
+ * var i = 0, l = items.length, c, result = [];
+ * for (; i < l; i++) {
+ * if (!(c = items[i]).isValid()) {
+ * result.push(c);
+ * }
+ * }
+ * return result;
+ * };
+ *
+ * var invalidFields = myFormPanel.query('field:invalid');
+ * if (invalidFields.length) {
+ * invalidFields[0].getEl().scrollIntoView(myFormPanel.body);
+ * for (var i = 0, l = invalidFields.length; i < l; i++) {
+ * invalidFields[i].getEl().frame("red");
+ * }
+ * }
+ *
+ * Default pseudos include:
+ *
+ * - not
+ * - last
+ *
+ * Queries return an array of components.
+ * Here are some example queries.
+ *
+ * // retrieve all Ext.Panels in the document by xtype
+ * var panelsArray = Ext.ComponentQuery.query('panel');
+ *
+ * // retrieve all Ext.Panels within the container with an id myCt
+ * var panelsWithinmyCt = Ext.ComponentQuery.query('#myCt panel');
+ *
+ * // retrieve all direct children which are Ext.Panels within myCt
+ * var directChildPanel = Ext.ComponentQuery.query('#myCt > panel');
+ *
+ * // retrieve all grids and trees
+ * var gridsAndTrees = Ext.ComponentQuery.query('gridpanel, treepanel');
+ *
+ * For easy access to queries based from a particular Container see the {@link Ext.container.Container#query},
+ * {@link Ext.container.Container#down} and {@link Ext.container.Container#child} methods. Also see
+ * {@link Ext.Component#up}.
+ */
+Ext.define('Ext.ComponentQuery', {
+ singleton: true,
+ uses: ['Ext.ComponentManager']
+}, function() {
+
+ var cq = this,
+
+ // A function source code pattern with a placeholder which accepts an expression which yields a truth value when applied
+ // as a member on each item in the passed array.
+ filterFnPattern = [
+ 'var r = [],',
+ 'i = 0,',
+ 'it = items,',
+ 'l = it.length,',
+ 'c;',
+ 'for (; i < l; i++) {',
+ 'c = it[i];',
+ 'if (c.{0}) {',
+ 'r.push(c);',
+ '}',
+ '}',
+ 'return r;'
+ ].join(''),
+
+ filterItems = function(items, operation) {
+ // Argument list for the operation is [ itemsArray, operationArg1, operationArg2...]
+ // The operation's method loops over each item in the candidate array and
+ // returns an array of items which match its criteria
+ return operation.method.apply(this, [ items ].concat(operation.args));
+ },
+
+ getItems = function(items, mode) {
+ var result = [],
+ i = 0,
+ length = items.length,
+ candidate,
+ deep = mode !== '>';
+
+ for (; i < length; i++) {
+ candidate = items[i];
+ if (candidate.getRefItems) {
+ result = result.concat(candidate.getRefItems(deep));
+ }
+ }
+ return result;
+ },
+
+ getAncestors = function(items) {
+ var result = [],
+ i = 0,
+ length = items.length,
+ candidate;
+ for (; i < length; i++) {
+ candidate = items[i];
+ while (!!(candidate = (candidate.ownerCt || candidate.floatParent))) {
+ result.push(candidate);
+ }
+ }
+ return result;
+ },
+
+ // Filters the passed candidate array and returns only items which match the passed xtype
+ filterByXType = function(items, xtype, shallow) {
+ if (xtype === '*') {
+ return items.slice();
+ }
+ else {
+ var result = [],
+ i = 0,
+ length = items.length,
+ candidate;
+ for (; i < length; i++) {
+ candidate = items[i];
+ if (candidate.isXType(xtype, shallow)) {
+ result.push(candidate);
+ }
+ }
+ return result;
+ }
+ },
+
+ // Filters the passed candidate array and returns only items which have the passed className
+ filterByClassName = function(items, className) {
+ var EA = Ext.Array,
+ result = [],
+ i = 0,
+ length = items.length,
+ candidate;
+ for (; i < length; i++) {
+ candidate = items[i];
+ if (candidate.el ? candidate.el.hasCls(className) : EA.contains(candidate.initCls(), className)) {
+ result.push(candidate);
+ }
+ }
+ return result;
+ },
+
+ // Filters the passed candidate array and returns only items which have the specified property match
+ filterByAttribute = function(items, property, operator, value) {
+ var result = [],
+ i = 0,
+ length = items.length,
+ candidate;
+ for (; i < length; i++) {
+ candidate = items[i];
+ if (!value ? !!candidate[property] : (String(candidate[property]) === value)) {
+ result.push(candidate);
+ }
+ }
+ return result;
+ },
+
+ // Filters the passed candidate array and returns only items which have the specified itemId or id
+ filterById = function(items, id) {
+ var result = [],
+ i = 0,
+ length = items.length,
+ candidate;
+ for (; i < length; i++) {
+ candidate = items[i];
+ if (candidate.getItemId() === id) {
+ result.push(candidate);
+ }
+ }
+ return result;
+ },
+
+ // Filters the passed candidate array and returns only items which the named pseudo class matcher filters in
+ filterByPseudo = function(items, name, value) {
+ return cq.pseudos[name](items, value);
+ },
+
+ // Determines leading mode
+ // > for direct child, and ^ to switch to ownerCt axis
+ modeRe = /^(\s?([>\^])\s?|\s|$)/,
+
+ // Matches a token with possibly (true|false) appended for the "shallow" parameter
+ tokenRe = /^(#)?([\w\-]+|\*)(?:\((true|false)\))?/,
+
+ matchers = [{
+ // Checks for .xtype with possibly (true|false) appended for the "shallow" parameter
+ re: /^\.([\w\-]+)(?:\((true|false)\))?/,
+ method: filterByXType
+ },{
+ // checks for [attribute=value]
+ re: /^(?:[\[](?:@)?([\w\-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]])/,
+ method: filterByAttribute
+ }, {
+ // checks for #cmpItemId
+ re: /^#([\w\-]+)/,
+ method: filterById
+ }, {
+ // checks for :<pseudo_class>(<selector>)
+ re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/,
+ method: filterByPseudo
+ }, {
+ // checks for {<member_expression>}
+ re: /^(?:\{([^\}]+)\})/,
+ method: filterFnPattern
+ }];
+
+ // @class Ext.ComponentQuery.Query
+ // This internal class is completely hidden in documentation.
+ cq.Query = Ext.extend(Object, {
+ constructor: function(cfg) {
+ cfg = cfg || {};
+ Ext.apply(this, cfg);
+ },
+
+ // Executes this Query upon the selected root.
+ // The root provides the initial source of candidate Component matches which are progressively
+ // filtered by iterating through this Query's operations cache.
+ // If no root is provided, all registered Components are searched via the ComponentManager.
+ // root may be a Container who's descendant Components are filtered
+ // root may be a Component with an implementation of getRefItems which provides some nested Components such as the
+ // docked items within a Panel.
+ // root may be an array of candidate Components to filter using this Query.
+ execute : function(root) {
+ var operations = this.operations,
+ i = 0,
+ length = operations.length,
+ operation,
+ workingItems;
+
+ // no root, use all Components in the document
+ if (!root) {
+ workingItems = Ext.ComponentManager.all.getArray();
+ }
+ // Root is a candidate Array
+ else if (Ext.isArray(root)) {
+ workingItems = root;
+ }
+
+ // We are going to loop over our operations and take care of them
+ // one by one.
+ for (; i < length; i++) {
+ operation = operations[i];
+
+ // The mode operation requires some custom handling.
+ // All other operations essentially filter down our current
+ // working items, while mode replaces our current working
+ // items by getting children from each one of our current
+ // working items. The type of mode determines the type of
+ // children we get. (e.g. > only gets direct children)
+ if (operation.mode === '^') {
+ workingItems = getAncestors(workingItems || [root]);
+ }
+ else if (operation.mode) {
+ workingItems = getItems(workingItems || [root], operation.mode);
+ }
+ else {
+ workingItems = filterItems(workingItems || getItems([root]), operation);
+ }
+
+ // If this is the last operation, it means our current working
+ // items are the final matched items. Thus return them!
+ if (i === length -1) {
+ return workingItems;
+ }
+ }
+ return [];
+ },
+
+ is: function(component) {
+ var operations = this.operations,
+ components = Ext.isArray(component) ? component : [component],
+ originalLength = components.length,
+ lastOperation = operations[operations.length-1],
+ ln, i;
+
+ components = filterItems(components, lastOperation);
+ if (components.length === originalLength) {
+ if (operations.length > 1) {
+ for (i = 0, ln = components.length; i < ln; i++) {
+ if (Ext.Array.indexOf(this.execute(), components[i]) === -1) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+
+ Ext.apply(this, {
+
+ // private cache of selectors and matching ComponentQuery.Query objects
+ cache: {},
+
+ // private cache of pseudo class filter functions
+ pseudos: {
+ not: function(components, selector){
+ var CQ = Ext.ComponentQuery,
+ i = 0,
+ length = components.length,
+ results = [],
+ index = -1,
+ component;
+
+ for(; i < length; ++i) {
+ component = components[i];
+ if (!CQ.is(component, selector)) {
+ results[++index] = component;
+ }
+ }
+ return results;
+ },
+ last: function(components) {
+ return components[components.length - 1];
+ }
+ },
+
+ /**
+ * Returns an array of matched Components from within the passed root object.
+ *
+ * This method filters returned Components in a similar way to how CSS selector based DOM
+ * queries work using a textual selector string.
+ *
+ * See class summary for details.
+ *
+ * @param {String} selector The selector string to filter returned Components
+ * @param {Ext.container.Container} root The Container within which to perform the query.
+ * If omitted, all Components within the document are included in the search.
+ *
+ * This parameter may also be an array of Components to filter according to the selector.</p>
+ * @returns {Ext.Component[]} The matched Components.
+ *
+ * @member Ext.ComponentQuery
+ */
+ query: function(selector, root) {
+ var selectors = selector.split(','),
+ length = selectors.length,
+ i = 0,
+ results = [],
+ noDupResults = [],
+ dupMatcher = {},
+ query, resultsLn, cmp;
+
+ for (; i < length; i++) {
+ selector = Ext.String.trim(selectors[i]);
+ query = this.cache[selector];
+ if (!query) {
+ this.cache[selector] = query = this.parse(selector);
+ }
+ results = results.concat(query.execute(root));
+ }
+
+ // multiple selectors, potential to find duplicates
+ // lets filter them out.
+ if (length > 1) {
+ resultsLn = results.length;
+ for (i = 0; i < resultsLn; i++) {
+ cmp = results[i];
+ if (!dupMatcher[cmp.id]) {
+ noDupResults.push(cmp);
+ dupMatcher[cmp.id] = true;
+ }
+ }
+ results = noDupResults;
+ }
+ return results;
+ },
+
+ /**
+ * Tests whether the passed Component matches the selector string.
+ * @param {Ext.Component} component The Component to test
+ * @param {String} selector The selector string to test against.
+ * @return {Boolean} True if the Component matches the selector.
+ * @member Ext.ComponentQuery
+ */
+ is: function(component, selector) {
+ if (!selector) {
+ return true;
+ }
+ var query = this.cache[selector];
+ if (!query) {
+ this.cache[selector] = query = this.parse(selector);
+ }
+ return query.is(component);
+ },
+
+ parse: function(selector) {
+ var operations = [],
+ length = matchers.length,
+ lastSelector,
+ tokenMatch,
+ matchedChar,
+ modeMatch,
+ selectorMatch,
+ i, matcher, method;
+
+ // We are going to parse the beginning of the selector over and
+ // over again, slicing off the selector any portions we converted into an
+ // operation, until it is an empty string.
+ while (selector && lastSelector !== selector) {
+ lastSelector = selector;
+
+ // First we check if we are dealing with a token like #, * or an xtype
+ tokenMatch = selector.match(tokenRe);
+
+ if (tokenMatch) {
+ matchedChar = tokenMatch[1];
+
+ // If the token is prefixed with a # we push a filterById operation to our stack
+ if (matchedChar === '#') {
+ operations.push({
+ method: filterById,
+ args: [Ext.String.trim(tokenMatch[2])]
+ });
+ }
+ // If the token is prefixed with a . we push a filterByClassName operation to our stack
+ // FIXME: Not enabled yet. just needs \. adding to the tokenRe prefix
+ else if (matchedChar === '.') {
+ operations.push({
+ method: filterByClassName,
+ args: [Ext.String.trim(tokenMatch[2])]
+ });
+ }
+ // If the token is a * or an xtype string, we push a filterByXType
+ // operation to the stack.
+ else {
+ operations.push({
+ method: filterByXType,
+ args: [Ext.String.trim(tokenMatch[2]), Boolean(tokenMatch[3])]
+ });
+ }
+
+ // Now we slice of the part we just converted into an operation
+ selector = selector.replace(tokenMatch[0], '');
+ }
+
+ // If the next part of the query is not a space or > or ^, it means we
+ // are going to check for more things that our current selection
+ // has to comply to.
+ while (!(modeMatch = selector.match(modeRe))) {
+ // Lets loop over each type of matcher and execute it
+ // on our current selector.
+ for (i = 0; selector && i < length; i++) {
+ matcher = matchers[i];
+ selectorMatch = selector.match(matcher.re);
+ method = matcher.method;
+
+ // If we have a match, add an operation with the method
+ // associated with this matcher, and pass the regular
+ // expression matches are arguments to the operation.
+ if (selectorMatch) {
+ operations.push({
+ method: Ext.isString(matcher.method)
+ // Turn a string method into a function by formatting the string with our selector matche expression
+ // A new method is created for different match expressions, eg {id=='textfield-1024'}
+ // Every expression may be different in different selectors.
+ ? Ext.functionFactory('items', Ext.String.format.apply(Ext.String, [method].concat(selectorMatch.slice(1))))
+ : matcher.method,
+ args: selectorMatch.slice(1)
+ });
+ selector = selector.replace(selectorMatch[0], '');
+ break; // Break on match
+ }
+ }
+ }
+
+ // Now we are going to check for a mode change. This means a space
+ // or a > to determine if we are going to select all the children
+ // of the currently matched items, or a ^ if we are going to use the
+ // ownerCt axis as the candidate source.
+ if (modeMatch[1]) { // Assignment, and test for truthiness!
+ operations.push({
+ mode: modeMatch[2]||modeMatch[1]
+ });
+ selector = selector.replace(modeMatch[0], '');
+ }
+ }
+
+ // Now that we have all our operations in an array, we are going
+ // to create a new Query using these operations.
+ return new cq.Query({
+ operations: operations
+ });
+ }
+ });
+});
+/**
+ * @class Ext.util.HashMap
+ * <p>
+ * Represents a collection of a set of key and value pairs. Each key in the HashMap
+ * must be unique, the same key cannot exist twice. Access to items is provided via
+ * the key only. Sample usage:
+ * <pre><code>
+var map = new Ext.util.HashMap();
+map.add('key1', 1);
+map.add('key2', 2);
+map.add('key3', 3);
+
+map.each(function(key, value, length){
+ console.log(key, value, length);
+});
+ * </code></pre>
+ * </p>
+ *
+ * <p>The HashMap is an unordered class,
+ * there is no guarantee when iterating over the items that they will be in any particular
+ * order. If this is required, then use a {@link Ext.util.MixedCollection}.
+ * </p>
+ */
+Ext.define('Ext.util.HashMap', {
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ /**
+ * @cfg {Function} keyFn A function that is used to retrieve a default key for a passed object.
+ * A default is provided that returns the <b>id</b> property on the object. This function is only used
+ * if the add method is called with a single argument.
+ */
+
+ /**
+ * Creates new HashMap.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ config = config || {};
+
+ var me = this,
+ keyFn = config.keyFn;
+
+ me.addEvents(
+ /**
+ * @event add
+ * Fires when a new item is added to the hash
+ * @param {Ext.util.HashMap} this.
+ * @param {String} key The key of the added item.
+ * @param {Object} value The value of the added item.
+ */
+ 'add',
+ /**
+ * @event clear
+ * Fires when the hash is cleared.
+ * @param {Ext.util.HashMap} this.
+ */
+ 'clear',
+ /**
+ * @event remove
+ * Fires when an item is removed from the hash.
+ * @param {Ext.util.HashMap} this.
+ * @param {String} key The key of the removed item.
+ * @param {Object} value The value of the removed item.
+ */
+ 'remove',
+ /**
+ * @event replace
+ * Fires when an item is replaced in the hash.
+ * @param {Ext.util.HashMap} this.
+ * @param {String} key The key of the replaced item.
+ * @param {Object} value The new value for the item.
+ * @param {Object} old The old value for the item.
+ */
+ 'replace'
+ );
+
+ me.mixins.observable.constructor.call(me, config);
+ me.clear(true);
+
+ if (keyFn) {
+ me.getKey = keyFn;
+ }
+ },
+
+ /**
+ * Gets the number of items in the hash.
+ * @return {Number} The number of items in the hash.
+ */
+ getCount: function() {
+ return this.length;
+ },
+
+ /**
+ * Implementation for being able to extract the key from an object if only
+ * a single argument is passed.
+ * @private
+ * @param {String} key The key
+ * @param {Object} value The value
+ * @return {Array} [key, value]
+ */
+ getData: function(key, value) {
+ // if we have no value, it means we need to get the key from the object
+ if (value === undefined) {
+ value = key;
+ key = this.getKey(value);
+ }
+
+ return [key, value];
+ },
+
+ /**
+ * Extracts the key from an object. This is a default implementation, it may be overridden
+ * @param {Object} o The object to get the key from
+ * @return {String} The key to use.
+ */
+ getKey: function(o) {
+ return o.id;
+ },
+
+ /**
+ * Adds an item to the collection. Fires the {@link #add} event when complete.
+ * @param {String} key <p>The key to associate with the item, or the new item.</p>
+ * <p>If a {@link #getKey} implementation was specified for this HashMap,
+ * or if the key of the stored items is in a property called <tt><b>id</b></tt>,
+ * the HashMap will be able to <i>derive</i> the key for the new item.
+ * In this case just pass the new item in this parameter.</p>
+ * @param {Object} o The item to add.
+ * @return {Object} The item added.
+ */
+ add: function(key, value) {
+ var me = this,
+ data;
+
+ if (arguments.length === 1) {
+ value = key;
+ key = me.getKey(value);
+ }
+
+ if (me.containsKey(key)) {
+ return me.replace(key, value);
+ }
+
+ data = me.getData(key, value);
+ key = data[0];
+ value = data[1];
+ me.map[key] = value;
+ ++me.length;
+ me.fireEvent('add', me, key, value);
+ return value;
+ },
+
+ /**
+ * Replaces an item in the hash. If the key doesn't exist, the
+ * {@link #add} method will be used.
+ * @param {String} key The key of the item.
+ * @param {Object} value The new value for the item.
+ * @return {Object} The new value of the item.
+ */
+ replace: function(key, value) {
+ var me = this,
+ map = me.map,
+ old;
+
+ if (!me.containsKey(key)) {
+ me.add(key, value);
+ }
+ old = map[key];
+ map[key] = value;
+ me.fireEvent('replace', me, key, value, old);
+ return value;
+ },
+
+ /**
+ * Remove an item from the hash.
+ * @param {Object} o The value of the item to remove.
+ * @return {Boolean} True if the item was successfully removed.
+ */
+ remove: function(o) {
+ var key = this.findKey(o);
+ if (key !== undefined) {
+ return this.removeAtKey(key);
+ }
+ return false;
+ },
+
+ /**
+ * Remove an item from the hash.
+ * @param {String} key The key to remove.
+ * @return {Boolean} True if the item was successfully removed.
+ */
+ removeAtKey: function(key) {
+ var me = this,
+ value;
+
+ if (me.containsKey(key)) {
+ value = me.map[key];
+ delete me.map[key];
+ --me.length;
+ me.fireEvent('remove', me, key, value);
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Retrieves an item with a particular key.
+ * @param {String} key The key to lookup.
+ * @return {Object} The value at that key. If it doesn't exist, <tt>undefined</tt> is returned.
+ */
+ get: function(key) {
+ return this.map[key];
+ },
+
+ /**
+ * Removes all items from the hash.
+ * @return {Ext.util.HashMap} this
+ */
+ clear: function(/* private */ initial) {
+ var me = this;
+ me.map = {};
+ me.length = 0;
+ if (initial !== true) {
+ me.fireEvent('clear', me);
+ }
+ return me;
+ },
+
+ /**
+ * Checks whether a key exists in the hash.
+ * @param {String} key The key to check for.
+ * @return {Boolean} True if they key exists in the hash.
+ */
+ containsKey: function(key) {
+ return this.map[key] !== undefined;
+ },
+
+ /**
+ * Checks whether a value exists in the hash.
+ * @param {Object} value The value to check for.
+ * @return {Boolean} True if the value exists in the dictionary.
+ */
+ contains: function(value) {
+ return this.containsKey(this.findKey(value));
+ },
+
+ /**
+ * Return all of the keys in the hash.
+ * @return {Array} An array of keys.
+ */
+ getKeys: function() {
+ return this.getArray(true);
+ },
+
+ /**
+ * Return all of the values in the hash.
+ * @return {Array} An array of values.
+ */
+ getValues: function() {
+ return this.getArray(false);
+ },
+
+ /**
+ * Gets either the keys/values in an array from the hash.
+ * @private
+ * @param {Boolean} isKey True to extract the keys, otherwise, the value
+ * @return {Array} An array of either keys/values from the hash.
+ */
+ getArray: function(isKey) {
+ var arr = [],
+ key,
+ map = this.map;
+ for (key in map) {
+ if (map.hasOwnProperty(key)) {
+ arr.push(isKey ? key: map[key]);
+ }
+ }
+ return arr;
+ },
+
+ /**
+ * Executes the specified function once for each item in the hash.
+ * Returning false from the function will cease iteration.
+ *
+ * The paramaters passed to the function are:
+ * <div class="mdetail-params"><ul>
+ * <li><b>key</b> : String<p class="sub-desc">The key of the item</p></li>
+ * <li><b>value</b> : Number<p class="sub-desc">The value of the item</p></li>
+ * <li><b>length</b> : Number<p class="sub-desc">The total number of items in the hash</p></li>
+ * </ul></div>
+ * @param {Function} fn The function to execute.
+ * @param {Object} scope The scope to execute in. Defaults to <tt>this</tt>.
+ * @return {Ext.util.HashMap} this
+ */
+ each: function(fn, scope) {
+ // copy items so they may be removed during iteration.
+ var items = Ext.apply({}, this.map),
+ key,
+ length = this.length;
+
+ scope = scope || this;
+ for (key in items) {
+ if (items.hasOwnProperty(key)) {
+ if (fn.call(scope, key, items[key], length) === false) {
+ break;
+ }
+ }
+ }
+ return this;
+ },
+
+ /**
+ * Performs a shallow copy on this hash.
+ * @return {Ext.util.HashMap} The new hash object.
+ */
+ clone: function() {
+ var hash = new this.self(),
+ map = this.map,
+ key;
+
+ hash.suspendEvents();
+ for (key in map) {
+ if (map.hasOwnProperty(key)) {
+ hash.add(key, map[key]);
+ }
+ }
+ hash.resumeEvents();
+ return hash;
+ },
+
+ /**
+ * @private
+ * Find the key for a value.
+ * @param {Object} value The value to find.
+ * @return {Object} The value of the item. Returns <tt>undefined</tt> if not found.
+ */
+ findKey: function(value) {
+ var key,
+ map = this.map;
+
+ for (key in map) {
+ if (map.hasOwnProperty(key) && map[key] === value) {
+ return key;
+ }
+ }
+ return undefined;
+ }
+});
+
+/**
+ * @class Ext.state.Manager
+ * This is the global state manager. By default all components that are "state aware" check this class
+ * for state information if you don't pass them a custom state provider. In order for this class
+ * to be useful, it must be initialized with a provider when your application initializes. Example usage:
+ <pre><code>
+// in your initialization function
+init : function(){
+ Ext.state.Manager.setProvider(new Ext.state.CookieProvider());
+ var win = new Window(...);
+ win.restoreState();
+}
+ </code></pre>
+ * This class passes on calls from components to the underlying {@link Ext.state.Provider} so that
+ * there is a common interface that can be used without needing to refer to a specific provider instance
+ * in every component.
+ * @singleton
+ * @docauthor Evan Trimboli <evan@sencha.com>
+ */
+Ext.define('Ext.state.Manager', {
+ singleton: true,
+ requires: ['Ext.state.Provider'],
+ constructor: function() {
+ this.provider = Ext.create('Ext.state.Provider');
+ },
+
+
+ /**
+ * Configures the default state provider for your application
+ * @param {Ext.state.Provider} stateProvider The state provider to set
+ */
+ setProvider : function(stateProvider){
+ this.provider = stateProvider;
+ },
+
+ /**
+ * Returns the current value for a key
+ * @param {String} name The key name
+ * @param {Object} defaultValue The default value to return if the key lookup does not match
+ * @return {Object} The state data
+ */
+ get : function(key, defaultValue){
+ return this.provider.get(key, defaultValue);
+ },
+
+ /**
+ * Sets the value for a key
+ * @param {String} name The key name
+ * @param {Object} value The state data
+ */
+ set : function(key, value){
+ this.provider.set(key, value);
+ },
+
+ /**
+ * Clears a value from the state
+ * @param {String} name The key name
+ */
+ clear : function(key){
+ this.provider.clear(key);
+ },
+
+ /**
+ * Gets the currently configured state provider
+ * @return {Ext.state.Provider} The state provider
+ */
+ getProvider : function(){
+ return this.provider;
+ }
+});
+/**
+ * @class Ext.state.Stateful
+ * A mixin for being able to save the state of an object to an underlying
+ * {@link Ext.state.Provider}.
+ */
+Ext.define('Ext.state.Stateful', {
+
+ /* Begin Definitions */
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ requires: ['Ext.state.Manager'],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Boolean} stateful
+ * <p>A flag which causes the object to attempt to restore the state of
+ * internal properties from a saved state on startup. The object must have
+ * a <code>{@link #stateId}</code> for state to be managed.
+ * Auto-generated ids are not guaranteed to be stable across page loads and
+ * cannot be relied upon to save and restore the same state for a object.<p>
+ * <p>For state saving to work, the state manager's provider must have been
+ * set to an implementation of {@link Ext.state.Provider} which overrides the
+ * {@link Ext.state.Provider#set set} and {@link Ext.state.Provider#get get}
+ * methods to save and recall name/value pairs. A built-in implementation,
+ * {@link Ext.state.CookieProvider} is available.</p>
+ * <p>To set the state provider for the current page:</p>
+ * <pre><code>
+Ext.state.Manager.setProvider(new Ext.state.CookieProvider({
+ expires: new Date(new Date().getTime()+(1000*60*60*24*7)), //7 days from now
+}));
+ * </code></pre>
+ * <p>A stateful object attempts to save state when one of the events
+ * listed in the <code>{@link #stateEvents}</code> configuration fires.</p>
+ * <p>To save state, a stateful object first serializes its state by
+ * calling <b><code>{@link #getState}</code></b>. By default, this function does
+ * nothing. The developer must provide an implementation which returns an
+ * object hash which represents the restorable state of the object.</p>
+ * <p>The value yielded by getState is passed to {@link Ext.state.Manager#set}
+ * which uses the configured {@link Ext.state.Provider} to save the object
+ * keyed by the <code>{@link #stateId}</code>.</p>
+ * <p>During construction, a stateful object attempts to <i>restore</i>
+ * its state by calling {@link Ext.state.Manager#get} passing the
+ * <code>{@link #stateId}</code></p>
+ * <p>The resulting object is passed to <b><code>{@link #applyState}</code></b>.
+ * The default implementation of <code>{@link #applyState}</code> simply copies
+ * properties into the object, but a developer may override this to support
+ * more behaviour.</p>
+ * <p>You can perform extra processing on state save and restore by attaching
+ * handlers to the {@link #beforestaterestore}, {@link #staterestore},
+ * {@link #beforestatesave} and {@link #statesave} events.</p>
+ */
+ stateful: true,
+
+ /**
+ * @cfg {String} stateId
+ * The unique id for this object to use for state management purposes.
+ * <p>See {@link #stateful} for an explanation of saving and restoring state.</p>
+ */
+
+ /**
+ * @cfg {String[]} stateEvents
+ * <p>An array of events that, when fired, should trigger this object to
+ * save its state. Defaults to none. <code>stateEvents</code> may be any type
+ * of event supported by this object, including browser or custom events
+ * (e.g., <tt>['click', 'customerchange']</tt>).</p>
+ * <p>See <code>{@link #stateful}</code> for an explanation of saving and
+ * restoring object state.</p>
+ */
+
+ /**
+ * @cfg {Number} saveDelay
+ * A buffer to be applied if many state events are fired within a short period.
+ */
+ saveDelay: 100,
+
+ autoGenIdRe: /^((\w+-)|(ext-comp-))\d{4,}$/i,
+
+ constructor: function(config) {
+ var me = this;
+
+ config = config || {};
+ if (Ext.isDefined(config.stateful)) {
+ me.stateful = config.stateful;
+ }
+ if (Ext.isDefined(config.saveDelay)) {
+ me.saveDelay = config.saveDelay;
+ }
+ me.stateId = me.stateId || config.stateId;
+
+ if (!me.stateEvents) {
+ me.stateEvents = [];
+ }
+ if (config.stateEvents) {
+ me.stateEvents.concat(config.stateEvents);
+ }
+ this.addEvents(
+ /**
+ * @event beforestaterestore
+ * Fires before the state of the object is restored. Return false from an event handler to stop the restore.
+ * @param {Ext.state.Stateful} this
+ * @param {Object} state The hash of state values returned from the StateProvider. If this
+ * event is not vetoed, then the state object is passed to <b><tt>applyState</tt></b>. By default,
+ * that simply copies property values into this object. The method maybe overriden to
+ * provide custom state restoration.
+ */
+ 'beforestaterestore',
+
+ /**
+ * @event staterestore
+ * Fires after the state of the object is restored.
+ * @param {Ext.state.Stateful} this
+ * @param {Object} state The hash of state values returned from the StateProvider. This is passed
+ * to <b><tt>applyState</tt></b>. By default, that simply copies property values into this
+ * object. The method maybe overriden to provide custom state restoration.
+ */
+ 'staterestore',
+
+ /**
+ * @event beforestatesave
+ * Fires before the state of the object is saved to the configured state provider. Return false to stop the save.
+ * @param {Ext.state.Stateful} this
+ * @param {Object} state The hash of state values. This is determined by calling
+ * <b><tt>getState()</tt></b> on the object. This method must be provided by the
+ * developer to return whetever representation of state is required, by default, Ext.state.Stateful
+ * has a null implementation.
+ */
+ 'beforestatesave',
+
+ /**
+ * @event statesave
+ * Fires after the state of the object is saved to the configured state provider.
+ * @param {Ext.state.Stateful} this
+ * @param {Object} state The hash of state values. This is determined by calling
+ * <b><tt>getState()</tt></b> on the object. This method must be provided by the
+ * developer to return whetever representation of state is required, by default, Ext.state.Stateful
+ * has a null implementation.
+ */
+ 'statesave'
+ );
+ me.mixins.observable.constructor.call(me);
+ if (me.stateful !== false) {
+ me.initStateEvents();
+ me.initState();
+ }
+ },
+
+ /**
+ * Initializes any state events for this object.
+ * @private
+ */
+ initStateEvents: function() {
+ this.addStateEvents(this.stateEvents);
+ },
+
+ /**
+ * Add events that will trigger the state to be saved.
+ * @param {String/String[]} events The event name or an array of event names.
+ */
+ addStateEvents: function(events){
+ if (!Ext.isArray(events)) {
+ events = [events];
+ }
+
+ var me = this,
+ i = 0,
+ len = events.length;
+
+ for (; i < len; ++i) {
+ me.on(events[i], me.onStateChange, me);
+ }
+ },
+
+ /**
+ * This method is called when any of the {@link #stateEvents} are fired.
+ * @private
+ */
+ onStateChange: function(){
+ var me = this,
+ delay = me.saveDelay;
+
+ if (delay > 0) {
+ if (!me.stateTask) {
+ me.stateTask = Ext.create('Ext.util.DelayedTask', me.saveState, me);
+ }
+ me.stateTask.delay(me.saveDelay);
+ } else {
+ me.saveState();
+ }
+ },
+
+ /**
+ * Saves the state of the object to the persistence store.
+ * @private
+ */
+ saveState: function() {
+ var me = this,
+ id,
+ state;
+
+ if (me.stateful !== false) {
+ id = me.getStateId();
+ if (id) {
+ state = me.getState();
+ if (me.fireEvent('beforestatesave', me, state) !== false) {
+ Ext.state.Manager.set(id, state);
+ me.fireEvent('statesave', me, state);
+ }
+ }
+ }
+ },
+
+ /**
+ * Gets the current state of the object. By default this function returns null,
+ * it should be overridden in subclasses to implement methods for getting the state.
+ * @return {Object} The current state
+ */
+ getState: function(){
+ return null;
+ },
+
+ /**
+ * Applies the state to the object. This should be overridden in subclasses to do
+ * more complex state operations. By default it applies the state properties onto
+ * the current object.
+ * @param {Object} state The state
+ */
+ applyState: function(state) {
+ if (state) {
+ Ext.apply(this, state);
+ }
+ },
+
+ /**
+ * Gets the state id for this object.
+ * @return {String} The state id, null if not found.
+ */
+ getStateId: function() {
+ var me = this,
+ id = me.stateId;
+
+ if (!id) {
+ id = me.autoGenIdRe.test(String(me.id)) ? null : me.id;
+ }
+ return id;
+ },
+
+ /**
+ * Initializes the state of the object upon construction.
+ * @private
+ */
+ initState: function(){
+ var me = this,
+ id = me.getStateId(),
+ state;
+
+ if (me.stateful !== false) {
+ if (id) {
+ state = Ext.state.Manager.get(id);
+ if (state) {
+ state = Ext.apply({}, state);
+ if (me.fireEvent('beforestaterestore', me, state) !== false) {
+ me.applyState(state);
+ me.fireEvent('staterestore', me, state);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Conditionally saves a single property from this object to the given state object.
+ * The idea is to only save state which has changed from the initial state so that
+ * current software settings do not override future software settings. Only those
+ * values that are user-changed state should be saved.
+ *
+ * @param {String} propName The name of the property to save.
+ * @param {Object} state The state object in to which to save the property.
+ * @param {String} stateName (optional) The name to use for the property in state.
+ * @return {Boolean} True if the property was saved, false if not.
+ */
+ savePropToState: function (propName, state, stateName) {
+ var me = this,
+ value = me[propName],
+ config = me.initialConfig;
+
+ if (me.hasOwnProperty(propName)) {
+ if (!config || config[propName] !== value) {
+ if (state) {
+ state[stateName || propName] = value;
+ }
+ return true;
+ }
+ }
+ return false;
+ },
+
+ savePropsToState: function (propNames, state) {
+ var me = this;
+ Ext.each(propNames, function (propName) {
+ me.savePropToState(propName, state);
+ });
+ return state;
+ },
+
+ /**
+ * Destroys this stateful object.
+ */
+ destroy: function(){
+ var task = this.stateTask;
+ if (task) {
+ task.cancel();
+ }
+ this.clearListeners();
+
+ }
+
+});
+
+/**
+ * Base Manager class
+ */
+Ext.define('Ext.AbstractManager', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.util.HashMap'],
+
+ /* End Definitions */
+
+ typeName: 'type',
+
+ constructor: function(config) {
+ Ext.apply(this, config || {});
+
+ /**
+ * @property {Ext.util.HashMap} all
+ * Contains all of the items currently managed
+ */
+ this.all = Ext.create('Ext.util.HashMap');
+
+ this.types = {};
+ },
+
+ /**
+ * Returns an item by id.
+ * For additional details see {@link Ext.util.HashMap#get}.
+ * @param {String} id The id of the item
+ * @return {Object} The item, undefined if not found.
+ */
+ get : function(id) {
+ return this.all.get(id);
+ },
+
+ /**
+ * Registers an item to be managed
+ * @param {Object} item The item to register
+ */
+ register: function(item) {
+ this.all.add(item);
+ },
+
+ /**
+ * Unregisters an item by removing it from this manager
+ * @param {Object} item The item to unregister
+ */
+ unregister: function(item) {
+ this.all.remove(item);
+ },
+
+ /**
+ * Registers a new item constructor, keyed by a type key.
+ * @param {String} type The mnemonic string by which the class may be looked up.
+ * @param {Function} cls The new instance class.
+ */
+ registerType : function(type, cls) {
+ this.types[type] = cls;
+ cls[this.typeName] = type;
+ },
+
+ /**
+ * Checks if an item type is registered.
+ * @param {String} type The mnemonic string by which the class may be looked up
+ * @return {Boolean} Whether the type is registered.
+ */
+ isRegistered : function(type){
+ return this.types[type] !== undefined;
+ },
+
+ /**
+ * Creates and returns an instance of whatever this manager manages, based on the supplied type and
+ * config object.
+ * @param {Object} config The config object
+ * @param {String} defaultType If no type is discovered in the config object, we fall back to this type
+ * @return {Object} The instance of whatever this manager is managing
+ */
+ create: function(config, defaultType) {
+ var type = config[this.typeName] || config.type || defaultType,
+ Constructor = this.types[type];
+
+
+ return new Constructor(config);
+ },
+
+ /**
+ * Registers a function that will be called when an item with the specified id is added to the manager.
+ * This will happen on instantiation.
+ * @param {String} id The item id
+ * @param {Function} fn The callback function. Called with a single parameter, the item.
+ * @param {Object} scope The scope (this reference) in which the callback is executed.
+ * Defaults to the item.
+ */
+ onAvailable : function(id, fn, scope){
+ var all = this.all,
+ item;
+
+ if (all.containsKey(id)) {
+ item = all.get(id);
+ fn.call(scope || item, item);
+ } else {
+ all.on('add', function(map, key, item){
+ if (key == id) {
+ fn.call(scope || item, item);
+ all.un('add', fn, scope);
+ }
+ });
+ }
+ },
+
+ /**
+ * Executes the specified function once for each item in the collection.
+ * @param {Function} fn The function to execute.
+ * @param {String} fn.key The key of the item
+ * @param {Number} fn.value The value of the item
+ * @param {Number} fn.length The total number of items in the collection
+ * @param {Boolean} fn.return False to cease iteration.
+ * @param {Object} scope The scope to execute in. Defaults to `this`.
+ */
+ each: function(fn, scope){
+ this.all.each(fn, scope || this);
+ },
+
+ /**
+ * Gets the number of items in the collection.
+ * @return {Number} The number of items in the collection.
+ */
+ getCount: function(){
+ return this.all.getCount();
+ }
+});
+
+/**
+ * @class Ext.ComponentManager
+ * @extends Ext.AbstractManager
+ * <p>Provides a registry of all Components (instances of {@link Ext.Component} or any subclass
+ * thereof) on a page so that they can be easily accessed by {@link Ext.Component component}
+ * {@link Ext.Component#id id} (see {@link #get}, or the convenience method {@link Ext#getCmp Ext.getCmp}).</p>
+ * <p>This object also provides a registry of available Component <i>classes</i>
+ * indexed by a mnemonic code known as the Component's {@link Ext.Component#xtype xtype}.
+ * The <code>xtype</code> provides a way to avoid instantiating child Components
+ * when creating a full, nested config object for a complete Ext page.</p>
+ * <p>A child Component may be specified simply as a <i>config object</i>
+ * as long as the correct <code>{@link Ext.Component#xtype xtype}</code> is specified so that if and when the Component
+ * needs rendering, the correct type can be looked up for lazy instantiation.</p>
+ * <p>For a list of all available <code>{@link Ext.Component#xtype xtypes}</code>, see {@link Ext.Component}.</p>
+ * @singleton
+ */
+Ext.define('Ext.ComponentManager', {
+ extend: 'Ext.AbstractManager',
+ alternateClassName: 'Ext.ComponentMgr',
+
+ singleton: true,
+
+ typeName: 'xtype',
+
+ /**
+ * Creates a new Component from the specified config object using the
+ * config object's xtype to determine the class to instantiate.
+ * @param {Object} config A configuration object for the Component you wish to create.
+ * @param {Function} defaultType (optional) The constructor to provide the default Component type if
+ * the config object does not contain a <code>xtype</code>. (Optional if the config contains a <code>xtype</code>).
+ * @return {Ext.Component} The newly instantiated Component.
+ */
+ create: function(component, defaultType){
+ if (component instanceof Ext.AbstractComponent) {
+ return component;
+ }
+ else if (Ext.isString(component)) {
+ return Ext.createByAlias('widget.' + component);
+ }
+ else {
+ var type = component.xtype || defaultType,
+ config = component;
+
+ return Ext.createByAlias('widget.' + type, config);
+ }
+ },
+
+ registerType: function(type, cls) {
+ this.types[type] = cls;
+ cls[this.typeName] = type;
+ cls.prototype[this.typeName] = type;
+ }
+});
+/**
+ * An abstract base class which provides shared methods for Components across the Sencha product line.
+ *
+ * Please refer to sub class's documentation
+ * @private
+ */
+Ext.define('Ext.AbstractComponent', {
+
+ /* Begin Definitions */
+ requires: [
+ 'Ext.ComponentQuery',
+ 'Ext.ComponentManager'
+ ],
+
+ mixins: {
+ observable: 'Ext.util.Observable',
+ animate: 'Ext.util.Animate',
+ state: 'Ext.state.Stateful'
+ },
+
+ // The "uses" property specifies class which are used in an instantiated AbstractComponent.
+ // They do *not* have to be loaded before this class may be defined - that is what "requires" is for.
+ uses: [
+ 'Ext.PluginManager',
+ 'Ext.ComponentManager',
+ 'Ext.Element',
+ 'Ext.DomHelper',
+ 'Ext.XTemplate',
+ 'Ext.ComponentQuery',
+ 'Ext.ComponentLoader',
+ 'Ext.EventManager',
+ 'Ext.layout.Layout',
+ 'Ext.layout.component.Auto',
+ 'Ext.LoadMask',
+ 'Ext.ZIndexManager'
+ ],
+
+ statics: {
+ AUTO_ID: 1000
+ },
+
+ /* End Definitions */
+
+ isComponent: true,
+
+ getAutoId: function() {
+ return ++Ext.AbstractComponent.AUTO_ID;
+ },
+
+
+ /**
+ * @cfg {String} id
+ * The **unique id of this component instance.**
+ *
+ * It should not be necessary to use this configuration except for singleton objects in your application. Components
+ * created with an id may be accessed globally using {@link Ext#getCmp Ext.getCmp}.
+ *
+ * Instead of using assigned ids, use the {@link #itemId} config, and {@link Ext.ComponentQuery ComponentQuery}
+ * which provides selector-based searching for Sencha Components analogous to DOM querying. The {@link
+ * Ext.container.Container Container} class contains {@link Ext.container.Container#down shortcut methods} to query
+ * its descendant Components by selector.
+ *
+ * Note that this id will also be used as the element id for the containing HTML element that is rendered to the
+ * page for this component. This allows you to write id-based CSS rules to style the specific instance of this
+ * component uniquely, and also to select sub-elements using this component's id as the parent.
+ *
+ * **Note**: to avoid complications imposed by a unique id also see `{@link #itemId}`.
+ *
+ * **Note**: to access the container of a Component see `{@link #ownerCt}`.
+ *
+ * Defaults to an {@link #getId auto-assigned id}.
+ */
+
+ /**
+ * @cfg {String} itemId
+ * An itemId can be used as an alternative way to get a reference to a component when no object reference is
+ * available. Instead of using an `{@link #id}` with {@link Ext}.{@link Ext#getCmp getCmp}, use `itemId` with
+ * {@link Ext.container.Container}.{@link Ext.container.Container#getComponent getComponent} which will retrieve
+ * `itemId`'s or {@link #id}'s. Since `itemId`'s are an index to the container's internal MixedCollection, the
+ * `itemId` is scoped locally to the container -- avoiding potential conflicts with {@link Ext.ComponentManager}
+ * which requires a **unique** `{@link #id}`.
+ *
+ * var c = new Ext.panel.Panel({ //
+ * {@link Ext.Component#height height}: 300,
+ * {@link #renderTo}: document.body,
+ * {@link Ext.container.Container#layout layout}: 'auto',
+ * {@link Ext.container.Container#items items}: [
+ * {
+ * itemId: 'p1',
+ * {@link Ext.panel.Panel#title title}: 'Panel 1',
+ * {@link Ext.Component#height height}: 150
+ * },
+ * {
+ * itemId: 'p2',
+ * {@link Ext.panel.Panel#title title}: 'Panel 2',
+ * {@link Ext.Component#height height}: 150
+ * }
+ * ]
+ * })
+ * p1 = c.{@link Ext.container.Container#getComponent getComponent}('p1'); // not the same as {@link Ext#getCmp Ext.getCmp()}
+ * p2 = p1.{@link #ownerCt}.{@link Ext.container.Container#getComponent getComponent}('p2'); // reference via a sibling
+ *
+ * Also see {@link #id}, `{@link Ext.container.Container#query}`, `{@link Ext.container.Container#down}` and
+ * `{@link Ext.container.Container#child}`.
+ *
+ * **Note**: to access the container of an item see {@link #ownerCt}.
+ */
+
+ /**
+ * @property {Ext.Container} ownerCt
+ * This Component's owner {@link Ext.container.Container Container} (is set automatically
+ * when this Component is added to a Container). Read-only.
+ *
+ * **Note**: to access items within the Container see {@link #itemId}.
+ */
+
+ /**
+ * @property {Boolean} layoutManagedWidth
+ * @private
+ * Flag set by the container layout to which this Component is added.
+ * If the layout manages this Component's width, it sets the value to 1.
+ * If it does NOT manage the width, it sets it to 2.
+ * If the layout MAY affect the width, but only if the owning Container has a fixed width, this is set to 0.
+ */
+
+ /**
+ * @property {Boolean} layoutManagedHeight
+ * @private
+ * Flag set by the container layout to which this Component is added.
+ * If the layout manages this Component's height, it sets the value to 1.
+ * If it does NOT manage the height, it sets it to 2.
+ * If the layout MAY affect the height, but only if the owning Container has a fixed height, this is set to 0.
+ */
+
+ /**
+ * @cfg {String/Object} autoEl
+ * A tag name or {@link Ext.DomHelper DomHelper} spec used to create the {@link #getEl Element} which will
+ * encapsulate this Component.
+ *
+ * You do not normally need to specify this. For the base classes {@link Ext.Component} and
+ * {@link Ext.container.Container}, this defaults to **'div'**. The more complex Sencha classes use a more
+ * complex DOM structure specified by their own {@link #renderTpl}s.
+ *
+ * This is intended to allow the developer to create application-specific utility Components encapsulated by
+ * different DOM elements. Example usage:
+ *
+ * {
+ * xtype: 'component',
+ * autoEl: {
+ * tag: 'img',
+ * src: 'http://www.example.com/example.jpg'
+ * }
+ * }, {
+ * xtype: 'component',
+ * autoEl: {
+ * tag: 'blockquote',
+ * html: 'autoEl is cool!'
+ * }
+ * }, {
+ * xtype: 'container',
+ * autoEl: 'ul',
+ * cls: 'ux-unordered-list',
+ * items: {
+ * xtype: 'component',
+ * autoEl: 'li',
+ * html: 'First list item'
+ * }
+ * }
+ */
+
+ /**
+ * @cfg {Ext.XTemplate/String/String[]} renderTpl
+ * An {@link Ext.XTemplate XTemplate} used to create the internal structure inside this Component's encapsulating
+ * {@link #getEl Element}.
+ *
+ * You do not normally need to specify this. For the base classes {@link Ext.Component} and
+ * {@link Ext.container.Container}, this defaults to **`null`** which means that they will be initially rendered
+ * with no internal structure; they render their {@link #getEl Element} empty. The more specialized ExtJS and Touch
+ * classes which use a more complex DOM structure, provide their own template definitions.
+ *
+ * This is intended to allow the developer to create application-specific utility Components with customized
+ * internal structure.
+ *
+ * Upon rendering, any created child elements may be automatically imported into object properties using the
+ * {@link #renderSelectors} and {@link #childEls} options.
+ */
+ renderTpl: null,
+
+ /**
+ * @cfg {Object} renderData
+ *
+ * The data used by {@link #renderTpl} in addition to the following property values of the component:
+ *
+ * - id
+ * - ui
+ * - uiCls
+ * - baseCls
+ * - componentCls
+ * - frame
+ *
+ * See {@link #renderSelectors} and {@link #childEls} for usage examples.
+ */
+
+ /**
+ * @cfg {Object} renderSelectors
+ * An object containing properties specifying {@link Ext.DomQuery DomQuery} selectors which identify child elements
+ * created by the render process.
+ *
+ * After the Component's internal structure is rendered according to the {@link #renderTpl}, this object is iterated through,
+ * and the found Elements are added as properties to the Component using the `renderSelector` property name.
+ *
+ * For example, a Component which renderes a title and description into its element:
+ *
+ * Ext.create('Ext.Component', {
+ * renderTo: Ext.getBody(),
+ * renderTpl: [
+ * '<h1 class="title">{title}</h1>',
+ * '<p>{desc}</p>'
+ * ],
+ * renderData: {
+ * title: "Error",
+ * desc: "Something went wrong"
+ * },
+ * renderSelectors: {
+ * titleEl: 'h1.title',
+ * descEl: 'p'
+ * },
+ * listeners: {
+ * afterrender: function(cmp){
+ * // After rendering the component will have a titleEl and descEl properties
+ * cmp.titleEl.setStyle({color: "red"});
+ * }
+ * }
+ * });
+ *
+ * For a faster, but less flexible, alternative that achieves the same end result (properties for child elements on the
+ * Component after render), see {@link #childEls} and {@link #addChildEls}.
+ */
+
+ /**
+ * @cfg {Object[]} childEls
+ * An array describing the child elements of the Component. Each member of the array
+ * is an object with these properties:
+ *
+ * - `name` - The property name on the Component for the child element.
+ * - `itemId` - The id to combine with the Component's id that is the id of the child element.
+ * - `id` - The id of the child element.
+ *
+ * If the array member is a string, it is equivalent to `{ name: m, itemId: m }`.
+ *
+ * For example, a Component which renders a title and body text:
+ *
+ * Ext.create('Ext.Component', {
+ * renderTo: Ext.getBody(),
+ * renderTpl: [
+ * '<h1 id="{id}-title">{title}</h1>',
+ * '<p>{msg}</p>',
+ * ],
+ * renderData: {
+ * title: "Error",
+ * msg: "Something went wrong"
+ * },
+ * childEls: ["title"],
+ * listeners: {
+ * afterrender: function(cmp){
+ * // After rendering the component will have a title property
+ * cmp.title.setStyle({color: "red"});
+ * }
+ * }
+ * });
+ *
+ * A more flexible, but somewhat slower, approach is {@link #renderSelectors}.
+ */
+
+ /**
+ * @cfg {String/HTMLElement/Ext.Element} renderTo
+ * Specify the id of the element, a DOM element or an existing Element that this component will be rendered into.
+ *
+ * **Notes:**
+ *
+ * Do *not* use this option if the Component is to be a child item of a {@link Ext.container.Container Container}.
+ * It is the responsibility of the {@link Ext.container.Container Container}'s
+ * {@link Ext.container.Container#layout layout manager} to render and manage its child items.
+ *
+ * When using this config, a call to render() is not required.
+ *
+ * See `{@link #render}` also.
+ */
+
+ /**
+ * @cfg {Boolean} frame
+ * Specify as `true` to have the Component inject framing elements within the Component at render time to provide a
+ * graphical rounded frame around the Component content.
+ *
+ * This is only necessary when running on outdated, or non standard-compliant browsers such as Microsoft's Internet
+ * Explorer prior to version 9 which do not support rounded corners natively.
+ *
+ * The extra space taken up by this framing is available from the read only property {@link #frameSize}.
+ */
+
+ /**
+ * @property {Object} frameSize
+ * Read-only property indicating the width of any framing elements which were added within the encapsulating element
+ * to provide graphical, rounded borders. See the {@link #frame} config.
+ *
+ * This is an object containing the frame width in pixels for all four sides of the Component containing the
+ * following properties:
+ *
+ * @property {Number} frameSize.top The width of the top framing element in pixels.
+ * @property {Number} frameSize.right The width of the right framing element in pixels.
+ * @property {Number} frameSize.bottom The width of the bottom framing element in pixels.
+ * @property {Number} frameSize.left The width of the left framing element in pixels.
+ */
+
+ /**
+ * @cfg {String/Object} componentLayout
+ * The sizing and positioning of a Component's internal Elements is the responsibility of the Component's layout
+ * manager which sizes a Component's internal structure in response to the Component being sized.
+ *
+ * Generally, developers will not use this configuration as all provided Components which need their internal
+ * elements sizing (Such as {@link Ext.form.field.Base input fields}) come with their own componentLayout managers.
+ *
+ * The {@link Ext.layout.container.Auto default layout manager} will be used on instances of the base Ext.Component
+ * class which simply sizes the Component's encapsulating element to the height and width specified in the
+ * {@link #setSize} method.
+ */
+
+ /**
+ * @cfg {Ext.XTemplate/Ext.Template/String/String[]} tpl
+ * An {@link Ext.Template}, {@link Ext.XTemplate} or an array of strings to form an Ext.XTemplate. Used in
+ * conjunction with the `{@link #data}` and `{@link #tplWriteMode}` configurations.
+ */
+
+ /**
+ * @cfg {Object} data
+ * The initial set of data to apply to the `{@link #tpl}` to update the content area of the Component.
+ */
+
+ /**
+ * @cfg {String} xtype
+ * The `xtype` configuration option can be used to optimize Component creation and rendering. It serves as a
+ * shortcut to the full componet name. For example, the component `Ext.button.Button` has an xtype of `button`.
+ *
+ * You can define your own xtype on a custom {@link Ext.Component component} by specifying the
+ * {@link Ext.Class#alias alias} config option with a prefix of `widget`. For example:
+ *
+ * Ext.define('PressMeButton', {
+ * extend: 'Ext.button.Button',
+ * alias: 'widget.pressmebutton',
+ * text: 'Press Me'
+ * })
+ *
+ * Any Component can be created implicitly as an object config with an xtype specified, allowing it to be
+ * declared and passed into the rendering pipeline without actually being instantiated as an object. Not only is
+ * rendering deferred, but the actual creation of the object itself is also deferred, saving memory and resources
+ * until they are actually needed. In complex, nested layouts containing many Components, this can make a
+ * noticeable improvement in performance.
+ *
+ * // Explicit creation of contained Components:
+ * var panel = new Ext.Panel({
+ * ...
+ * items: [
+ * Ext.create('Ext.button.Button', {
+ * text: 'OK'
+ * })
+ * ]
+ * };
+ *
+ * // Implicit creation using xtype:
+ * var panel = new Ext.Panel({
+ * ...
+ * items: [{
+ * xtype: 'button',
+ * text: 'OK'
+ * }]
+ * };
+ *
+ * In the first example, the button will always be created immediately during the panel's initialization. With
+ * many added Components, this approach could potentially slow the rendering of the page. In the second example,
+ * the button will not be created or rendered until the panel is actually displayed in the browser. If the panel
+ * is never displayed (for example, if it is a tab that remains hidden) then the button will never be created and
+ * will never consume any resources whatsoever.
+ */
+
+ /**
+ * @cfg {String} tplWriteMode
+ * The Ext.(X)Template method to use when updating the content area of the Component.
+ * See `{@link Ext.XTemplate#overwrite}` for information on default mode.
+ */
+ tplWriteMode: 'overwrite',
+
+ /**
+ * @cfg {String} [baseCls='x-component']
+ * The base CSS class to apply to this components's element. This will also be prepended to elements within this
+ * component like Panel's body will get a class x-panel-body. This means that if you create a subclass of Panel, and
+ * you want it to get all the Panels styling for the element and the body, you leave the baseCls x-panel and use
+ * componentCls to add specific styling for this component.
+ */
+ baseCls: Ext.baseCSSPrefix + 'component',
+
+ /**
+ * @cfg {String} componentCls
+ * CSS Class to be added to a components root level element to give distinction to it via styling.
+ */
+
+ /**
+ * @cfg {String} [cls='']
+ * An optional extra CSS class that will be added to this component's Element. This can be useful
+ * for adding customized styles to the component or any of its children using standard CSS rules.
+ */
+
+ /**
+ * @cfg {String} [overCls='']
+ * An optional extra CSS class that will be added to this component's Element when the mouse moves over the Element,
+ * and removed when the mouse moves out. This can be useful for adding customized 'active' or 'hover' styles to the
+ * component or any of its children using standard CSS rules.
+ */
+
+ /**
+ * @cfg {String} [disabledCls='x-item-disabled']
+ * CSS class to add when the Component is disabled. Defaults to 'x-item-disabled'.
+ */
+ disabledCls: Ext.baseCSSPrefix + 'item-disabled',
+
+ /**
+ * @cfg {String/String[]} ui
+ * A set style for a component. Can be a string or an Array of multiple strings (UIs)
+ */
+ ui: 'default',
+
+ /**
+ * @cfg {String[]} uiCls
+ * An array of of classNames which are currently applied to this component
+ * @private
+ */
+ uiCls: [],
+
+ /**
+ * @cfg {String} style
+ * A custom style specification to be applied to this component's Element. Should be a valid argument to
+ * {@link Ext.Element#applyStyles}.
+ *
+ * new Ext.panel.Panel({
+ * title: 'Some Title',
+ * renderTo: Ext.getBody(),
+ * width: 400, height: 300,
+ * layout: 'form',
+ * items: [{
+ * xtype: 'textarea',
+ * style: {
+ * width: '95%',
+ * marginBottom: '10px'
+ * }
+ * },
+ * new Ext.button.Button({
+ * text: 'Send',
+ * minWidth: '100',
+ * style: {
+ * marginBottom: '10px'
+ * }
+ * })
+ * ]
+ * });
+ */
+
+ /**
+ * @cfg {Number} width
+ * The width of this component in pixels.
+ */
+
+ /**
+ * @cfg {Number} height
+ * The height of this component in pixels.
+ */
+
+ /**
+ * @cfg {Number/String} border
+ * Specifies the border for this component. The border can be a single numeric value to apply to all sides or it can
+ * be a CSS style specification for each style, for example: '10 5 3 10'.
+ */
+
+ /**
+ * @cfg {Number/String} padding
+ * Specifies the padding for this component. The padding can be a single numeric value to apply to all sides or it
+ * can be a CSS style specification for each style, for example: '10 5 3 10'.
+ */
+
+ /**
+ * @cfg {Number/String} margin
+ * Specifies the margin for this component. The margin can be a single numeric value to apply to all sides or it can
+ * be a CSS style specification for each style, for example: '10 5 3 10'.
+ */
+
+ /**
+ * @cfg {Boolean} hidden
+ * True to hide the component.
+ */
+ hidden: false,
+
+ /**
+ * @cfg {Boolean} disabled
+ * True to disable the component.
+ */
+ disabled: false,
+
+ /**
+ * @cfg {Boolean} [draggable=false]
+ * Allows the component to be dragged.
+ */
+
+ /**
+ * @property {Boolean} draggable
+ * Read-only property indicating whether or not the component can be dragged
+ */
+ draggable: false,
+
+ /**
+ * @cfg {Boolean} floating
+ * Create the Component as a floating and use absolute positioning.
+ *
+ * The z-index of floating Components is handled by a ZIndexManager. If you simply render a floating Component into the DOM, it will be managed
+ * by the global {@link Ext.WindowManager WindowManager}.
+ *
+ * If you include a floating Component as a child item of a Container, then upon render, ExtJS will seek an ancestor floating Component to house a new
+ * ZIndexManager instance to manage its descendant floaters. If no floating ancestor can be found, the global WindowManager will be used.
+ *
+ * When a floating Component which has a ZindexManager managing descendant floaters is destroyed, those descendant floaters will also be destroyed.
+ */
+ floating: false,
+
+ /**
+ * @cfg {String} hideMode
+ * A String which specifies how this Component's encapsulating DOM element will be hidden. Values may be:
+ *
+ * - `'display'` : The Component will be hidden using the `display: none` style.
+ * - `'visibility'` : The Component will be hidden using the `visibility: hidden` style.
+ * - `'offsets'` : The Component will be hidden by absolutely positioning it out of the visible area of the document.
+ * This is useful when a hidden Component must maintain measurable dimensions. Hiding using `display` results in a
+ * Component having zero dimensions.
+ */
+ hideMode: 'display',
+
+ /**
+ * @cfg {String} contentEl
+ * Specify an existing HTML element, or the `id` of an existing HTML element to use as the content for this component.
+ *
+ * This config option is used to take an existing HTML element and place it in the layout element of a new component
+ * (it simply moves the specified DOM element _after the Component is rendered_ to use as the content.
+ *
+ * **Notes:**
+ *
+ * The specified HTML element is appended to the layout element of the component _after any configured
+ * {@link #html HTML} has been inserted_, and so the document will not contain this element at the time
+ * the {@link #render} event is fired.
+ *
+ * The specified HTML element used will not participate in any **`{@link Ext.container.Container#layout layout}`**
+ * scheme that the Component may use. It is just HTML. Layouts operate on child
+ * **`{@link Ext.container.Container#items items}`**.
+ *
+ * Add either the `x-hidden` or the `x-hide-display` CSS class to prevent a brief flicker of the content before it
+ * is rendered to the panel.
+ */
+
+ /**
+ * @cfg {String/Object} [html='']
+ * An HTML fragment, or a {@link Ext.DomHelper DomHelper} specification to use as the layout element content.
+ * The HTML content is added after the component is rendered, so the document will not contain this HTML at the time
+ * the {@link #render} event is fired. This content is inserted into the body _before_ any configured {@link #contentEl}
+ * is appended.
+ */
+
+ /**
+ * @cfg {Boolean} styleHtmlContent
+ * True to automatically style the html inside the content target of this component (body for panels).
+ */
+ styleHtmlContent: false,
+
+ /**
+ * @cfg {String} [styleHtmlCls='x-html']
+ * The class that is added to the content target when you set styleHtmlContent to true.
+ */
+ styleHtmlCls: Ext.baseCSSPrefix + 'html',
+
+ /**
+ * @cfg {Number} minHeight
+ * The minimum value in pixels which this Component will set its height to.
+ *
+ * **Warning:** This will override any size management applied by layout managers.
+ */
+ /**
+ * @cfg {Number} minWidth
+ * The minimum value in pixels which this Component will set its width to.
+ *
+ * **Warning:** This will override any size management applied by layout managers.
+ */
+ /**
+ * @cfg {Number} maxHeight
+ * The maximum value in pixels which this Component will set its height to.
+ *
+ * **Warning:** This will override any size management applied by layout managers.
+ */
+ /**
+ * @cfg {Number} maxWidth
+ * The maximum value in pixels which this Component will set its width to.
+ *
+ * **Warning:** This will override any size management applied by layout managers.
+ */
+
+ /**
+ * @cfg {Ext.ComponentLoader/Object} loader
+ * A configuration object or an instance of a {@link Ext.ComponentLoader} to load remote content for this Component.
+ */
+
+ /**
+ * @cfg {Boolean} autoShow
+ * True to automatically show the component upon creation. This config option may only be used for
+ * {@link #floating} components or components that use {@link #autoRender}. Defaults to false.
+ */
+ autoShow: false,
+
+ /**
+ * @cfg {Boolean/String/HTMLElement/Ext.Element} autoRender
+ * This config is intended mainly for non-{@link #floating} Components which may or may not be shown. Instead of using
+ * {@link #renderTo} in the configuration, and rendering upon construction, this allows a Component to render itself
+ * upon first _{@link #show}_. If {@link #floating} is true, the value of this config is omited as if it is `true`.
+ *
+ * Specify as `true` to have this Component render to the document body upon first show.
+ *
+ * Specify as an element, or the ID of an element to have this Component render to a specific element upon first
+ * show.
+ *
+ * **This defaults to `true` for the {@link Ext.window.Window Window} class.**
+ */
+ autoRender: false,
+
+ needsLayout: false,
+
+ // @private
+ allowDomMove: true,
+
+ /**
+ * @cfg {Object/Object[]} plugins
+ * An object or array of objects that will provide custom functionality for this component. The only requirement for
+ * a valid plugin is that it contain an init method that accepts a reference of type Ext.Component. When a component
+ * is created, if any plugins are available, the component will call the init method on each plugin, passing a
+ * reference to itself. Each plugin can then call methods or respond to events on the component as needed to provide
+ * its functionality.
+ */
+
+ /**
+ * @property {Boolean} rendered
+ * Read-only property indicating whether or not the component has been rendered.
+ */
+ rendered: false,
+
+ /**
+ * @property {Number} componentLayoutCounter
+ * @private
+ * The number of component layout calls made on this object.
+ */
+ componentLayoutCounter: 0,
+
+ weight: 0,
+
+ trimRe: /^\s+|\s+$/g,
+ spacesRe: /\s+/,
+
+
+ /**
+ * @property {Boolean} maskOnDisable
+ * This is an internal flag that you use when creating custom components. By default this is set to true which means
+ * that every component gets a mask when its disabled. Components like FieldContainer, FieldSet, Field, Button, Tab
+ * override this property to false since they want to implement custom disable logic.
+ */
+ maskOnDisable: true,
+
+ /**
+ * Creates new Component.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor : function(config) {
+ var me = this,
+ i, len;
+
+ config = config || {};
+ me.initialConfig = config;
+ Ext.apply(me, config);
+
+ me.addEvents(
+ /**
+ * @event beforeactivate
+ * Fires before a Component has been visually activated. Returning false from an event listener can prevent
+ * the activate from occurring.
+ * @param {Ext.Component} this
+ */
+ 'beforeactivate',
+ /**
+ * @event activate
+ * Fires after a Component has been visually activated.
+ * @param {Ext.Component} this
+ */
+ 'activate',
+ /**
+ * @event beforedeactivate
+ * Fires before a Component has been visually deactivated. Returning false from an event listener can
+ * prevent the deactivate from occurring.
+ * @param {Ext.Component} this
+ */
+ 'beforedeactivate',
+ /**
+ * @event deactivate
+ * Fires after a Component has been visually deactivated.
+ * @param {Ext.Component} this
+ */
+ 'deactivate',
+ /**
+ * @event added
+ * Fires after a Component had been added to a Container.
+ * @param {Ext.Component} this
+ * @param {Ext.container.Container} container Parent Container
+ * @param {Number} pos position of Component
+ */
+ 'added',
+ /**
+ * @event disable
+ * Fires after the component is disabled.
+ * @param {Ext.Component} this
+ */
+ 'disable',
+ /**
+ * @event enable
+ * Fires after the component is enabled.
+ * @param {Ext.Component} this
+ */
+ 'enable',
+ /**
+ * @event beforeshow
+ * Fires before the component is shown when calling the {@link #show} method. Return false from an event
+ * handler to stop the show.
+ * @param {Ext.Component} this
+ */
+ 'beforeshow',
+ /**
+ * @event show
+ * Fires after the component is shown when calling the {@link #show} method.
+ * @param {Ext.Component} this
+ */
+ 'show',
+ /**
+ * @event beforehide
+ * Fires before the component is hidden when calling the {@link #hide} method. Return false from an event
+ * handler to stop the hide.
+ * @param {Ext.Component} this
+ */
+ 'beforehide',
+ /**
+ * @event hide
+ * Fires after the component is hidden. Fires after the component is hidden when calling the {@link #hide}
+ * method.
+ * @param {Ext.Component} this
+ */
+ 'hide',
+ /**
+ * @event removed
+ * Fires when a component is removed from an Ext.container.Container
+ * @param {Ext.Component} this
+ * @param {Ext.container.Container} ownerCt Container which holds the component
+ */
+ 'removed',
+ /**
+ * @event beforerender
+ * Fires before the component is {@link #rendered}. Return false from an event handler to stop the
+ * {@link #render}.
+ * @param {Ext.Component} this
+ */
+ 'beforerender',
+ /**
+ * @event render
+ * Fires after the component markup is {@link #rendered}.
+ * @param {Ext.Component} this
+ */
+ 'render',
+ /**
+ * @event afterrender
+ * Fires after the component rendering is finished.
+ *
+ * The afterrender event is fired after this Component has been {@link #rendered}, been postprocesed by any
+ * afterRender method defined for the Component.
+ * @param {Ext.Component} this
+ */
+ 'afterrender',
+ /**
+ * @event beforedestroy
+ * Fires before the component is {@link #destroy}ed. Return false from an event handler to stop the
+ * {@link #destroy}.
+ * @param {Ext.Component} this
+ */
+ 'beforedestroy',
+ /**
+ * @event destroy
+ * Fires after the component is {@link #destroy}ed.
+ * @param {Ext.Component} this
+ */
+ 'destroy',
+ /**
+ * @event resize
+ * Fires after the component is resized.
+ * @param {Ext.Component} this
+ * @param {Number} adjWidth The box-adjusted width that was set
+ * @param {Number} adjHeight The box-adjusted height that was set
+ */
+ 'resize',
+ /**
+ * @event move
+ * Fires after the component is moved.
+ * @param {Ext.Component} this
+ * @param {Number} x The new x position
+ * @param {Number} y The new y position
+ */
+ 'move'
+ );
+
+ me.getId();
+
+ me.mons = [];
+ me.additionalCls = [];
+ me.renderData = me.renderData || {};
+ me.renderSelectors = me.renderSelectors || {};
+
+ if (me.plugins) {
+ me.plugins = [].concat(me.plugins);
+ me.constructPlugins();
+ }
+
+ me.initComponent();
+
+ // ititComponent gets a chance to change the id property before registering
+ Ext.ComponentManager.register(me);
+
+ // Dont pass the config so that it is not applied to 'this' again
+ me.mixins.observable.constructor.call(me);
+ me.mixins.state.constructor.call(me, config);
+
+ // Save state on resize.
+ this.addStateEvents('resize');
+
+ // Move this into Observable?
+ if (me.plugins) {
+ me.plugins = [].concat(me.plugins);
+ for (i = 0, len = me.plugins.length; i < len; i++) {
+ me.plugins[i] = me.initPlugin(me.plugins[i]);
+ }
+ }
+
+ me.loader = me.getLoader();
+
+ if (me.renderTo) {
+ me.render(me.renderTo);
+ // EXTJSIV-1935 - should be a way to do afterShow or something, but that
+ // won't work. Likewise, rendering hidden and then showing (w/autoShow) has
+ // implications to afterRender so we cannot do that.
+ }
+
+ if (me.autoShow) {
+ me.show();
+ }
+
+ },
+
+ initComponent: function () {
+ // This is called again here to allow derived classes to add plugin configs to the
+ // plugins array before calling down to this, the base initComponent.
+ this.constructPlugins();
+ },
+
+ /**
+ * The supplied default state gathering method for the AbstractComponent class.
+ *
+ * This method returns dimension settings such as `flex`, `anchor`, `width` and `height` along with `collapsed`
+ * state.
+ *
+ * Subclasses which implement more complex state should call the superclass's implementation, and apply their state
+ * to the result if this basic state is to be saved.
+ *
+ * Note that Component state will only be saved if the Component has a {@link #stateId} and there as a StateProvider
+ * configured for the document.
+ *
+ * @return {Object}
+ */
+ getState: function() {
+ var me = this,
+ layout = me.ownerCt ? (me.shadowOwnerCt || me.ownerCt).getLayout() : null,
+ state = {
+ collapsed: me.collapsed
+ },
+ width = me.width,
+ height = me.height,
+ cm = me.collapseMemento,
+ anchors;
+
+ // If a Panel-local collapse has taken place, use remembered values as the dimensions.
+ // TODO: remove this coupling with Panel's privates! All collapse/expand logic should be refactored into one place.
+ if (me.collapsed && cm) {
+ if (Ext.isDefined(cm.data.width)) {
+ width = cm.width;
+ }
+ if (Ext.isDefined(cm.data.height)) {
+ height = cm.height;
+ }
+ }
+
+ // If we have flex, only store the perpendicular dimension.
+ if (layout && me.flex) {
+ state.flex = me.flex;
+ if (layout.perpendicularPrefix) {
+ state[layout.perpendicularPrefix] = me['get' + layout.perpendicularPrefixCap]();
+ } else {
+ }
+ }
+ // If we have anchor, only store dimensions which are *not* being anchored
+ else if (layout && me.anchor) {
+ state.anchor = me.anchor;
+ anchors = me.anchor.split(' ').concat(null);
+ if (!anchors[0]) {
+ if (me.width) {
+ state.width = width;
+ }
+ }
+ if (!anchors[1]) {
+ if (me.height) {
+ state.height = height;
+ }
+ }
+ }
+ // Store dimensions.
+ else {
+ if (me.width) {
+ state.width = width;
+ }
+ if (me.height) {
+ state.height = height;
+ }
+ }
+
+ // Don't save dimensions if they are unchanged from the original configuration.
+ if (state.width == me.initialConfig.width) {
+ delete state.width;
+ }
+ if (state.height == me.initialConfig.height) {
+ delete state.height;
+ }
+
+ // If a Box layout was managing the perpendicular dimension, don't save that dimension
+ if (layout && layout.align && (layout.align.indexOf('stretch') !== -1)) {
+ delete state[layout.perpendicularPrefix];
+ }
+ return state;
+ },
+
+ show: Ext.emptyFn,
+
+ animate: function(animObj) {
+ var me = this,
+ to;
+
+ animObj = animObj || {};
+ to = animObj.to || {};
+
+ if (Ext.fx.Manager.hasFxBlock(me.id)) {
+ return me;
+ }
+ // Special processing for animating Component dimensions.
+ if (!animObj.dynamic && (to.height || to.width)) {
+ var curWidth = me.getWidth(),
+ w = curWidth,
+ curHeight = me.getHeight(),
+ h = curHeight,
+ needsResize = false;
+
+ if (to.height && to.height > curHeight) {
+ h = to.height;
+ needsResize = true;
+ }
+ if (to.width && to.width > curWidth) {
+ w = to.width;
+ needsResize = true;
+ }
+
+ // If any dimensions are being increased, we must resize the internal structure
+ // of the Component, but then clip it by sizing its encapsulating element back to original dimensions.
+ // The animation will then progressively reveal the larger content.
+ if (needsResize) {
+ var clearWidth = !Ext.isNumber(me.width),
+ clearHeight = !Ext.isNumber(me.height);
+
+ me.componentLayout.childrenChanged = true;
+ me.setSize(w, h, me.ownerCt);
+ me.el.setSize(curWidth, curHeight);
+ if (clearWidth) {
+ delete me.width;
+ }
+ if (clearHeight) {
+ delete me.height;
+ }
+ }
+ }
+ return me.mixins.animate.animate.apply(me, arguments);
+ },
+
+ /**
+ * This method finds the topmost active layout who's processing will eventually determine the size and position of
+ * this Component.
+ *
+ * This method is useful when dynamically adding Components into Containers, and some processing must take place
+ * after the final sizing and positioning of the Component has been performed.
+ *
+ * @return {Ext.Component}
+ */
+ findLayoutController: function() {
+ return this.findParentBy(function(c) {
+ // Return true if we are at the root of the Container tree
+ // or this Container's layout is busy but the next one up is not.
+ return !c.ownerCt || (c.layout.layoutBusy && !c.ownerCt.layout.layoutBusy);
+ });
+ },
+
+ onShow : function() {
+ // Layout if needed
+ var needsLayout = this.needsLayout;
+ if (Ext.isObject(needsLayout)) {
+ this.doComponentLayout(needsLayout.width, needsLayout.height, needsLayout.isSetSize, needsLayout.ownerCt);
+ }
+ },
+
+ constructPlugin: function(plugin) {
+ if (plugin.ptype && typeof plugin.init != 'function') {
+ plugin.cmp = this;
+ plugin = Ext.PluginManager.create(plugin);
+ }
+ else if (typeof plugin == 'string') {
+ plugin = Ext.PluginManager.create({
+ ptype: plugin,
+ cmp: this
+ });
+ }
+ return plugin;
+ },
+
+ /**
+ * Ensures that the plugins array contains fully constructed plugin instances. This converts any configs into their
+ * appropriate instances.
+ */
+ constructPlugins: function() {
+ var me = this,
+ plugins = me.plugins,
+ i, len;
+
+ if (plugins) {
+ for (i = 0, len = plugins.length; i < len; i++) {
+ // this just returns already-constructed plugin instances...
+ plugins[i] = me.constructPlugin(plugins[i]);
+ }
+ }
+ },
+
+ // @private
+ initPlugin : function(plugin) {
+ plugin.init(this);
+
+ return plugin;
+ },
+
+ /**
+ * Handles autoRender. Floating Components may have an ownerCt. If they are asking to be constrained, constrain them
+ * within that ownerCt, and have their z-index managed locally. Floating Components are always rendered to
+ * document.body
+ */
+ doAutoRender: function() {
+ var me = this;
+ if (me.floating) {
+ me.render(document.body);
+ } else {
+ me.render(Ext.isBoolean(me.autoRender) ? Ext.getBody() : me.autoRender);
+ }
+ },
+
+ // @private
+ render : function(container, position) {
+ var me = this;
+
+ if (!me.rendered && me.fireEvent('beforerender', me) !== false) {
+
+ // Flag set during the render process.
+ // It can be used to inhibit event-driven layout calls during the render phase
+ me.rendering = true;
+
+ // If this.el is defined, we want to make sure we are dealing with
+ // an Ext Element.
+ if (me.el) {
+ me.el = Ext.get(me.el);
+ }
+
+ // Perform render-time processing for floating Components
+ if (me.floating) {
+ me.onFloatRender();
+ }
+
+ container = me.initContainer(container);
+
+ me.onRender(container, position);
+
+ // Tell the encapsulating element to hide itself in the way the Component is configured to hide
+ // This means DISPLAY, VISIBILITY or OFFSETS.
+ me.el.setVisibilityMode(Ext.Element[me.hideMode.toUpperCase()]);
+
+ if (me.overCls) {
+ me.el.hover(me.addOverCls, me.removeOverCls, me);
+ }
+
+ me.fireEvent('render', me);
+
+ me.initContent();
+
+ me.afterRender(container);
+ me.fireEvent('afterrender', me);
+
+ me.initEvents();
+
+ if (me.hidden) {
+ // Hiding during the render process should not perform any ancillary
+ // actions that the full hide process does; It is not hiding, it begins in a hidden state.'
+ // So just make the element hidden according to the configured hideMode
+ me.el.hide();
+ }
+
+ if (me.disabled) {
+ // pass silent so the event doesn't fire the first time.
+ me.disable(true);
+ }
+
+ // Delete the flag once the rendering is done.
+ delete me.rendering;
+ }
+ return me;
+ },
+
+ // @private
+ onRender : function(container, position) {
+ var me = this,
+ el = me.el,
+ styles = me.initStyles(),
+ renderTpl, renderData, i;
+
+ position = me.getInsertPosition(position);
+
+ if (!el) {
+ if (position) {
+ el = Ext.DomHelper.insertBefore(position, me.getElConfig(), true);
+ }
+ else {
+ el = Ext.DomHelper.append(container, me.getElConfig(), true);
+ }
+ }
+ else if (me.allowDomMove !== false) {
+ if (position) {
+ container.dom.insertBefore(el.dom, position);
+ } else {
+ container.dom.appendChild(el.dom);
+ }
+ }
+
+ if (Ext.scopeResetCSS && !me.ownerCt) {
+ // If this component's el is the body element, we add the reset class to the html tag
+ if (el.dom == Ext.getBody().dom) {
+ el.parent().addCls(Ext.baseCSSPrefix + 'reset');
+ }
+ else {
+ // Else we wrap this element in an element that adds the reset class.
+ me.resetEl = el.wrap({
+ cls: Ext.baseCSSPrefix + 'reset'
+ });
+ }
+ }
+
+ me.setUI(me.ui);
+
+ el.addCls(me.initCls());
+ el.setStyle(styles);
+
+ // Here we check if the component has a height set through style or css.
+ // If it does then we set the this.height to that value and it won't be
+ // considered an auto height component
+ // if (this.height === undefined) {
+ // var height = el.getHeight();
+ // // This hopefully means that the panel has an explicit height set in style or css
+ // if (height - el.getPadding('tb') - el.getBorderWidth('tb') > 0) {
+ // this.height = height;
+ // }
+ // }
+
+ me.el = el;
+
+ me.initFrame();
+
+ renderTpl = me.initRenderTpl();
+ if (renderTpl) {
+ renderData = me.initRenderData();
+ renderTpl.append(me.getTargetEl(), renderData);
+ }
+
+ me.applyRenderSelectors();
+
+ me.rendered = true;
+ },
+
+ // @private
+ afterRender : function() {
+ var me = this,
+ pos,
+ xy;
+
+ me.getComponentLayout();
+
+ // Set the size if a size is configured, or if this is the outermost Container.
+ // Also, if this is a collapsed Panel, it needs an initial component layout
+ // to lay out its header so that it can have a height determined.
+ if (me.collapsed || (!me.ownerCt || (me.height || me.width))) {
+ me.setSize(me.width, me.height);
+ } else {
+ // It is expected that child items be rendered before this method returns and
+ // the afterrender event fires. Since we aren't going to do the layout now, we
+ // must render the child items. This is handled implicitly above in the layout
+ // caused by setSize.
+ me.renderChildren();
+ }
+
+ // For floaters, calculate x and y if they aren't defined by aligning
+ // the sized element to the center of either the container or the ownerCt
+ if (me.floating && (me.x === undefined || me.y === undefined)) {
+ if (me.floatParent) {
+ xy = me.el.getAlignToXY(me.floatParent.getTargetEl(), 'c-c');
+ pos = me.floatParent.getTargetEl().translatePoints(xy[0], xy[1]);
+ } else {
+ xy = me.el.getAlignToXY(me.container, 'c-c');
+ pos = me.container.translatePoints(xy[0], xy[1]);
+ }
+ me.x = me.x === undefined ? pos.left: me.x;
+ me.y = me.y === undefined ? pos.top: me.y;
+ }
+
+ if (Ext.isDefined(me.x) || Ext.isDefined(me.y)) {
+ me.setPosition(me.x, me.y);
+ }
+
+ if (me.styleHtmlContent) {
+ me.getTargetEl().addCls(me.styleHtmlCls);
+ }
+ },
+
+ /**
+ * @private
+ * Called by Component#doAutoRender
+ *
+ * Register a Container configured `floating: true` with this Component's {@link Ext.ZIndexManager ZIndexManager}.
+ *
+ * Components added in ths way will not participate in any layout, but will be rendered
+ * upon first show in the way that {@link Ext.window.Window Window}s are.
+ */
+ registerFloatingItem: function(cmp) {
+ var me = this;
+ if (!me.floatingItems) {
+ me.floatingItems = Ext.create('Ext.ZIndexManager', me);
+ }
+ me.floatingItems.register(cmp);
+ },
+
+ renderChildren: function () {
+ var me = this,
+ layout = me.getComponentLayout();
+
+ me.suspendLayout = true;
+ layout.renderChildren();
+ delete me.suspendLayout;
+ },
+
+ frameCls: Ext.baseCSSPrefix + 'frame',
+
+ frameIdRegex: /[-]frame\d+[TMB][LCR]$/,
+
+ frameElementCls: {
+ tl: [],
+ tc: [],
+ tr: [],
+ ml: [],
+ mc: [],
+ mr: [],
+ bl: [],
+ bc: [],
+ br: []
+ },
+
+ frameTpl: [
+ '<tpl if="top">',
+ '<tpl if="left"><div id="{fgid}TL" class="{frameCls}-tl {baseCls}-tl {baseCls}-{ui}-tl<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tl</tpl></tpl>" style="background-position: {tl}; padding-left: {frameWidth}px" role="presentation"></tpl>',
+ '<tpl if="right"><div id="{fgid}TR" class="{frameCls}-tr {baseCls}-tr {baseCls}-{ui}-tr<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tr</tpl></tpl>" style="background-position: {tr}; padding-right: {frameWidth}px" role="presentation"></tpl>',
+ '<div id="{fgid}TC" class="{frameCls}-tc {baseCls}-tc {baseCls}-{ui}-tc<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tc</tpl></tpl>" style="background-position: {tc}; height: {frameWidth}px" role="presentation"></div>',
+ '<tpl if="right"></div></tpl>',
+ '<tpl if="left"></div></tpl>',
+ '</tpl>',
+ '<tpl if="left"><div id="{fgid}ML" class="{frameCls}-ml {baseCls}-ml {baseCls}-{ui}-ml<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-ml</tpl></tpl>" style="background-position: {ml}; padding-left: {frameWidth}px" role="presentation"></tpl>',
+ '<tpl if="right"><div id="{fgid}MR" class="{frameCls}-mr {baseCls}-mr {baseCls}-{ui}-mr<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-mr</tpl></tpl>" style="background-position: {mr}; padding-right: {frameWidth}px" role="presentation"></tpl>',
+ '<div id="{fgid}MC" class="{frameCls}-mc {baseCls}-mc {baseCls}-{ui}-mc<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-mc</tpl></tpl>" role="presentation"></div>',
+ '<tpl if="right"></div></tpl>',
+ '<tpl if="left"></div></tpl>',
+ '<tpl if="bottom">',
+ '<tpl if="left"><div id="{fgid}BL" class="{frameCls}-bl {baseCls}-bl {baseCls}-{ui}-bl<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-bl</tpl></tpl>" style="background-position: {bl}; padding-left: {frameWidth}px" role="presentation"></tpl>',
+ '<tpl if="right"><div id="{fgid}BR" class="{frameCls}-br {baseCls}-br {baseCls}-{ui}-br<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-br</tpl></tpl>" style="background-position: {br}; padding-right: {frameWidth}px" role="presentation"></tpl>',
+ '<div id="{fgid}BC" class="{frameCls}-bc {baseCls}-bc {baseCls}-{ui}-bc<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-bc</tpl></tpl>" style="background-position: {bc}; height: {frameWidth}px" role="presentation"></div>',
+ '<tpl if="right"></div></tpl>',
+ '<tpl if="left"></div></tpl>',
+ '</tpl>'
+ ],
+
+ frameTableTpl: [
+ '<table><tbody>',
+ '<tpl if="top">',
+ '<tr>',
+ '<tpl if="left"><td id="{fgid}TL" class="{frameCls}-tl {baseCls}-tl {baseCls}-{ui}-tl<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tl</tpl></tpl>" style="background-position: {tl}; padding-left:{frameWidth}px" role="presentation"></td></tpl>',
+ '<td id="{fgid}TC" class="{frameCls}-tc {baseCls}-tc {baseCls}-{ui}-tc<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tc</tpl></tpl>" style="background-position: {tc}; height: {frameWidth}px" role="presentation"></td>',
+ '<tpl if="right"><td id="{fgid}TR" class="{frameCls}-tr {baseCls}-tr {baseCls}-{ui}-tr<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-tr</tpl></tpl>" style="background-position: {tr}; padding-left: {frameWidth}px" role="presentation"></td></tpl>',
+ '</tr>',
+ '</tpl>',
+ '<tr>',
+ '<tpl if="left"><td id="{fgid}ML" class="{frameCls}-ml {baseCls}-ml {baseCls}-{ui}-ml<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-ml</tpl></tpl>" style="background-position: {ml}; padding-left: {frameWidth}px" role="presentation"></td></tpl>',
+ '<td id="{fgid}MC" class="{frameCls}-mc {baseCls}-mc {baseCls}-{ui}-mc<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-mc</tpl></tpl>" style="background-position: 0 0;" role="presentation"></td>',
+ '<tpl if="right"><td id="{fgid}MR" class="{frameCls}-mr {baseCls}-mr {baseCls}-{ui}-mr<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-mr</tpl></tpl>" style="background-position: {mr}; padding-left: {frameWidth}px" role="presentation"></td></tpl>',
+ '</tr>',
+ '<tpl if="bottom">',
+ '<tr>',
+ '<tpl if="left"><td id="{fgid}BL" class="{frameCls}-bl {baseCls}-bl {baseCls}-{ui}-bl<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-bl</tpl></tpl>" style="background-position: {bl}; padding-left: {frameWidth}px" role="presentation"></td></tpl>',
+ '<td id="{fgid}BC" class="{frameCls}-bc {baseCls}-bc {baseCls}-{ui}-bc<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-bc</tpl></tpl>" style="background-position: {bc}; height: {frameWidth}px" role="presentation"></td>',
+ '<tpl if="right"><td id="{fgid}BR" class="{frameCls}-br {baseCls}-br {baseCls}-{ui}-br<tpl if="uiCls"><tpl for="uiCls"> {parent.baseCls}-{parent.ui}-{.}-br</tpl></tpl>" style="background-position: {br}; padding-left: {frameWidth}px" role="presentation"></td></tpl>',
+ '</tr>',
+ '</tpl>',
+ '</tbody></table>'
+ ],
+
+ /**
+ * @private
+ */
+ initFrame : function() {
+ if (Ext.supports.CSS3BorderRadius) {
+ return false;
+ }
+
+ var me = this,
+ frameInfo = me.getFrameInfo(),
+ frameWidth = frameInfo.width,
+ frameTpl = me.getFrameTpl(frameInfo.table),
+ frameGenId;
+
+ if (me.frame) {
+ // since we render id's into the markup and id's NEED to be unique, we have a
+ // simple strategy for numbering their generations.
+ me.frameGenId = frameGenId = (me.frameGenId || 0) + 1;
+ frameGenId = me.id + '-frame' + frameGenId;
+
+ // Here we render the frameTpl to this component. This inserts the 9point div or the table framing.
+ frameTpl.insertFirst(me.el, Ext.apply({}, {
+ fgid: frameGenId,
+ ui: me.ui,
+ uiCls: me.uiCls,
+ frameCls: me.frameCls,
+ baseCls: me.baseCls,
+ frameWidth: frameWidth,
+ top: !!frameInfo.top,
+ left: !!frameInfo.left,
+ right: !!frameInfo.right,
+ bottom: !!frameInfo.bottom
+ }, me.getFramePositions(frameInfo)));
+
+ // The frameBody is returned in getTargetEl, so that layouts render items to the correct target.=
+ me.frameBody = me.el.down('.' + me.frameCls + '-mc');
+
+ // Clean out the childEls for the old frame elements (the majority of the els)
+ me.removeChildEls(function (c) {
+ return c.id && me.frameIdRegex.test(c.id);
+ });
+
+ // Add the childEls for each of the new frame elements
+ Ext.each(['TL','TC','TR','ML','MC','MR','BL','BC','BR'], function (suffix) {
+ me.childEls.push({ name: 'frame' + suffix, id: frameGenId + suffix });
+ });
+ }
+ },
+
+ updateFrame: function() {
+ if (Ext.supports.CSS3BorderRadius) {
+ return false;
+ }
+
+ var me = this,
+ wasTable = this.frameSize && this.frameSize.table,
+ oldFrameTL = this.frameTL,
+ oldFrameBL = this.frameBL,
+ oldFrameML = this.frameML,
+ oldFrameMC = this.frameMC,
+ newMCClassName;
+
+ this.initFrame();
+
+ if (oldFrameMC) {
+ if (me.frame) {
+ // Reapply render selectors
+ delete me.frameTL;
+ delete me.frameTC;
+ delete me.frameTR;
+ delete me.frameML;
+ delete me.frameMC;
+ delete me.frameMR;
+ delete me.frameBL;
+ delete me.frameBC;
+ delete me.frameBR;
+ this.applyRenderSelectors();
+
+ // Store the class names set on the new mc
+ newMCClassName = this.frameMC.dom.className;
+
+ // Replace the new mc with the old mc
+ oldFrameMC.insertAfter(this.frameMC);
+ this.frameMC.remove();
+
+ // Restore the reference to the old frame mc as the framebody
+ this.frameBody = this.frameMC = oldFrameMC;
+
+ // Apply the new mc classes to the old mc element
+ oldFrameMC.dom.className = newMCClassName;
+
+ // Remove the old framing
+ if (wasTable) {
+ me.el.query('> table')[1].remove();
+ }
+ else {
+ if (oldFrameTL) {
+ oldFrameTL.remove();
+ }
+ if (oldFrameBL) {
+ oldFrameBL.remove();
+ }
+ oldFrameML.remove();
+ }
+ }
+ else {
+ // We were framed but not anymore. Move all content from the old frame to the body
+
+ }
+ }
+ else if (me.frame) {
+ this.applyRenderSelectors();
+ }
+ },
+
+ getFrameInfo: function() {
+ if (Ext.supports.CSS3BorderRadius) {
+ return false;
+ }
+
+ var me = this,
+ left = me.el.getStyle('background-position-x'),
+ top = me.el.getStyle('background-position-y'),
+ info, frameInfo = false, max;
+
+ // Some browsers dont support background-position-x and y, so for those
+ // browsers let's split background-position into two parts.
+ if (!left && !top) {
+ info = me.el.getStyle('background-position').split(' ');
+ left = info[0];
+ top = info[1];
+ }
+
+ // We actually pass a string in the form of '[type][tl][tr]px [type][br][bl]px' as
+ // the background position of this.el from the css to indicate to IE that this component needs
+ // framing. We parse it here and change the markup accordingly.
+ if (parseInt(left, 10) >= 1000000 && parseInt(top, 10) >= 1000000) {
+ max = Math.max;
+
+ frameInfo = {
+ // Table markup starts with 110, div markup with 100.
+ table: left.substr(0, 3) == '110',
+
+ // Determine if we are dealing with a horizontal or vertical component
+ vertical: top.substr(0, 3) == '110',
+
+ // Get and parse the different border radius sizes
+ top: max(left.substr(3, 2), left.substr(5, 2)),
+ right: max(left.substr(5, 2), top.substr(3, 2)),
+ bottom: max(top.substr(3, 2), top.substr(5, 2)),
+ left: max(top.substr(5, 2), left.substr(3, 2))
+ };
+
+ frameInfo.width = max(frameInfo.top, frameInfo.right, frameInfo.bottom, frameInfo.left);
+
+ // Just to be sure we set the background image of the el to none.
+ me.el.setStyle('background-image', 'none');
+ }
+
+ // This happens when you set frame: true explicitly without using the x-frame mixin in sass.
+ // This way IE can't figure out what sizes to use and thus framing can't work.
+ if (me.frame === true && !frameInfo) {
+ }
+
+ me.frame = me.frame || !!frameInfo;
+ me.frameSize = frameInfo || false;
+
+ return frameInfo;
+ },
+
+ getFramePositions: function(frameInfo) {
+ var me = this,
+ frameWidth = frameInfo.width,
+ dock = me.dock,
+ positions, tc, bc, ml, mr;
+
+ if (frameInfo.vertical) {
+ tc = '0 -' + (frameWidth * 0) + 'px';
+ bc = '0 -' + (frameWidth * 1) + 'px';
+
+ if (dock && dock == "right") {
+ tc = 'right -' + (frameWidth * 0) + 'px';
+ bc = 'right -' + (frameWidth * 1) + 'px';
+ }
+
+ positions = {
+ tl: '0 -' + (frameWidth * 0) + 'px',
+ tr: '0 -' + (frameWidth * 1) + 'px',
+ bl: '0 -' + (frameWidth * 2) + 'px',
+ br: '0 -' + (frameWidth * 3) + 'px',
+
+ ml: '-' + (frameWidth * 1) + 'px 0',
+ mr: 'right 0',
+
+ tc: tc,
+ bc: bc
+ };
+ } else {
+ ml = '-' + (frameWidth * 0) + 'px 0';
+ mr = 'right 0';
+
+ if (dock && dock == "bottom") {
+ ml = 'left bottom';
+ mr = 'right bottom';
+ }
+
+ positions = {
+ tl: '0 -' + (frameWidth * 2) + 'px',
+ tr: 'right -' + (frameWidth * 3) + 'px',
+ bl: '0 -' + (frameWidth * 4) + 'px',
+ br: 'right -' + (frameWidth * 5) + 'px',
+
+ ml: ml,
+ mr: mr,
+
+ tc: '0 -' + (frameWidth * 0) + 'px',
+ bc: '0 -' + (frameWidth * 1) + 'px'
+ };
+ }
+
+ return positions;
+ },
+
+ /**
+ * @private
+ */
+ getFrameTpl : function(table) {
+ return table ? this.getTpl('frameTableTpl') : this.getTpl('frameTpl');
+ },
+
+ /**
+ * Creates an array of class names from the configurations to add to this Component's `el` on render.
+ *
+ * Private, but (possibly) used by ComponentQuery for selection by class name if Component is not rendered.
+ *
+ * @return {String[]} An array of class names with which the Component's element will be rendered.
+ * @private
+ */
+ initCls: function() {
+ var me = this,
+ cls = [];
+
+ cls.push(me.baseCls);
+
+ if (Ext.isDefined(me.cmpCls)) {
+ if (Ext.isDefined(Ext.global.console)) {
+ Ext.global.console.warn('Ext.Component: cmpCls has been deprecated. Please use componentCls.');
+ }
+ me.componentCls = me.cmpCls;
+ delete me.cmpCls;
+ }
+
+ if (me.componentCls) {
+ cls.push(me.componentCls);
+ } else {
+ me.componentCls = me.baseCls;
+ }
+ if (me.cls) {
+ cls.push(me.cls);
+ delete me.cls;
+ }
+
+ return cls.concat(me.additionalCls);
+ },
+
+ /**
+ * Sets the UI for the component. This will remove any existing UIs on the component. It will also loop through any
+ * uiCls set on the component and rename them so they include the new UI
+ * @param {String} ui The new UI for the component
+ */
+ setUI: function(ui) {
+ var me = this,
+ oldUICls = Ext.Array.clone(me.uiCls),
+ newUICls = [],
+ classes = [],
+ cls,
+ i;
+
+ //loop through all exisiting uiCls and update the ui in them
+ for (i = 0; i < oldUICls.length; i++) {
+ cls = oldUICls[i];
+
+ classes = classes.concat(me.removeClsWithUI(cls, true));
+ newUICls.push(cls);
+ }
+
+ if (classes.length) {
+ me.removeCls(classes);
+ }
+
+ //remove the UI from the element
+ me.removeUIFromElement();
+
+ //set the UI
+ me.ui = ui;
+
+ //add the new UI to the elemend
+ me.addUIToElement();
+
+ //loop through all exisiting uiCls and update the ui in them
+ classes = [];
+ for (i = 0; i < newUICls.length; i++) {
+ cls = newUICls[i];
+ classes = classes.concat(me.addClsWithUI(cls, true));
+ }
+
+ if (classes.length) {
+ me.addCls(classes);
+ }
+ },
+
+ /**
+ * Adds a cls to the uiCls array, which will also call {@link #addUIClsToElement} and adds to all elements of this
+ * component.
+ * @param {String/String[]} cls A string or an array of strings to add to the uiCls
+ * @param {Object} skip (Boolean) skip True to skip adding it to the class and do it later (via the return)
+ */
+ addClsWithUI: function(cls, skip) {
+ var me = this,
+ classes = [],
+ i;
+
+ if (!Ext.isArray(cls)) {
+ cls = [cls];
+ }
+
+ for (i = 0; i < cls.length; i++) {
+ if (cls[i] && !me.hasUICls(cls[i])) {
+ me.uiCls = Ext.Array.clone(me.uiCls);
+ me.uiCls.push(cls[i]);
+
+ classes = classes.concat(me.addUIClsToElement(cls[i]));
+ }
+ }
+
+ if (skip !== true) {
+ me.addCls(classes);
+ }
+
+ return classes;
+ },
+
+ /**
+ * Removes a cls to the uiCls array, which will also call {@link #removeUIClsFromElement} and removes it from all
+ * elements of this component.
+ * @param {String/String[]} cls A string or an array of strings to remove to the uiCls
+ */
+ removeClsWithUI: function(cls, skip) {
+ var me = this,
+ classes = [],
+ i;
+
+ if (!Ext.isArray(cls)) {
+ cls = [cls];
+ }
+
+ for (i = 0; i < cls.length; i++) {
+ if (cls[i] && me.hasUICls(cls[i])) {
+ me.uiCls = Ext.Array.remove(me.uiCls, cls[i]);
+
+ classes = classes.concat(me.removeUIClsFromElement(cls[i]));
+ }
+ }
+
+ if (skip !== true) {
+ me.removeCls(classes);
+ }
+
+ return classes;
+ },
+
+ /**
+ * Checks if there is currently a specified uiCls
+ * @param {String} cls The cls to check
+ */
+ hasUICls: function(cls) {
+ var me = this,
+ uiCls = me.uiCls || [];
+
+ return Ext.Array.contains(uiCls, cls);
+ },
+
+ /**
+ * Method which adds a specified UI + uiCls to the components element. Can be overridden to remove the UI from more
+ * than just the components element.
+ * @param {String} ui The UI to remove from the element
+ */
+ addUIClsToElement: function(cls, force) {
+ var me = this,
+ result = [],
+ frameElementCls = me.frameElementCls;
+
+ result.push(Ext.baseCSSPrefix + cls);
+ result.push(me.baseCls + '-' + cls);
+ result.push(me.baseCls + '-' + me.ui + '-' + cls);
+
+ if (!force && me.frame && !Ext.supports.CSS3BorderRadius) {
+ // define each element of the frame
+ var els = ['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'],
+ classes, i, j, el;
+
+ // loop through each of them, and if they are defined add the ui
+ for (i = 0; i < els.length; i++) {
+ el = me['frame' + els[i].toUpperCase()];
+ classes = [me.baseCls + '-' + me.ui + '-' + els[i], me.baseCls + '-' + me.ui + '-' + cls + '-' + els[i]];
+ if (el && el.dom) {
+ el.addCls(classes);
+ } else {
+ for (j = 0; j < classes.length; j++) {
+ if (Ext.Array.indexOf(frameElementCls[els[i]], classes[j]) == -1) {
+ frameElementCls[els[i]].push(classes[j]);
+ }
+ }
+ }
+ }
+ }
+
+ me.frameElementCls = frameElementCls;
+
+ return result;
+ },
+
+ /**
+ * Method which removes a specified UI + uiCls from the components element. The cls which is added to the element
+ * will be: `this.baseCls + '-' + ui`
+ * @param {String} ui The UI to add to the element
+ */
+ removeUIClsFromElement: function(cls, force) {
+ var me = this,
+ result = [],
+ frameElementCls = me.frameElementCls;
+
+ result.push(Ext.baseCSSPrefix + cls);
+ result.push(me.baseCls + '-' + cls);
+ result.push(me.baseCls + '-' + me.ui + '-' + cls);
+
+ if (!force && me.frame && !Ext.supports.CSS3BorderRadius) {
+ // define each element of the frame
+ var els = ['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'],
+ i, el;
+ cls = me.baseCls + '-' + me.ui + '-' + cls + '-' + els[i];
+ // loop through each of them, and if they are defined add the ui
+ for (i = 0; i < els.length; i++) {
+ el = me['frame' + els[i].toUpperCase()];
+ if (el && el.dom) {
+ el.removeCls(cls);
+ } else {
+ Ext.Array.remove(frameElementCls[els[i]], cls);
+ }
+ }
+ }
+
+ me.frameElementCls = frameElementCls;
+
+ return result;
+ },
+
+ /**
+ * Method which adds a specified UI to the components element.
+ * @private
+ */
+ addUIToElement: function(force) {
+ var me = this,
+ frameElementCls = me.frameElementCls;
+
+ me.addCls(me.baseCls + '-' + me.ui);
+
+ if (me.frame && !Ext.supports.CSS3BorderRadius) {
+ // define each element of the frame
+ var els = ['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'],
+ i, el, cls;
+
+ // loop through each of them, and if they are defined add the ui
+ for (i = 0; i < els.length; i++) {
+ el = me['frame' + els[i].toUpperCase()];
+ cls = me.baseCls + '-' + me.ui + '-' + els[i];
+ if (el) {
+ el.addCls(cls);
+ } else {
+ if (!Ext.Array.contains(frameElementCls[els[i]], cls)) {
+ frameElementCls[els[i]].push(cls);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Method which removes a specified UI from the components element.
+ * @private
+ */
+ removeUIFromElement: function() {
+ var me = this,
+ frameElementCls = me.frameElementCls;
+
+ me.removeCls(me.baseCls + '-' + me.ui);
+
+ if (me.frame && !Ext.supports.CSS3BorderRadius) {
+ // define each element of the frame
+ var els = ['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'],
+ i, j, el, cls;
+
+ // loop through each of them, and if they are defined add the ui
+ for (i = 0; i < els.length; i++) {
+ el = me['frame' + els[i].toUpperCase()];
+ cls = me.baseCls + '-' + me.ui + '-' + els[i];
+
+ if (el) {
+ el.removeCls(cls);
+ } else {
+ Ext.Array.remove(frameElementCls[els[i]], cls);
+ }
+ }
+ }
+ },
+
+ getElConfig : function() {
+ if (Ext.isString(this.autoEl)) {
+ this.autoEl = {
+ tag: this.autoEl
+ };
+ }
+
+ var result = this.autoEl || {tag: 'div'};
+ result.id = this.id;
+ return result;
+ },
+
+ /**
+ * This function takes the position argument passed to onRender and returns a DOM element that you can use in the
+ * insertBefore.
+ * @param {String/Number/Ext.Element/HTMLElement} position Index, element id or element you want to put this
+ * component before.
+ * @return {HTMLElement} DOM element that you can use in the insertBefore
+ */
+ getInsertPosition: function(position) {
+ // Convert the position to an element to insert before
+ if (position !== undefined) {
+ if (Ext.isNumber(position)) {
+ position = this.container.dom.childNodes[position];
+ }
+ else {
+ position = Ext.getDom(position);
+ }
+ }
+
+ return position;
+ },
+
+ /**
+ * Adds ctCls to container.
+ * @return {Ext.Element} The initialized container
+ * @private
+ */
+ initContainer: function(container) {
+ var me = this;
+
+ // If you render a component specifying the el, we get the container
+ // of the el, and make sure we dont move the el around in the dom
+ // during the render
+ if (!container && me.el) {
+ container = me.el.dom.parentNode;
+ me.allowDomMove = false;
+ }
+
+ me.container = Ext.get(container);
+
+ if (me.ctCls) {
+ me.container.addCls(me.ctCls);
+ }
+
+ return me.container;
+ },
+
+ /**
+ * Initialized the renderData to be used when rendering the renderTpl.
+ * @return {Object} Object with keys and values that are going to be applied to the renderTpl
+ * @private
+ */
+ initRenderData: function() {
+ var me = this;
+
+ return Ext.applyIf(me.renderData, {
+ id: me.id,
+ ui: me.ui,
+ uiCls: me.uiCls,
+ baseCls: me.baseCls,
+ componentCls: me.componentCls,
+ frame: me.frame
+ });
+ },
+
+ /**
+ * @private
+ */
+ getTpl: function(name) {
+ var me = this,
+ prototype = me.self.prototype,
+ ownerPrototype,
+ tpl;
+
+ if (me.hasOwnProperty(name)) {
+ tpl = me[name];
+ if (tpl && !(tpl instanceof Ext.XTemplate)) {
+ me[name] = Ext.ClassManager.dynInstantiate('Ext.XTemplate', tpl);
+ }
+
+ return me[name];
+ }
+
+ if (!(prototype[name] instanceof Ext.XTemplate)) {
+ ownerPrototype = prototype;
+
+ do {
+ if (ownerPrototype.hasOwnProperty(name)) {
+ tpl = ownerPrototype[name];
+ if (tpl && !(tpl instanceof Ext.XTemplate)) {
+ ownerPrototype[name] = Ext.ClassManager.dynInstantiate('Ext.XTemplate', tpl);
+ break;
+ }
+ }
+
+ ownerPrototype = ownerPrototype.superclass;
+ } while (ownerPrototype);
+ }
+
+ return prototype[name];
+ },
+
+ /**
+ * Initializes the renderTpl.
+ * @return {Ext.XTemplate} The renderTpl XTemplate instance.
+ * @private
+ */
+ initRenderTpl: function() {
+ return this.getTpl('renderTpl');
+ },
+
+ /**
+ * Converts style definitions to String.
+ * @return {String} A CSS style string with style, padding, margin and border.
+ * @private
+ */
+ initStyles: function() {
+ var style = {},
+ me = this,
+ Element = Ext.Element;
+
+ if (Ext.isString(me.style)) {
+ style = Element.parseStyles(me.style);
+ } else {
+ style = Ext.apply({}, me.style);
+ }
+
+ // Convert the padding, margin and border properties from a space seperated string
+ // into a proper style string
+ if (me.padding !== undefined) {
+ style.padding = Element.unitizeBox((me.padding === true) ? 5 : me.padding);
+ }
+
+ if (me.margin !== undefined) {
+ style.margin = Element.unitizeBox((me.margin === true) ? 5 : me.margin);
+ }
+
+ delete me.style;
+ return style;
+ },
+
+ /**
+ * Initializes this components contents. It checks for the properties html, contentEl and tpl/data.
+ * @private
+ */
+ initContent: function() {
+ var me = this,
+ target = me.getTargetEl(),
+ contentEl,
+ pre;
+
+ if (me.html) {
+ target.update(Ext.DomHelper.markup(me.html));
+ delete me.html;
+ }
+
+ if (me.contentEl) {
+ contentEl = Ext.get(me.contentEl);
+ pre = Ext.baseCSSPrefix;
+ contentEl.removeCls([pre + 'hidden', pre + 'hide-display', pre + 'hide-offsets', pre + 'hide-nosize']);
+ target.appendChild(contentEl.dom);
+ }
+
+ if (me.tpl) {
+ // Make sure this.tpl is an instantiated XTemplate
+ if (!me.tpl.isTemplate) {
+ me.tpl = Ext.create('Ext.XTemplate', me.tpl);
+ }
+
+ if (me.data) {
+ me.tpl[me.tplWriteMode](target, me.data);
+ delete me.data;
+ }
+ }
+ },
+
+ // @private
+ initEvents : function() {
+ var me = this,
+ afterRenderEvents = me.afterRenderEvents,
+ el,
+ property,
+ fn = function(listeners){
+ me.mon(el, listeners);
+ };
+ if (afterRenderEvents) {
+ for (property in afterRenderEvents) {
+ if (afterRenderEvents.hasOwnProperty(property)) {
+ el = me[property];
+ if (el && el.on) {
+ Ext.each(afterRenderEvents[property], fn);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Adds each argument passed to this method to the {@link #childEls} array.
+ */
+ addChildEls: function () {
+ var me = this,
+ childEls = me.childEls || (me.childEls = []);
+
+ childEls.push.apply(childEls, arguments);
+ },
+
+ /**
+ * Removes items in the childEls array based on the return value of a supplied test function. The function is called
+ * with a entry in childEls and if the test function return true, that entry is removed. If false, that entry is
+ * kept.
+ * @param {Function} testFn The test function.
+ */
+ removeChildEls: function (testFn) {
+ var me = this,
+ old = me.childEls,
+ keepers = (me.childEls = []),
+ n, i, cel;
+
+ for (i = 0, n = old.length; i < n; ++i) {
+ cel = old[i];
+ if (!testFn(cel)) {
+ keepers.push(cel);
+ }
+ }
+ },
+
+ /**
+ * Sets references to elements inside the component. This applies {@link #renderSelectors}
+ * as well as {@link #childEls}.
+ * @private
+ */
+ applyRenderSelectors: function() {
+ var me = this,
+ childEls = me.childEls,
+ selectors = me.renderSelectors,
+ el = me.el,
+ dom = el.dom,
+ baseId, childName, childId, i, selector;
+
+ if (childEls) {
+ baseId = me.id + '-';
+ for (i = childEls.length; i--; ) {
+ childName = childId = childEls[i];
+ if (typeof(childName) != 'string') {
+ childId = childName.id || (baseId + childName.itemId);
+ childName = childName.name;
+ } else {
+ childId = baseId + childId;
+ }
+
+ // We don't use Ext.get because that is 3x (or more) slower on IE6-8. Since
+ // we know the el's are children of our el we use getById instead:
+ me[childName] = el.getById(childId);
+ }
+ }
+
+ // We still support renderSelectors. There are a few places in the framework that
+ // need them and they are a documented part of the API. In fact, we support mixing
+ // childEls and renderSelectors (no reason not to).
+ if (selectors) {
+ for (selector in selectors) {
+ if (selectors.hasOwnProperty(selector) && selectors[selector]) {
+ me[selector] = Ext.get(Ext.DomQuery.selectNode(selectors[selector], dom));
+ }
+ }
+ }
+ },
+
+ /**
+ * Tests whether this Component matches the selector string.
+ * @param {String} selector The selector string to test against.
+ * @return {Boolean} True if this Component matches the selector.
+ */
+ is: function(selector) {
+ return Ext.ComponentQuery.is(this, selector);
+ },
+
+ /**
+ * Walks up the `ownerCt` axis looking for an ancestor Container which matches the passed simple selector.
+ *
+ * Example:
+ *
+ * var owningTabPanel = grid.up('tabpanel');
+ *
+ * @param {String} [selector] The simple selector to test.
+ * @return {Ext.container.Container} The matching ancestor Container (or `undefined` if no match was found).
+ */
+ up: function(selector) {
+ var result = this.ownerCt;
+ if (selector) {
+ for (; result; result = result.ownerCt) {
+ if (Ext.ComponentQuery.is(result, selector)) {
+ return result;
+ }
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Returns the next sibling of this Component.
+ *
+ * Optionally selects the next sibling which matches the passed {@link Ext.ComponentQuery ComponentQuery} selector.
+ *
+ * May also be refered to as **`next()`**
+ *
+ * Note that this is limited to siblings, and if no siblings of the item match, `null` is returned. Contrast with
+ * {@link #nextNode}
+ * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter the following items.
+ * @return {Ext.Component} The next sibling (or the next sibling which matches the selector).
+ * Returns null if there is no matching sibling.
+ */
+ nextSibling: function(selector) {
+ var o = this.ownerCt, it, last, idx, c;
+ if (o) {
+ it = o.items;
+ idx = it.indexOf(this) + 1;
+ if (idx) {
+ if (selector) {
+ for (last = it.getCount(); idx < last; idx++) {
+ if ((c = it.getAt(idx)).is(selector)) {
+ return c;
+ }
+ }
+ } else {
+ if (idx < it.getCount()) {
+ return it.getAt(idx);
+ }
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Returns the previous sibling of this Component.
+ *
+ * Optionally selects the previous sibling which matches the passed {@link Ext.ComponentQuery ComponentQuery}
+ * selector.
+ *
+ * May also be refered to as **`prev()`**
+ *
+ * Note that this is limited to siblings, and if no siblings of the item match, `null` is returned. Contrast with
+ * {@link #previousNode}
+ * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter the preceding items.
+ * @return {Ext.Component} The previous sibling (or the previous sibling which matches the selector).
+ * Returns null if there is no matching sibling.
+ */
+ previousSibling: function(selector) {
+ var o = this.ownerCt, it, idx, c;
+ if (o) {
+ it = o.items;
+ idx = it.indexOf(this);
+ if (idx != -1) {
+ if (selector) {
+ for (--idx; idx >= 0; idx--) {
+ if ((c = it.getAt(idx)).is(selector)) {
+ return c;
+ }
+ }
+ } else {
+ if (idx) {
+ return it.getAt(--idx);
+ }
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Returns the previous node in the Component tree in tree traversal order.
+ *
+ * Note that this is not limited to siblings, and if invoked upon a node with no matching siblings, will walk the
+ * tree in reverse order to attempt to find a match. Contrast with {@link #previousSibling}.
+ * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter the preceding nodes.
+ * @return {Ext.Component} The previous node (or the previous node which matches the selector).
+ * Returns null if there is no matching node.
+ */
+ previousNode: function(selector, includeSelf) {
+ var node = this,
+ result,
+ it, len, i;
+
+ // If asked to include self, test me
+ if (includeSelf && node.is(selector)) {
+ return node;
+ }
+
+ result = this.prev(selector);
+ if (result) {
+ return result;
+ }
+
+ if (node.ownerCt) {
+ for (it = node.ownerCt.items.items, i = Ext.Array.indexOf(it, node) - 1; i > -1; i--) {
+ if (it[i].query) {
+ result = it[i].query(selector);
+ result = result[result.length - 1];
+ if (result) {
+ return result;
+ }
+ }
+ }
+ return node.ownerCt.previousNode(selector, true);
+ }
+ },
+
+ /**
+ * Returns the next node in the Component tree in tree traversal order.
+ *
+ * Note that this is not limited to siblings, and if invoked upon a node with no matching siblings, will walk the
+ * tree to attempt to find a match. Contrast with {@link #nextSibling}.
+ * @param {String} [selector] A {@link Ext.ComponentQuery ComponentQuery} selector to filter the following nodes.
+ * @return {Ext.Component} The next node (or the next node which matches the selector).
+ * Returns null if there is no matching node.
+ */
+ nextNode: function(selector, includeSelf) {
+ var node = this,
+ result,
+ it, len, i;
+
+ // If asked to include self, test me
+ if (includeSelf && node.is(selector)) {
+ return node;
+ }
+
+ result = this.next(selector);
+ if (result) {
+ return result;
+ }
+
+ if (node.ownerCt) {
+ for (it = node.ownerCt.items, i = it.indexOf(node) + 1, it = it.items, len = it.length; i < len; i++) {
+ if (it[i].down) {
+ result = it[i].down(selector);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ return node.ownerCt.nextNode(selector);
+ }
+ },
+
+ /**
+ * Retrieves the id of this component. Will autogenerate an id if one has not already been set.
+ * @return {String}
+ */
+ getId : function() {
+ return this.id || (this.id = 'ext-comp-' + (this.getAutoId()));
+ },
+
+ getItemId : function() {
+ return this.itemId || this.id;
+ },
+
+ /**
+ * Retrieves the top level element representing this component.
+ * @return {Ext.core.Element}
+ */
+ getEl : function() {
+ return this.el;
+ },
+
+ /**
+ * This is used to determine where to insert the 'html', 'contentEl' and 'items' in this component.
+ * @private
+ */
+ getTargetEl: function() {
+ return this.frameBody || this.el;
+ },
+
+ /**
+ * Tests whether or not this Component is of a specific xtype. This can test whether this Component is descended
+ * from the xtype (default) or whether it is directly of the xtype specified (shallow = true).
+ *
+ * **If using your own subclasses, be aware that a Component must register its own xtype to participate in
+ * determination of inherited xtypes.**
+ *
+ * For a list of all available xtypes, see the {@link Ext.Component} header.
+ *
+ * Example usage:
+ *
+ * var t = new Ext.form.field.Text();
+ * var isText = t.isXType('textfield'); // true
+ * var isBoxSubclass = t.isXType('field'); // true, descended from Ext.form.field.Base
+ * var isBoxInstance = t.isXType('field', true); // false, not a direct Ext.form.field.Base instance
+ *
+ * @param {String} xtype The xtype to check for this Component
+ * @param {Boolean} [shallow=false] True to check whether this Component is directly of the specified xtype, false to
+ * check whether this Component is descended from the xtype.
+ * @return {Boolean} True if this component descends from the specified xtype, false otherwise.
+ */
+ isXType: function(xtype, shallow) {
+ //assume a string by default
+ if (Ext.isFunction(xtype)) {
+ xtype = xtype.xtype;
+ //handle being passed the class, e.g. Ext.Component
+ } else if (Ext.isObject(xtype)) {
+ xtype = xtype.statics().xtype;
+ //handle being passed an instance
+ }
+
+ return !shallow ? ('/' + this.getXTypes() + '/').indexOf('/' + xtype + '/') != -1: this.self.xtype == xtype;
+ },
+
+ /**
+ * Returns this Component's xtype hierarchy as a slash-delimited string. For a list of all available xtypes, see the
+ * {@link Ext.Component} header.
+ *
+ * **If using your own subclasses, be aware that a Component must register its own xtype to participate in
+ * determination of inherited xtypes.**
+ *
+ * Example usage:
+ *
+ * var t = new Ext.form.field.Text();
+ * alert(t.getXTypes()); // alerts 'component/field/textfield'
+ *
+ * @return {String} The xtype hierarchy string
+ */
+ getXTypes: function() {
+ var self = this.self,
+ xtypes, parentPrototype, parentXtypes;
+
+ if (!self.xtypes) {
+ xtypes = [];
+ parentPrototype = this;
+
+ while (parentPrototype) {
+ parentXtypes = parentPrototype.xtypes;
+
+ if (parentXtypes !== undefined) {
+ xtypes.unshift.apply(xtypes, parentXtypes);
+ }
+
+ parentPrototype = parentPrototype.superclass;
+ }
+
+ self.xtypeChain = xtypes;
+ self.xtypes = xtypes.join('/');
+ }
+
+ return self.xtypes;
+ },
+
+ /**
+ * Update the content area of a component.
+ * @param {String/Object} htmlOrData If this component has been configured with a template via the tpl config then
+ * it will use this argument as data to populate the template. If this component was not configured with a template,
+ * the components content area will be updated via Ext.Element update
+ * @param {Boolean} [loadScripts=false] Only legitimate when using the html configuration.
+ * @param {Function} [callback] Only legitimate when using the html configuration. Callback to execute when
+ * scripts have finished loading
+ */
+ update : function(htmlOrData, loadScripts, cb) {
+ var me = this;
+
+ if (me.tpl && !Ext.isString(htmlOrData)) {
+ me.data = htmlOrData;
+ if (me.rendered) {
+ me.tpl[me.tplWriteMode](me.getTargetEl(), htmlOrData || {});
+ }
+ } else {
+ me.html = Ext.isObject(htmlOrData) ? Ext.DomHelper.markup(htmlOrData) : htmlOrData;
+ if (me.rendered) {
+ me.getTargetEl().update(me.html, loadScripts, cb);
+ }
+ }
+
+ if (me.rendered) {
+ me.doComponentLayout();
+ }
+ },
+
+ /**
+ * Convenience function to hide or show this component by boolean.
+ * @param {Boolean} visible True to show, false to hide
+ * @return {Ext.Component} this
+ */
+ setVisible : function(visible) {
+ return this[visible ? 'show': 'hide']();
+ },
+
+ /**
+ * Returns true if this component is visible.
+ *
+ * @param {Boolean} [deep=false] Pass `true` to interrogate the visibility status of all parent Containers to
+ * determine whether this Component is truly visible to the user.
+ *
+ * Generally, to determine whether a Component is hidden, the no argument form is needed. For example when creating
+ * dynamically laid out UIs in a hidden Container before showing them.
+ *
+ * @return {Boolean} True if this component is visible, false otherwise.
+ */
+ isVisible: function(deep) {
+ var me = this,
+ child = me,
+ visible = !me.hidden,
+ ancestor = me.ownerCt;
+
+ // Clear hiddenOwnerCt property
+ me.hiddenAncestor = false;
+ if (me.destroyed) {
+ return false;
+ }
+
+ if (deep && visible && me.rendered && ancestor) {
+ while (ancestor) {
+ // If any ancestor is hidden, then this is hidden.
+ // If an ancestor Panel (only Panels have a collapse method) is collapsed,
+ // then its layoutTarget (body) is hidden, so this is hidden unless its within a
+ // docked item; they are still visible when collapsed (Unless they themseves are hidden)
+ if (ancestor.hidden || (ancestor.collapsed &&
+ !(ancestor.getDockedItems && Ext.Array.contains(ancestor.getDockedItems(), child)))) {
+ // Store hiddenOwnerCt property if needed
+ me.hiddenAncestor = ancestor;
+ visible = false;
+ break;
+ }
+ child = ancestor;
+ ancestor = ancestor.ownerCt;
+ }
+ }
+ return visible;
+ },
+
+ /**
+ * Enable the component
+ * @param {Boolean} [silent=false] Passing true will supress the 'enable' event from being fired.
+ */
+ enable: function(silent) {
+ var me = this;
+
+ if (me.rendered) {
+ me.el.removeCls(me.disabledCls);
+ me.el.dom.disabled = false;
+ me.onEnable();
+ }
+
+ me.disabled = false;
+
+ if (silent !== true) {
+ me.fireEvent('enable', me);
+ }
+
+ return me;
+ },
+
+ /**
+ * Disable the component.
+ * @param {Boolean} [silent=false] Passing true will supress the 'disable' event from being fired.
+ */
+ disable: function(silent) {
+ var me = this;
+
+ if (me.rendered) {
+ me.el.addCls(me.disabledCls);
+ me.el.dom.disabled = true;
+ me.onDisable();
+ }
+
+ me.disabled = true;
+
+ if (silent !== true) {
+ me.fireEvent('disable', me);
+ }
+
+ return me;
+ },
+
+ // @private
+ onEnable: function() {
+ if (this.maskOnDisable) {
+ this.el.unmask();
+ }
+ },
+
+ // @private
+ onDisable : function() {
+ if (this.maskOnDisable) {
+ this.el.mask();
+ }
+ },
+
+ /**
+ * Method to determine whether this Component is currently disabled.
+ * @return {Boolean} the disabled state of this Component.
+ */
+ isDisabled : function() {
+ return this.disabled;
+ },
+
+ /**
+ * Enable or disable the component.
+ * @param {Boolean} disabled True to disable.
+ */
+ setDisabled : function(disabled) {
+ return this[disabled ? 'disable': 'enable']();
+ },
+
+ /**
+ * Method to determine whether this Component is currently set to hidden.
+ * @return {Boolean} the hidden state of this Component.
+ */
+ isHidden : function() {
+ return this.hidden;
+ },
+
+ /**
+ * Adds a CSS class to the top level element representing this component.
+ * @param {String} cls The CSS class name to add
+ * @return {Ext.Component} Returns the Component to allow method chaining.
+ */
+ addCls : function(className) {
+ var me = this;
+ if (!className) {
+ return me;
+ }
+ if (!Ext.isArray(className)){
+ className = className.replace(me.trimRe, '').split(me.spacesRe);
+ }
+ if (me.rendered) {
+ me.el.addCls(className);
+ }
+ else {
+ me.additionalCls = Ext.Array.unique(me.additionalCls.concat(className));
+ }
+ return me;
+ },
+
+ /**
+ * Adds a CSS class to the top level element representing this component.
+ * @param {String} cls The CSS class name to add
+ * @return {Ext.Component} Returns the Component to allow method chaining.
+ */
+ addClass : function() {
+ return this.addCls.apply(this, arguments);
+ },
+
+ /**
+ * Removes a CSS class from the top level element representing this component.
+ * @param {Object} className
+ * @return {Ext.Component} Returns the Component to allow method chaining.
+ */
+ removeCls : function(className) {
+ var me = this;
+
+ if (!className) {
+ return me;
+ }
+ if (!Ext.isArray(className)){
+ className = className.replace(me.trimRe, '').split(me.spacesRe);
+ }
+ if (me.rendered) {
+ me.el.removeCls(className);
+ }
+ else if (me.additionalCls.length) {
+ Ext.each(className, function(cls) {
+ Ext.Array.remove(me.additionalCls, cls);
+ });
+ }
+ return me;
+ },
+
+
+ addOverCls: function() {
+ var me = this;
+ if (!me.disabled) {
+ me.el.addCls(me.overCls);
+ }
+ },
+
+ removeOverCls: function() {
+ this.el.removeCls(this.overCls);
+ },
+
+ addListener : function(element, listeners, scope, options) {
+ var me = this,
+ fn,
+ option;
+
+ if (Ext.isString(element) && (Ext.isObject(listeners) || options && options.element)) {
+ if (options.element) {
+ fn = listeners;
+
+ listeners = {};
+ listeners[element] = fn;
+ element = options.element;
+ if (scope) {
+ listeners.scope = scope;
+ }
+
+ for (option in options) {
+ if (options.hasOwnProperty(option)) {
+ if (me.eventOptionsRe.test(option)) {
+ listeners[option] = options[option];
+ }
+ }
+ }
+ }
+
+ // At this point we have a variable called element,
+ // and a listeners object that can be passed to on
+ if (me[element] && me[element].on) {
+ me.mon(me[element], listeners);
+ } else {
+ me.afterRenderEvents = me.afterRenderEvents || {};
+ if (!me.afterRenderEvents[element]) {
+ me.afterRenderEvents[element] = [];
+ }
+ me.afterRenderEvents[element].push(listeners);
+ }
+ }
+
+ return me.mixins.observable.addListener.apply(me, arguments);
+ },
+
+ // inherit docs
+ removeManagedListenerItem: function(isClear, managedListener, item, ename, fn, scope){
+ var me = this,
+ element = managedListener.options ? managedListener.options.element : null;
+
+ if (element) {
+ element = me[element];
+ if (element && element.un) {
+ if (isClear || (managedListener.item === item && managedListener.ename === ename && (!fn || managedListener.fn === fn) && (!scope || managedListener.scope === scope))) {
+ element.un(managedListener.ename, managedListener.fn, managedListener.scope);
+ if (!isClear) {
+ Ext.Array.remove(me.managedListeners, managedListener);
+ }
+ }
+ }
+ } else {
+ return me.mixins.observable.removeManagedListenerItem.apply(me, arguments);
+ }
+ },
+
+ /**
+ * Provides the link for Observable's fireEvent method to bubble up the ownership hierarchy.
+ * @return {Ext.container.Container} the Container which owns this Component.
+ */
+ getBubbleTarget : function() {
+ return this.ownerCt;
+ },
+
+ /**
+ * Method to determine whether this Component is floating.
+ * @return {Boolean} the floating state of this component.
+ */
+ isFloating : function() {
+ return this.floating;
+ },
+
+ /**
+ * Method to determine whether this Component is draggable.
+ * @return {Boolean} the draggable state of this component.
+ */
+ isDraggable : function() {
+ return !!this.draggable;
+ },
+
+ /**
+ * Method to determine whether this Component is droppable.
+ * @return {Boolean} the droppable state of this component.
+ */
+ isDroppable : function() {
+ return !!this.droppable;
+ },
+
+ /**
+ * @private
+ * Method to manage awareness of when components are added to their
+ * respective Container, firing an added event.
+ * References are established at add time rather than at render time.
+ * @param {Ext.container.Container} container Container which holds the component
+ * @param {Number} pos Position at which the component was added
+ */
+ onAdded : function(container, pos) {
+ this.ownerCt = container;
+ this.fireEvent('added', this, container, pos);
+ },
+
+ /**
+ * @private
+ * Method to manage awareness of when components are removed from their
+ * respective Container, firing an removed event. References are properly
+ * cleaned up after removing a component from its owning container.
+ */
+ onRemoved : function() {
+ var me = this;
+
+ me.fireEvent('removed', me, me.ownerCt);
+ delete me.ownerCt;
+ },
+
+ // @private
+ beforeDestroy : Ext.emptyFn,
+ // @private
+ // @private
+ onResize : Ext.emptyFn,
+
+ /**
+ * Sets the width and height of this Component. This method fires the {@link #resize} event. This method can accept
+ * either width and height as separate arguments, or you can pass a size object like `{width:10, height:20}`.
+ *
+ * @param {Number/String/Object} width The new width to set. This may be one of:
+ *
+ * - A Number specifying the new width in the {@link #getEl Element}'s {@link Ext.Element#defaultUnit}s (by default, pixels).
+ * - A String used to set the CSS width style.
+ * - A size object in the format `{width: widthValue, height: heightValue}`.
+ * - `undefined` to leave the width unchanged.
+ *
+ * @param {Number/String} height The new height to set (not required if a size object is passed as the first arg).
+ * This may be one of:
+ *
+ * - A Number specifying the new height in the {@link #getEl Element}'s {@link Ext.Element#defaultUnit}s (by default, pixels).
+ * - A String used to set the CSS height style. Animation may **not** be used.
+ * - `undefined` to leave the height unchanged.
+ *
+ * @return {Ext.Component} this
+ */
+ setSize : function(width, height) {
+ var me = this,
+ layoutCollection;
+
+ // support for standard size objects
+ if (Ext.isObject(width)) {
+ height = width.height;
+ width = width.width;
+ }
+
+ // Constrain within configured maxima
+ if (Ext.isNumber(width)) {
+ width = Ext.Number.constrain(width, me.minWidth, me.maxWidth);
+ }
+ if (Ext.isNumber(height)) {
+ height = Ext.Number.constrain(height, me.minHeight, me.maxHeight);
+ }
+
+ if (!me.rendered || !me.isVisible()) {
+ // If an ownerCt is hidden, add my reference onto the layoutOnShow stack. Set the needsLayout flag.
+ if (me.hiddenAncestor) {
+ layoutCollection = me.hiddenAncestor.layoutOnShow;
+ layoutCollection.remove(me);
+ layoutCollection.add(me);
+ }
+ me.needsLayout = {
+ width: width,
+ height: height,
+ isSetSize: true
+ };
+ if (!me.rendered) {
+ me.width = (width !== undefined) ? width : me.width;
+ me.height = (height !== undefined) ? height : me.height;
+ }
+ return me;
+ }
+ me.doComponentLayout(width, height, true);
+
+ return me;
+ },
+
+ isFixedWidth: function() {
+ var me = this,
+ layoutManagedWidth = me.layoutManagedWidth;
+
+ if (Ext.isDefined(me.width) || layoutManagedWidth == 1) {
+ return true;
+ }
+ if (layoutManagedWidth == 2) {
+ return false;
+ }
+ return (me.ownerCt && me.ownerCt.isFixedWidth());
+ },
+
+ isFixedHeight: function() {
+ var me = this,
+ layoutManagedHeight = me.layoutManagedHeight;
+
+ if (Ext.isDefined(me.height) || layoutManagedHeight == 1) {
+ return true;
+ }
+ if (layoutManagedHeight == 2) {
+ return false;
+ }
+ return (me.ownerCt && me.ownerCt.isFixedHeight());
+ },
+
+ setCalculatedSize : function(width, height, callingContainer) {
+ var me = this,
+ layoutCollection;
+
+ // support for standard size objects
+ if (Ext.isObject(width)) {
+ callingContainer = width.ownerCt;
+ height = width.height;
+ width = width.width;
+ }
+
+ // Constrain within configured maxima
+ if (Ext.isNumber(width)) {
+ width = Ext.Number.constrain(width, me.minWidth, me.maxWidth);
+ }
+ if (Ext.isNumber(height)) {
+ height = Ext.Number.constrain(height, me.minHeight, me.maxHeight);
+ }
+
+ if (!me.rendered || !me.isVisible()) {
+ // If an ownerCt is hidden, add my reference onto the layoutOnShow stack. Set the needsLayout flag.
+ if (me.hiddenAncestor) {
+ layoutCollection = me.hiddenAncestor.layoutOnShow;
+ layoutCollection.remove(me);
+ layoutCollection.add(me);
+ }
+ me.needsLayout = {
+ width: width,
+ height: height,
+ isSetSize: false,
+ ownerCt: callingContainer
+ };
+ return me;
+ }
+ me.doComponentLayout(width, height, false, callingContainer);
+
+ return me;
+ },
+
+ /**
+ * This method needs to be called whenever you change something on this component that requires the Component's
+ * layout to be recalculated.
+ * @param {Object} width
+ * @param {Object} height
+ * @param {Object} isSetSize
+ * @param {Object} callingContainer
+ * @return {Ext.container.Container} this
+ */
+ doComponentLayout : function(width, height, isSetSize, callingContainer) {
+ var me = this,
+ componentLayout = me.getComponentLayout(),
+ lastComponentSize = componentLayout.lastComponentSize || {
+ width: undefined,
+ height: undefined
+ };
+
+ // collapsed state is not relevant here, so no testing done.
+ // Only Panels have a collapse method, and that just sets the width/height such that only
+ // a single docked Header parallel to the collapseTo side are visible, and the Panel body is hidden.
+ if (me.rendered && componentLayout) {
+ // If no width passed, then only insert a value if the Component is NOT ALLOWED to autowidth itself.
+ if (!Ext.isDefined(width)) {
+ if (me.isFixedWidth()) {
+ width = Ext.isDefined(me.width) ? me.width : lastComponentSize.width;
+ }
+ }
+ // If no height passed, then only insert a value if the Component is NOT ALLOWED to autoheight itself.
+ if (!Ext.isDefined(height)) {
+ if (me.isFixedHeight()) {
+ height = Ext.isDefined(me.height) ? me.height : lastComponentSize.height;
+ }
+ }
+
+ if (isSetSize) {
+ me.width = width;
+ me.height = height;
+ }
+
+ componentLayout.layout(width, height, isSetSize, callingContainer);
+ }
+
+ return me;
+ },
+
+ /**
+ * Forces this component to redo its componentLayout.
+ */
+ forceComponentLayout: function () {
+ this.doComponentLayout();
+ },
+
+ // @private
+ setComponentLayout : function(layout) {
+ var currentLayout = this.componentLayout;
+ if (currentLayout && currentLayout.isLayout && currentLayout != layout) {
+ currentLayout.setOwner(null);
+ }
+ this.componentLayout = layout;
+ layout.setOwner(this);
+ },
+
+ getComponentLayout : function() {
+ var me = this;
+
+ if (!me.componentLayout || !me.componentLayout.isLayout) {
+ me.setComponentLayout(Ext.layout.Layout.create(me.componentLayout, 'autocomponent'));
+ }
+ return me.componentLayout;
+ },
+
+ /**
+ * Occurs after componentLayout is run.
+ * @param {Number} adjWidth The box-adjusted width that was set
+ * @param {Number} adjHeight The box-adjusted height that was set
+ * @param {Boolean} isSetSize Whether or not the height/width are stored on the component permanently
+ * @param {Ext.Component} callingContainer Container requesting the layout. Only used when isSetSize is false.
+ */
+ afterComponentLayout: function(width, height, isSetSize, callingContainer) {
+ var me = this,
+ layout = me.componentLayout,
+ oldSize = me.preLayoutSize;
+
+ ++me.componentLayoutCounter;
+ if (!oldSize || ((width !== oldSize.width) || (height !== oldSize.height))) {
+ me.fireEvent('resize', me, width, height);
+ }
+ },
+
+ /**
+ * Occurs before componentLayout is run. Returning false from this method will prevent the componentLayout from
+ * being executed.
+ * @param {Number} adjWidth The box-adjusted width that was set
+ * @param {Number} adjHeight The box-adjusted height that was set
+ * @param {Boolean} isSetSize Whether or not the height/width are stored on the component permanently
+ * @param {Ext.Component} callingContainer Container requesting sent the layout. Only used when isSetSize is false.
+ */
+ beforeComponentLayout: function(width, height, isSetSize, callingContainer) {
+ this.preLayoutSize = this.componentLayout.lastComponentSize;
+ return true;
+ },
+
+ /**
+ * Sets the left and top of the component. To set the page XY position instead, use
+ * {@link Ext.Component#setPagePosition setPagePosition}. This method fires the {@link #move} event.
+ * @param {Number} left The new left
+ * @param {Number} top The new top
+ * @return {Ext.Component} this
+ */
+ setPosition : function(x, y) {
+ var me = this;
+
+ if (Ext.isObject(x)) {
+ y = x.y;
+ x = x.x;
+ }
+
+ if (!me.rendered) {
+ return me;
+ }
+
+ if (x !== undefined || y !== undefined) {
+ me.el.setBox(x, y);
+ me.onPosition(x, y);
+ me.fireEvent('move', me, x, y);
+ }
+ return me;
+ },
+
+ /**
+ * @private
+ * Called after the component is moved, this method is empty by default but can be implemented by any
+ * subclass that needs to perform custom logic after a move occurs.
+ * @param {Number} x The new x position
+ * @param {Number} y The new y position
+ */
+ onPosition: Ext.emptyFn,
+
+ /**
+ * Sets the width of the component. This method fires the {@link #resize} event.
+ *
+ * @param {Number} width The new width to setThis may be one of:
+ *
+ * - A Number specifying the new width in the {@link #getEl Element}'s {@link Ext.Element#defaultUnit}s (by default, pixels).
+ * - A String used to set the CSS width style.
+ *
+ * @return {Ext.Component} this
+ */
+ setWidth : function(width) {
+ return this.setSize(width);
+ },
+
+ /**
+ * Sets the height of the component. This method fires the {@link #resize} event.
+ *
+ * @param {Number} height The new height to set. This may be one of:
+ *
+ * - A Number specifying the new height in the {@link #getEl Element}'s {@link Ext.Element#defaultUnit}s (by default, pixels).
+ * - A String used to set the CSS height style.
+ * - _undefined_ to leave the height unchanged.
+ *
+ * @return {Ext.Component} this
+ */
+ setHeight : function(height) {
+ return this.setSize(undefined, height);
+ },
+
+ /**
+ * Gets the current size of the component's underlying element.
+ * @return {Object} An object containing the element's size {width: (element width), height: (element height)}
+ */
+ getSize : function() {
+ return this.el.getSize();
+ },
+
+ /**
+ * Gets the current width of the component's underlying element.
+ * @return {Number}
+ */
+ getWidth : function() {
+ return this.el.getWidth();
+ },
+
+ /**
+ * Gets the current height of the component's underlying element.
+ * @return {Number}
+ */
+ getHeight : function() {
+ return this.el.getHeight();
+ },
+
+ /**
+ * Gets the {@link Ext.ComponentLoader} for this Component.
+ * @return {Ext.ComponentLoader} The loader instance, null if it doesn't exist.
+ */
+ getLoader: function(){
+ var me = this,
+ autoLoad = me.autoLoad ? (Ext.isObject(me.autoLoad) ? me.autoLoad : {url: me.autoLoad}) : null,
+ loader = me.loader || autoLoad;
+
+ if (loader) {
+ if (!loader.isLoader) {
+ me.loader = Ext.create('Ext.ComponentLoader', Ext.apply({
+ target: me,
+ autoLoad: autoLoad
+ }, loader));
+ } else {
+ loader.setTarget(me);
+ }
+ return me.loader;
+
+ }
+ return null;
+ },
+
+ /**
+ * This method allows you to show or hide a LoadMask on top of this component.
+ *
+ * @param {Boolean/Object/String} load True to show the default LoadMask, a config object that will be passed to the
+ * LoadMask constructor, or a message String to show. False to hide the current LoadMask.
+ * @param {Boolean} [targetEl=false] True to mask the targetEl of this Component instead of the `this.el`. For example,
+ * setting this to true on a Panel will cause only the body to be masked.
+ * @return {Ext.LoadMask} The LoadMask instance that has just been shown.
+ */
+ setLoading : function(load, targetEl) {
+ var me = this,
+ config;
+
+ if (me.rendered) {
+ if (load !== false && !me.collapsed) {
+ if (Ext.isObject(load)) {
+ config = load;
+ }
+ else if (Ext.isString(load)) {
+ config = {msg: load};
+ }
+ else {
+ config = {};
+ }
+ me.loadMask = me.loadMask || Ext.create('Ext.LoadMask', targetEl ? me.getTargetEl() : me.el, config);
+ me.loadMask.show();
+ } else if (me.loadMask) {
+ Ext.destroy(me.loadMask);
+ me.loadMask = null;
+ }
+ }
+
+ return me.loadMask;
+ },
+
+ /**
+ * Sets the dock position of this component in its parent panel. Note that this only has effect if this item is part
+ * of the dockedItems collection of a parent that has a DockLayout (note that any Panel has a DockLayout by default)
+ * @param {Object} dock The dock position.
+ * @param {Boolean} [layoutParent=false] True to re-layout parent.
+ * @return {Ext.Component} this
+ */
+ setDocked : function(dock, layoutParent) {
+ var me = this;
+
+ me.dock = dock;
+ if (layoutParent && me.ownerCt && me.rendered) {
+ me.ownerCt.doComponentLayout();
+ }
+ return me;
+ },
+
+ onDestroy : function() {
+ var me = this;
+
+ if (me.monitorResize && Ext.EventManager.resizeEvent) {
+ Ext.EventManager.resizeEvent.removeListener(me.setSize, me);
+ }
+ // Destroying the floatingItems ZIndexManager will also destroy descendant floating Components
+ Ext.destroy(
+ me.componentLayout,
+ me.loadMask,
+ me.floatingItems
+ );
+ },
+
+ /**
+ * Remove any references to elements added via renderSelectors/childEls
+ * @private
+ */
+ cleanElementRefs: function(){
+ var me = this,
+ i = 0,
+ childEls = me.childEls,
+ selectors = me.renderSelectors,
+ selector,
+ name,
+ len;
+
+ if (me.rendered) {
+ if (childEls) {
+ for (len = childEls.length; i < len; ++i) {
+ name = childEls[i];
+ if (typeof(name) != 'string') {
+ name = name.name;
+ }
+ delete me[name];
+ }
+ }
+
+ if (selectors) {
+ for (selector in selectors) {
+ if (selectors.hasOwnProperty(selector)) {
+ delete me[selector];
+ }
+ }
+ }
+ }
+ delete me.rendered;
+ delete me.el;
+ delete me.frameBody;
+ },
+
+ /**
+ * Destroys the Component.
+ */
+ destroy : function() {
+ var me = this;
+
+ if (!me.isDestroyed) {
+ if (me.fireEvent('beforedestroy', me) !== false) {
+ me.destroying = true;
+ me.beforeDestroy();
+
+ if (me.floating) {
+ delete me.floatParent;
+ // A zIndexManager is stamped into a *floating* Component when it is added to a Container.
+ // If it has no zIndexManager at render time, it is assigned to the global Ext.WindowManager instance.
+ if (me.zIndexManager) {
+ me.zIndexManager.unregister(me);
+ }
+ } else if (me.ownerCt && me.ownerCt.remove) {
+ me.ownerCt.remove(me, false);
+ }
+
+ me.onDestroy();
+
+ // Attempt to destroy all plugins
+ Ext.destroy(me.plugins);
+
+ if (me.rendered) {
+ me.el.remove();
+ }
+
+ me.fireEvent('destroy', me);
+ Ext.ComponentManager.unregister(me);
+
+ me.mixins.state.destroy.call(me);
+
+ me.clearListeners();
+ // make sure we clean up the element references after removing all events
+ me.cleanElementRefs();
+ me.destroying = false;
+ me.isDestroyed = true;
+ }
+ }
+ },
+
+ /**
+ * Retrieves a plugin by its pluginId which has been bound to this component.
+ * @param {Object} pluginId
+ * @return {Ext.AbstractPlugin} plugin instance.
+ */
+ getPlugin: function(pluginId) {
+ var i = 0,
+ plugins = this.plugins,
+ ln = plugins.length;
+ for (; i < ln; i++) {
+ if (plugins[i].pluginId === pluginId) {
+ return plugins[i];
+ }
+ }
+ },
+
+ /**
+ * Determines whether this component is the descendant of a particular container.
+ * @param {Ext.Container} container
+ * @return {Boolean} True if it is.
+ */
+ isDescendantOf: function(container) {
+ return !!this.findParentBy(function(p){
+ return p === container;
+ });
+ }
+}, function() {
+ this.createAlias({
+ on: 'addListener',
+ prev: 'previousSibling',
+ next: 'nextSibling'
+ });
+});
+
+/**
+ * The AbstractPlugin class is the base class from which user-implemented plugins should inherit.
+ *
+ * This class defines the essential API of plugins as used by Components by defining the following methods:
+ *
+ * - `init` : The plugin initialization method which the owning Component calls at Component initialization time.
+ *
+ * The Component passes itself as the sole parameter.
+ *
+ * Subclasses should set up bidirectional links between the plugin and its client Component here.
+ *
+ * - `destroy` : The plugin cleanup method which the owning Component calls at Component destruction time.
+ *
+ * Use this method to break links between the plugin and the Component and to free any allocated resources.
+ *
+ * - `enable` : The base implementation just sets the plugin's `disabled` flag to `false`
+ *
+ * - `disable` : The base implementation just sets the plugin's `disabled` flag to `true`
+ */
+Ext.define('Ext.AbstractPlugin', {
+ disabled: false,
+
+ constructor: function(config) {
+ Ext.apply(this, config);
+ },
+
+ getCmp: function() {
+ return this.cmp;
+ },
+
+ /**
+ * @method
+ * The init method is invoked after initComponent method has been run for the client Component.
+ *
+ * The supplied implementation is empty. Subclasses should perform plugin initialization, and set up bidirectional
+ * links between the plugin and its client Component in their own implementation of this method.
+ * @param {Ext.Component} client The client Component which owns this plugin.
+ */
+ init: Ext.emptyFn,
+
+ /**
+ * @method
+ * The destroy method is invoked by the owning Component at the time the Component is being destroyed.
+ *
+ * The supplied implementation is empty. Subclasses should perform plugin cleanup in their own implementation of
+ * this method.
+ */
+ destroy: Ext.emptyFn,
+
+ /**
+ * The base implementation just sets the plugin's `disabled` flag to `false`
+ *
+ * Plugin subclasses which need more complex processing may implement an overriding implementation.
+ */
+ enable: function() {
+ this.disabled = false;
+ },
+
+ /**
+ * The base implementation just sets the plugin's `disabled` flag to `true`
+ *
+ * Plugin subclasses which need more complex processing may implement an overriding implementation.
+ */
+ disable: function() {
+ this.disabled = true;
+ }
+});
+/**
+ * The Connection class encapsulates a connection to the page's originating domain, allowing requests to be made either
+ * to a configured URL, or to a URL specified at request time.
+ *
+ * Requests made by this class are asynchronous, and will return immediately. No data from the server will be available
+ * to the statement immediately following the {@link #request} call. To process returned data, use a success callback
+ * in the request options object, or an {@link #requestcomplete event listener}.
+ *
+ * # File Uploads
+ *
+ * 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 `<`, `&` as
+ * `&` 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.
+ *
+ * Also note that it's not possible to check the response code of the hidden iframe, so the success handler will ALWAYS fire.
+ */
+Ext.define('Ext.data.Connection', {
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ statics: {
+ requestId: 0
+ },
+
+ url: null,
+ async: true,
+ method: null,
+ username: '',
+ password: '',
+
+ /**
+ * @cfg {Boolean} disableCaching
+ * True to add a unique cache-buster param to GET requests.
+ */
+ disableCaching: true,
+
+ /**
+ * @cfg {Boolean} withCredentials
+ * True to set `withCredentials = true` on the XHR object
+ */
+ withCredentials: false,
+
+ /**
+ * @cfg {Boolean} cors
+ * True to enable CORS support on the XHR object. Currently the only effect of this option
+ * is to use the XDomainRequest object instead of XMLHttpRequest if the browser is IE8 or above.
+ */
+ cors: false,
+
+ /**
+ * @cfg {String} disableCachingParam
+ * Change the parameter which is sent went disabling caching through a cache buster.
+ */
+ disableCachingParam: '_dc',
+
+ /**
+ * @cfg {Number} timeout
+ * The timeout in milliseconds to be used for requests.
+ */
+ timeout : 30000,
+
+ /**
+ * @cfg {Object} extraParams
+ * Any parameters to be appended to the request.
+ */
+
+ useDefaultHeader : true,
+ defaultPostHeader : 'application/x-www-form-urlencoded; charset=UTF-8',
+ useDefaultXhrHeader : true,
+ defaultXhrHeader : 'XMLHttpRequest',
+
+ constructor : function(config) {
+ config = config || {};
+ Ext.apply(this, config);
+
+ this.addEvents(
+ /**
+ * @event beforerequest
+ * Fires before a network request is made to retrieve a data object.
+ * @param {Ext.data.Connection} conn This Connection object.
+ * @param {Object} options The options config object passed to the {@link #request} method.
+ */
+ 'beforerequest',
+ /**
+ * @event requestcomplete
+ * Fires if the request was successfully completed.
+ * @param {Ext.data.Connection} conn This Connection object.
+ * @param {Object} response The XHR object containing the response data.
+ * See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
+ * @param {Object} options The options config object passed to the {@link #request} method.
+ */
+ 'requestcomplete',
+ /**
+ * @event requestexception
+ * Fires if an error HTTP status was returned from the server.
+ * See [HTTP Status Code Definitions](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
+ * for details of HTTP status codes.
+ * @param {Ext.data.Connection} conn This Connection object.
+ * @param {Object} response The XHR object containing the response data.
+ * See [The XMLHttpRequest Object](http://www.w3.org/TR/XMLHttpRequest/) for details.
+ * @param {Object} options The options config object passed to the {@link #request} method.
+ */
+ 'requestexception'
+ );
+ this.requests = {};
+ this.mixins.observable.constructor.call(this);
+ },
+
+ /**
+ * Sends an HTTP request to a remote server.
+ *
+ * **Important:** Ajax server requests are asynchronous, and this call will
+ * return before the response has been received. Process any returned data
+ * in a callback function.
+ *
+ * Ext.Ajax.request({
+ * url: 'ajax_demo/sample.json',
+ * success: function(response, opts) {
+ * var obj = Ext.decode(response.responseText);
+ * console.dir(obj);
+ * },
+ * failure: function(response, opts) {
+ * console.log('server-side failure with status code ' + response.status);
+ * }
+ * });
+ *
+ * To execute a callback function in the correct scope, use the `scope` option.
+ *
+ * @param {Object} options An object which may contain the following properties:
+ *
+ * (The options object may also contain any other property which might be needed to perform
+ * postprocessing in a callback because it is passed to callback functions.)
+ *
+ * @param {String/Function} options.url The URL to which to send the request, or a function
+ * to call which returns a URL string. The scope of the function is specified by the `scope` option.
+ * Defaults to the configured `url`.
+ *
+ * @param {Object/String/Function} options.params An object containing properties which are
+ * used as parameters to the request, a url encoded string or a function to call to get either. The scope
+ * of the function is specified by the `scope` option.
+ *
+ * @param {String} options.method The HTTP method to use
+ * for the request. Defaults to the configured method, or if no method was configured,
+ * "GET" if no parameters are being sent, and "POST" if parameters are being sent. Note that
+ * the method name is case-sensitive and should be all caps.
+ *
+ * @param {Function} options.callback The function to be called upon receipt of the HTTP response.
+ * The callback is called regardless of success or failure and is passed the following parameters:
+ * @param {Object} options.callback.options The parameter to the request call.
+ * @param {Boolean} options.callback.success True if the request succeeded.
+ * @param {Object} options.callback.response The XMLHttpRequest object containing the response data.
+ * See [www.w3.org/TR/XMLHttpRequest/](http://www.w3.org/TR/XMLHttpRequest/) for details about
+ * accessing elements of the response.
+ *
+ * @param {Function} options.success The function to be called upon success of the request.
+ * The callback is passed the following parameters:
+ * @param {Object} options.success.response The XMLHttpRequest object containing the response data.
+ * @param {Object} options.success.options The parameter to the request call.
+ *
+ * @param {Function} options.failure The function to be called upon success of the request.
+ * The callback is passed the following parameters:
+ * @param {Object} options.failure.response The XMLHttpRequest object containing the response data.
+ * @param {Object} options.failure.options The parameter to the request call.
+ *
+ * @param {Object} options.scope The scope in which to execute the callbacks: The "this" object for
+ * the callback function. If the `url`, or `params` options were specified as functions from which to
+ * draw values, then this also serves as the scope for those function calls. Defaults to the browser
+ * window.
+ *
+ * @param {Number} options.timeout The timeout in milliseconds to be used for this request.
+ * Defaults to 30 seconds.
+ *
+ * @param {Ext.Element/HTMLElement/String} options.form The `<form>` Element or the id of the `<form>`
+ * to pull parameters from.
+ *
+ * @param {Boolean} options.isUpload **Only meaningful when used with the `form` option.**
+ *
+ * True if the form object is a file upload (will be set automatically if the form was configured
+ * with **`enctype`** `"multipart/form-data"`).
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * [target]: http://www.w3.org/TR/REC-html40/present/frames.html#adef-target
+ * [Content-Type]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17
+ * [multipart/form]: http://www.faqs.org/rfcs/rfc2388.html
+ *
+ * @param {Object} options.headers Request headers to set for the request.
+ *
+ * @param {Object} options.xmlData XML document to use for the post. Note: This will be used instead
+ * of params for the post data. Any params will be appended to the URL.
+ *
+ * @param {Object/String} options.jsonData JSON data to use as the post. Note: This will be used
+ * instead of params for the post data. Any params will be appended to the URL.
+ *
+ * @param {Boolean} options.disableCaching True to add a unique cache-buster param to GET requests.
+ *
+ * @param {Boolean} options.withCredentials True to add the withCredentials property to the XHR object
+ *
+ * @return {Object} The request object. This may be used to cancel the request.
+ */
+ request : function(options) {
+ options = options || {};
+ var me = this,
+ scope = options.scope || window,
+ username = options.username || me.username,
+ password = options.password || me.password || '',
+ async,
+ requestOptions,
+ request,
+ headers,
+ xhr;
+
+ if (me.fireEvent('beforerequest', me, options) !== false) {
+
+ requestOptions = me.setOptions(options, scope);
+
+ if (this.isFormUpload(options) === true) {
+ this.upload(options.form, requestOptions.url, requestOptions.data, options);
+ return null;
+ }
+
+ // if autoabort is set, cancel the current transactions
+ if (options.autoAbort === true || me.autoAbort) {
+ me.abort();
+ }
+
+ // create a connection object
+
+ if ((options.cors === true || me.cors === true) && Ext.isIE && Ext.ieVersion >= 8) {
+ xhr = new XDomainRequest();
+ } else {
+ xhr = this.getXhrInstance();
+ }
+
+ async = options.async !== false ? (options.async || me.async) : false;
+
+ // open the request
+ if (username) {
+ xhr.open(requestOptions.method, requestOptions.url, async, username, password);
+ } else {
+ xhr.open(requestOptions.method, requestOptions.url, async);
+ }
+
+ if (options.withCredentials === true || me.withCredentials === true) {
+ xhr.withCredentials = true;
+ }
+
+ headers = me.setupHeaders(xhr, options, requestOptions.data, requestOptions.params);
+
+ // create the transaction object
+ request = {
+ id: ++Ext.data.Connection.requestId,
+ xhr: xhr,
+ headers: headers,
+ options: options,
+ async: async,
+ timeout: setTimeout(function() {
+ request.timedout = true;
+ me.abort(request);
+ }, options.timeout || me.timeout)
+ };
+ me.requests[request.id] = request;
+ me.latestId = request.id;
+ // bind our statechange listener
+ if (async) {
+ xhr.onreadystatechange = Ext.Function.bind(me.onStateChange, me, [request]);
+ }
+
+ if ((options.cors === true || me.cors === true) && Ext.isIE && Ext.ieVersion >= 8) {
+ xhr.onload = function() {
+ me.onComplete(request);
+ }
+ }
+
+ // start the request!
+ xhr.send(requestOptions.data);
+ if (!async) {
+ return this.onComplete(request);
+ }
+ return request;
+ } else {
+ Ext.callback(options.callback, options.scope, [options, undefined, undefined]);
+ return null;
+ }
+ },
+
+ /**
+ * Uploads a form using a hidden iframe.
+ * @param {String/HTMLElement/Ext.Element} form The form to upload
+ * @param {String} url The url to post to
+ * @param {String} params Any extra parameters to pass
+ * @param {Object} options The initial options
+ */
+ upload: function(form, url, params, options) {
+ form = Ext.getDom(form);
+ options = options || {};
+
+ var id = Ext.id(),
+ frame = document.createElement('iframe'),
+ hiddens = [],
+ encoding = 'multipart/form-data',
+ buf = {
+ target: form.target,
+ method: form.method,
+ encoding: form.encoding,
+ enctype: form.enctype,
+ action: form.action
+ }, hiddenItem;
+
+ /*
+ * Originally this behaviour was modified for Opera 10 to apply the secure URL after
+ * the frame had been added to the document. It seems this has since been corrected in
+ * Opera so the behaviour has been reverted, the URL will be set before being added.
+ */
+ Ext.fly(frame).set({
+ id: id,
+ name: id,
+ cls: Ext.baseCSSPrefix + 'hide-display',
+ src: Ext.SSL_SECURE_URL
+ });
+
+ document.body.appendChild(frame);
+
+ // This is required so that IE doesn't pop the response up in a new window.
+ if (document.frames) {
+ document.frames[id].name = id;
+ }
+
+ Ext.fly(form).set({
+ target: id,
+ method: 'POST',
+ enctype: encoding,
+ encoding: encoding,
+ action: url || buf.action
+ });
+
+ // add dynamic params
+ if (params) {
+ Ext.iterate(Ext.Object.fromQueryString(params), function(name, value){
+ hiddenItem = document.createElement('input');
+ Ext.fly(hiddenItem).set({
+ type: 'hidden',
+ value: value,
+ name: name
+ });
+ form.appendChild(hiddenItem);
+ hiddens.push(hiddenItem);
+ });
+ }
+
+ Ext.fly(frame).on('load', Ext.Function.bind(this.onUploadComplete, this, [frame, options]), null, {single: true});
+ form.submit();
+
+ Ext.fly(form).set(buf);
+ Ext.each(hiddens, function(h) {
+ Ext.removeNode(h);
+ });
+ },
+
+ /**
+ * @private
+ * Callback handler for the upload function. After we've submitted the form via the iframe this creates a bogus
+ * response object to simulate an XHR and populates its responseText from the now-loaded iframe's document body
+ * (or a textarea inside the body). We then clean up by removing the iframe
+ */
+ onUploadComplete: function(frame, options) {
+ var me = this,
+ // bogus response object
+ response = {
+ responseText: '',
+ responseXML: null
+ }, doc, firstChild;
+
+ try {
+ doc = frame.contentWindow.document || frame.contentDocument || window.frames[frame.id].document;
+ if (doc) {
+ if (doc.body) {
+ if (/textarea/i.test((firstChild = doc.body.firstChild || {}).tagName)) { // json response wrapped in textarea
+ response.responseText = firstChild.value;
+ } else {
+ response.responseText = doc.body.innerHTML;
+ }
+ }
+ //in IE the document may still have a body even if returns XML.
+ response.responseXML = doc.XMLDocument || doc;
+ }
+ } catch (e) {
+ }
+
+ me.fireEvent('requestcomplete', me, response, options);
+
+ Ext.callback(options.success, options.scope, [response, options]);
+ Ext.callback(options.callback, options.scope, [options, true, response]);
+
+ setTimeout(function(){
+ Ext.removeNode(frame);
+ }, 100);
+ },
+
+ /**
+ * Detects whether the form is intended to be used for an upload.
+ * @private
+ */
+ isFormUpload: function(options){
+ var form = this.getForm(options);
+ if (form) {
+ return (options.isUpload || (/multipart\/form-data/i).test(form.getAttribute('enctype')));
+ }
+ return false;
+ },
+
+ /**
+ * Gets the form object from options.
+ * @private
+ * @param {Object} options The request options
+ * @return {HTMLElement} The form, null if not passed
+ */
+ getForm: function(options){
+ return Ext.getDom(options.form) || null;
+ },
+
+ /**
+ * Sets various options such as the url, params for the request
+ * @param {Object} options The initial options
+ * @param {Object} scope The scope to execute in
+ * @return {Object} The params for the request
+ */
+ setOptions: function(options, scope){
+ var me = this,
+ params = options.params || {},
+ extraParams = me.extraParams,
+ urlParams = options.urlParams,
+ url = options.url || me.url,
+ jsonData = options.jsonData,
+ method,
+ disableCache,
+ data;
+
+
+ // allow params to be a method that returns the params object
+ if (Ext.isFunction(params)) {
+ params = params.call(scope, options);
+ }
+
+ // allow url to be a method that returns the actual url
+ if (Ext.isFunction(url)) {
+ url = url.call(scope, options);
+ }
+
+ url = this.setupUrl(options, url);
+
+
+ // check for xml or json data, and make sure json data is encoded
+ data = options.rawData || options.xmlData || jsonData || null;
+ if (jsonData && !Ext.isPrimitive(jsonData)) {
+ data = Ext.encode(data);
+ }
+
+ // make sure params are a url encoded string and include any extraParams if specified
+ if (Ext.isObject(params)) {
+ params = Ext.Object.toQueryString(params);
+ }
+
+ if (Ext.isObject(extraParams)) {
+ extraParams = Ext.Object.toQueryString(extraParams);
+ }
+
+ params = params + ((extraParams) ? ((params) ? '&' : '') + extraParams : '');
+
+ urlParams = Ext.isObject(urlParams) ? Ext.Object.toQueryString(urlParams) : urlParams;
+
+ params = this.setupParams(options, params);
+
+ // decide the proper method for this request
+ method = (options.method || me.method || ((params || data) ? 'POST' : 'GET')).toUpperCase();
+ this.setupMethod(options, method);
+
+
+ disableCache = options.disableCaching !== false ? (options.disableCaching || me.disableCaching) : false;
+ // if the method is get append date to prevent caching
+ if (method === 'GET' && disableCache) {
+ url = Ext.urlAppend(url, (options.disableCachingParam || me.disableCachingParam) + '=' + (new Date().getTime()));
+ }
+
+ // if the method is get or there is json/xml data append the params to the url
+ if ((method == 'GET' || data) && params) {
+ url = Ext.urlAppend(url, params);
+ params = null;
+ }
+
+ // allow params to be forced into the url
+ if (urlParams) {
+ url = Ext.urlAppend(url, urlParams);
+ }
+
+ return {
+ url: url,
+ method: method,
+ data: data || params || null
+ };
+ },
+
+ /**
+ * Template method for overriding url
+ * @template
+ * @private
+ * @param {Object} options
+ * @param {String} url
+ * @return {String} The modified url
+ */
+ setupUrl: function(options, url){
+ var form = this.getForm(options);
+ if (form) {
+ url = url || form.action;
+ }
+ return url;
+ },
+
+
+ /**
+ * Template method for overriding params
+ * @template
+ * @private
+ * @param {Object} options
+ * @param {String} params
+ * @return {String} The modified params
+ */
+ setupParams: function(options, params) {
+ var form = this.getForm(options),
+ serializedForm;
+ if (form && !this.isFormUpload(options)) {
+ serializedForm = Ext.Element.serializeForm(form);
+ params = params ? (params + '&' + serializedForm) : serializedForm;
+ }
+ return params;
+ },
+
+ /**
+ * Template method for overriding method
+ * @template
+ * @private
+ * @param {Object} options
+ * @param {String} method
+ * @return {String} The modified method
+ */
+ setupMethod: function(options, method){
+ if (this.isFormUpload(options)) {
+ return 'POST';
+ }
+ return method;
+ },
+
+ /**
+ * Setup all the headers for the request
+ * @private
+ * @param {Object} xhr The xhr object
+ * @param {Object} options The options for the request
+ * @param {Object} data The data for the request
+ * @param {Object} params The params for the request
+ */
+ setupHeaders: function(xhr, options, data, params){
+ var me = this,
+ headers = Ext.apply({}, options.headers || {}, me.defaultHeaders || {}),
+ contentType = me.defaultPostHeader,
+ jsonData = options.jsonData,
+ xmlData = options.xmlData,
+ key,
+ header;
+
+ if (!headers['Content-Type'] && (data || params)) {
+ if (data) {
+ if (options.rawData) {
+ contentType = 'text/plain';
+ } else {
+ if (xmlData && Ext.isDefined(xmlData)) {
+ contentType = 'text/xml';
+ } else if (jsonData && Ext.isDefined(jsonData)) {
+ contentType = 'application/json';
+ }
+ }
+ }
+ headers['Content-Type'] = contentType;
+ }
+
+ if (me.useDefaultXhrHeader && !headers['X-Requested-With']) {
+ headers['X-Requested-With'] = me.defaultXhrHeader;
+ }
+ // set up all the request headers on the xhr object
+ try{
+ for (key in headers) {
+ if (headers.hasOwnProperty(key)) {
+ header = headers[key];
+ xhr.setRequestHeader(key, header);
+ }
+
+ }
+ } catch(e) {
+ me.fireEvent('exception', key, header);
+ }
+ return headers;
+ },
+
+ /**
+ * Creates the appropriate XHR transport for the browser.
+ * @private
+ */
+ getXhrInstance: (function(){
+ var options = [function(){
+ return new XMLHttpRequest();
+ }, function(){
+ return new ActiveXObject('MSXML2.XMLHTTP.3.0');
+ }, function(){
+ return new ActiveXObject('MSXML2.XMLHTTP');
+ }, function(){
+ return new ActiveXObject('Microsoft.XMLHTTP');
+ }], i = 0,
+ len = options.length,
+ xhr;
+
+ for(; i < len; ++i) {
+ try{
+ xhr = options[i];
+ xhr();
+ break;
+ }catch(e){}
+ }
+ return xhr;
+ })(),
+
+ /**
+ * Determines whether this object has a request outstanding.
+ * @param {Object} [request] Defaults to the last transaction
+ * @return {Boolean} True if there is an outstanding request.
+ */
+ isLoading : function(request) {
+ if (!request) {
+ request = this.getLatest();
+ }
+ if (!(request && request.xhr)) {
+ return false;
+ }
+ // if there is a connection and readyState is not 0 or 4
+ var state = request.xhr.readyState;
+ return !(state === 0 || state == 4);
+ },
+
+ /**
+ * Aborts an active request.
+ * @param {Object} [request] Defaults to the last request
+ */
+ abort : function(request) {
+ var me = this;
+
+ if (!request) {
+ request = me.getLatest();
+ }
+
+ if (request && me.isLoading(request)) {
+ /*
+ * Clear out the onreadystatechange here, this allows us
+ * greater control, the browser may/may not fire the function
+ * depending on a series of conditions.
+ */
+ request.xhr.onreadystatechange = null;
+ request.xhr.abort();
+ me.clearTimeout(request);
+ if (!request.timedout) {
+ request.aborted = true;
+ }
+ me.onComplete(request);
+ me.cleanup(request);
+ }
+ },
+
+ /**
+ * Aborts all active requests
+ */
+ abortAll: function(){
+ var requests = this.requests,
+ id;
+
+ for (id in requests) {
+ if (requests.hasOwnProperty(id)) {
+ this.abort(requests[id]);
+ }
+ }
+ },
+
+ /**
+ * Gets the most recent request
+ * @private
+ * @return {Object} The request. Null if there is no recent request
+ */
+ getLatest: function(){
+ var id = this.latestId,
+ request;
+
+ if (id) {
+ request = this.requests[id];
+ }
+ return request || null;
+ },
+
+ /**
+ * Fires when the state of the xhr changes
+ * @private
+ * @param {Object} request The request
+ */
+ onStateChange : function(request) {
+ if (request.xhr.readyState == 4) {
+ this.clearTimeout(request);
+ this.onComplete(request);
+ this.cleanup(request);
+ }
+ },
+
+ /**
+ * Clears the timeout on the request
+ * @private
+ * @param {Object} The request
+ */
+ clearTimeout: function(request){
+ clearTimeout(request.timeout);
+ delete request.timeout;
+ },
+
+ /**
+ * Cleans up any left over information from the request
+ * @private
+ * @param {Object} The request
+ */
+ cleanup: function(request){
+ request.xhr = null;
+ delete request.xhr;
+ },
+
+ /**
+ * To be called when the request has come back from the server
+ * @private
+ * @param {Object} request
+ * @return {Object} The response
+ */
+ onComplete : function(request) {
+ var me = this,
+ options = request.options,
+ result,
+ success,
+ response;
+
+ try {
+ result = me.parseStatus(request.xhr.status);
+ } catch (e) {
+ // in some browsers we can't access the status if the readyState is not 4, so the request has failed
+ result = {
+ success : false,
+ isException : false
+ };
+ }
+ success = result.success;
+
+ if (success) {
+ response = me.createResponse(request);
+ me.fireEvent('requestcomplete', me, response, options);
+ Ext.callback(options.success, options.scope, [response, options]);
+ } else {
+ if (result.isException || request.aborted || request.timedout) {
+ response = me.createException(request);
+ } else {
+ response = me.createResponse(request);
+ }
+ me.fireEvent('requestexception', me, response, options);
+ Ext.callback(options.failure, options.scope, [response, options]);
+ }
+ Ext.callback(options.callback, options.scope, [options, success, response]);
+ delete me.requests[request.id];
+ return response;
+ },
+
+ /**
+ * Checks if the response status was successful
+ * @param {Number} status The status code
+ * @return {Object} An object containing success/status state
+ */
+ parseStatus: function(status) {
+ // see: https://prototype.lighthouseapp.com/projects/8886/tickets/129-ie-mangles-http-response-status-code-204-to-1223
+ status = status == 1223 ? 204 : status;
+
+ var success = (status >= 200 && status < 300) || status == 304,
+ isException = false;
+
+ if (!success) {
+ switch (status) {
+ case 12002:
+ case 12029:
+ case 12030:
+ case 12031:
+ case 12152:
+ case 13030:
+ isException = true;
+ break;
+ }
+ }
+ return {
+ success: success,
+ isException: isException
+ };
+ },
+
+ /**
+ * Creates the response object
+ * @private
+ * @param {Object} request
+ */
+ createResponse : function(request) {
+ var xhr = request.xhr,
+ headers = {},
+ lines = xhr.getAllResponseHeaders ? xhr.getAllResponseHeaders().replace(/\r\n/g, '\n').split('\n') : [],
+ count = lines.length,
+ line, index, key, value, response;
+
+ while (count--) {
+ line = lines[count];
+ index = line.indexOf(':');
+ if(index >= 0) {
+ key = line.substr(0, index).toLowerCase();
+ if (line.charAt(index + 1) == ' ') {
+ ++index;
+ }
+ headers[key] = line.substr(index + 1);
+ }
+ }
+
+ request.xhr = null;
+ delete request.xhr;
+
+ response = {
+ request: request,
+ requestId : request.id,
+ status : xhr.status,
+ statusText : xhr.statusText,
+ getResponseHeader : function(header){ return headers[header.toLowerCase()]; },
+ getAllResponseHeaders : function(){ return headers; },
+ responseText : xhr.responseText,
+ responseXML : xhr.responseXML
+ };
+
+ // If we don't explicitly tear down the xhr reference, IE6/IE7 will hold this in the closure of the
+ // functions created with getResponseHeader/getAllResponseHeaders
+ xhr = null;
+ return response;
+ },
+
+ /**
+ * Creates the exception object
+ * @private
+ * @param {Object} request
+ */
+ createException : function(request) {
+ return {
+ request : request,
+ requestId : request.id,
+ status : request.aborted ? -1 : 0,
+ statusText : request.aborted ? 'transaction aborted' : 'communication failure',
+ aborted: request.aborted,
+ timedout: request.timedout
+ };
+ }
+});
+
+/**
+ * @class Ext.Ajax
+ * @singleton
+ * @markdown
+ * @extends Ext.data.Connection
+
+A singleton instance of an {@link Ext.data.Connection}. This class
+is used to communicate with your server side code. It can be used as follows:
+
+ Ext.Ajax.request({
+ url: 'page.php',
+ params: {
+ id: 1
+ },
+ success: function(response){
+ var text = response.responseText;
+ // process server response here
+ }
+ });
+
+Default options for all requests can be set by changing a property on the Ext.Ajax class:
+
+ Ext.Ajax.timeout = 60000; // 60 seconds
+
+Any options specified in the request method for the Ajax request will override any
+defaults set on the Ext.Ajax class. In the code sample below, the timeout for the
+request will be 60 seconds.
+
+ Ext.Ajax.timeout = 120000; // 120 seconds
+ Ext.Ajax.request({
+ url: 'page.aspx',
+ timeout: 60000
+ });
+
+In general, this class will be used for all Ajax requests in your application.
+The main reason for creating a separate {@link Ext.data.Connection} is for a
+series of requests that share common settings that are different to all other
+requests in the application.
+
+ */
+Ext.define('Ext.Ajax', {
+ extend: 'Ext.data.Connection',
+ singleton: true,
+
+ /**
+ * @cfg {String} url @hide
+ */
+ /**
+ * @cfg {Object} extraParams @hide
+ */
+ /**
+ * @cfg {Object} defaultHeaders @hide
+ */
+ /**
+ * @cfg {String} method (Optional) @hide
+ */
+ /**
+ * @cfg {Number} timeout (Optional) @hide
+ */
+ /**
+ * @cfg {Boolean} autoAbort (Optional) @hide
+ */
+
+ /**
+ * @cfg {Boolean} disableCaching (Optional) @hide
+ */
+
+ /**
+ * @property {Boolean} disableCaching
+ * True to add a unique cache-buster param to GET requests. Defaults to true.
+ */
+ /**
+ * @property {String} url
+ * The default URL to be used for requests to the server.
+ * If the server receives all requests through one URL, setting this once is easier than
+ * entering it on every request.
+ */
+ /**
+ * @property {Object} extraParams
+ * An object containing properties which are used as extra parameters to each request made
+ * by this object. Session information and other data that you need
+ * to pass with each request are commonly put here.
+ */
+ /**
+ * @property {Object} defaultHeaders
+ * An object containing request headers which are added to each request made by this object.
+ */
+ /**
+ * @property {String} method
+ * The default HTTP method to be used for requests. Note that this is case-sensitive and
+ * should be all caps (if not set but params are present will use
+ * <tt>"POST"</tt>, otherwise will use <tt>"GET"</tt>.)
+ */
+ /**
+ * @property {Number} timeout
+ * The timeout in milliseconds to be used for requests. Defaults to 30000.
+ */
+
+ /**
+ * @property {Boolean} autoAbort
+ * Whether a new request should abort any pending requests.
+ */
+ autoAbort : false
+});
+/**
+ * A class used to load remote content to an Element. Sample usage:
+ *
+ * Ext.get('el').load({
+ * url: 'myPage.php',
+ * scripts: true,
+ * params: {
+ * id: 1
+ * }
+ * });
+ *
+ * In general this class will not be instanced directly, rather the {@link Ext.Element#load} method
+ * will be used.
+ */
+Ext.define('Ext.ElementLoader', {
+
+ /* Begin Definitions */
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ uses: [
+ 'Ext.data.Connection',
+ 'Ext.Ajax'
+ ],
+
+ statics: {
+ Renderer: {
+ Html: function(loader, response, active){
+ loader.getTarget().update(response.responseText, active.scripts === true);
+ return true;
+ }
+ }
+ },
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String} url
+ * The url to retrieve the content from.
+ */
+ url: null,
+
+ /**
+ * @cfg {Object} params
+ * Any params to be attached to the Ajax request. These parameters will
+ * be overridden by any params in the load options.
+ */
+ params: null,
+
+ /**
+ * @cfg {Object} baseParams Params that will be attached to every request. These parameters
+ * will not be overridden by any params in the load options.
+ */
+ baseParams: null,
+
+ /**
+ * @cfg {Boolean/Object} autoLoad
+ * True to have the loader make a request as soon as it is created.
+ * This argument can also be a set of options that will be passed to {@link #load} is called.
+ */
+ autoLoad: false,
+
+ /**
+ * @cfg {HTMLElement/Ext.Element/String} target
+ * The target element for the loader. It can be the DOM element, the id or an {@link Ext.Element}.
+ */
+ target: null,
+
+ /**
+ * @cfg {Boolean/String} loadMask
+ * True or a string to show when the element is loading.
+ */
+ loadMask: false,
+
+ /**
+ * @cfg {Object} ajaxOptions
+ * Any additional options to be passed to the request, for example timeout or headers.
+ */
+ ajaxOptions: null,
+
+ /**
+ * @cfg {Boolean} scripts
+ * True to parse any inline script tags in the response.
+ */
+ scripts: false,
+
+ /**
+ * @cfg {Function} success
+ * A function to be called when a load request is successful.
+ * Will be called with the following config parameters:
+ *
+ * - this - The ElementLoader instance.
+ * - response - The response object.
+ * - options - Ajax options.
+ */
+
+ /**
+ * @cfg {Function} failure A function to be called when a load request fails.
+ * Will be called with the following config parameters:
+ *
+ * - this - The ElementLoader instance.
+ * - response - The response object.
+ * - options - Ajax options.
+ */
+
+ /**
+ * @cfg {Function} callback A function to be called when a load request finishes.
+ * Will be called with the following config parameters:
+ *
+ * - this - The ElementLoader instance.
+ * - success - True if successful request.
+ * - response - The response object.
+ * - options - Ajax options.
+ */
+
+ /**
+ * @cfg {Object} scope
+ * The scope to execute the {@link #success} and {@link #failure} functions in.
+ */
+
+ /**
+ * @cfg {Function} renderer
+ * A custom function to render the content to the element. The passed parameters are:
+ *
+ * - The loader
+ * - The response
+ * - The active request
+ */
+
+ isLoader: true,
+
+ constructor: function(config) {
+ var me = this,
+ autoLoad;
+
+ config = config || {};
+ Ext.apply(me, config);
+ me.setTarget(me.target);
+ me.addEvents(
+ /**
+ * @event beforeload
+ * Fires before a load request is made to the server.
+ * Returning false from an event listener can prevent the load
+ * from occurring.
+ * @param {Ext.ElementLoader} this
+ * @param {Object} options The options passed to the request
+ */
+ 'beforeload',
+
+ /**
+ * @event exception
+ * Fires after an unsuccessful load.
+ * @param {Ext.ElementLoader} this
+ * @param {Object} response The response from the server
+ * @param {Object} options The options passed to the request
+ */
+ 'exception',
+
+ /**
+ * @event load
+ * Fires after a successful load.
+ * @param {Ext.ElementLoader} this
+ * @param {Object} response The response from the server
+ * @param {Object} options The options passed to the request
+ */
+ 'load'
+ );
+
+ // don't pass config because we have already applied it.
+ me.mixins.observable.constructor.call(me);
+
+ if (me.autoLoad) {
+ autoLoad = me.autoLoad;
+ if (autoLoad === true) {
+ autoLoad = {};
+ }
+ me.load(autoLoad);
+ }
+ },
+
+ /**
+ * Sets an {@link Ext.Element} as the target of this loader.
+ * Note that if the target is changed, any active requests will be aborted.
+ * @param {String/HTMLElement/Ext.Element} target The element or its ID.
+ */
+ setTarget: function(target){
+ var me = this;
+ target = Ext.get(target);
+ if (me.target && me.target != target) {
+ me.abort();
+ }
+ me.target = target;
+ },
+
+ /**
+ * Returns the target of this loader.
+ * @return {Ext.Component} The target or null if none exists.
+ */
+ getTarget: function(){
+ return this.target || null;
+ },
+
+ /**
+ * Aborts the active load request
+ */
+ abort: function(){
+ var active = this.active;
+ if (active !== undefined) {
+ Ext.Ajax.abort(active.request);
+ if (active.mask) {
+ this.removeMask();
+ }
+ delete this.active;
+ }
+ },
+
+ /**
+ * Removes the mask on the target
+ * @private
+ */
+ removeMask: function(){
+ this.target.unmask();
+ },
+
+ /**
+ * Adds the mask on the target
+ * @private
+ * @param {Boolean/Object} mask The mask configuration
+ */
+ addMask: function(mask){
+ this.target.mask(mask === true ? null : mask);
+ },
+
+ /**
+ * Loads new data from the server.
+ * @param {Object} options The options for the request. They can be any configuration option that can be specified for
+ * the class, with the exception of the target option. Note that any options passed to the method will override any
+ * class defaults.
+ */
+ load: function(options) {
+
+ options = Ext.apply({}, options);
+
+ var me = this,
+ target = me.target,
+ mask = Ext.isDefined(options.loadMask) ? options.loadMask : me.loadMask,
+ params = Ext.apply({}, options.params),
+ ajaxOptions = Ext.apply({}, options.ajaxOptions),
+ callback = options.callback || me.callback,
+ scope = options.scope || me.scope || me,
+ request;
+
+ Ext.applyIf(ajaxOptions, me.ajaxOptions);
+ Ext.applyIf(options, ajaxOptions);
+
+ Ext.applyIf(params, me.params);
+ Ext.apply(params, me.baseParams);
+
+ Ext.applyIf(options, {
+ url: me.url
+ });
+
+
+ Ext.apply(options, {
+ scope: me,
+ params: params,
+ callback: me.onComplete
+ });
+
+ if (me.fireEvent('beforeload', me, options) === false) {
+ return;
+ }
+
+ if (mask) {
+ me.addMask(mask);
+ }
+
+ request = Ext.Ajax.request(options);
+ me.active = {
+ request: request,
+ options: options,
+ mask: mask,
+ scope: scope,
+ callback: callback,
+ success: options.success || me.success,
+ failure: options.failure || me.failure,
+ renderer: options.renderer || me.renderer,
+ scripts: Ext.isDefined(options.scripts) ? options.scripts : me.scripts
+ };
+ me.setOptions(me.active, options);
+ },
+
+ /**
+ * Sets any additional options on the active request
+ * @private
+ * @param {Object} active The active request
+ * @param {Object} options The initial options
+ */
+ setOptions: Ext.emptyFn,
+
+ /**
+ * Parses the response after the request completes
+ * @private
+ * @param {Object} options Ajax options
+ * @param {Boolean} success Success status of the request
+ * @param {Object} response The response object
+ */
+ onComplete: function(options, success, response) {
+ var me = this,
+ active = me.active,
+ scope = active.scope,
+ renderer = me.getRenderer(active.renderer);
+
+
+ if (success) {
+ success = renderer.call(me, me, response, active);
+ }
+
+ if (success) {
+ Ext.callback(active.success, scope, [me, response, options]);
+ me.fireEvent('load', me, response, options);
+ } else {
+ Ext.callback(active.failure, scope, [me, response, options]);
+ me.fireEvent('exception', me, response, options);
+ }
+ Ext.callback(active.callback, scope, [me, success, response, options]);
+
+ if (active.mask) {
+ me.removeMask();
+ }
+
+ delete me.active;
+ },
+
+ /**
+ * Gets the renderer to use
+ * @private
+ * @param {String/Function} renderer The renderer to use
+ * @return {Function} A rendering function to use.
+ */
+ getRenderer: function(renderer){
+ if (Ext.isFunction(renderer)) {
+ return renderer;
+ }
+ return this.statics().Renderer.Html;
+ },
+
+ /**
+ * Automatically refreshes the content over a specified period.
+ * @param {Number} interval The interval to refresh in ms.
+ * @param {Object} options (optional) The options to pass to the load method. See {@link #load}
+ */
+ startAutoRefresh: function(interval, options){
+ var me = this;
+ me.stopAutoRefresh();
+ me.autoRefresh = setInterval(function(){
+ me.load(options);
+ }, interval);
+ },
+
+ /**
+ * Clears any auto refresh. See {@link #startAutoRefresh}.
+ */
+ stopAutoRefresh: function(){
+ clearInterval(this.autoRefresh);
+ delete this.autoRefresh;
+ },
+
+ /**
+ * Checks whether the loader is automatically refreshing. See {@link #startAutoRefresh}.
+ * @return {Boolean} True if the loader is automatically refreshing
+ */
+ isAutoRefreshing: function(){
+ return Ext.isDefined(this.autoRefresh);
+ },
+
+ /**
+ * Destroys the loader. Any active requests will be aborted.
+ */
+ destroy: function(){
+ var me = this;
+ me.stopAutoRefresh();
+ delete me.target;
+ me.abort();
+ me.clearListeners();
+ }
+});
+
+/**
+ * @class Ext.ComponentLoader
+ * @extends Ext.ElementLoader
+ *
+ * This class is used to load content via Ajax into a {@link Ext.Component}. In general
+ * this class will not be instanced directly, rather a loader configuration will be passed to the
+ * constructor of the {@link Ext.Component}.
+ *
+ * ## HTML Renderer
+ * By default, the content loaded will be processed as raw html. The response text
+ * from the request is taken and added to the component. This can be used in
+ * conjunction with the {@link #scripts} option to execute any inline scripts in
+ * the resulting content. Using this renderer has the same effect as passing the
+ * {@link Ext.Component#html} configuration option.
+ *
+ * ## Data Renderer
+ * This renderer allows content to be added by using JSON data and a {@link Ext.XTemplate}.
+ * The content received from the response is passed to the {@link Ext.Component#update} method.
+ * This content is run through the attached {@link Ext.Component#tpl} and the data is added to
+ * the Component. Using this renderer has the same effect as using the {@link Ext.Component#data}
+ * configuration in conjunction with a {@link Ext.Component#tpl}.
+ *
+ * ## Component Renderer
+ * This renderer can only be used with a {@link Ext.container.Container} and subclasses. It allows for
+ * Components to be loaded remotely into a Container. The response is expected to be a single/series of
+ * {@link Ext.Component} configuration objects. When the response is received, the data is decoded
+ * and then passed to {@link Ext.container.Container#add}. Using this renderer has the same effect as specifying
+ * the {@link Ext.container.Container#items} configuration on a Container.
+ *
+ * ## Custom Renderer
+ * A custom function can be passed to handle any other special case, see the {@link #renderer} option.
+ *
+ * ## Example Usage
+ * new Ext.Component({
+ * tpl: '{firstName} - {lastName}',
+ * loader: {
+ * url: 'myPage.php',
+ * renderer: 'data',
+ * params: {
+ * userId: 1
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.ComponentLoader', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.ElementLoader',
+
+ statics: {
+ Renderer: {
+ Data: function(loader, response, active){
+ var success = true;
+ try {
+ loader.getTarget().update(Ext.decode(response.responseText));
+ } catch (e) {
+ success = false;
+ }
+ return success;
+ },
+
+ Component: function(loader, response, active){
+ var success = true,
+ target = loader.getTarget(),
+ items = [];
+
+
+ try {
+ items = Ext.decode(response.responseText);
+ } catch (e) {
+ success = false;
+ }
+
+ if (success) {
+ if (active.removeAll) {
+ target.removeAll();
+ }
+ target.add(items);
+ }
+ return success;
+ }
+ }
+ },
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Ext.Component/String} target The target {@link Ext.Component} for the loader.
+ * If a string is passed it will be looked up via the id.
+ */
+ target: null,
+
+ /**
+ * @cfg {Boolean/Object} loadMask True or a {@link Ext.LoadMask} configuration to enable masking during loading.
+ */
+ loadMask: false,
+
+ /**
+ * @cfg {Boolean} scripts True to parse any inline script tags in the response. This only used when using the html
+ * {@link #renderer}.
+ */
+
+ /**
+ * @cfg {String/Function} renderer
+
+The type of content that is to be loaded into, which can be one of 3 types:
+
++ **html** : Loads raw html content, see {@link Ext.Component#html}
++ **data** : Loads raw html content, see {@link Ext.Component#data}
++ **component** : Loads child {Ext.Component} instances. This option is only valid when used with a Container.
+
+Alternatively, you can pass a function which is called with the following parameters.
+
++ loader - Loader instance
++ response - The server response
++ active - The active request
+
+The function must return false is loading is not successful. Below is a sample of using a custom renderer:
+
+ new Ext.Component({
+ loader: {
+ url: 'myPage.php',
+ renderer: function(loader, response, active) {
+ var text = response.responseText;
+ loader.getTarget().update('The response is ' + text);
+ return true;
+ }
+ }
+ });
+ */
+ renderer: 'html',
+
+ /**
+ * Set a {Ext.Component} as the target of this loader. Note that if the target is changed,
+ * any active requests will be aborted.
+ * @param {String/Ext.Component} target The component to be the target of this loader. If a string is passed
+ * it will be looked up via its id.
+ */
+ setTarget: function(target){
+ var me = this;
+
+ if (Ext.isString(target)) {
+ target = Ext.getCmp(target);
+ }
+
+ if (me.target && me.target != target) {
+ me.abort();
+ }
+ me.target = target;
+ },
+
+ // inherit docs
+ removeMask: function(){
+ this.target.setLoading(false);
+ },
+
+ /**
+ * Add the mask on the target
+ * @private
+ * @param {Boolean/Object} mask The mask configuration
+ */
+ addMask: function(mask){
+ this.target.setLoading(mask);
+ },
+
+ /**
+ * Get the target of this loader.
+ * @return {Ext.Component} target The target, null if none exists.
+ */
+
+ setOptions: function(active, options){
+ active.removeAll = Ext.isDefined(options.removeAll) ? options.removeAll : this.removeAll;
+ },
+
+ /**
+ * Gets the renderer to use
+ * @private
+ * @param {String/Function} renderer The renderer to use
+ * @return {Function} A rendering function to use.
+ */
+ getRenderer: function(renderer){
+ if (Ext.isFunction(renderer)) {
+ return renderer;
+ }
+
+ var renderers = this.statics().Renderer;
+ switch (renderer) {
+ case 'component':
+ return renderers.Component;
+ case 'data':
+ return renderers.Data;
+ default:
+ return Ext.ElementLoader.Renderer.Html;
+ }
+ }
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * Associations enable you to express relationships between different {@link Ext.data.Model Models}. Let's say we're
+ * writing an ecommerce system where Users can make Orders - there's a relationship between these Models that we can
+ * express like this:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', 'email'],
+ *
+ * hasMany: {model: 'Order', name: 'orders'}
+ * });
+ *
+ * Ext.define('Order', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'user_id', 'status', 'price'],
+ *
+ * belongsTo: 'User'
+ * });
+ *
+ * We've set up two models - User and Order - and told them about each other. You can set up as many associations on
+ * each Model as you need using the two default types - {@link Ext.data.HasManyAssociation hasMany} and {@link
+ * Ext.data.BelongsToAssociation belongsTo}. There's much more detail on the usage of each of those inside their
+ * documentation pages. If you're not familiar with Models already, {@link Ext.data.Model there is plenty on those too}.
+ *
+ * **Further Reading**
+ *
+ * - {@link Ext.data.HasManyAssociation hasMany associations}
+ * - {@link Ext.data.BelongsToAssociation belongsTo associations}
+ * - {@link Ext.data.Model using Models}
+ *
+ * # Self association models
+ *
+ * We can also have models that create parent/child associations between the same type. Below is an example, where
+ * groups can be nested inside other groups:
+ *
+ * // Server Data
+ * {
+ * "groups": {
+ * "id": 10,
+ * "parent_id": 100,
+ * "name": "Main Group",
+ * "parent_group": {
+ * "id": 100,
+ * "parent_id": null,
+ * "name": "Parent Group"
+ * },
+ * "child_groups": [{
+ * "id": 2,
+ * "parent_id": 10,
+ * "name": "Child Group 1"
+ * },{
+ * "id": 3,
+ * "parent_id": 10,
+ * "name": "Child Group 2"
+ * },{
+ * "id": 4,
+ * "parent_id": 10,
+ * "name": "Child Group 3"
+ * }]
+ * }
+ * }
+ *
+ * // Client code
+ * Ext.define('Group', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'parent_id', 'name'],
+ * proxy: {
+ * type: 'ajax',
+ * url: 'data.json',
+ * reader: {
+ * type: 'json',
+ * root: 'groups'
+ * }
+ * },
+ * associations: [{
+ * type: 'hasMany',
+ * model: 'Group',
+ * primaryKey: 'id',
+ * foreignKey: 'parent_id',
+ * autoLoad: true,
+ * associationKey: 'child_groups' // read child data from child_groups
+ * }, {
+ * type: 'belongsTo',
+ * model: 'Group',
+ * primaryKey: 'id',
+ * foreignKey: 'parent_id',
+ * associationKey: 'parent_group' // read parent data from parent_group
+ * }]
+ * });
+ *
+ * Ext.onReady(function(){
+ *
+ * Group.load(10, {
+ * success: function(group){
+ * console.log(group.getGroup().get('name'));
+ *
+ * group.groups().each(function(rec){
+ * console.log(rec.get('name'));
+ * });
+ * }
+ * });
+ *
+ * });
+ *
+ */
+Ext.define('Ext.data.Association', {
+ /**
+ * @cfg {String} ownerModel (required)
+ * The string name of the model that owns the association.
+ */
+
+ /**
+ * @cfg {String} associatedModel (required)
+ * The string name of the model that is being associated with.
+ */
+
+ /**
+ * @cfg {String} primaryKey
+ * The name of the primary key on the associated model. In general this will be the
+ * {@link Ext.data.Model#idProperty} of the Model.
+ */
+ primaryKey: 'id',
+
+ /**
+ * @cfg {Ext.data.reader.Reader} reader
+ * A special reader to read associated data
+ */
+
+ /**
+ * @cfg {String} associationKey
+ * The name of the property in the data to read the association from. Defaults to the name of the associated model.
+ */
+
+ defaultReaderType: 'json',
+
+ statics: {
+ create: function(association){
+ if (!association.isAssociation) {
+ if (Ext.isString(association)) {
+ association = {
+ type: association
+ };
+ }
+
+ switch (association.type) {
+ case 'belongsTo':
+ return Ext.create('Ext.data.BelongsToAssociation', association);
+ case 'hasMany':
+ return Ext.create('Ext.data.HasManyAssociation', association);
+ //TODO Add this back when it's fixed
+// case 'polymorphic':
+// return Ext.create('Ext.data.PolymorphicAssociation', association);
+ default:
+ }
+ }
+ return association;
+ }
+ },
+
+ /**
+ * Creates the Association object.
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config) {
+ Ext.apply(this, config);
+
+ var types = Ext.ModelManager.types,
+ ownerName = config.ownerModel,
+ associatedName = config.associatedModel,
+ ownerModel = types[ownerName],
+ associatedModel = types[associatedName],
+ ownerProto;
+
+
+ this.ownerModel = ownerModel;
+ this.associatedModel = associatedModel;
+
+ /**
+ * @property {String} ownerName
+ * The name of the model that 'owns' the association
+ */
+
+ /**
+ * @property {String} associatedName
+ * The name of the model is on the other end of the association (e.g. if a User model hasMany Orders, this is
+ * 'Order')
+ */
+
+ Ext.applyIf(this, {
+ ownerName : ownerName,
+ associatedName: associatedName
+ });
+ },
+
+ /**
+ * Get a specialized reader for reading associated data
+ * @return {Ext.data.reader.Reader} The reader, null if not supplied
+ */
+ getReader: function(){
+ var me = this,
+ reader = me.reader,
+ model = me.associatedModel;
+
+ if (reader) {
+ if (Ext.isString(reader)) {
+ reader = {
+ type: reader
+ };
+ }
+ if (reader.isReader) {
+ reader.setModel(model);
+ } else {
+ Ext.applyIf(reader, {
+ model: model,
+ type : me.defaultReaderType
+ });
+ }
+ me.reader = Ext.createByAlias('reader.' + reader.type, reader);
+ }
+ return me.reader || null;
+ }
+});
+
+/**
+ * @author Ed Spencer
+ * @class Ext.ModelManager
+ * @extends Ext.AbstractManager
+
+The ModelManager keeps track of all {@link Ext.data.Model} types defined in your application.
+
+__Creating Model Instances__
+
+Model instances can be created by using the {@link Ext#create Ext.create} method. Ext.create replaces
+the deprecated {@link #create Ext.ModelManager.create} method. It is also possible to create a model instance
+this by using the Model type directly. The following 3 snippets are equivalent:
+
+ Ext.define('User', {
+ extend: 'Ext.data.Model',
+ fields: ['first', 'last']
+ });
+
+ // method 1, create using Ext.create (recommended)
+ Ext.create('User', {
+ first: 'Ed',
+ last: 'Spencer'
+ });
+
+ // method 2, create through the manager (deprecated)
+ Ext.ModelManager.create({
+ first: 'Ed',
+ last: 'Spencer'
+ }, 'User');
+
+ // method 3, create on the type directly
+ new User({
+ first: 'Ed',
+ last: 'Spencer'
+ });
+
+__Accessing Model Types__
+
+A reference to a Model type can be obtained by using the {@link #getModel} function. Since models types
+are normal classes, you can access the type directly. The following snippets are equivalent:
+
+ Ext.define('User', {
+ extend: 'Ext.data.Model',
+ fields: ['first', 'last']
+ });
+
+ // method 1, access model type through the manager
+ var UserType = Ext.ModelManager.getModel('User');
+
+ // method 2, reference the type directly
+ var UserType = User;
+
+ * @markdown
+ * @singleton
+ */
+Ext.define('Ext.ModelManager', {
+ extend: 'Ext.AbstractManager',
+ alternateClassName: 'Ext.ModelMgr',
+ requires: ['Ext.data.Association'],
+
+ singleton: true,
+
+ typeName: 'mtype',
+
+ /**
+ * Private stack of associations that must be created once their associated model has been defined
+ * @property {Ext.data.Association[]} associationStack
+ */
+ associationStack: [],
+
+ /**
+ * Registers a model definition. All model plugins marked with isDefault: true are bootstrapped
+ * immediately, as are any addition plugins defined in the model config.
+ * @private
+ */
+ registerType: function(name, config) {
+ var proto = config.prototype,
+ model;
+ if (proto && proto.isModel) {
+ // registering an already defined model
+ model = config;
+ } else {
+ // passing in a configuration
+ if (!config.extend) {
+ config.extend = 'Ext.data.Model';
+ }
+ model = Ext.define(name, config);
+ }
+ this.types[name] = model;
+ return model;
+ },
+
+ /**
+ * @private
+ * Private callback called whenever a model has just been defined. This sets up any associations
+ * that were waiting for the given model to be defined
+ * @param {Function} model The model that was just created
+ */
+ onModelDefined: function(model) {
+ var stack = this.associationStack,
+ length = stack.length,
+ create = [],
+ association, i, created;
+
+ for (i = 0; i < length; i++) {
+ association = stack[i];
+
+ if (association.associatedModel == model.modelName) {
+ create.push(association);
+ }
+ }
+
+ for (i = 0, length = create.length; i < length; i++) {
+ created = create[i];
+ this.types[created.ownerModel].prototype.associations.add(Ext.data.Association.create(created));
+ Ext.Array.remove(stack, created);
+ }
+ },
+
+ /**
+ * Registers an association where one of the models defined doesn't exist yet.
+ * The ModelManager will check when new models are registered if it can link them
+ * together
+ * @private
+ * @param {Ext.data.Association} association The association
+ */
+ registerDeferredAssociation: function(association){
+ this.associationStack.push(association);
+ },
+
+ /**
+ * Returns the {@link Ext.data.Model} for a given model name
+ * @param {String/Object} id The id of the model or the model instance.
+ * @return {Ext.data.Model} a model class.
+ */
+ getModel: function(id) {
+ var model = id;
+ if (typeof model == 'string') {
+ model = this.types[model];
+ }
+ return model;
+ },
+
+ /**
+ * Creates a new instance of a Model using the given data.
+ *
+ * This method is deprecated. Use {@link Ext#create Ext.create} instead. For example:
+ *
+ * Ext.create('User', {
+ * first: 'Ed',
+ * last: 'Spencer'
+ * });
+ *
+ * @param {Object} data Data to initialize the Model's fields with
+ * @param {String} name The name of the model to create
+ * @param {Number} id (Optional) unique id of the Model instance (see {@link Ext.data.Model})
+ */
+ create: function(config, name, id) {
+ var con = typeof name == 'function' ? name : this.types[name || config.name];
+
+ return new con(config, id);
+ }
+}, function() {
+
+ /**
+ * Old way for creating Model classes. Instead use:
+ *
+ * Ext.define("MyModel", {
+ * extend: "Ext.data.Model",
+ * fields: []
+ * });
+ *
+ * @param {String} name Name of the Model class.
+ * @param {Object} config A configuration object for the Model you wish to create.
+ * @return {Ext.data.Model} The newly registered Model
+ * @member Ext
+ * @deprecated 4.0.0 Use {@link Ext#define} instead.
+ */
+ Ext.regModel = function() {
+ return this.ModelManager.registerType.apply(this.ModelManager, arguments);
+ };
+});
+
+/**
+ * @singleton
+ *
+ * Provides a registry of available Plugin classes indexed by a mnemonic code known as the Plugin's ptype.
+ *
+ * A plugin may be specified simply as a *config object* as long as the correct `ptype` is specified:
+ *
+ * {
+ * ptype: 'gridviewdragdrop',
+ * dragText: 'Drag and drop to reorganize'
+ * }
+ *
+ * Or just use the ptype on its own:
+ *
+ * 'gridviewdragdrop'
+ *
+ * Alternatively you can instantiate the plugin with Ext.create:
+ *
+ * Ext.create('Ext.view.plugin.AutoComplete', {
+ * ptype: 'gridviewdragdrop',
+ * dragText: 'Drag and drop to reorganize'
+ * })
+ */
+Ext.define('Ext.PluginManager', {
+ extend: 'Ext.AbstractManager',
+ alternateClassName: 'Ext.PluginMgr',
+ singleton: true,
+ typeName: 'ptype',
+
+ /**
+ * Creates a new Plugin from the specified config object using the config object's ptype to determine the class to
+ * instantiate.
+ * @param {Object} config A configuration object for the Plugin you wish to create.
+ * @param {Function} defaultType (optional) The constructor to provide the default Plugin type if the config object does not
+ * contain a `ptype`. (Optional if the config contains a `ptype`).
+ * @return {Ext.Component} The newly instantiated Plugin.
+ */
+ //create: function(plugin, defaultType) {
+ // if (plugin instanceof this) {
+ // return plugin;
+ // } else {
+ // var type, config = {};
+ //
+ // if (Ext.isString(plugin)) {
+ // type = plugin;
+ // }
+ // else {
+ // type = plugin[this.typeName] || defaultType;
+ // config = plugin;
+ // }
+ //
+ // return Ext.createByAlias('plugin.' + type, config);
+ // }
+ //},
+
+ create : function(config, defaultType){
+ if (config.init) {
+ return config;
+ } else {
+ return Ext.createByAlias('plugin.' + (config.ptype || defaultType), config);
+ }
+
+ // Prior system supported Singleton plugins.
+ //var PluginCls = this.types[config.ptype || defaultType];
+ //if (PluginCls.init) {
+ // return PluginCls;
+ //} else {
+ // return new PluginCls(config);
+ //}
+ },
+
+ /**
+ * Returns all plugins registered with the given type. Here, 'type' refers to the type of plugin, not its ptype.
+ * @param {String} type The type to search for
+ * @param {Boolean} defaultsOnly True to only return plugins of this type where the plugin's isDefault property is
+ * truthy
+ * @return {Ext.AbstractPlugin[]} All matching plugins
+ */
+ findByType: function(type, defaultsOnly) {
+ var matches = [],
+ types = this.types;
+
+ for (var name in types) {
+ if (!types.hasOwnProperty(name)) {
+ continue;
+ }
+ var item = types[name];
+
+ if (item.type == type && (!defaultsOnly || (defaultsOnly === true && item.isDefault))) {
+ matches.push(item);
+ }
+ }
+
+ return matches;
+ }
+}, function() {
+ /**
+ * Shorthand for {@link Ext.PluginManager#registerType}
+ * @param {String} ptype The ptype mnemonic string by which the Plugin class
+ * may be looked up.
+ * @param {Function} cls The new Plugin class.
+ * @member Ext
+ * @method preg
+ */
+ Ext.preg = function() {
+ return Ext.PluginManager.registerType.apply(Ext.PluginManager, arguments);
+ };
+});
+
+/**
+ * Represents an HTML fragment template. Templates may be {@link #compile precompiled} for greater performance.
+ *
+ * An instance of this class may be created by passing to the constructor either a single argument, or multiple
+ * arguments:
+ *
+ * # Single argument: String/Array
+ *
+ * The single argument may be either a String or an Array:
+ *
+ * - String:
+ *
+ * var t = new Ext.Template("<div>Hello {0}.</div>");
+ * t.{@link #append}('some-element', ['foo']);
+ *
+ * - Array:
+ *
+ * An Array will be combined with `join('')`.
+ *
+ * var t = new Ext.Template([
+ * '<div name="{id}">',
+ * '<span class="{cls}">{name:trim} {value:ellipsis(10)}</span>',
+ * '</div>',
+ * ]);
+ * t.{@link #compile}();
+ * t.{@link #append}('some-element', {id: 'myid', cls: 'myclass', name: 'foo', value: 'bar'});
+ *
+ * # Multiple arguments: String, Object, Array, ...
+ *
+ * Multiple arguments will be combined with `join('')`.
+ *
+ * var t = new Ext.Template(
+ * '<div name="{id}">',
+ * '<span class="{cls}">{name} {value}</span>',
+ * '</div>',
+ * // a configuration object:
+ * {
+ * compiled: true, // {@link #compile} immediately
+ * }
+ * );
+ *
+ * # Notes
+ *
+ * - For a list of available format functions, see {@link Ext.util.Format}.
+ * - `disableFormats` reduces `{@link #apply}` time when no formatting is required.
+ */
+Ext.define('Ext.Template', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.DomHelper', 'Ext.util.Format'],
+
+ inheritableStatics: {
+ /**
+ * Creates a template from the passed element's value (_display:none_ textarea, preferred) or innerHTML.
+ * @param {String/HTMLElement} el A DOM element or its id
+ * @param {Object} config (optional) Config object
+ * @return {Ext.Template} The created template
+ * @static
+ * @inheritable
+ */
+ from: function(el, config) {
+ el = Ext.getDom(el);
+ return new this(el.value || el.innerHTML, config || '');
+ }
+ },
+
+ /* End Definitions */
+
+ /**
+ * Creates new template.
+ *
+ * @param {String...} html List of strings to be concatenated into template.
+ * Alternatively an array of strings can be given, but then no config object may be passed.
+ * @param {Object} config (optional) Config object
+ */
+ constructor: function(html) {
+ var me = this,
+ args = arguments,
+ buffer = [],
+ i = 0,
+ length = args.length,
+ value;
+
+ me.initialConfig = {};
+
+ if (length > 1) {
+ for (; i < length; i++) {
+ value = args[i];
+ if (typeof value == 'object') {
+ Ext.apply(me.initialConfig, value);
+ Ext.apply(me, value);
+ } else {
+ buffer.push(value);
+ }
+ }
+ html = buffer.join('');
+ } else {
+ if (Ext.isArray(html)) {
+ buffer.push(html.join(''));
+ } else {
+ buffer.push(html);
+ }
+ }
+
+ // @private
+ me.html = buffer.join('');
+
+ if (me.compiled) {
+ me.compile();
+ }
+ },
+
+ isTemplate: true,
+
+ /**
+ * @cfg {Boolean} compiled
+ * True to immediately compile the template. Defaults to false.
+ */
+
+ /**
+ * @cfg {Boolean} disableFormats
+ * True to disable format functions in the template. If the template doesn't contain
+ * format functions, setting disableFormats to true will reduce apply time. Defaults to false.
+ */
+ disableFormats: false,
+
+ re: /\{([\w\-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g,
+
+ /**
+ * Returns an HTML fragment of this template with the specified values applied.
+ *
+ * @param {Object/Array} values The template values. Can be an array if your params are numeric:
+ *
+ * var tpl = new Ext.Template('Name: {0}, Age: {1}');
+ * tpl.applyTemplate(['John', 25]);
+ *
+ * or an object:
+ *
+ * var tpl = new Ext.Template('Name: {name}, Age: {age}');
+ * tpl.applyTemplate({name: 'John', age: 25});
+ *
+ * @return {String} The HTML fragment
+ */
+ applyTemplate: function(values) {
+ var me = this,
+ useFormat = me.disableFormats !== true,
+ fm = Ext.util.Format,
+ tpl = me;
+
+ if (me.compiled) {
+ return me.compiled(values);
+ }
+ function fn(m, name, format, args) {
+ if (format && useFormat) {
+ if (args) {
+ args = [values[name]].concat(Ext.functionFactory('return ['+ args +'];')());
+ } else {
+ args = [values[name]];
+ }
+ if (format.substr(0, 5) == "this.") {
+ return tpl[format.substr(5)].apply(tpl, args);
+ }
+ else {
+ return fm[format].apply(fm, args);
+ }
+ }
+ else {
+ return values[name] !== undefined ? values[name] : "";
+ }
+ }
+ return me.html.replace(me.re, fn);
+ },
+
+ /**
+ * Sets the HTML used as the template and optionally compiles it.
+ * @param {String} html
+ * @param {Boolean} compile (optional) True to compile the template.
+ * @return {Ext.Template} this
+ */
+ set: function(html, compile) {
+ var me = this;
+ me.html = html;
+ me.compiled = null;
+ return compile ? me.compile() : me;
+ },
+
+ compileARe: /\\/g,
+ compileBRe: /(\r\n|\n)/g,
+ compileCRe: /'/g,
+
+ /**
+ * Compiles the template into an internal function, eliminating the RegEx overhead.
+ * @return {Ext.Template} this
+ */
+ compile: function() {
+ var me = this,
+ fm = Ext.util.Format,
+ useFormat = me.disableFormats !== true,
+ body, bodyReturn;
+
+ function fn(m, name, format, args) {
+ if (format && useFormat) {
+ args = args ? ',' + args: "";
+ if (format.substr(0, 5) != "this.") {
+ format = "fm." + format + '(';
+ }
+ else {
+ format = 'this.' + format.substr(5) + '(';
+ }
+ }
+ else {
+ args = '';
+ format = "(values['" + name + "'] == undefined ? '' : ";
+ }
+ return "'," + format + "values['" + name + "']" + args + ") ,'";
+ }
+
+ bodyReturn = me.html.replace(me.compileARe, '\\\\').replace(me.compileBRe, '\\n').replace(me.compileCRe, "\\'").replace(me.re, fn);
+ body = "this.compiled = function(values){ return ['" + bodyReturn + "'].join('');};";
+ eval(body);
+ return me;
+ },
+
+ /**
+ * Applies the supplied values to the template and inserts the new node(s) as the first child of el.
+ *
+ * @param {String/HTMLElement/Ext.Element} el The context element
+ * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
+ * @param {Boolean} returnElement (optional) true to return a Ext.Element.
+ * @return {HTMLElement/Ext.Element} The new node or Element
+ */
+ insertFirst: function(el, values, returnElement) {
+ return this.doInsert('afterBegin', el, values, returnElement);
+ },
+
+ /**
+ * Applies the supplied values to the template and inserts the new node(s) before el.
+ *
+ * @param {String/HTMLElement/Ext.Element} el The context element
+ * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
+ * @param {Boolean} returnElement (optional) true to return a Ext.Element.
+ * @return {HTMLElement/Ext.Element} The new node or Element
+ */
+ insertBefore: function(el, values, returnElement) {
+ return this.doInsert('beforeBegin', el, values, returnElement);
+ },
+
+ /**
+ * Applies the supplied values to the template and inserts the new node(s) after el.
+ *
+ * @param {String/HTMLElement/Ext.Element} el The context element
+ * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
+ * @param {Boolean} returnElement (optional) true to return a Ext.Element.
+ * @return {HTMLElement/Ext.Element} The new node or Element
+ */
+ insertAfter: function(el, values, returnElement) {
+ return this.doInsert('afterEnd', el, values, returnElement);
+ },
+
+ /**
+ * Applies the supplied `values` to the template and appends the new node(s) to the specified `el`.
+ *
+ * For example usage see {@link Ext.Template Ext.Template class docs}.
+ *
+ * @param {String/HTMLElement/Ext.Element} el The context element
+ * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
+ * @param {Boolean} returnElement (optional) true to return an Ext.Element.
+ * @return {HTMLElement/Ext.Element} The new node or Element
+ */
+ append: function(el, values, returnElement) {
+ return this.doInsert('beforeEnd', el, values, returnElement);
+ },
+
+ doInsert: function(where, el, values, returnEl) {
+ el = Ext.getDom(el);
+ var newNode = Ext.DomHelper.insertHtml(where, el, this.applyTemplate(values));
+ return returnEl ? Ext.get(newNode, true) : newNode;
+ },
+
+ /**
+ * Applies the supplied values to the template and overwrites the content of el with the new node(s).
+ *
+ * @param {String/HTMLElement/Ext.Element} el The context element
+ * @param {Object/Array} values The template values. See {@link #applyTemplate} for details.
+ * @param {Boolean} returnElement (optional) true to return a Ext.Element.
+ * @return {HTMLElement/Ext.Element} The new node or Element
+ */
+ overwrite: function(el, values, returnElement) {
+ el = Ext.getDom(el);
+ el.innerHTML = this.applyTemplate(values);
+ return returnElement ? Ext.get(el.firstChild, true) : el.firstChild;
+ }
+}, function() {
+
+ /**
+ * @method apply
+ * @member Ext.Template
+ * Alias for {@link #applyTemplate}.
+ * @alias Ext.Template#applyTemplate
+ */
+ this.createAlias('apply', 'applyTemplate');
+});
+
+/**
+ * A template class that supports advanced functionality like:
+ *
+ * - Autofilling arrays using templates and sub-templates
+ * - Conditional processing with basic comparison operators
+ * - Basic math function support
+ * - Execute arbitrary inline code with special built-in template variables
+ * - Custom member functions
+ * - Many special tags and built-in operators that aren't defined as part of the API, but are supported in the templates that can be created
+ *
+ * XTemplate provides the templating mechanism built into:
+ *
+ * - {@link Ext.view.View}
+ *
+ * The {@link Ext.Template} describes the acceptable parameters to pass to the constructor. The following examples
+ * demonstrate all of the supported features.
+ *
+ * # Sample Data
+ *
+ * This is the data object used for reference in each code example:
+ *
+ * var data = {
+ * name: 'Tommy Maintz',
+ * title: 'Lead Developer',
+ * company: 'Sencha Inc.',
+ * email: 'tommy@sencha.com',
+ * address: '5 Cups Drive',
+ * city: 'Palo Alto',
+ * state: 'CA',
+ * zip: '44102',
+ * drinks: ['Coffee', 'Soda', 'Water'],
+ * kids: [
+ * {
+ * name: 'Joshua',
+ * age:3
+ * },
+ * {
+ * name: 'Matthew',
+ * age:2
+ * },
+ * {
+ * name: 'Solomon',
+ * age:0
+ * }
+ * ]
+ * };
+ *
+ * # Auto filling of arrays
+ *
+ * The **tpl** tag and the **for** operator are used to process the provided data object:
+ *
+ * - If the value specified in for is an array, it will auto-fill, repeating the template block inside the tpl
+ * tag for each item in the array.
+ * - If for="." is specified, the data object provided is examined.
+ * - While processing an array, the special variable {#} will provide the current array index + 1 (starts at 1, not 0).
+ *
+ * Examples:
+ *
+ * <tpl for=".">...</tpl> // loop through array at root node
+ * <tpl for="foo">...</tpl> // loop through array at foo node
+ * <tpl for="foo.bar">...</tpl> // loop through array at foo.bar node
+ *
+ * Using the sample data above:
+ *
+ * var tpl = new Ext.XTemplate(
+ * '<p>Kids: ',
+ * '<tpl for=".">', // process the data.kids node
+ * '<p>{#}. {name}</p>', // use current array index to autonumber
+ * '</tpl></p>'
+ * );
+ * tpl.overwrite(panel.body, data.kids); // pass the kids property of the data object
+ *
+ * An example illustrating how the **for** property can be leveraged to access specified members of the provided data
+ * object to populate the template:
+ *
+ * var tpl = new Ext.XTemplate(
+ * '<p>Name: {name}</p>',
+ * '<p>Title: {title}</p>',
+ * '<p>Company: {company}</p>',
+ * '<p>Kids: ',
+ * '<tpl for="kids">', // interrogate the kids property within the data
+ * '<p>{name}</p>',
+ * '</tpl></p>'
+ * );
+ * tpl.overwrite(panel.body, data); // pass the root node of the data object
+ *
+ * Flat arrays that contain values (and not objects) can be auto-rendered using the special **`{.}`** variable inside a
+ * loop. This variable will represent the value of the array at the current index:
+ *
+ * var tpl = new Ext.XTemplate(
+ * '<p>{name}\'s favorite beverages:</p>',
+ * '<tpl for="drinks">',
+ * '<div> - {.}</div>',
+ * '</tpl>'
+ * );
+ * tpl.overwrite(panel.body, data);
+ *
+ * When processing a sub-template, for example while looping through a child array, you can access the parent object's
+ * members via the **parent** object:
+ *
+ * var tpl = new Ext.XTemplate(
+ * '<p>Name: {name}</p>',
+ * '<p>Kids: ',
+ * '<tpl for="kids">',
+ * '<tpl if="age > 1">',
+ * '<p>{name}</p>',
+ * '<p>Dad: {parent.name}</p>',
+ * '</tpl>',
+ * '</tpl></p>'
+ * );
+ * tpl.overwrite(panel.body, data);
+ *
+ * # Conditional processing with basic comparison operators
+ *
+ * The **tpl** tag and the **if** operator are used to provide conditional checks for deciding whether or not to render
+ * specific parts of the template. Notes:
+ *
+ * - Double quotes must be encoded if used within the conditional
+ * - There is no else operator -- if needed, two opposite if statements should be used.
+ *
+ * Examples:
+ *
+ * <tpl if="age > 1 && age < 10">Child</tpl>
+ * <tpl if="age >= 10 && age < 18">Teenager</tpl>
+ * <tpl if="this.isGirl(name)">...</tpl>
+ * <tpl if="id==\'download\'">...</tpl>
+ * <tpl if="needsIcon"><img src="{icon}" class="{iconCls}"/></tpl>
+ * // no good:
+ * <tpl if="name == "Tommy"">Hello</tpl>
+ * // encode " if it is part of the condition, e.g.
+ * <tpl if="name == "Tommy"">Hello</tpl>
+ *
+ * Using the sample data above:
+ *
+ * var tpl = new Ext.XTemplate(
+ * '<p>Name: {name}</p>',
+ * '<p>Kids: ',
+ * '<tpl for="kids">',
+ * '<tpl if="age > 1">',
+ * '<p>{name}</p>',
+ * '</tpl>',
+ * '</tpl></p>'
+ * );
+ * tpl.overwrite(panel.body, data);
+ *
+ * # Basic math support
+ *
+ * The following basic math operators may be applied directly on numeric data values:
+ *
+ * + - * /
+ *
+ * For example:
+ *
+ * var tpl = new Ext.XTemplate(
+ * '<p>Name: {name}</p>',
+ * '<p>Kids: ',
+ * '<tpl for="kids">',
+ * '<tpl if="age > 1">', // <-- Note that the > is encoded
+ * '<p>{#}: {name}</p>', // <-- Auto-number each item
+ * '<p>In 5 Years: {age+5}</p>', // <-- Basic math
+ * '<p>Dad: {parent.name}</p>',
+ * '</tpl>',
+ * '</tpl></p>'
+ * );
+ * tpl.overwrite(panel.body, data);
+ *
+ * # Execute arbitrary inline code with special built-in template variables
+ *
+ * Anything between `{[ ... ]}` is considered code to be executed in the scope of the template. There are some special
+ * variables available in that code:
+ *
+ * - **values**: The values in the current scope. If you are using scope changing sub-templates,
+ * you can change what values is.
+ * - **parent**: The scope (values) of the ancestor template.
+ * - **xindex**: If you are in a looping template, the index of the loop you are in (1-based).
+ * - **xcount**: If you are in a looping template, the total length of the array you are looping.
+ *
+ * This example demonstrates basic row striping using an inline code block and the xindex variable:
+ *
+ * var tpl = new Ext.XTemplate(
+ * '<p>Name: {name}</p>',
+ * '<p>Company: {[values.company.toUpperCase() + ", " + values.title]}</p>',
+ * '<p>Kids: ',
+ * '<tpl for="kids">',
+ * '<div class="{[xindex % 2 === 0 ? "even" : "odd"]}">',
+ * '{name}',
+ * '</div>',
+ * '</tpl></p>'
+ * );
+ * tpl.overwrite(panel.body, data);
+ *
+ * # Template member functions
+ *
+ * One or more member functions can be specified in a configuration object passed into the XTemplate constructor for
+ * more complex processing:
+ *
+ * var tpl = new Ext.XTemplate(
+ * '<p>Name: {name}</p>',
+ * '<p>Kids: ',
+ * '<tpl for="kids">',
+ * '<tpl if="this.isGirl(name)">',
+ * '<p>Girl: {name} - {age}</p>',
+ * '</tpl>',
+ * // use opposite if statement to simulate 'else' processing:
+ * '<tpl if="this.isGirl(name) == false">',
+ * '<p>Boy: {name} - {age}</p>',
+ * '</tpl>',
+ * '<tpl if="this.isBaby(age)">',
+ * '<p>{name} is a baby!</p>',
+ * '</tpl>',
+ * '</tpl></p>',
+ * {
+ * // XTemplate configuration:
+ * disableFormats: true,
+ * // member functions:
+ * isGirl: function(name){
+ * return name == 'Sara Grace';
+ * },
+ * isBaby: function(age){
+ * return age < 1;
+ * }
+ * }
+ * );
+ * tpl.overwrite(panel.body, data);
+ */
+Ext.define('Ext.XTemplate', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.Template',
+
+ /* End Definitions */
+
+ argsRe: /<tpl\b[^>]*>((?:(?=([^<]+))\2|<(?!tpl\b[^>]*>))*?)<\/tpl>/,
+ nameRe: /^<tpl\b[^>]*?for="(.*?)"/,
+ ifRe: /^<tpl\b[^>]*?if="(.*?)"/,
+ execRe: /^<tpl\b[^>]*?exec="(.*?)"/,
+ constructor: function() {
+ this.callParent(arguments);
+
+ var me = this,
+ html = me.html,
+ argsRe = me.argsRe,
+ nameRe = me.nameRe,
+ ifRe = me.ifRe,
+ execRe = me.execRe,
+ id = 0,
+ tpls = [],
+ VALUES = 'values',
+ PARENT = 'parent',
+ XINDEX = 'xindex',
+ XCOUNT = 'xcount',
+ RETURN = 'return ',
+ WITHVALUES = 'with(values){ ',
+ m, matchName, matchIf, matchExec, exp, fn, exec, name, i;
+
+ html = ['<tpl>', html, '</tpl>'].join('');
+
+ while ((m = html.match(argsRe))) {
+ exp = null;
+ fn = null;
+ exec = null;
+ matchName = m[0].match(nameRe);
+ matchIf = m[0].match(ifRe);
+ matchExec = m[0].match(execRe);
+
+ exp = matchIf ? matchIf[1] : null;
+ if (exp) {
+ fn = Ext.functionFactory(VALUES, PARENT, XINDEX, XCOUNT, WITHVALUES + 'try{' + RETURN + Ext.String.htmlDecode(exp) + ';}catch(e){return;}}');
+ }
+
+ exp = matchExec ? matchExec[1] : null;
+ if (exp) {
+ exec = Ext.functionFactory(VALUES, PARENT, XINDEX, XCOUNT, WITHVALUES + Ext.String.htmlDecode(exp) + ';}');
+ }
+
+ name = matchName ? matchName[1] : null;
+ if (name) {
+ if (name === '.') {
+ name = VALUES;
+ } else if (name === '..') {
+ name = PARENT;
+ }
+ name = Ext.functionFactory(VALUES, PARENT, 'try{' + WITHVALUES + RETURN + name + ';}}catch(e){return;}');
+ }
+
+ tpls.push({
+ id: id,
+ target: name,
+ exec: exec,
+ test: fn,
+ body: m[1] || ''
+ });
+
+ html = html.replace(m[0], '{xtpl' + id + '}');
+ id = id + 1;
+ }
+
+ for (i = tpls.length - 1; i >= 0; --i) {
+ me.compileTpl(tpls[i]);
+ }
+ me.master = tpls[tpls.length - 1];
+ me.tpls = tpls;
+ },
+
+ // @private
+ applySubTemplate: function(id, values, parent, xindex, xcount) {
+ var me = this, t = me.tpls[id];
+ return t.compiled.call(me, values, parent, xindex, xcount);
+ },
+
+ /**
+ * @cfg {RegExp} codeRe
+ * The regular expression used to match code variables. Default: matches {[expression]}.
+ */
+ codeRe: /\{\[((?:\\\]|.|\n)*?)\]\}/g,
+
+ /**
+ * @cfg {Boolean} compiled
+ * Only applies to {@link Ext.Template}, XTemplates are compiled automatically.
+ */
+
+ re: /\{([\w-\.\#]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\/]\s?[\d\.\+\-\*\/\(\)]+)?\}/g,
+
+ // @private
+ compileTpl: function(tpl) {
+ var fm = Ext.util.Format,
+ me = this,
+ useFormat = me.disableFormats !== true,
+ body, bodyReturn, evaluatedFn;
+
+ function fn(m, name, format, args, math) {
+ var v;
+ // name is what is inside the {}
+ // Name begins with xtpl, use a Sub Template
+ if (name.substr(0, 4) == 'xtpl') {
+ return "',this.applySubTemplate(" + name.substr(4) + ", values, parent, xindex, xcount),'";
+ }
+ // name = "." - Just use the values object.
+ if (name == '.') {
+ // filter to not include arrays/objects/nulls
+ v = 'Ext.Array.indexOf(["string", "number", "boolean"], typeof values) > -1 || Ext.isDate(values) ? values : ""';
+ }
+
+ // name = "#" - Use the xindex
+ else if (name == '#') {
+ v = 'xindex';
+ }
+ else if (name.substr(0, 7) == "parent.") {
+ v = name;
+ }
+ // name has a . in it - Use object literal notation, starting from values
+ else if (name.indexOf('.') != -1) {
+ v = "values." + name;
+ }
+
+ // name is a property of values
+ else {
+ v = "values['" + name + "']";
+ }
+ if (math) {
+ v = '(' + v + math + ')';
+ }
+ if (format && useFormat) {
+ args = args ? ',' + args : "";
+ if (format.substr(0, 5) != "this.") {
+ format = "fm." + format + '(';
+ }
+ else {
+ format = 'this.' + format.substr(5) + '(';
+ }
+ }
+ else {
+ args = '';
+ format = "(" + v + " === undefined ? '' : ";
+ }
+ return "'," + format + v + args + "),'";
+ }
+
+ function codeFn(m, code) {
+ // Single quotes get escaped when the template is compiled, however we want to undo this when running code.
+ return "',(" + code.replace(me.compileARe, "'") + "),'";
+ }
+
+ bodyReturn = tpl.body.replace(me.compileBRe, '\\n').replace(me.compileCRe, "\\'").replace(me.re, fn).replace(me.codeRe, codeFn);
+ body = "evaluatedFn = function(values, parent, xindex, xcount){return ['" + bodyReturn + "'].join('');};";
+ eval(body);
+
+ tpl.compiled = function(values, parent, xindex, xcount) {
+ var vs,
+ length,
+ buffer,
+ i;
+
+ if (tpl.test && !tpl.test.call(me, values, parent, xindex, xcount)) {
+ return '';
+ }
+
+ vs = tpl.target ? tpl.target.call(me, values, parent) : values;
+ if (!vs) {
+ return '';
+ }
+
+ parent = tpl.target ? values : parent;
+ if (tpl.target && Ext.isArray(vs)) {
+ buffer = [];
+ length = vs.length;
+ if (tpl.exec) {
+ for (i = 0; i < length; i++) {
+ buffer[buffer.length] = evaluatedFn.call(me, vs[i], parent, i + 1, length);
+ tpl.exec.call(me, vs[i], parent, i + 1, length);
+ }
+ } else {
+ for (i = 0; i < length; i++) {
+ buffer[buffer.length] = evaluatedFn.call(me, vs[i], parent, i + 1, length);
+ }
+ }
+ return buffer.join('');
+ }
+
+ if (tpl.exec) {
+ tpl.exec.call(me, vs, parent, xindex, xcount);
+ }
+ return evaluatedFn.call(me, vs, parent, xindex, xcount);
+ };
+
+ return this;
+ },
+
+ // inherit docs from Ext.Template
+ applyTemplate: function(values) {
+ return this.master.compiled.call(this, values, {}, 1, 1);
+ },
+
+ /**
+ * Does nothing. XTemplates are compiled automatically, so this function simply returns this.
+ * @return {Ext.XTemplate} this
+ */
+ compile: function() {
+ return this;
+ }
+}, function() {
+ // re-create the alias, inheriting it from Ext.Template doesn't work as intended.
+ this.createAlias('apply', 'applyTemplate');
+});
+
+/**
+ * @class Ext.app.Controller
+ *
+ * Controllers are the glue that binds an application together. All they really do is listen for events (usually from
+ * views) and take some action. Here's how we might create a Controller to manage Users:
+ *
+ * Ext.define('MyApp.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * init: function() {
+ * console.log('Initialized Users! This happens before the Application launch function is called');
+ * }
+ * });
+ *
+ * The init function is a special method that is called when your application boots. It is called before the
+ * {@link Ext.app.Application Application}'s launch function is executed so gives a hook point to run any code before
+ * your Viewport is created.
+ *
+ * The init function is a great place to set up how your controller interacts with the view, and is usually used in
+ * conjunction with another Controller function - {@link Ext.app.Controller#control control}. The control function
+ * makes it easy to listen to events on your view classes and take some action with a handler function. Let's update
+ * our Users controller to tell us when the panel is rendered:
+ *
+ * Ext.define('MyApp.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * init: function() {
+ * this.control({
+ * 'viewport > panel': {
+ * render: this.onPanelRendered
+ * }
+ * });
+ * },
+ *
+ * onPanelRendered: function() {
+ * console.log('The panel was rendered');
+ * }
+ * });
+ *
+ * We've updated the init function to use this.control to set up listeners on views in our application. The control
+ * function uses the new ComponentQuery engine to quickly and easily get references to components on the page. If you
+ * are not familiar with ComponentQuery yet, be sure to check out the {@link Ext.ComponentQuery documentation}. In brief though,
+ * it allows us to pass a CSS-like selector that will find every matching component on the page.
+ *
+ * In our init function above we supplied 'viewport > panel', which translates to "find me every Panel that is a direct
+ * child of a Viewport". We then supplied an object that maps event names (just 'render' in this case) to handler
+ * functions. The overall effect is that whenever any component that matches our selector fires a 'render' event, our
+ * onPanelRendered function is called.
+ *
+ * <u>Using refs</u>
+ *
+ * One of the most useful parts of Controllers is the new ref system. These use the new {@link Ext.ComponentQuery} to
+ * make it really easy to get references to Views on your page. Let's look at an example of this now:
+ *
+ * Ext.define('MyApp.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * refs: [
+ * {
+ * ref: 'list',
+ * selector: 'grid'
+ * }
+ * ],
+ *
+ * init: function() {
+ * this.control({
+ * 'button': {
+ * click: this.refreshGrid
+ * }
+ * });
+ * },
+ *
+ * refreshGrid: function() {
+ * this.getList().store.load();
+ * }
+ * });
+ *
+ * This example assumes the existence of a {@link Ext.grid.Panel Grid} on the page, which contains a single button to
+ * refresh the Grid when clicked. In our refs array, we set up a reference to the grid. There are two parts to this -
+ * the 'selector', which is a {@link Ext.ComponentQuery ComponentQuery} selector which finds any grid on the page and
+ * assigns it to the reference 'list'.
+ *
+ * By giving the reference a name, we get a number of things for free. The first is the getList function that we use in
+ * the refreshGrid method above. This is generated automatically by the Controller based on the name of our ref, which
+ * was capitalized and prepended with get to go from 'list' to 'getList'.
+ *
+ * The way this works is that the first time getList is called by your code, the ComponentQuery selector is run and the
+ * first component that matches the selector ('grid' in this case) will be returned. All future calls to getList will
+ * use a cached reference to that grid. Usually it is advised to use a specific ComponentQuery selector that will only
+ * match a single View in your application (in the case above our selector will match any grid on the page).
+ *
+ * Bringing it all together, our init function is called when the application boots, at which time we call this.control
+ * to listen to any click on a {@link Ext.button.Button button} and call our refreshGrid function (again, this will
+ * match any button on the page so we advise a more specific selector than just 'button', but have left it this way for
+ * simplicity). When the button is clicked we use out getList function to refresh the grid.
+ *
+ * You can create any number of refs and control any number of components this way, simply adding more functions to
+ * your Controller as you go. For an example of real-world usage of Controllers see the Feed Viewer example in the
+ * examples/app/feed-viewer folder in the SDK download.
+ *
+ * <u>Generated getter methods</u>
+ *
+ * Refs aren't the only thing that generate convenient getter methods. Controllers often have to deal with Models and
+ * Stores so the framework offers a couple of easy ways to get access to those too. Let's look at another example:
+ *
+ * Ext.define('MyApp.controller.Users', {
+ * extend: 'Ext.app.Controller',
+ *
+ * models: ['User'],
+ * stores: ['AllUsers', 'AdminUsers'],
+ *
+ * init: function() {
+ * var User = this.getUserModel(),
+ * allUsers = this.getAllUsersStore();
+ *
+ * var ed = new User({name: 'Ed'});
+ * allUsers.add(ed);
+ * }
+ * });
+ *
+ * By specifying Models and Stores that the Controller cares about, it again dynamically loads them from the appropriate
+ * locations (app/model/User.js, app/store/AllUsers.js and app/store/AdminUsers.js in this case) and creates getter
+ * functions for them all. The example above will create a new User model instance and add it to the AllUsers Store.
+ * Of course, you could do anything in this function but in this case we just did something simple to demonstrate the
+ * functionality.
+ *
+ * <u>Further Reading</u>
+ *
+ * For more information about writing Ext JS 4 applications, please see the
+ * [application architecture guide](#/guide/application_architecture). Also see the {@link Ext.app.Application} documentation.
+ *
+ * @docauthor Ed Spencer
+ */
+Ext.define('Ext.app.Controller', {
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ /**
+ * @cfg {String} id The id of this controller. You can use this id when dispatching.
+ */
+
+ /**
+ * @cfg {String[]} models
+ * Array of models to require from AppName.model namespace. For example:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ * models: ['User', 'Vehicle']
+ * });
+ *
+ * This is equivalent of:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ * requires: ['MyApp.model.User', 'MyApp.model.Vehicle']
+ * });
+ *
+ */
+
+ /**
+ * @cfg {String[]} views
+ * Array of views to require from AppName.view namespace. For example:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ * views: ['List', 'Detail']
+ * });
+ *
+ * This is equivalent of:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ * requires: ['MyApp.view.List', 'MyApp.view.Detail']
+ * });
+ *
+ */
+
+ /**
+ * @cfg {String[]} stores
+ * Array of stores to require from AppName.store namespace. For example:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ * stores: ['Users', 'Vehicles']
+ * });
+ *
+ * This is equivalent of:
+ *
+ * Ext.define("MyApp.controller.Foo", {
+ * extend: "Ext.app.Controller",
+ * requires: ['MyApp.store.Users', 'MyApp.store.Vehicles']
+ * });
+ *
+ */
+
+ onClassExtended: function(cls, data) {
+ var className = Ext.getClassName(cls),
+ match = className.match(/^(.*)\.controller\./);
+
+ if (match !== null) {
+ var namespace = Ext.Loader.getPrefix(className) || match[1],
+ onBeforeClassCreated = data.onBeforeClassCreated,
+ requires = [],
+ modules = ['model', 'view', 'store'],
+ prefix;
+
+ data.onBeforeClassCreated = function(cls, data) {
+ var i, ln, module,
+ items, j, subLn, item;
+
+ for (i = 0,ln = modules.length; i < ln; i++) {
+ module = modules[i];
+
+ items = Ext.Array.from(data[module + 's']);
+
+ for (j = 0,subLn = items.length; j < subLn; j++) {
+ item = items[j];
+
+ prefix = Ext.Loader.getPrefix(item);
+
+ if (prefix === '' || prefix === item) {
+ requires.push(namespace + '.' + module + '.' + item);
+ }
+ else {
+ requires.push(item);
+ }
+ }
+ }
+
+ Ext.require(requires, Ext.Function.pass(onBeforeClassCreated, arguments, this));
+ };
+ }
+ },
+
+ /**
+ * Creates new Controller.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ this.mixins.observable.constructor.call(this, config);
+
+ Ext.apply(this, config || {});
+
+ this.createGetters('model', this.models);
+ this.createGetters('store', this.stores);
+ this.createGetters('view', this.views);
+
+ if (this.refs) {
+ this.ref(this.refs);
+ }
+ },
+
+ /**
+ * A template method that is called when your application boots. It is called before the
+ * {@link Ext.app.Application Application}'s launch function is executed so gives a hook point to run any code before
+ * your Viewport is created.
+ *
+ * @param {Ext.app.Application} application
+ * @template
+ */
+ init: function(application) {},
+
+ /**
+ * A template method like {@link #init}, but called after the viewport is created.
+ * This is called after the {@link Ext.app.Application#launch launch} method of Application is executed.
+ *
+ * @param {Ext.app.Application} application
+ * @template
+ */
+ onLaunch: function(application) {},
+
+ createGetters: function(type, refs) {
+ type = Ext.String.capitalize(type);
+ Ext.Array.each(refs, function(ref) {
+ var fn = 'get',
+ parts = ref.split('.');
+
+ // Handle namespaced class names. E.g. feed.Add becomes getFeedAddView etc.
+ Ext.Array.each(parts, function(part) {
+ fn += Ext.String.capitalize(part);
+ });
+ fn += type;
+
+ if (!this[fn]) {
+ this[fn] = Ext.Function.pass(this['get' + type], [ref], this);
+ }
+ // Execute it right away
+ this[fn](ref);
+ },
+ this);
+ },
+
+ ref: function(refs) {
+ var me = this;
+ refs = Ext.Array.from(refs);
+ Ext.Array.each(refs, function(info) {
+ var ref = info.ref,
+ fn = 'get' + Ext.String.capitalize(ref);
+ if (!me[fn]) {
+ me[fn] = Ext.Function.pass(me.getRef, [ref, info], me);
+ }
+ });
+ },
+
+ getRef: function(ref, info, config) {
+ this.refCache = this.refCache || {};
+ info = info || {};
+ config = config || {};
+
+ Ext.apply(info, config);
+
+ if (info.forceCreate) {
+ return Ext.ComponentManager.create(info, 'component');
+ }
+
+ var me = this,
+ selector = info.selector,
+ cached = me.refCache[ref];
+
+ if (!cached) {
+ me.refCache[ref] = cached = Ext.ComponentQuery.query(info.selector)[0];
+ if (!cached && info.autoCreate) {
+ me.refCache[ref] = cached = Ext.ComponentManager.create(info, 'component');
+ }
+ if (cached) {
+ cached.on('beforedestroy', function() {
+ me.refCache[ref] = null;
+ });
+ }
+ }
+
+ return cached;
+ },
+
+ /**
+ * Adds listeners to components selected via {@link Ext.ComponentQuery}. Accepts an
+ * object containing component paths mapped to a hash of listener functions.
+ *
+ * In the following example the `updateUser` function is mapped to to the `click`
+ * event on a button component, which is a child of the `useredit` component.
+ *
+ * Ext.define('AM.controller.Users', {
+ * init: function() {
+ * this.control({
+ * 'useredit button[action=save]': {
+ * click: this.updateUser
+ * }
+ * });
+ * },
+ *
+ * updateUser: function(button) {
+ * console.log('clicked the Save button');
+ * }
+ * });
+ *
+ * See {@link Ext.ComponentQuery} for more information on component selectors.
+ *
+ * @param {String/Object} selectors If a String, the second argument is used as the
+ * listeners, otherwise an object of selectors -> listeners is assumed
+ * @param {Object} listeners
+ */
+ control: function(selectors, listeners) {
+ this.application.control(selectors, listeners, this);
+ },
+
+ /**
+ * Returns instance of a {@link Ext.app.Controller controller} with the given name.
+ * When controller doesn't exist yet, it's created.
+ * @param {String} name
+ * @return {Ext.app.Controller} a controller instance.
+ */
+ getController: function(name) {
+ return this.application.getController(name);
+ },
+
+ /**
+ * Returns instance of a {@link Ext.data.Store Store} with the given name.
+ * When store doesn't exist yet, it's created.
+ * @param {String} name
+ * @return {Ext.data.Store} a store instance.
+ */
+ getStore: function(name) {
+ return this.application.getStore(name);
+ },
+
+ /**
+ * Returns a {@link Ext.data.Model Model} class with the given name.
+ * A shorthand for using {@link Ext.ModelManager#getModel}.
+ * @param {String} name
+ * @return {Ext.data.Model} a model class.
+ */
+ getModel: function(model) {
+ return this.application.getModel(model);
+ },
+
+ /**
+ * Returns a View class with the given name. To create an instance of the view,
+ * you can use it like it's used by Application to create the Viewport:
+ *
+ * this.getView('Viewport').create();
+ *
+ * @param {String} name
+ * @return {Ext.Base} a view class.
+ */
+ getView: function(view) {
+ return this.application.getView(view);
+ }
+});
+
+/**
+ * @author Don Griffin
+ *
+ * This class is a base for all id generators. It also provides lookup of id generators by
+ * their id.
+ *
+ * Generally, id generators are used to generate a primary key for new model instances. There
+ * are different approaches to solving this problem, so this mechanism has both simple use
+ * cases and is open to custom implementations. A {@link Ext.data.Model} requests id generation
+ * using the {@link Ext.data.Model#idgen} property.
+ *
+ * # Identity, Type and Shared IdGenerators
+ *
+ * It is often desirable to share IdGenerators to ensure uniqueness or common configuration.
+ * This is done by giving IdGenerator instances an id property by which they can be looked
+ * up using the {@link #get} method. To configure two {@link Ext.data.Model Model} classes
+ * to share one {@link Ext.data.SequentialIdGenerator sequential} id generator, you simply
+ * assign them the same id:
+ *
+ * Ext.define('MyApp.data.MyModelA', {
+ * extend: 'Ext.data.Model',
+ * idgen: {
+ * type: 'sequential',
+ * id: 'foo'
+ * }
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelB', {
+ * extend: 'Ext.data.Model',
+ * idgen: {
+ * type: 'sequential',
+ * id: 'foo'
+ * }
+ * });
+ *
+ * To make this as simple as possible for generator types that are shared by many (or all)
+ * Models, the IdGenerator types (such as 'sequential' or 'uuid') are also reserved as
+ * generator id's. This is used by the {@link Ext.data.UuidGenerator} which has an id equal
+ * to its type ('uuid'). In other words, the following Models share the same generator:
+ *
+ * Ext.define('MyApp.data.MyModelX', {
+ * extend: 'Ext.data.Model',
+ * idgen: 'uuid'
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelY', {
+ * extend: 'Ext.data.Model',
+ * idgen: 'uuid'
+ * });
+ *
+ * This can be overridden (by specifying the id explicitly), but there is no particularly
+ * good reason to do so for this generator type.
+ *
+ * # Creating Custom Generators
+ *
+ * An id generator should derive from this class and implement the {@link #generate} method.
+ * The constructor will apply config properties on new instances, so a constructor is often
+ * not necessary.
+ *
+ * To register an id generator type, a derived class should provide an `alias` like so:
+ *
+ * Ext.define('MyApp.data.CustomIdGenerator', {
+ * extend: 'Ext.data.IdGenerator',
+ * alias: 'idgen.custom',
+ *
+ * configProp: 42, // some config property w/default value
+ *
+ * generate: function () {
+ * return ... // a new id
+ * }
+ * });
+ *
+ * Using the custom id generator is then straightforward:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * idgen: 'custom'
+ * });
+ * // or...
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * idgen: {
+ * type: 'custom',
+ * configProp: value
+ * }
+ * });
+ *
+ * It is not recommended to mix shared generators with generator configuration. This leads
+ * to unpredictable results unless all configurations match (which is also redundant). In
+ * such cases, a custom generator with a default id is the best approach.
+ *
+ * Ext.define('MyApp.data.CustomIdGenerator', {
+ * extend: 'Ext.data.SequentialIdGenerator',
+ * alias: 'idgen.custom',
+ *
+ * id: 'custom', // shared by default
+ *
+ * prefix: 'ID_',
+ * seed: 1000
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelX', {
+ * extend: 'Ext.data.Model',
+ * idgen: 'custom'
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelY', {
+ * extend: 'Ext.data.Model',
+ * idgen: 'custom'
+ * });
+ *
+ * // the above models share a generator that produces ID_1000, ID_1001, etc..
+ *
+ */
+Ext.define('Ext.data.IdGenerator', {
+
+ isGenerator: true,
+
+ /**
+ * Initializes a new instance.
+ * @param {Object} config (optional) Configuration object to be applied to the new instance.
+ */
+ constructor: function(config) {
+ var me = this;
+
+ Ext.apply(me, config);
+
+ if (me.id) {
+ Ext.data.IdGenerator.all[me.id] = me;
+ }
+ },
+
+ /**
+ * @cfg {String} id
+ * The id by which to register a new instance. This instance can be found using the
+ * {@link Ext.data.IdGenerator#get} static method.
+ */
+
+ getRecId: function (rec) {
+ return rec.modelName + '-' + rec.internalId;
+ },
+
+ /**
+ * Generates and returns the next id. This method must be implemented by the derived
+ * class.
+ *
+ * @return {String} The next id.
+ * @method generate
+ * @abstract
+ */
+
+ statics: {
+ /**
+ * @property {Object} all
+ * This object is keyed by id to lookup instances.
+ * @private
+ * @static
+ */
+ all: {},
+
+ /**
+ * Returns the IdGenerator given its config description.
+ * @param {String/Object} config If this parameter is an IdGenerator instance, it is
+ * simply returned. If this is a string, it is first used as an id for lookup and
+ * then, if there is no match, as a type to create a new instance. This parameter
+ * can also be a config object that contains a `type` property (among others) that
+ * are used to create and configure the instance.
+ * @static
+ */
+ get: function (config) {
+ var generator,
+ id,
+ type;
+
+ if (typeof config == 'string') {
+ id = type = config;
+ config = null;
+ } else if (config.isGenerator) {
+ return config;
+ } else {
+ id = config.id || config.type;
+ type = config.type;
+ }
+
+ generator = this.all[id];
+ if (!generator) {
+ generator = Ext.create('idgen.' + type, config);
+ }
+
+ return generator;
+ }
+ }
+});
+
+/**
+ * @class Ext.data.SortTypes
+ * This class defines a series of static methods that are used on a
+ * {@link Ext.data.Field} for performing sorting. The methods cast the
+ * underlying values into a data type that is appropriate for sorting on
+ * that particular field. If a {@link Ext.data.Field#type} is specified,
+ * the sortType will be set to a sane default if the sortType is not
+ * explicitly defined on the field. The sortType will make any necessary
+ * modifications to the value and return it.
+ * <ul>
+ * <li><b>asText</b> - Removes any tags and converts the value to a string</li>
+ * <li><b>asUCText</b> - Removes any tags and converts the value to an uppercase string</li>
+ * <li><b>asUCText</b> - Converts the value to an uppercase string</li>
+ * <li><b>asDate</b> - Converts the value into Unix epoch time</li>
+ * <li><b>asFloat</b> - Converts the value to a floating point number</li>
+ * <li><b>asInt</b> - Converts the value to an integer number</li>
+ * </ul>
+ * <p>
+ * It is also possible to create a custom sortType that can be used throughout
+ * an application.
+ * <pre><code>
+Ext.apply(Ext.data.SortTypes, {
+ asPerson: function(person){
+ // expects an object with a first and last name property
+ return person.lastName.toUpperCase() + person.firstName.toLowerCase();
+ }
+});
+
+Ext.define('Employee', {
+ extend: 'Ext.data.Model',
+ fields: [{
+ name: 'person',
+ sortType: 'asPerson'
+ }, {
+ name: 'salary',
+ type: 'float' // sortType set to asFloat
+ }]
+});
+ * </code></pre>
+ * </p>
+ * @singleton
+ * @docauthor Evan Trimboli <evan@sencha.com>
+ */
+Ext.define('Ext.data.SortTypes', {
+
+ singleton: true,
+
+ /**
+ * Default sort that does nothing
+ * @param {Object} s The value being converted
+ * @return {Object} The comparison value
+ */
+ none : function(s) {
+ return s;
+ },
+
+ /**
+ * The regular expression used to strip tags
+ * @type {RegExp}
+ * @property
+ */
+ stripTagsRE : /<\/?[^>]+>/gi,
+
+ /**
+ * Strips all HTML tags to sort on text only
+ * @param {Object} s The value being converted
+ * @return {String} The comparison value
+ */
+ asText : function(s) {
+ return String(s).replace(this.stripTagsRE, "");
+ },
+
+ /**
+ * Strips all HTML tags to sort on text only - Case insensitive
+ * @param {Object} s The value being converted
+ * @return {String} The comparison value
+ */
+ asUCText : function(s) {
+ return String(s).toUpperCase().replace(this.stripTagsRE, "");
+ },
+
+ /**
+ * Case insensitive string
+ * @param {Object} s The value being converted
+ * @return {String} The comparison value
+ */
+ asUCString : function(s) {
+ return String(s).toUpperCase();
+ },
+
+ /**
+ * Date sorting
+ * @param {Object} s The value being converted
+ * @return {Number} The comparison value
+ */
+ asDate : function(s) {
+ if(!s){
+ return 0;
+ }
+ if(Ext.isDate(s)){
+ return s.getTime();
+ }
+ return Date.parse(String(s));
+ },
+
+ /**
+ * Float sorting
+ * @param {Object} s The value being converted
+ * @return {Number} The comparison value
+ */
+ asFloat : function(s) {
+ var val = parseFloat(String(s).replace(/,/g, ""));
+ return isNaN(val) ? 0 : val;
+ },
+
+ /**
+ * Integer sorting
+ * @param {Object} s The value being converted
+ * @return {Number} The comparison value
+ */
+ asInt : function(s) {
+ var val = parseInt(String(s).replace(/,/g, ""), 10);
+ return isNaN(val) ? 0 : val;
+ }
+});
+/**
+ * Represents a filter that can be applied to a {@link Ext.util.MixedCollection MixedCollection}. Can either simply
+ * filter on a property/value pair or pass in a filter function with custom logic. Filters are always used in the
+ * context of MixedCollections, though {@link Ext.data.Store Store}s frequently create them when filtering and searching
+ * on their records. Example usage:
+ *
+ * //set up a fictional MixedCollection containing a few people to filter on
+ * var allNames = new Ext.util.MixedCollection();
+ * allNames.addAll([
+ * {id: 1, name: 'Ed', age: 25},
+ * {id: 2, name: 'Jamie', age: 37},
+ * {id: 3, name: 'Abe', age: 32},
+ * {id: 4, name: 'Aaron', age: 26},
+ * {id: 5, name: 'David', age: 32}
+ * ]);
+ *
+ * var ageFilter = new Ext.util.Filter({
+ * property: 'age',
+ * value : 32
+ * });
+ *
+ * var longNameFilter = new Ext.util.Filter({
+ * filterFn: function(item) {
+ * return item.name.length > 4;
+ * }
+ * });
+ *
+ * //a new MixedCollection with the 3 names longer than 4 characters
+ * var longNames = allNames.filter(longNameFilter);
+ *
+ * //a new MixedCollection with the 2 people of age 24:
+ * var youngFolk = allNames.filter(ageFilter);
+ *
+ */
+Ext.define('Ext.util.Filter', {
+
+ /* Begin Definitions */
+
+ /* End Definitions */
+ /**
+ * @cfg {String} property
+ * The property to filter on. Required unless a {@link #filterFn} is passed
+ */
+
+ /**
+ * @cfg {Function} filterFn
+ * A custom filter function which is passed each item in the {@link Ext.util.MixedCollection} in turn. Should return
+ * true to accept each item or false to reject it
+ */
+
+ /**
+ * @cfg {Boolean} anyMatch
+ * True to allow any match - no regex start/end line anchors will be added.
+ */
+ anyMatch: false,
+
+ /**
+ * @cfg {Boolean} exactMatch
+ * True to force exact match (^ and $ characters added to the regex). Ignored if anyMatch is true.
+ */
+ exactMatch: false,
+
+ /**
+ * @cfg {Boolean} caseSensitive
+ * True to make the regex case sensitive (adds 'i' switch to regex).
+ */
+ caseSensitive: false,
+
+ /**
+ * @cfg {String} root
+ * Optional root property. This is mostly useful when filtering a Store, in which case we set the root to 'data' to
+ * make the filter pull the {@link #property} out of the data object of each item
+ */
+
+ /**
+ * Creates new Filter.
+ * @param {Object} [config] Config object
+ */
+ constructor: function(config) {
+ var me = this;
+ Ext.apply(me, config);
+
+ //we're aliasing filter to filterFn mostly for API cleanliness reasons, despite the fact it dirties the code here.
+ //Ext.util.Sorter takes a sorterFn property but allows .sort to be called - we do the same here
+ me.filter = me.filter || me.filterFn;
+
+ if (me.filter === undefined) {
+ if (me.property === undefined || me.value === undefined) {
+ // Commented this out temporarily because it stops us using string ids in models. TODO: Remove this once
+ // Model has been updated to allow string ids
+
+ // Ext.Error.raise("A Filter requires either a property or a filterFn to be set");
+ } else {
+ me.filter = me.createFilterFn();
+ }
+
+ me.filterFn = me.filter;
+ }
+ },
+
+ /**
+ * @private
+ * Creates a filter function for the configured property/value/anyMatch/caseSensitive options for this Filter
+ */
+ createFilterFn: function() {
+ var me = this,
+ matcher = me.createValueMatcher(),
+ property = me.property;
+
+ return function(item) {
+ var value = me.getRoot.call(me, item)[property];
+ return matcher === null ? value === null : matcher.test(value);
+ };
+ },
+
+ /**
+ * @private
+ * Returns the root property of the given item, based on the configured {@link #root} property
+ * @param {Object} item The item
+ * @return {Object} The root property of the object
+ */
+ getRoot: function(item) {
+ var root = this.root;
+ return root === undefined ? item : item[root];
+ },
+
+ /**
+ * @private
+ * Returns a regular expression based on the given value and matching options
+ */
+ createValueMatcher : function() {
+ var me = this,
+ value = me.value,
+ anyMatch = me.anyMatch,
+ exactMatch = me.exactMatch,
+ caseSensitive = me.caseSensitive,
+ escapeRe = Ext.String.escapeRegex;
+
+ if (value === null) {
+ return value;
+ }
+
+ if (!value.exec) { // not a regex
+ value = String(value);
+
+ if (anyMatch === true) {
+ value = escapeRe(value);
+ } else {
+ value = '^' + escapeRe(value);
+ if (exactMatch === true) {
+ value += '$';
+ }
+ }
+ value = new RegExp(value, caseSensitive ? '' : 'i');
+ }
+
+ return value;
+ }
+});
+/**
+ * Represents a single sorter that can be applied to a Store. The sorter is used
+ * to compare two values against each other for the purpose of ordering them. Ordering
+ * is achieved by specifying either:
+ *
+ * - {@link #property A sorting property}
+ * - {@link #sorterFn A sorting function}
+ *
+ * As a contrived example, we can specify a custom sorter that sorts by rank:
+ *
+ * Ext.define('Person', {
+ * extend: 'Ext.data.Model',
+ * fields: ['name', 'rank']
+ * });
+ *
+ * Ext.create('Ext.data.Store', {
+ * model: 'Person',
+ * proxy: 'memory',
+ * sorters: [{
+ * sorterFn: function(o1, o2){
+ * var getRank = function(o){
+ * var name = o.get('rank');
+ * if (name === 'first') {
+ * return 1;
+ * } else if (name === 'second') {
+ * return 2;
+ * } else {
+ * return 3;
+ * }
+ * },
+ * rank1 = getRank(o1),
+ * rank2 = getRank(o2);
+ *
+ * if (rank1 === rank2) {
+ * return 0;
+ * }
+ *
+ * return rank1 < rank2 ? -1 : 1;
+ * }
+ * }],
+ * data: [{
+ * name: 'Person1',
+ * rank: 'second'
+ * }, {
+ * name: 'Person2',
+ * rank: 'third'
+ * }, {
+ * name: 'Person3',
+ * rank: 'first'
+ * }]
+ * });
+ */
+Ext.define('Ext.util.Sorter', {
+
+ /**
+ * @cfg {String} property
+ * The property to sort by. Required unless {@link #sorterFn} is provided. The property is extracted from the object
+ * directly and compared for sorting using the built in comparison operators.
+ */
+
+ /**
+ * @cfg {Function} sorterFn
+ * A specific sorter function to execute. Can be passed instead of {@link #property}. This sorter function allows
+ * for any kind of custom/complex comparisons. The sorterFn receives two arguments, the objects being compared. The
+ * function should return:
+ *
+ * - -1 if o1 is "less than" o2
+ * - 0 if o1 is "equal" to o2
+ * - 1 if o1 is "greater than" o2
+ */
+
+ /**
+ * @cfg {String} root
+ * Optional root property. This is mostly useful when sorting a Store, in which case we set the root to 'data' to
+ * make the filter pull the {@link #property} out of the data object of each item
+ */
+
+ /**
+ * @cfg {Function} transform
+ * A function that will be run on each value before it is compared in the sorter. The function will receive a single
+ * argument, the value.
+ */
+
+ /**
+ * @cfg {String} direction
+ * The direction to sort by.
+ */
+ direction: "ASC",
+
+ constructor: function(config) {
+ var me = this;
+
+ Ext.apply(me, config);
+
+
+ me.updateSortFunction();
+ },
+
+ /**
+ * @private
+ * Creates and returns a function which sorts an array by the given property and direction
+ * @return {Function} A function which sorts by the property/direction combination provided
+ */
+ createSortFunction: function(sorterFn) {
+ var me = this,
+ property = me.property,
+ direction = me.direction || "ASC",
+ modifier = direction.toUpperCase() == "DESC" ? -1 : 1;
+
+ //create a comparison function. Takes 2 objects, returns 1 if object 1 is greater,
+ //-1 if object 2 is greater or 0 if they are equal
+ return function(o1, o2) {
+ return modifier * sorterFn.call(me, o1, o2);
+ };
+ },
+
+ /**
+ * @private
+ * Basic default sorter function that just compares the defined property of each object
+ */
+ defaultSorterFn: function(o1, o2) {
+ var me = this,
+ transform = me.transform,
+ v1 = me.getRoot(o1)[me.property],
+ v2 = me.getRoot(o2)[me.property];
+
+ if (transform) {
+ v1 = transform(v1);
+ v2 = transform(v2);
+ }
+
+ return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0);
+ },
+
+ /**
+ * @private
+ * Returns the root property of the given item, based on the configured {@link #root} property
+ * @param {Object} item The item
+ * @return {Object} The root property of the object
+ */
+ getRoot: function(item) {
+ return this.root === undefined ? item : item[this.root];
+ },
+
+ /**
+ * Set the sorting direction for this sorter.
+ * @param {String} direction The direction to sort in. Should be either 'ASC' or 'DESC'.
+ */
+ setDirection: function(direction) {
+ var me = this;
+ me.direction = direction;
+ me.updateSortFunction();
+ },
+
+ /**
+ * Toggles the sorting direction for this sorter.
+ */
+ toggle: function() {
+ var me = this;
+ me.direction = Ext.String.toggle(me.direction, "ASC", "DESC");
+ me.updateSortFunction();
+ },
+
+ /**
+ * Update the sort function for this sorter.
+ * @param {Function} [fn] A new sorter function for this sorter. If not specified it will use the default
+ * sorting function.
+ */
+ updateSortFunction: function(fn) {
+ var me = this;
+ fn = fn || me.sorterFn || me.defaultSorterFn;
+ me.sort = me.createSortFunction(fn);
+ }
+});
+/**
+ * @author Ed Spencer
+ *
+ * Represents a single read or write operation performed by a {@link Ext.data.proxy.Proxy Proxy}. Operation objects are
+ * used to enable communication between Stores and Proxies. Application developers should rarely need to interact with
+ * Operation objects directly.
+ *
+ * Several Operations can be batched together in a {@link Ext.data.Batch batch}.
+ */
+Ext.define('Ext.data.Operation', {
+ /**
+ * @cfg {Boolean} synchronous
+ * True if this Operation is to be executed synchronously. This property is inspected by a
+ * {@link Ext.data.Batch Batch} to see if a series of Operations can be executed in parallel or not.
+ */
+ synchronous: true,
+
+ /**
+ * @cfg {String} action
+ * The action being performed by this Operation. Should be one of 'create', 'read', 'update' or 'destroy'.
+ */
+ action: undefined,
+
+ /**
+ * @cfg {Ext.util.Filter[]} filters
+ * Optional array of filter objects. Only applies to 'read' actions.
+ */
+ filters: undefined,
+
+ /**
+ * @cfg {Ext.util.Sorter[]} sorters
+ * Optional array of sorter objects. Only applies to 'read' actions.
+ */
+ sorters: undefined,
+
+ /**
+ * @cfg {Ext.util.Grouper} group
+ * Optional grouping configuration. Only applies to 'read' actions where grouping is desired.
+ */
+ group: undefined,
+
+ /**
+ * @cfg {Number} start
+ * The start index (offset), used in paging when running a 'read' action.
+ */
+ start: undefined,
+
+ /**
+ * @cfg {Number} limit
+ * The number of records to load. Used on 'read' actions when paging is being used.
+ */
+ limit: undefined,
+
+ /**
+ * @cfg {Ext.data.Batch} batch
+ * The batch that this Operation is a part of.
+ */
+ batch: undefined,
+
+ /**
+ * @cfg {Function} callback
+ * Function to execute when operation completed. Will be called with the following parameters:
+ *
+ * - records : Array of Ext.data.Model objects.
+ * - operation : The Ext.data.Operation itself.
+ * - success : True when operation completed successfully.
+ */
+ callback: undefined,
+
+ /**
+ * @cfg {Object} scope
+ * Scope for the {@link #callback} function.
+ */
+ scope: undefined,
+
+ /**
+ * @property {Boolean} started
+ * Read-only property tracking the start status of this Operation. Use {@link #isStarted}.
+ * @private
+ */
+ started: false,
+
+ /**
+ * @property {Boolean} running
+ * Read-only property tracking the run status of this Operation. Use {@link #isRunning}.
+ * @private
+ */
+ running: false,
+
+ /**
+ * @property {Boolean} complete
+ * Read-only property tracking the completion status of this Operation. Use {@link #isComplete}.
+ * @private
+ */
+ complete: false,
+
+ /**
+ * @property {Boolean} success
+ * Read-only property tracking whether the Operation was successful or not. This starts as undefined and is set to true
+ * or false by the Proxy that is executing the Operation. It is also set to false by {@link #setException}. Use
+ * {@link #wasSuccessful} to query success status.
+ * @private
+ */
+ success: undefined,
+
+ /**
+ * @property {Boolean} exception
+ * Read-only property tracking the exception status of this Operation. Use {@link #hasException} and see {@link #getError}.
+ * @private
+ */
+ exception: false,
+
+ /**
+ * @property {String/Object} error
+ * The error object passed when {@link #setException} was called. This could be any object or primitive.
+ * @private
+ */
+ error: undefined,
+
+ /**
+ * @property {RegExp} actionCommitRecordsRe
+ * The RegExp used to categorize actions that require record commits.
+ */
+ actionCommitRecordsRe: /^(?:create|update)$/i,
+
+ /**
+ * @property {RegExp} actionSkipSyncRe
+ * The RegExp used to categorize actions that skip local record synchronization. This defaults
+ * to match 'destroy'.
+ */
+ actionSkipSyncRe: /^destroy$/i,
+
+ /**
+ * Creates new Operation object.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ Ext.apply(this, config || {});
+ },
+
+ /**
+ * This method is called to commit data to this instance's records given the records in
+ * the server response. This is followed by calling {@link Ext.data.Model#commit} on all
+ * those records (for 'create' and 'update' actions).
+ *
+ * If this {@link #action} is 'destroy', any server records are ignored and the
+ * {@link Ext.data.Model#commit} method is not called.
+ *
+ * @param {Ext.data.Model[]} serverRecords An array of {@link Ext.data.Model} objects returned by
+ * the server.
+ * @markdown
+ */
+ commitRecords: function (serverRecords) {
+ var me = this,
+ mc, index, clientRecords, serverRec, clientRec;
+
+ if (!me.actionSkipSyncRe.test(me.action)) {
+ clientRecords = me.records;
+
+ if (clientRecords && clientRecords.length) {
+ mc = Ext.create('Ext.util.MixedCollection', true, function(r) {return r.getId();});
+ mc.addAll(clientRecords);
+
+ for (index = serverRecords ? serverRecords.length : 0; index--; ) {
+ serverRec = serverRecords[index];
+ clientRec = mc.get(serverRec.getId());
+
+ if (clientRec) {
+ clientRec.beginEdit();
+ clientRec.set(serverRec.data);
+ clientRec.endEdit(true);
+ }
+ }
+
+ if (me.actionCommitRecordsRe.test(me.action)) {
+ for (index = clientRecords.length; index--; ) {
+ clientRecords[index].commit();
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Marks the Operation as started.
+ */
+ setStarted: function() {
+ this.started = true;
+ this.running = true;
+ },
+
+ /**
+ * Marks the Operation as completed.
+ */
+ setCompleted: function() {
+ this.complete = true;
+ this.running = false;
+ },
+
+ /**
+ * Marks the Operation as successful.
+ */
+ setSuccessful: function() {
+ this.success = true;
+ },
+
+ /**
+ * Marks the Operation as having experienced an exception. Can be supplied with an option error message/object.
+ * @param {String/Object} error (optional) error string/object
+ */
+ setException: function(error) {
+ this.exception = true;
+ this.success = false;
+ this.running = false;
+ this.error = error;
+ },
+
+ /**
+ * Returns true if this Operation encountered an exception (see also {@link #getError})
+ * @return {Boolean} True if there was an exception
+ */
+ hasException: function() {
+ return this.exception === true;
+ },
+
+ /**
+ * Returns the error string or object that was set using {@link #setException}
+ * @return {String/Object} The error object
+ */
+ getError: function() {
+ return this.error;
+ },
+
+ /**
+ * Returns an array of Ext.data.Model instances as set by the Proxy.
+ * @return {Ext.data.Model[]} Any loaded Records
+ */
+ getRecords: function() {
+ var resultSet = this.getResultSet();
+
+ return (resultSet === undefined ? this.records : resultSet.records);
+ },
+
+ /**
+ * Returns the ResultSet object (if set by the Proxy). This object will contain the {@link Ext.data.Model model}
+ * instances as well as meta data such as number of instances fetched, number available etc
+ * @return {Ext.data.ResultSet} The ResultSet object
+ */
+ getResultSet: function() {
+ return this.resultSet;
+ },
+
+ /**
+ * Returns true if the Operation has been started. Note that the Operation may have started AND completed, see
+ * {@link #isRunning} to test if the Operation is currently running.
+ * @return {Boolean} True if the Operation has started
+ */
+ isStarted: function() {
+ return this.started === true;
+ },
+
+ /**
+ * Returns true if the Operation has been started but has not yet completed.
+ * @return {Boolean} True if the Operation is currently running
+ */
+ isRunning: function() {
+ return this.running === true;
+ },
+
+ /**
+ * Returns true if the Operation has been completed
+ * @return {Boolean} True if the Operation is complete
+ */
+ isComplete: function() {
+ return this.complete === true;
+ },
+
+ /**
+ * Returns true if the Operation has completed and was successful
+ * @return {Boolean} True if successful
+ */
+ wasSuccessful: function() {
+ return this.isComplete() && this.success === true;
+ },
+
+ /**
+ * @private
+ * Associates this Operation with a Batch
+ * @param {Ext.data.Batch} batch The batch
+ */
+ setBatch: function(batch) {
+ this.batch = batch;
+ },
+
+ /**
+ * Checks whether this operation should cause writing to occur.
+ * @return {Boolean} Whether the operation should cause a write to occur.
+ */
+ allowWrite: function() {
+ return this.action != 'read';
+ }
+});
+/**
+ * @author Ed Spencer
+ *
+ * This singleton contains a set of validation functions that can be used to validate any type of data. They are most
+ * often used in {@link Ext.data.Model Models}, where they are automatically set up and executed.
+ */
+Ext.define('Ext.data.validations', {
+ singleton: true,
+
+ /**
+ * @property {String} presenceMessage
+ * The default error message used when a presence validation fails.
+ */
+ presenceMessage: 'must be present',
+
+ /**
+ * @property {String} lengthMessage
+ * The default error message used when a length validation fails.
+ */
+ lengthMessage: 'is the wrong length',
+
+ /**
+ * @property {Boolean} formatMessage
+ * The default error message used when a format validation fails.
+ */
+ formatMessage: 'is the wrong format',
+
+ /**
+ * @property {String} inclusionMessage
+ * The default error message used when an inclusion validation fails.
+ */
+ inclusionMessage: 'is not included in the list of acceptable values',
+
+ /**
+ * @property {String} exclusionMessage
+ * The default error message used when an exclusion validation fails.
+ */
+ exclusionMessage: 'is not an acceptable value',
+
+ /**
+ * @property {String} emailMessage
+ * The default error message used when an email validation fails
+ */
+ emailMessage: 'is not a valid email address',
+
+ /**
+ * @property {RegExp} emailRe
+ * The regular expression used to validate email addresses
+ */
+ emailRe: /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/,
+
+ /**
+ * Validates that the given value is present.
+ * For example:
+ *
+ * validations: [{type: 'presence', field: 'age'}]
+ *
+ * @param {Object} config Config object
+ * @param {Object} value The value to validate
+ * @return {Boolean} True if validation passed
+ */
+ presence: function(config, value) {
+ if (value === undefined) {
+ value = config;
+ }
+
+ //we need an additional check for zero here because zero is an acceptable form of present data
+ return !!value || value === 0;
+ },
+
+ /**
+ * Returns true if the given value is between the configured min and max values.
+ * For example:
+ *
+ * validations: [{type: 'length', field: 'name', min: 2}]
+ *
+ * @param {Object} config Config object
+ * @param {String} value The value to validate
+ * @return {Boolean} True if the value passes validation
+ */
+ length: function(config, value) {
+ if (value === undefined || value === null) {
+ return false;
+ }
+
+ var length = value.length,
+ min = config.min,
+ max = config.max;
+
+ if ((min && length < min) || (max && length > max)) {
+ return false;
+ } else {
+ return true;
+ }
+ },
+
+ /**
+ * Validates that an email string is in the correct format
+ * @param {Object} config Config object
+ * @param {String} email The email address
+ * @return {Boolean} True if the value passes validation
+ */
+ email: function(config, email) {
+ return Ext.data.validations.emailRe.test(email);
+ },
+
+ /**
+ * Returns true if the given value passes validation against the configured `matcher` regex.
+ * For example:
+ *
+ * validations: [{type: 'format', field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}]
+ *
+ * @param {Object} config Config object
+ * @param {String} value The value to validate
+ * @return {Boolean} True if the value passes the format validation
+ */
+ format: function(config, value) {
+ return !!(config.matcher && config.matcher.test(value));
+ },
+
+ /**
+ * Validates that the given value is present in the configured `list`.
+ * For example:
+ *
+ * validations: [{type: 'inclusion', field: 'gender', list: ['Male', 'Female']}]
+ *
+ * @param {Object} config Config object
+ * @param {String} value The value to validate
+ * @return {Boolean} True if the value is present in the list
+ */
+ inclusion: function(config, value) {
+ return config.list && Ext.Array.indexOf(config.list,value) != -1;
+ },
+
+ /**
+ * Validates that the given value is present in the configured `list`.
+ * For example:
+ *
+ * validations: [{type: 'exclusion', field: 'username', list: ['Admin', 'Operator']}]
+ *
+ * @param {Object} config Config object
+ * @param {String} value The value to validate
+ * @return {Boolean} True if the value is not present in the list
+ */
+ exclusion: function(config, value) {
+ return config.list && Ext.Array.indexOf(config.list,value) == -1;
+ }
+});
+/**
+ * @author Ed Spencer
+ *
+ * Simple wrapper class that represents a set of records returned by a Proxy.
+ */
+Ext.define('Ext.data.ResultSet', {
+ /**
+ * @cfg {Boolean} loaded
+ * True if the records have already been loaded. This is only meaningful when dealing with
+ * SQL-backed proxies.
+ */
+ loaded: true,
+
+ /**
+ * @cfg {Number} count
+ * The number of records in this ResultSet. Note that total may differ from this number.
+ */
+ count: 0,
+
+ /**
+ * @cfg {Number} total
+ * The total number of records reported by the data source. This ResultSet may form a subset of
+ * those records (see {@link #count}).
+ */
+ total: 0,
+
+ /**
+ * @cfg {Boolean} success
+ * True if the ResultSet loaded successfully, false if any errors were encountered.
+ */
+ success: false,
+
+ /**
+ * @cfg {Ext.data.Model[]} records (required)
+ * The array of record instances.
+ */
+
+ /**
+ * Creates the resultSet
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config) {
+ Ext.apply(this, config);
+
+ /**
+ * @property {Number} totalRecords
+ * Copy of this.total.
+ * @deprecated Will be removed in Ext JS 5.0. Use {@link #total} instead.
+ */
+ this.totalRecords = this.total;
+
+ if (config.count === undefined) {
+ this.count = this.records.length;
+ }
+ }
+});
+/**
+ * @author Ed Spencer
+ *
+ * Base Writer class used by most subclasses of {@link Ext.data.proxy.Server}. This class is responsible for taking a
+ * set of {@link Ext.data.Operation} objects and a {@link Ext.data.Request} object and modifying that request based on
+ * the Operations.
+ *
+ * For example a Ext.data.writer.Json would format the Operations and their {@link Ext.data.Model} instances based on
+ * the config options passed to the JsonWriter's constructor.
+ *
+ * Writers are not needed for any kind of local storage - whether via a {@link Ext.data.proxy.WebStorage Web Storage
+ * proxy} (see {@link Ext.data.proxy.LocalStorage localStorage} and {@link Ext.data.proxy.SessionStorage
+ * sessionStorage}) or just in memory via a {@link Ext.data.proxy.Memory MemoryProxy}.
+ */
+Ext.define('Ext.data.writer.Writer', {
+ alias: 'writer.base',
+ alternateClassName: ['Ext.data.DataWriter', 'Ext.data.Writer'],
+
+ /**
+ * @cfg {Boolean} writeAllFields
+ * True to write all fields from the record to the server. If set to false it will only send the fields that were
+ * modified. Note that any fields that have {@link Ext.data.Field#persist} set to false will still be ignored.
+ */
+ writeAllFields: true,
+
+ /**
+ * @cfg {String} nameProperty
+ * This property is used to read the key for each value that will be sent to the server. For example:
+ *
+ * Ext.define('Person', {
+ * extend: 'Ext.data.Model',
+ * fields: [{
+ * name: 'first',
+ * mapping: 'firstName'
+ * }, {
+ * name: 'last',
+ * mapping: 'lastName'
+ * }, {
+ * name: 'age'
+ * }]
+ * });
+ * new Ext.data.writer.Writer({
+ * writeAllFields: true,
+ * nameProperty: 'mapping'
+ * });
+ *
+ * // This will be sent to the server
+ * {
+ * firstName: 'first name value',
+ * lastName: 'last name value',
+ * age: 1
+ * }
+ *
+ * If the value is not present, the field name will always be used.
+ */
+ nameProperty: 'name',
+
+ /**
+ * Creates new Writer.
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config) {
+ Ext.apply(this, config);
+ },
+
+ /**
+ * Prepares a Proxy's Ext.data.Request object
+ * @param {Ext.data.Request} request The request object
+ * @return {Ext.data.Request} The modified request object
+ */
+ write: function(request) {
+ var operation = request.operation,
+ records = operation.records || [],
+ len = records.length,
+ i = 0,
+ data = [];
+
+ for (; i < len; i++) {
+ data.push(this.getRecordData(records[i]));
+ }
+ return this.writeRecords(request, data);
+ },
+
+ /**
+ * Formats the data for each record before sending it to the server. This method should be overridden to format the
+ * data in a way that differs from the default.
+ * @param {Object} record The record that we are writing to the server.
+ * @return {Object} An object literal of name/value keys to be written to the server. By default this method returns
+ * the data property on the record.
+ */
+ getRecordData: function(record) {
+ var isPhantom = record.phantom === true,
+ writeAll = this.writeAllFields || isPhantom,
+ nameProperty = this.nameProperty,
+ fields = record.fields,
+ data = {},
+ changes,
+ name,
+ field,
+ key;
+
+ if (writeAll) {
+ fields.each(function(field){
+ if (field.persist) {
+ name = field[nameProperty] || field.name;
+ data[name] = record.get(field.name);
+ }
+ });
+ } else {
+ // Only write the changes
+ changes = record.getChanges();
+ for (key in changes) {
+ if (changes.hasOwnProperty(key)) {
+ field = fields.get(key);
+ name = field[nameProperty] || field.name;
+ data[name] = changes[key];
+ }
+ }
+ if (!isPhantom) {
+ // always include the id for non phantoms
+ data[record.idProperty] = record.getId();
+ }
+ }
+ return data;
+ }
+});
+
+/**
+ * A mixin to add floating capability to a Component.
+ */
+Ext.define('Ext.util.Floating', {
+
+ uses: ['Ext.Layer', 'Ext.window.Window'],
+
+ /**
+ * @cfg {Boolean} focusOnToFront
+ * Specifies whether the floated component should be automatically {@link Ext.Component#focus focused} when
+ * it is {@link #toFront brought to the front}.
+ */
+ focusOnToFront: true,
+
+ /**
+ * @cfg {String/Boolean} shadow
+ * Specifies whether the floating component should be given a shadow. Set to true to automatically create an {@link
+ * Ext.Shadow}, or a string indicating the shadow's display {@link Ext.Shadow#mode}. Set to false to disable the
+ * shadow.
+ */
+ shadow: 'sides',
+
+ constructor: function(config) {
+ var me = this;
+
+ me.floating = true;
+ me.el = Ext.create('Ext.Layer', Ext.apply({}, config, {
+ hideMode: me.hideMode,
+ hidden: me.hidden,
+ shadow: Ext.isDefined(me.shadow) ? me.shadow : 'sides',
+ shadowOffset: me.shadowOffset,
+ constrain: false,
+ shim: me.shim === false ? false : undefined
+ }), me.el);
+ },
+
+ onFloatRender: function() {
+ var me = this;
+ me.zIndexParent = me.getZIndexParent();
+ me.setFloatParent(me.ownerCt);
+ delete me.ownerCt;
+
+ if (me.zIndexParent) {
+ me.zIndexParent.registerFloatingItem(me);
+ } else {
+ Ext.WindowManager.register(me);
+ }
+ },
+
+ setFloatParent: function(floatParent) {
+ var me = this;
+
+ // Remove listeners from previous floatParent
+ if (me.floatParent) {
+ me.mun(me.floatParent, {
+ hide: me.onFloatParentHide,
+ show: me.onFloatParentShow,
+ scope: me
+ });
+ }
+
+ me.floatParent = floatParent;
+
+ // Floating Components as children of Containers must hide when their parent hides.
+ if (floatParent) {
+ me.mon(me.floatParent, {
+ hide: me.onFloatParentHide,
+ show: me.onFloatParentShow,
+ scope: me
+ });
+ }
+
+ // If a floating Component is configured to be constrained, but has no configured
+ // constrainTo setting, set its constrainTo to be it's ownerCt before rendering.
+ if ((me.constrain || me.constrainHeader) && !me.constrainTo) {
+ me.constrainTo = floatParent ? floatParent.getTargetEl() : me.container;
+ }
+ },
+
+ onFloatParentHide: function() {
+ var me = this;
+
+ if (me.hideOnParentHide !== false) {
+ me.showOnParentShow = me.isVisible();
+ me.hide();
+ }
+ },
+
+ onFloatParentShow: function() {
+ if (this.showOnParentShow) {
+ delete this.showOnParentShow;
+ this.show();
+ }
+ },
+
+ /**
+ * @private
+ * Finds the ancestor Container responsible for allocating zIndexes for the passed Component.
+ *
+ * That will be the outermost floating Container (a Container which has no ownerCt and has floating:true).
+ *
+ * If we have no ancestors, or we walk all the way up to the document body, there's no zIndexParent,
+ * and the global Ext.WindowManager will be used.
+ */
+ getZIndexParent: function() {
+ var p = this.ownerCt,
+ c;
+
+ if (p) {
+ while (p) {
+ c = p;
+ p = p.ownerCt;
+ }
+ if (c.floating) {
+ return c;
+ }
+ }
+ },
+
+ // private
+ // z-index is managed by the zIndexManager and may be overwritten at any time.
+ // Returns the next z-index to be used.
+ // If this is a Container, then it will have rebased any managed floating Components,
+ // and so the next available z-index will be approximately 10000 above that.
+ setZIndex: function(index) {
+ var me = this;
+ me.el.setZIndex(index);
+
+ // Next item goes 10 above;
+ index += 10;
+
+ // When a Container with floating items has its z-index set, it rebases any floating items it is managing.
+ // The returned value is a round number approximately 10000 above the last z-index used.
+ if (me.floatingItems) {
+ index = Math.floor(me.floatingItems.setBase(index) / 100) * 100 + 10000;
+ }
+ return index;
+ },
+
+ /**
+ * Moves this floating Component into a constrain region.
+ *
+ * By default, this Component is constrained to be within the container it was added to, or the element it was
+ * rendered to.
+ *
+ * An alternative constraint may be passed.
+ * @param {String/HTMLElement/Ext.Element/Ext.util.Region} constrainTo (Optional) The Element or {@link Ext.util.Region Region} into which this Component is
+ * to be constrained. Defaults to the element into which this floating Component was rendered.
+ */
+ doConstrain: function(constrainTo) {
+ var me = this,
+ vector = me.getConstrainVector(constrainTo || me.el.getScopeParent()),
+ xy;
+
+ if (vector) {
+ xy = me.getPosition();
+ xy[0] += vector[0];
+ xy[1] += vector[1];
+ me.setPosition(xy);
+ }
+ },
+
+
+ /**
+ * Gets the x/y offsets to constrain this float
+ * @private
+ * @param {String/HTMLElement/Ext.Element/Ext.util.Region} constrainTo (Optional) The Element or {@link Ext.util.Region Region} into which this Component is to be constrained.
+ * @return {Number[]} The x/y constraints
+ */
+ getConstrainVector: function(constrainTo){
+ var me = this,
+ el;
+
+ if (me.constrain || me.constrainHeader) {
+ el = me.constrainHeader ? me.header.el : me.el;
+ constrainTo = constrainTo || (me.floatParent && me.floatParent.getTargetEl()) || me.container;
+ return el.getConstrainVector(constrainTo);
+ }
+ },
+
+ /**
+ * Aligns this floating Component to the specified element
+ *
+ * @param {Ext.Component/Ext.Element/HTMLElement/String} element
+ * The element or {@link Ext.Component} to align to. If passing a component, it must be a
+ * omponent instance. If a string id is passed, it will be used as an element id.
+ * @param {String} [position="tl-bl?"] The position to align to (see {@link
+ * Ext.Element#alignTo} for more details).
+ * @param {Number[]} [offsets] Offset the positioning by [x, y]
+ * @return {Ext.Component} this
+ */
+ alignTo: function(element, position, offsets) {
+ if (element.isComponent) {
+ element = element.getEl();
+ }
+ var xy = this.el.getAlignToXY(element, position, offsets);
+ this.setPagePosition(xy);
+ return this;
+ },
+
+ /**
+ * Brings this floating Component to the front of any other visible, floating Components managed by the same {@link
+ * Ext.ZIndexManager ZIndexManager}
+ *
+ * If this Component is modal, inserts the modal mask just below this Component in the z-index stack.
+ *
+ * @param {Boolean} [preventFocus=false] Specify `true` to prevent the Component from being focused.
+ * @return {Ext.Component} this
+ */
+ toFront: function(preventFocus) {
+ var me = this;
+
+ // Find the floating Component which provides the base for this Component's zIndexing.
+ // That must move to front to then be able to rebase its zIndex stack and move this to the front
+ if (me.zIndexParent) {
+ me.zIndexParent.toFront(true);
+ }
+ if (me.zIndexManager.bringToFront(me)) {
+ if (!Ext.isDefined(preventFocus)) {
+ preventFocus = !me.focusOnToFront;
+ }
+ if (!preventFocus) {
+ // Kick off a delayed focus request.
+ // If another floating Component is toFronted before the delay expires
+ // this will not receive focus.
+ me.focus(false, true);
+ }
+ }
+ return me;
+ },
+
+ /**
+ * This method is called internally by {@link Ext.ZIndexManager} to signal that a floating Component has either been
+ * moved to the top of its zIndex stack, or pushed from the top of its zIndex stack.
+ *
+ * If a _Window_ is superceded by another Window, deactivating it hides its shadow.
+ *
+ * This method also fires the {@link Ext.Component#activate activate} or
+ * {@link Ext.Component#deactivate deactivate} event depending on which action occurred.
+ *
+ * @param {Boolean} [active=false] True to activate the Component, false to deactivate it.
+ * @param {Ext.Component} [newActive] The newly active Component which is taking over topmost zIndex position.
+ */
+ setActive: function(active, newActive) {
+ var me = this;
+
+ if (active) {
+ if (me.el.shadow && !me.maximized) {
+ me.el.enableShadow(true);
+ }
+ me.fireEvent('activate', me);
+ } else {
+ // Only the *Windows* in a zIndex stack share a shadow. All other types of floaters
+ // can keep their shadows all the time
+ if ((me instanceof Ext.window.Window) && (newActive instanceof Ext.window.Window)) {
+ me.el.disableShadow();
+ }
+ me.fireEvent('deactivate', me);
+ }
+ },
+
+ /**
+ * Sends this Component to the back of (lower z-index than) any other visible windows
+ * @return {Ext.Component} this
+ */
+ toBack: function() {
+ this.zIndexManager.sendToBack(this);
+ return this;
+ },
+
+ /**
+ * Center this Component in its container.
+ * @return {Ext.Component} this
+ */
+ center: function() {
+ var me = this,
+ xy = me.el.getAlignToXY(me.container, 'c-c');
+ me.setPagePosition(xy);
+ return me;
+ },
+
+ // private
+ syncShadow : function(){
+ if (this.floating) {
+ this.el.sync(true);
+ }
+ },
+
+ // private
+ fitContainer: function() {
+ var parent = this.floatParent,
+ container = parent ? parent.getTargetEl() : this.container,
+ size = container.getViewSize(false);
+
+ this.setSize(size);
+ }
+});
+/**
+ * Base Layout class - extended by ComponentLayout and ContainerLayout
+ */
+Ext.define('Ext.layout.Layout', {
+
+ /* Begin Definitions */
+
+ /* End Definitions */
+
+ isLayout: true,
+ initialized: false,
+
+ statics: {
+ create: function(layout, defaultType) {
+ var type;
+ if (layout instanceof Ext.layout.Layout) {
+ return Ext.createByAlias('layout.' + layout);
+ } else {
+ if (!layout || typeof layout === 'string') {
+ type = layout || defaultType;
+ layout = {};
+ }
+ else {
+ type = layout.type || defaultType;
+ }
+ return Ext.createByAlias('layout.' + type, layout || {});
+ }
+ }
+ },
+
+ constructor : function(config) {
+ this.id = Ext.id(null, this.type + '-');
+ Ext.apply(this, config);
+ },
+
+ /**
+ * @private
+ */
+ layout : function() {
+ var me = this;
+ me.layoutBusy = true;
+ me.initLayout();
+
+ if (me.beforeLayout.apply(me, arguments) !== false) {
+ me.layoutCancelled = false;
+ me.onLayout.apply(me, arguments);
+ me.childrenChanged = false;
+ me.owner.needsLayout = false;
+ me.layoutBusy = false;
+ me.afterLayout.apply(me, arguments);
+ }
+ else {
+ me.layoutCancelled = true;
+ }
+ me.layoutBusy = false;
+ me.doOwnerCtLayouts();
+ },
+
+ beforeLayout : function() {
+ this.renderChildren();
+ return true;
+ },
+
+ renderChildren: function () {
+ this.renderItems(this.getLayoutItems(), this.getRenderTarget());
+ },
+
+ /**
+ * @private
+ * Iterates over all passed items, ensuring they are rendered. If the items are already rendered,
+ * also determines if the items are in the proper place dom.
+ */
+ renderItems : function(items, target) {
+ var me = this,
+ ln = items.length,
+ i = 0,
+ item;
+
+ for (; i < ln; i++) {
+ item = items[i];
+ if (item && !item.rendered) {
+ me.renderItem(item, target, i);
+ } else if (!me.isValidParent(item, target, i)) {
+ me.moveItem(item, target, i);
+ } else {
+ // still need to configure the item, it may have moved in the container.
+ me.configureItem(item);
+ }
+ }
+ },
+
+ // @private - Validates item is in the proper place in the dom.
+ isValidParent : function(item, target, position) {
+ var dom = item.el ? item.el.dom : Ext.getDom(item);
+ if (dom && target && target.dom) {
+ if (Ext.isNumber(position) && dom !== target.dom.childNodes[position]) {
+ return false;
+ }
+ return (dom.parentNode == (target.dom || target));
+ }
+ return false;
+ },
+
+ /**
+ * @private
+ * Renders the given Component into the target Element.
+ * @param {Ext.Component} item The Component to render
+ * @param {Ext.Element} target The target Element
+ * @param {Number} position The position within the target to render the item to
+ */
+ renderItem : function(item, target, position) {
+ var me = this;
+ if (!item.rendered) {
+ if (me.itemCls) {
+ item.addCls(me.itemCls);
+ }
+ if (me.owner.itemCls) {
+ item.addCls(me.owner.itemCls);
+ }
+ item.render(target, position);
+ me.configureItem(item);
+ me.childrenChanged = true;
+ }
+ },
+
+ /**
+ * @private
+ * Moved Component to the provided target instead.
+ */
+ moveItem : function(item, target, position) {
+ // Make sure target is a dom element
+ target = target.dom || target;
+ if (typeof position == 'number') {
+ position = target.childNodes[position];
+ }
+ target.insertBefore(item.el.dom, position || null);
+ item.container = Ext.get(target);
+ this.configureItem(item);
+ this.childrenChanged = true;
+ },
+
+ /**
+ * @private
+ * Adds the layout's targetCls if necessary and sets
+ * initialized flag when complete.
+ */
+ initLayout : function() {
+ var me = this,
+ targetCls = me.targetCls;
+
+ if (!me.initialized && !Ext.isEmpty(targetCls)) {
+ me.getTarget().addCls(targetCls);
+ }
+ me.initialized = true;
+ },
+
+ // @private Sets the layout owner
+ setOwner : function(owner) {
+ this.owner = owner;
+ },
+
+ // @private - Returns empty array
+ getLayoutItems : function() {
+ return [];
+ },
+
+ /**
+ * @private
+ * Applies itemCls
+ * Empty template method
+ */
+ configureItem: Ext.emptyFn,
+
+ // Placeholder empty functions for subclasses to extend
+ onLayout : Ext.emptyFn,
+ afterLayout : Ext.emptyFn,
+ onRemove : Ext.emptyFn,
+ onDestroy : Ext.emptyFn,
+ doOwnerCtLayouts : Ext.emptyFn,
+
+ /**
+ * @private
+ * Removes itemCls
+ */
+ afterRemove : function(item) {
+ var el = item.el,
+ owner = this.owner,
+ itemCls = this.itemCls,
+ ownerCls = owner.itemCls;
+
+ // Clear managed dimensions flag when removed from the layout.
+ if (item.rendered && !item.isDestroyed) {
+ if (itemCls) {
+ el.removeCls(itemCls);
+ }
+ if (ownerCls) {
+ el.removeCls(ownerCls);
+ }
+ }
+
+ // These flags are set at the time a child item is added to a layout.
+ // The layout must decide if it is managing the item's width, or its height, or both.
+ // See AbstractComponent for docs on these properties.
+ delete item.layoutManagedWidth;
+ delete item.layoutManagedHeight;
+ },
+
+ /**
+ * Destroys this layout. This is a template method that is empty by default, but should be implemented
+ * by subclasses that require explicit destruction to purge event handlers or remove DOM nodes.
+ * @template
+ */
+ destroy : function() {
+ var targetCls = this.targetCls,
+ target;
+
+ if (!Ext.isEmpty(targetCls)) {
+ target = this.getTarget();
+ if (target) {
+ target.removeCls(targetCls);
+ }
+ }
+ this.onDestroy();
+ }
+});
+/**
+ * @class Ext.ZIndexManager
+ * <p>A class that manages a group of {@link Ext.Component#floating} Components and provides z-order management,
+ * and Component activation behavior, including masking below the active (topmost) Component.</p>
+ * <p>{@link Ext.Component#floating Floating} Components which are rendered directly into the document (such as {@link Ext.window.Window Window}s) which are
+ * {@link Ext.Component#show show}n are managed by a {@link Ext.WindowManager global instance}.</p>
+ * <p>{@link Ext.Component#floating Floating} Components which are descendants of {@link Ext.Component#floating floating} <i>Containers</i>
+ * (for example a {@link Ext.view.BoundList BoundList} within an {@link Ext.window.Window Window}, or a {@link Ext.menu.Menu Menu}),
+ * are managed by a ZIndexManager owned by that floating Container. Therefore ComboBox dropdowns within Windows will have managed z-indices
+ * guaranteed to be correct, relative to the Window.</p>
+ */
+Ext.define('Ext.ZIndexManager', {
+
+ alternateClassName: 'Ext.WindowGroup',
+
+ statics: {
+ zBase : 9000
+ },
+
+ constructor: function(container) {
+ var me = this;
+
+ me.list = {};
+ me.zIndexStack = [];
+ me.front = null;
+
+ if (container) {
+
+ // This is the ZIndexManager for an Ext.container.Container, base its zseed on the zIndex of the Container's element
+ if (container.isContainer) {
+ container.on('resize', me._onContainerResize, me);
+ me.zseed = Ext.Number.from(container.getEl().getStyle('zIndex'), me.getNextZSeed());
+ // The containing element we will be dealing with (eg masking) is the content target
+ me.targetEl = container.getTargetEl();
+ me.container = container;
+ }
+ // This is the ZIndexManager for a DOM element
+ else {
+ Ext.EventManager.onWindowResize(me._onContainerResize, me);
+ me.zseed = me.getNextZSeed();
+ me.targetEl = Ext.get(container);
+ }
+ }
+ // No container passed means we are the global WindowManager. Our target is the doc body.
+ // DOM must be ready to collect that ref.
+ else {
+ Ext.EventManager.onWindowResize(me._onContainerResize, me);
+ me.zseed = me.getNextZSeed();
+ Ext.onDocumentReady(function() {
+ me.targetEl = Ext.getBody();
+ });
+ }
+ },
+
+ getNextZSeed: function() {
+ return (Ext.ZIndexManager.zBase += 10000);
+ },
+
+ setBase: function(baseZIndex) {
+ this.zseed = baseZIndex;
+ return this.assignZIndices();
+ },
+
+ // private
+ assignZIndices: function() {
+ var a = this.zIndexStack,
+ len = a.length,
+ i = 0,
+ zIndex = this.zseed,
+ comp;
+
+ for (; i < len; i++) {
+ comp = a[i];
+ if (comp && !comp.hidden) {
+
+ // Setting the zIndex of a Component returns the topmost zIndex consumed by
+ // that Component.
+ // If it's just a plain floating Component such as a BoundList, then the
+ // return value is the passed value plus 10, ready for the next item.
+ // If a floating *Container* has its zIndex set, it re-orders its managed
+ // floating children, starting from that new base, and returns a value 10000 above
+ // the highest zIndex which it allocates.
+ zIndex = comp.setZIndex(zIndex);
+ }
+ }
+ this._activateLast();
+ return zIndex;
+ },
+
+ // private
+ _setActiveChild: function(comp) {
+ if (comp !== this.front) {
+
+ if (this.front) {
+ this.front.setActive(false, comp);
+ }
+ this.front = comp;
+ if (comp) {
+ comp.setActive(true);
+ if (comp.modal) {
+ this._showModalMask(comp);
+ }
+ }
+ }
+ },
+
+ // private
+ _activateLast: function(justHidden) {
+ var comp,
+ lastActivated = false,
+ i;
+
+ // Go down through the z-index stack.
+ // Activate the next visible one down.
+ // Keep going down to find the next visible modal one to shift the modal mask down under
+ for (i = this.zIndexStack.length-1; i >= 0; --i) {
+ comp = this.zIndexStack[i];
+ if (!comp.hidden) {
+ if (!lastActivated) {
+ this._setActiveChild(comp);
+ lastActivated = true;
+ }
+
+ // Move any modal mask down to just under the next modal floater down the stack
+ if (comp.modal) {
+ this._showModalMask(comp);
+ return;
+ }
+ }
+ }
+
+ // none to activate, so there must be no modal mask.
+ // And clear the currently active property
+ this._hideModalMask();
+ if (!lastActivated) {
+ this._setActiveChild(null);
+ }
+ },
+
+ _showModalMask: function(comp) {
+ var zIndex = comp.el.getStyle('zIndex') - 4,
+ maskTarget = comp.floatParent ? comp.floatParent.getTargetEl() : Ext.get(comp.getEl().dom.parentNode),
+ parentBox;
+
+ if (!maskTarget) {
+ return;
+ }
+
+ parentBox = maskTarget.getBox();
+
+ if (!this.mask) {
+ this.mask = Ext.getBody().createChild({
+ cls: Ext.baseCSSPrefix + 'mask'
+ });
+ this.mask.setVisibilityMode(Ext.Element.DISPLAY);
+ this.mask.on('click', this._onMaskClick, this);
+ }
+ if (maskTarget.dom === document.body) {
+ parentBox.height = Ext.Element.getViewHeight();
+ }
+ maskTarget.addCls(Ext.baseCSSPrefix + 'body-masked');
+ this.mask.setBox(parentBox);
+ this.mask.setStyle('zIndex', zIndex);
+ this.mask.show();
+ },
+
+ _hideModalMask: function() {
+ if (this.mask && this.mask.dom.parentNode) {
+ Ext.get(this.mask.dom.parentNode).removeCls(Ext.baseCSSPrefix + 'body-masked');
+ this.mask.hide();
+ }
+ },
+
+ _onMaskClick: function() {
+ if (this.front) {
+ this.front.focus();
+ }
+ },
+
+ _onContainerResize: function() {
+ if (this.mask && this.mask.isVisible()) {
+ this.mask.setSize(Ext.get(this.mask.dom.parentNode).getViewSize(true));
+ }
+ },
+
+ /**
+ * <p>Registers a floating {@link Ext.Component} with this ZIndexManager. This should not
+ * need to be called under normal circumstances. Floating Components (such as Windows, BoundLists and Menus) are automatically registered
+ * with a {@link Ext.Component#zIndexManager zIndexManager} at render time.</p>
+ * <p>Where this may be useful is moving Windows between two ZIndexManagers. For example,
+ * to bring the Ext.MessageBox dialog under the same manager as the Desktop's
+ * ZIndexManager in the desktop sample app:</p><code><pre>
+MyDesktop.getDesktop().getManager().register(Ext.MessageBox);
+</pre></code>
+ * @param {Ext.Component} comp The Component to register.
+ */
+ register : function(comp) {
+ if (comp.zIndexManager) {
+ comp.zIndexManager.unregister(comp);
+ }
+ comp.zIndexManager = this;
+
+ this.list[comp.id] = comp;
+ this.zIndexStack.push(comp);
+ comp.on('hide', this._activateLast, this);
+ },
+
+ /**
+ * <p>Unregisters a {@link Ext.Component} from this ZIndexManager. This should not
+ * need to be called. Components are automatically unregistered upon destruction.
+ * See {@link #register}.</p>
+ * @param {Ext.Component} comp The Component to unregister.
+ */
+ unregister : function(comp) {
+ delete comp.zIndexManager;
+ if (this.list && this.list[comp.id]) {
+ delete this.list[comp.id];
+ comp.un('hide', this._activateLast);
+ Ext.Array.remove(this.zIndexStack, comp);
+
+ // Destruction requires that the topmost visible floater be activated. Same as hiding.
+ this._activateLast(comp);
+ }
+ },
+
+ /**
+ * Gets a registered Component by id.
+ * @param {String/Object} id The id of the Component or a {@link Ext.Component} instance
+ * @return {Ext.Component}
+ */
+ get : function(id) {
+ return typeof id == "object" ? id : this.list[id];
+ },
+
+ /**
+ * Brings the specified Component to the front of any other active Components in this ZIndexManager.
+ * @param {String/Object} comp The id of the Component or a {@link Ext.Component} instance
+ * @return {Boolean} True if the dialog was brought to the front, else false
+ * if it was already in front
+ */
+ bringToFront : function(comp) {
+ comp = this.get(comp);
+ if (comp !== this.front) {
+ Ext.Array.remove(this.zIndexStack, comp);
+ this.zIndexStack.push(comp);
+ this.assignZIndices();
+ return true;
+ }
+ if (comp.modal) {
+ this._showModalMask(comp);
+ }
+ return false;
+ },
+
+ /**
+ * Sends the specified Component to the back of other active Components in this ZIndexManager.
+ * @param {String/Object} comp The id of the Component or a {@link Ext.Component} instance
+ * @return {Ext.Component} The Component
+ */
+ sendToBack : function(comp) {
+ comp = this.get(comp);
+ Ext.Array.remove(this.zIndexStack, comp);
+ this.zIndexStack.unshift(comp);
+ this.assignZIndices();
+ return comp;
+ },
+
+ /**
+ * Hides all Components managed by this ZIndexManager.
+ */
+ hideAll : function() {
+ for (var id in this.list) {
+ if (this.list[id].isComponent && this.list[id].isVisible()) {
+ this.list[id].hide();
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Temporarily hides all currently visible managed Components. This is for when
+ * dragging a Window which may manage a set of floating descendants in its ZIndexManager;
+ * they should all be hidden just for the duration of the drag.
+ */
+ hide: function() {
+ var i = 0,
+ ln = this.zIndexStack.length,
+ comp;
+
+ this.tempHidden = [];
+ for (; i < ln; i++) {
+ comp = this.zIndexStack[i];
+ if (comp.isVisible()) {
+ this.tempHidden.push(comp);
+ comp.hide();
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Restores temporarily hidden managed Components to visibility.
+ */
+ show: function() {
+ var i = 0,
+ ln = this.tempHidden.length,
+ comp,
+ x,
+ y;
+
+ for (; i < ln; i++) {
+ comp = this.tempHidden[i];
+ x = comp.x;
+ y = comp.y;
+ comp.show();
+ comp.setPosition(x, y);
+ }
+ delete this.tempHidden;
+ },
+
+ /**
+ * Gets the currently-active Component in this ZIndexManager.
+ * @return {Ext.Component} The active Component
+ */
+ getActive : function() {
+ return this.front;
+ },
+
+ /**
+ * Returns zero or more Components in this ZIndexManager using the custom search function passed to this method.
+ * The function should accept a single {@link Ext.Component} reference as its only argument and should
+ * return true if the Component matches the search criteria, otherwise it should return false.
+ * @param {Function} fn The search function
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the Component being tested.
+ * that gets passed to the function if not specified)
+ * @return {Array} An array of zero or more matching windows
+ */
+ getBy : function(fn, scope) {
+ var r = [],
+ i = 0,
+ len = this.zIndexStack.length,
+ comp;
+
+ for (; i < len; i++) {
+ comp = this.zIndexStack[i];
+ if (fn.call(scope||comp, comp) !== false) {
+ r.push(comp);
+ }
+ }
+ return r;
+ },
+
+ /**
+ * Executes the specified function once for every Component in this ZIndexManager, passing each
+ * Component 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 (<code>this</code> reference) in which the function is executed. Defaults to the current Component in the iteration.
+ */
+ each : function(fn, scope) {
+ var comp;
+ for (var id in this.list) {
+ comp = this.list[id];
+ if (comp.isComponent && fn.call(scope || comp, comp) === false) {
+ return;
+ }
+ }
+ },
+
+ /**
+ * Executes the specified function once for every Component in this ZIndexManager, passing each
+ * Component as the only parameter. Returning false from the function will stop the iteration.
+ * The components are passed to the function starting at the bottom and proceeding to the top.
+ * @param {Function} fn The function to execute for each item
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function
+ * is executed. Defaults to the current Component in the iteration.
+ */
+ eachBottomUp: function (fn, scope) {
+ var comp,
+ stack = this.zIndexStack,
+ i, n;
+
+ for (i = 0, n = stack.length ; i < n; i++) {
+ comp = stack[i];
+ if (comp.isComponent && fn.call(scope || comp, comp) === false) {
+ return;
+ }
+ }
+ },
+
+ /**
+ * Executes the specified function once for every Component in this ZIndexManager, passing each
+ * Component as the only parameter. Returning false from the function will stop the iteration.
+ * The components are passed to the function starting at the top and proceeding to the bottom.
+ * @param {Function} fn The function to execute for each item
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function
+ * is executed. Defaults to the current Component in the iteration.
+ */
+ eachTopDown: function (fn, scope) {
+ var comp,
+ stack = this.zIndexStack,
+ i;
+
+ for (i = stack.length ; i-- > 0; ) {
+ comp = stack[i];
+ if (comp.isComponent && fn.call(scope || comp, comp) === false) {
+ return;
+ }
+ }
+ },
+
+ destroy: function() {
+ this.each(function(c) {
+ c.destroy();
+ });
+ delete this.zIndexStack;
+ delete this.list;
+ delete this.container;
+ delete this.targetEl;
+ }
+}, function() {
+ /**
+ * @class Ext.WindowManager
+ * @extends Ext.ZIndexManager
+ * <p>The default global floating Component group that is available automatically.</p>
+ * <p>This manages instances of floating Components which were rendered programatically without
+ * being added to a {@link Ext.container.Container Container}, and for floating Components which were added into non-floating Containers.</p>
+ * <p><i>Floating</i> Containers create their own instance of ZIndexManager, and floating Components added at any depth below
+ * there are managed by that ZIndexManager.</p>
+ * @singleton
+ */
+ Ext.WindowManager = Ext.WindowMgr = new this();
+});
+
+/**
+ * @private
+ * Base class for Box Layout overflow handlers. These specialized classes are invoked when a Box Layout
+ * (either an HBox or a VBox) has child items that are either too wide (for HBox) or too tall (for VBox)
+ * for its container.
+ */
+Ext.define('Ext.layout.container.boxOverflow.None', {
+
+ alternateClassName: 'Ext.layout.boxOverflow.None',
+
+ constructor: function(layout, config) {
+ this.layout = layout;
+ Ext.apply(this, config || {});
+ },
+
+ handleOverflow: Ext.emptyFn,
+
+ clearOverflow: Ext.emptyFn,
+
+ onRemove: Ext.emptyFn,
+
+ /**
+ * @private
+ * Normalizes an item reference, string id or numerical index into a reference to the item
+ * @param {Ext.Component/String/Number} item The item reference, id or index
+ * @return {Ext.Component} The item
+ */
+ getItem: function(item) {
+ return this.layout.owner.getComponent(item);
+ },
+
+ onRemove: Ext.emptyFn
+});
+/**
+ * @class Ext.util.KeyMap
+ * Handles mapping keys to actions for an element. One key map can be used for multiple actions.
+ * The constructor accepts the same config object as defined by {@link #addBinding}.
+ * If you bind a callback function to a KeyMap, anytime the KeyMap handles an expected key
+ * combination it will call the function with this signature (if the match is a multi-key
+ * combination the callback will still be called only once): (String key, Ext.EventObject e)
+ * A KeyMap can also handle a string representation of keys. By default KeyMap starts enabled.<br />
+ * Usage:
+ <pre><code>
+// map one key by key code
+var map = new Ext.util.KeyMap("my-element", {
+ key: 13, // or Ext.EventObject.ENTER
+ fn: myHandler,
+ scope: myObject
+});
+
+// map multiple keys to one action by string
+var map = new Ext.util.KeyMap("my-element", {
+ key: "a\r\n\t",
+ fn: myHandler,
+ scope: myObject
+});
+
+// map multiple keys to multiple actions by strings and array of codes
+var map = new Ext.util.KeyMap("my-element", [
+ {
+ key: [10,13],
+ fn: function(){ alert("Return was pressed"); }
+ }, {
+ key: "abc",
+ fn: function(){ alert('a, b or c was pressed'); }
+ }, {
+ key: "\t",
+ ctrl:true,
+ shift:true,
+ fn: function(){ alert('Control + shift + tab was pressed.'); }
+ }
+]);
+</code></pre>
+ */
+Ext.define('Ext.util.KeyMap', {
+ alternateClassName: 'Ext.KeyMap',
+
+ /**
+ * Creates new KeyMap.
+ * @param {String/HTMLElement/Ext.Element} el The element or its ID to bind to
+ * @param {Object} binding The binding (see {@link #addBinding})
+ * @param {String} [eventName="keydown"] The event to bind to
+ */
+ constructor: function(el, binding, eventName){
+ var me = this;
+
+ Ext.apply(me, {
+ el: Ext.get(el),
+ eventName: eventName || me.eventName,
+ bindings: []
+ });
+ if (binding) {
+ me.addBinding(binding);
+ }
+ me.enable();
+ },
+
+ eventName: 'keydown',
+
+ /**
+ * Add a new binding to this KeyMap. The following config object properties are supported:
+ * <pre>
+Property Type Description
+---------- --------------- ----------------------------------------------------------------------
+key String/Array A single keycode or an array of keycodes to handle
+shift Boolean True to handle key only when shift is pressed, False to handle the key only when shift is not pressed (defaults to undefined)
+ctrl Boolean True to handle key only when ctrl is pressed, False to handle the key only when ctrl is not pressed (defaults to undefined)
+alt Boolean True to handle key only when alt is pressed, False to handle the key only when alt is not pressed (defaults to undefined)
+handler Function The function to call when KeyMap finds the expected key combination
+fn Function Alias of handler (for backwards-compatibility)
+scope Object The scope of the callback function
+defaultEventAction String A default action to apply to the event. Possible values are: stopEvent, stopPropagation, preventDefault. If no value is set no action is performed.
+</pre>
+ *
+ * Usage:
+ * <pre><code>
+// Create a KeyMap
+var map = new Ext.util.KeyMap(document, {
+ key: Ext.EventObject.ENTER,
+ fn: handleKey,
+ scope: this
+});
+
+//Add a new binding to the existing KeyMap later
+map.addBinding({
+ key: 'abc',
+ shift: true,
+ fn: handleKey,
+ scope: this
+});
+</code></pre>
+ * @param {Object/Object[]} binding A single KeyMap config or an array of configs
+ */
+ addBinding : function(binding){
+ if (Ext.isArray(binding)) {
+ Ext.each(binding, this.addBinding, this);
+ return;
+ }
+
+ var keyCode = binding.key,
+ processed = false,
+ key,
+ keys,
+ keyString,
+ i,
+ len;
+
+ if (Ext.isString(keyCode)) {
+ keys = [];
+ keyString = keyCode.toUpperCase();
+
+ for (i = 0, len = keyString.length; i < len; ++i){
+ keys.push(keyString.charCodeAt(i));
+ }
+ keyCode = keys;
+ processed = true;
+ }
+
+ if (!Ext.isArray(keyCode)) {
+ keyCode = [keyCode];
+ }
+
+ if (!processed) {
+ for (i = 0, len = keyCode.length; i < len; ++i) {
+ key = keyCode[i];
+ if (Ext.isString(key)) {
+ keyCode[i] = key.toUpperCase().charCodeAt(0);
+ }
+ }
+ }
+
+ this.bindings.push(Ext.apply({
+ keyCode: keyCode
+ }, binding));
+ },
+
+ /**
+ * Process any keydown events on the element
+ * @private
+ * @param {Ext.EventObject} event
+ */
+ handleKeyDown: function(event) {
+ if (this.enabled) { //just in case
+ var bindings = this.bindings,
+ i = 0,
+ len = bindings.length;
+
+ event = this.processEvent(event);
+ for(; i < len; ++i){
+ this.processBinding(bindings[i], event);
+ }
+ }
+ },
+
+ /**
+ * Ugly hack to allow this class to be tested. Currently WebKit gives
+ * no way to raise a key event properly with both
+ * a) A keycode
+ * b) The alt/ctrl/shift modifiers
+ * So we have to simulate them here. Yuk!
+ * This is a stub method intended to be overridden by tests.
+ * More info: https://bugs.webkit.org/show_bug.cgi?id=16735
+ * @private
+ */
+ processEvent: function(event){
+ return event;
+ },
+
+ /**
+ * Process a particular binding and fire the handler if necessary.
+ * @private
+ * @param {Object} binding The binding information
+ * @param {Ext.EventObject} event
+ */
+ processBinding: function(binding, event){
+ if (this.checkModifiers(binding, event)) {
+ var key = event.getKey(),
+ handler = binding.fn || binding.handler,
+ scope = binding.scope || this,
+ keyCode = binding.keyCode,
+ defaultEventAction = binding.defaultEventAction,
+ i,
+ len,
+ keydownEvent = new Ext.EventObjectImpl(event);
+
+
+ for (i = 0, len = keyCode.length; i < len; ++i) {
+ if (key === keyCode[i]) {
+ if (handler.call(scope, key, event) !== true && defaultEventAction) {
+ keydownEvent[defaultEventAction]();
+ }
+ break;
+ }
+ }
+ }
+ },
+
+ /**
+ * Check if the modifiers on the event match those on the binding
+ * @private
+ * @param {Object} binding
+ * @param {Ext.EventObject} event
+ * @return {Boolean} True if the event matches the binding
+ */
+ checkModifiers: function(binding, e){
+ var keys = ['shift', 'ctrl', 'alt'],
+ i = 0,
+ len = keys.length,
+ val, key;
+
+ for (; i < len; ++i){
+ key = keys[i];
+ val = binding[key];
+ if (!(val === undefined || (val === e[key + 'Key']))) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Shorthand for adding a single key listener
+ * @param {Number/Number[]/Object} key Either the numeric key code, array of key codes or an object with the
+ * following options:
+ * {key: (number or array), shift: (true/false), ctrl: (true/false), alt: (true/false)}
+ * @param {Function} fn The function to call
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
+ */
+ on: function(key, fn, scope) {
+ var keyCode, shift, ctrl, alt;
+ if (Ext.isObject(key) && !Ext.isArray(key)) {
+ keyCode = key.key;
+ shift = key.shift;
+ ctrl = key.ctrl;
+ alt = key.alt;
+ } else {
+ keyCode = key;
+ }
+ this.addBinding({
+ key: keyCode,
+ shift: shift,
+ ctrl: ctrl,
+ alt: alt,
+ fn: fn,
+ scope: scope
+ });
+ },
+
+ /**
+ * Returns true if this KeyMap is enabled
+ * @return {Boolean}
+ */
+ isEnabled : function(){
+ return this.enabled;
+ },
+
+ /**
+ * Enables this KeyMap
+ */
+ enable: function(){
+ var me = this;
+
+ if (!me.enabled) {
+ me.el.on(me.eventName, me.handleKeyDown, me);
+ me.enabled = true;
+ }
+ },
+
+ /**
+ * Disable this KeyMap
+ */
+ disable: function(){
+ var me = this;
+
+ if (me.enabled) {
+ me.el.removeListener(me.eventName, me.handleKeyDown, me);
+ me.enabled = false;
+ }
+ },
+
+ /**
+ * Convenience function for setting disabled/enabled by boolean.
+ * @param {Boolean} disabled
+ */
+ setDisabled : function(disabled){
+ if (disabled) {
+ this.disable();
+ } else {
+ this.enable();
+ }
+ },
+
+ /**
+ * Destroys the KeyMap instance and removes all handlers.
+ * @param {Boolean} removeEl True to also remove the attached element
+ */
+ destroy: function(removeEl){
+ var me = this;
+
+ me.bindings = [];
+ me.disable();
+ if (removeEl === true) {
+ me.el.remove();
+ }
+ delete me.el;
+ }
+});
+/**
+ * @class Ext.util.ClickRepeater
+ * @extends Ext.util.Observable
+ *
+ * A wrapper class which can be applied to any element. Fires a "click" event while the
+ * mouse is pressed. The interval between firings may be specified in the config but
+ * defaults to 20 milliseconds.
+ *
+ * Optionally, a CSS class may be applied to the element during the time it is pressed.
+ *
+ */
+Ext.define('Ext.util.ClickRepeater', {
+ extend: 'Ext.util.Observable',
+
+ /**
+ * Creates new ClickRepeater.
+ * @param {String/HTMLElement/Ext.Element} el The element or its ID to listen on
+ * @param {Object} config (optional) Config object.
+ */
+ constructor : function(el, config){
+ this.el = Ext.get(el);
+ this.el.unselectable();
+
+ Ext.apply(this, config);
+
+ this.addEvents(
+ /**
+ * @event mousedown
+ * Fires when the mouse button is depressed.
+ * @param {Ext.util.ClickRepeater} this
+ * @param {Ext.EventObject} e
+ */
+ "mousedown",
+ /**
+ * @event click
+ * Fires on a specified interval during the time the element is pressed.
+ * @param {Ext.util.ClickRepeater} this
+ * @param {Ext.EventObject} e
+ */
+ "click",
+ /**
+ * @event mouseup
+ * Fires when the mouse key is released.
+ * @param {Ext.util.ClickRepeater} this
+ * @param {Ext.EventObject} e
+ */
+ "mouseup"
+ );
+
+ if(!this.disabled){
+ this.disabled = true;
+ this.enable();
+ }
+
+ // allow inline handler
+ if(this.handler){
+ this.on("click", this.handler, this.scope || this);
+ }
+
+ this.callParent();
+ },
+
+ /**
+ * @cfg {String/HTMLElement/Ext.Element} el The element to act as a button.
+ */
+
+ /**
+ * @cfg {String} pressedCls A CSS class name to be applied to the element while pressed.
+ */
+
+ /**
+ * @cfg {Boolean} accelerate True if autorepeating should start slowly and accelerate.
+ * "interval" and "delay" are ignored.
+ */
+
+ /**
+ * @cfg {Number} interval The interval between firings of the "click" event. Default 20 ms.
+ */
+ interval : 20,
+
+ /**
+ * @cfg {Number} delay The initial delay before the repeating event begins firing.
+ * Similar to an autorepeat key delay.
+ */
+ delay: 250,
+
+ /**
+ * @cfg {Boolean} preventDefault True to prevent the default click event
+ */
+ preventDefault : true,
+ /**
+ * @cfg {Boolean} stopDefault True to stop the default click event
+ */
+ stopDefault : false,
+
+ timer : 0,
+
+ /**
+ * Enables the repeater and allows events to fire.
+ */
+ enable: function(){
+ if(this.disabled){
+ this.el.on('mousedown', this.handleMouseDown, this);
+ if (Ext.isIE){
+ this.el.on('dblclick', this.handleDblClick, this);
+ }
+ if(this.preventDefault || this.stopDefault){
+ this.el.on('click', this.eventOptions, this);
+ }
+ }
+ this.disabled = false;
+ },
+
+ /**
+ * Disables the repeater and stops events from firing.
+ */
+ disable: function(/* private */ force){
+ if(force || !this.disabled){
+ clearTimeout(this.timer);
+ if(this.pressedCls){
+ this.el.removeCls(this.pressedCls);
+ }
+ Ext.getDoc().un('mouseup', this.handleMouseUp, this);
+ this.el.removeAllListeners();
+ }
+ this.disabled = true;
+ },
+
+ /**
+ * Convenience function for setting disabled/enabled by boolean.
+ * @param {Boolean} disabled
+ */
+ setDisabled: function(disabled){
+ this[disabled ? 'disable' : 'enable']();
+ },
+
+ eventOptions: function(e){
+ if(this.preventDefault){
+ e.preventDefault();
+ }
+ if(this.stopDefault){
+ e.stopEvent();
+ }
+ },
+
+ // private
+ destroy : function() {
+ this.disable(true);
+ Ext.destroy(this.el);
+ this.clearListeners();
+ },
+
+ handleDblClick : function(e){
+ clearTimeout(this.timer);
+ this.el.blur();
+
+ this.fireEvent("mousedown", this, e);
+ this.fireEvent("click", this, e);
+ },
+
+ // private
+ handleMouseDown : function(e){
+ clearTimeout(this.timer);
+ this.el.blur();
+ if(this.pressedCls){
+ this.el.addCls(this.pressedCls);
+ }
+ this.mousedownTime = new Date();
+
+ Ext.getDoc().on("mouseup", this.handleMouseUp, this);
+ this.el.on("mouseout", this.handleMouseOut, this);
+
+ this.fireEvent("mousedown", this, e);
+ this.fireEvent("click", this, e);
+
+ // Do not honor delay or interval if acceleration wanted.
+ if (this.accelerate) {
+ this.delay = 400;
+ }
+
+ // Re-wrap the event object in a non-shared object, so it doesn't lose its context if
+ // the global shared EventObject gets a new Event put into it before the timer fires.
+ e = new Ext.EventObjectImpl(e);
+
+ this.timer = Ext.defer(this.click, this.delay || this.interval, this, [e]);
+ },
+
+ // private
+ click : function(e){
+ this.fireEvent("click", this, e);
+ this.timer = Ext.defer(this.click, this.accelerate ?
+ this.easeOutExpo(Ext.Date.getElapsed(this.mousedownTime),
+ 400,
+ -390,
+ 12000) :
+ this.interval, this, [e]);
+ },
+
+ easeOutExpo : function (t, b, c, d) {
+ return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
+ },
+
+ // private
+ handleMouseOut : function(){
+ clearTimeout(this.timer);
+ if(this.pressedCls){
+ this.el.removeCls(this.pressedCls);
+ }
+ this.el.on("mouseover", this.handleMouseReturn, this);
+ },
+
+ // private
+ handleMouseReturn : function(){
+ this.el.un("mouseover", this.handleMouseReturn, this);
+ if(this.pressedCls){
+ this.el.addCls(this.pressedCls);
+ }
+ this.click();
+ },
+
+ // private
+ handleMouseUp : function(e){
+ clearTimeout(this.timer);
+ this.el.un("mouseover", this.handleMouseReturn, this);
+ this.el.un("mouseout", this.handleMouseOut, this);
+ Ext.getDoc().un("mouseup", this.handleMouseUp, this);
+ if(this.pressedCls){
+ this.el.removeCls(this.pressedCls);
+ }
+ this.fireEvent("mouseup", this, e);
+ }
+});
+
+/**
+ * @class Ext.layout.component.Component
+ * @extends Ext.layout.Layout
+ *
+ * This class is intended to be extended or created via the {@link Ext.Component#componentLayout layout}
+ * configuration property. See {@link Ext.Component#componentLayout} for additional details.
+ *
+ * @private
+ */
+Ext.define('Ext.layout.component.Component', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.layout.Layout',
+
+ /* End Definitions */
+
+ type: 'component',
+
+ monitorChildren: true,
+
+ initLayout : function() {
+ var me = this,
+ owner = me.owner,
+ ownerEl = owner.el;
+
+ if (!me.initialized) {
+ if (owner.frameSize) {
+ me.frameSize = owner.frameSize;
+ }
+ else {
+ owner.frameSize = me.frameSize = {
+ top: 0,
+ left: 0,
+ bottom: 0,
+ right: 0
+ };
+ }
+ }
+ me.callParent(arguments);
+ },
+
+ beforeLayout : function(width, height, isSetSize, callingContainer) {
+ this.callParent(arguments);
+
+ var me = this,
+ owner = me.owner,
+ ownerCt = owner.ownerCt,
+ layout = owner.layout,
+ isVisible = owner.isVisible(true),
+ ownerElChild = owner.el.child,
+ layoutCollection;
+
+ // Cache the size we began with so we can see if there has been any effect.
+ me.previousComponentSize = me.lastComponentSize;
+
+ // Do not allow autoing of any dimensions which are fixed
+ if (!isSetSize
+ && ((!Ext.isNumber(width) && owner.isFixedWidth()) ||
+ (!Ext.isNumber(height) && owner.isFixedHeight()))
+ // unless we are being told to do so by the ownerCt's layout
+ && callingContainer && callingContainer !== ownerCt) {
+
+ me.doContainerLayout();
+ return false;
+ }
+
+ // If an ownerCt is hidden, add my reference onto the layoutOnShow stack. Set the needsLayout flag.
+ // If the owner itself is a directly hidden floater, set the needsLayout object on that for when it is shown.
+ if (!isVisible && (owner.hiddenAncestor || owner.floating)) {
+ if (owner.hiddenAncestor) {
+ layoutCollection = owner.hiddenAncestor.layoutOnShow;
+ layoutCollection.remove(owner);
+ layoutCollection.add(owner);
+ }
+ owner.needsLayout = {
+ width: width,
+ height: height,
+ isSetSize: false
+ };
+ }
+
+ if (isVisible && this.needsLayout(width, height)) {
+ return owner.beforeComponentLayout(width, height, isSetSize, callingContainer);
+ }
+ else {
+ return false;
+ }
+ },
+
+ /**
+ * Check if the new size is different from the current size and only
+ * trigger a layout if it is necessary.
+ * @param {Number} width The new width to set.
+ * @param {Number} height The new height to set.
+ */
+ needsLayout : function(width, height) {
+ var me = this,
+ widthBeingChanged,
+ heightBeingChanged;
+ me.lastComponentSize = me.lastComponentSize || {
+ width: -Infinity,
+ height: -Infinity
+ };
+
+ // If autoWidthing, or an explicitly different width is passed, then the width is being changed.
+ widthBeingChanged = !Ext.isDefined(width) || me.lastComponentSize.width !== width;
+
+ // If autoHeighting, or an explicitly different height is passed, then the height is being changed.
+ heightBeingChanged = !Ext.isDefined(height) || me.lastComponentSize.height !== height;
+
+
+ // isSizing flag added to prevent redundant layouts when going up the layout chain
+ return !me.isSizing && (me.childrenChanged || widthBeingChanged || heightBeingChanged);
+ },
+
+ /**
+ * Set the size of any element supporting undefined, null, and values.
+ * @param {Number} width The new width to set.
+ * @param {Number} height The new height to set.
+ */
+ setElementSize: function(el, width, height) {
+ if (width !== undefined && height !== undefined) {
+ el.setSize(width, height);
+ }
+ else if (height !== undefined) {
+ el.setHeight(height);
+ }
+ else if (width !== undefined) {
+ el.setWidth(width);
+ }
+ },
+
+ /**
+ * Returns the owner component's resize element.
+ * @return {Ext.Element}
+ */
+ getTarget : function() {
+ return this.owner.el;
+ },
+
+ /**
+ * <p>Returns the element into which rendering must take place. Defaults to the owner Component's encapsulating element.</p>
+ * May be overridden in Component layout managers which implement an inner element.
+ * @return {Ext.Element}
+ */
+ getRenderTarget : function() {
+ return this.owner.el;
+ },
+
+ /**
+ * Set the size of the target element.
+ * @param {Number} width The new width to set.
+ * @param {Number} height The new height to set.
+ */
+ setTargetSize : function(width, height) {
+ var me = this;
+ me.setElementSize(me.owner.el, width, height);
+
+ if (me.owner.frameBody) {
+ var targetInfo = me.getTargetInfo(),
+ padding = targetInfo.padding,
+ border = targetInfo.border,
+ frameSize = me.frameSize;
+
+ me.setElementSize(me.owner.frameBody,
+ Ext.isNumber(width) ? (width - frameSize.left - frameSize.right - padding.left - padding.right - border.left - border.right) : width,
+ Ext.isNumber(height) ? (height - frameSize.top - frameSize.bottom - padding.top - padding.bottom - border.top - border.bottom) : height
+ );
+ }
+
+ me.autoSized = {
+ width: !Ext.isNumber(width),
+ height: !Ext.isNumber(height)
+ };
+
+ me.lastComponentSize = {
+ width: width,
+ height: height
+ };
+ },
+
+ getTargetInfo : function() {
+ if (!this.targetInfo) {
+ var target = this.getTarget(),
+ body = this.owner.getTargetEl();
+
+ this.targetInfo = {
+ padding: {
+ top: target.getPadding('t'),
+ right: target.getPadding('r'),
+ bottom: target.getPadding('b'),
+ left: target.getPadding('l')
+ },
+ border: {
+ top: target.getBorderWidth('t'),
+ right: target.getBorderWidth('r'),
+ bottom: target.getBorderWidth('b'),
+ left: target.getBorderWidth('l')
+ },
+ bodyMargin: {
+ top: body.getMargin('t'),
+ right: body.getMargin('r'),
+ bottom: body.getMargin('b'),
+ left: body.getMargin('l')
+ }
+ };
+ }
+ return this.targetInfo;
+ },
+
+ // Start laying out UP the ownerCt's layout when flagged to do so.
+ doOwnerCtLayouts: function() {
+ var owner = this.owner,
+ ownerCt = owner.ownerCt,
+ ownerCtComponentLayout, ownerCtContainerLayout,
+ curSize = this.lastComponentSize,
+ prevSize = this.previousComponentSize,
+ widthChange = (prevSize && curSize && Ext.isNumber(curSize.width )) ? curSize.width !== prevSize.width : true,
+ heightChange = (prevSize && curSize && Ext.isNumber(curSize.height)) ? curSize.height !== prevSize.height : true;
+
+ // If size has not changed, do not inform upstream layouts
+ if (!ownerCt || (!widthChange && !heightChange)) {
+ return;
+ }
+
+ ownerCtComponentLayout = ownerCt.componentLayout;
+ ownerCtContainerLayout = ownerCt.layout;
+
+ if (!owner.floating && ownerCtComponentLayout && ownerCtComponentLayout.monitorChildren && !ownerCtComponentLayout.layoutBusy) {
+ if (!ownerCt.suspendLayout && ownerCtContainerLayout && !ownerCtContainerLayout.layoutBusy) {
+
+ // If the owning Container may be adjusted in any of the the dimension which have changed, perform its Component layout
+ if (((widthChange && !ownerCt.isFixedWidth()) || (heightChange && !ownerCt.isFixedHeight()))) {
+ // Set the isSizing flag so that the upstream Container layout (called after a Component layout) can omit this component from sizing operations
+ this.isSizing = true;
+ ownerCt.doComponentLayout();
+ this.isSizing = false;
+ }
+ // Execute upstream Container layout
+ else if (ownerCtContainerLayout.bindToOwnerCtContainer === true) {
+ ownerCtContainerLayout.layout();
+ }
+ }
+ }
+ },
+
+ doContainerLayout: function() {
+ var me = this,
+ owner = me.owner,
+ ownerCt = owner.ownerCt,
+ layout = owner.layout,
+ ownerCtComponentLayout;
+
+ // Run the container layout if it exists (layout for child items)
+ // **Unless automatic laying out is suspended, or the layout is currently running**
+ if (!owner.suspendLayout && layout && layout.isLayout && !layout.layoutBusy && !layout.isAutoDock) {
+ layout.layout();
+ }
+
+ // Tell the ownerCt that it's child has changed and can be re-layed by ignoring the lastComponentSize cache.
+ if (ownerCt && ownerCt.componentLayout) {
+ ownerCtComponentLayout = ownerCt.componentLayout;
+ if (!owner.floating && ownerCtComponentLayout.monitorChildren && !ownerCtComponentLayout.layoutBusy) {
+ ownerCtComponentLayout.childrenChanged = true;
+ }
+ }
+ },
+
+ afterLayout : function(width, height, isSetSize, layoutOwner) {
+ this.doContainerLayout();
+ this.owner.afterComponentLayout(width, height, isSetSize, layoutOwner);
+ }
+});
+
+/**
+ * Provides precise pixel measurements for blocks of text so that you can determine exactly how high and
+ * wide, in pixels, a given block of text will be. Note that when measuring text, it should be plain text and
+ * should not contain any HTML, otherwise it may not be measured correctly.
+ *
+ * The measurement works by copying the relevant CSS styles that can affect the font related display,
+ * then checking the size of an element that is auto-sized. Note that if the text is multi-lined, you must
+ * provide a **fixed width** when doing the measurement.
+ *
+ * If multiple measurements are being done on the same element, you create a new instance to initialize
+ * to avoid the overhead of copying the styles to the element repeatedly.
+ */
+Ext.define('Ext.util.TextMetrics', {
+ statics: {
+ shared: null,
+ /**
+ * Measures the size of the specified text
+ * @param {String/HTMLElement} el The element, dom node or id from which to copy existing CSS styles
+ * that can affect the size of the rendered text
+ * @param {String} text The text to measure
+ * @param {Number} fixedWidth (optional) If the text will be multiline, you have to set a fixed width
+ * in order to accurately measure the text height
+ * @return {Object} An object containing the text's size `{width: (width), height: (height)}`
+ */
+ measure: function(el, text, fixedWidth){
+ var me = this,
+ shared = me.shared;
+
+ if(!shared){
+ shared = me.shared = new me(el, fixedWidth);
+ }
+ shared.bind(el);
+ shared.setFixedWidth(fixedWidth || 'auto');
+ return shared.getSize(text);
+ },
+
+ /**
+ * Destroy the TextMetrics instance created by {@link #measure}.
+ */
+ destroy: function(){
+ var me = this;
+ Ext.destroy(me.shared);
+ me.shared = null;
+ }
+ },
+
+ /**
+ * Creates new TextMetrics.
+ * @param {String/HTMLElement/Ext.Element} bindTo The element or its ID to bind to.
+ * @param {Number} fixedWidth (optional) A fixed width to apply to the measuring element.
+ */
+ constructor: function(bindTo, fixedWidth){
+ var measure = this.measure = Ext.getBody().createChild({
+ cls: 'x-textmetrics'
+ });
+ this.el = Ext.get(bindTo);
+
+ measure.position('absolute');
+ measure.setLeftTop(-1000, -1000);
+ measure.hide();
+
+ if (fixedWidth) {
+ measure.setWidth(fixedWidth);
+ }
+ },
+
+ /**
+ * Returns the size of the specified text based on the internal element's style and width properties
+ * @param {String} text The text to measure
+ * @return {Object} An object containing the text's size `{width: (width), height: (height)}`
+ */
+ getSize: function(text){
+ var measure = this.measure,
+ size;
+
+ measure.update(text);
+ size = measure.getSize();
+ measure.update('');
+ return size;
+ },
+
+ /**
+ * Binds this TextMetrics instance to a new element
+ * @param {String/HTMLElement/Ext.Element} el The element or its ID.
+ */
+ bind: function(el){
+ var me = this;
+
+ me.el = Ext.get(el);
+ me.measure.setStyle(
+ me.el.getStyles('font-size','font-style', 'font-weight', 'font-family','line-height', 'text-transform', 'letter-spacing')
+ );
+ },
+
+ /**
+ * Sets a fixed width on the internal measurement element. If the text will be multiline, you have
+ * to set a fixed width in order to accurately measure the text height.
+ * @param {Number} width The width to set on the element
+ */
+ setFixedWidth : function(width){
+ this.measure.setWidth(width);
+ },
+
+ /**
+ * Returns the measured width of the specified text
+ * @param {String} text The text to measure
+ * @return {Number} width The width in pixels
+ */
+ getWidth : function(text){
+ this.measure.dom.style.width = 'auto';
+ return this.getSize(text).width;
+ },
+
+ /**
+ * Returns the measured height of the specified text
+ * @param {String} text The text to measure
+ * @return {Number} height The height in pixels
+ */
+ getHeight : function(text){
+ return this.getSize(text).height;
+ },
+
+ /**
+ * Destroy this instance
+ */
+ destroy: function(){
+ var me = this;
+ me.measure.remove();
+ delete me.el;
+ delete me.measure;
+ }
+}, function(){
+ Ext.Element.addMethods({
+ /**
+ * Returns the width in pixels of the passed text, or the width of the text in this Element.
+ * @param {String} text The text to measure. Defaults to the innerHTML of the element.
+ * @param {Number} min (optional) The minumum value to return.
+ * @param {Number} max (optional) The maximum value to return.
+ * @return {Number} The text width in pixels.
+ * @member Ext.Element
+ */
+ getTextWidth : function(text, min, max){
+ return Ext.Number.constrain(Ext.util.TextMetrics.measure(this.dom, Ext.value(text, this.dom.innerHTML, true)).width, min || 0, max || 1000000);
+ }
+ });
+});
+
+/**
+ * @class Ext.layout.container.boxOverflow.Scroller
+ * @extends Ext.layout.container.boxOverflow.None
+ * @private
+ */
+Ext.define('Ext.layout.container.boxOverflow.Scroller', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.layout.container.boxOverflow.None',
+ requires: ['Ext.util.ClickRepeater', 'Ext.Element'],
+ alternateClassName: 'Ext.layout.boxOverflow.Scroller',
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Boolean} animateScroll
+ * True to animate the scrolling of items within the layout (ignored if enableScroll is false)
+ */
+ animateScroll: false,
+
+ /**
+ * @cfg {Number} scrollIncrement
+ * The number of pixels to scroll by on scroller click
+ */
+ scrollIncrement: 20,
+
+ /**
+ * @cfg {Number} wheelIncrement
+ * The number of pixels to increment on mouse wheel scrolling.
+ */
+ wheelIncrement: 10,
+
+ /**
+ * @cfg {Number} scrollRepeatInterval
+ * Number of milliseconds between each scroll while a scroller button is held down
+ */
+ scrollRepeatInterval: 60,
+
+ /**
+ * @cfg {Number} scrollDuration
+ * Number of milliseconds that each scroll animation lasts
+ */
+ scrollDuration: 400,
+
+ /**
+ * @cfg {String} beforeCtCls
+ * CSS class added to the beforeCt element. This is the element that holds any special items such as scrollers,
+ * which must always be present at the leftmost edge of the Container
+ */
+
+ /**
+ * @cfg {String} afterCtCls
+ * CSS class added to the afterCt element. This is the element that holds any special items such as scrollers,
+ * which must always be present at the rightmost edge of the Container
+ */
+
+ /**
+ * @cfg {String} [scrollerCls='x-box-scroller']
+ * CSS class added to both scroller elements if enableScroll is used
+ */
+ scrollerCls: Ext.baseCSSPrefix + 'box-scroller',
+
+ /**
+ * @cfg {String} beforeScrollerCls
+ * CSS class added to the left scroller element if enableScroll is used
+ */
+
+ /**
+ * @cfg {String} afterScrollerCls
+ * CSS class added to the right scroller element if enableScroll is used
+ */
+
+ constructor: function(layout, config) {
+ this.layout = layout;
+ Ext.apply(this, config || {});
+
+ this.addEvents(
+ /**
+ * @event scroll
+ * @param {Ext.layout.container.boxOverflow.Scroller} scroller The layout scroller
+ * @param {Number} newPosition The new position of the scroller
+ * @param {Boolean/Object} animate If animating or not. If true, it will be a animation configuration, else it will be false
+ */
+ 'scroll'
+ );
+ },
+
+ initCSSClasses: function() {
+ var me = this,
+ layout = me.layout;
+
+ if (!me.CSSinitialized) {
+ me.beforeCtCls = me.beforeCtCls || Ext.baseCSSPrefix + 'box-scroller-' + layout.parallelBefore;
+ me.afterCtCls = me.afterCtCls || Ext.baseCSSPrefix + 'box-scroller-' + layout.parallelAfter;
+ me.beforeScrollerCls = me.beforeScrollerCls || Ext.baseCSSPrefix + layout.owner.getXType() + '-scroll-' + layout.parallelBefore;
+ me.afterScrollerCls = me.afterScrollerCls || Ext.baseCSSPrefix + layout.owner.getXType() + '-scroll-' + layout.parallelAfter;
+ me.CSSinitializes = true;
+ }
+ },
+
+ handleOverflow: function(calculations, targetSize) {
+ var me = this,
+ layout = me.layout,
+ methodName = 'get' + layout.parallelPrefixCap,
+ newSize = {};
+
+ me.initCSSClasses();
+ me.callParent(arguments);
+ this.createInnerElements();
+ this.showScrollers();
+ newSize[layout.perpendicularPrefix] = targetSize[layout.perpendicularPrefix];
+ newSize[layout.parallelPrefix] = targetSize[layout.parallelPrefix] - (me.beforeCt[methodName]() + me.afterCt[methodName]());
+ return { targetSize: newSize };
+ },
+
+ /**
+ * @private
+ * Creates the beforeCt and afterCt elements if they have not already been created
+ */
+ createInnerElements: function() {
+ var me = this,
+ target = me.layout.getRenderTarget();
+
+ //normal items will be rendered to the innerCt. beforeCt and afterCt allow for fixed positioning of
+ //special items such as scrollers or dropdown menu triggers
+ if (!me.beforeCt) {
+ target.addCls(Ext.baseCSSPrefix + me.layout.direction + '-box-overflow-body');
+ me.beforeCt = target.insertSibling({cls: Ext.layout.container.Box.prototype.innerCls + ' ' + me.beforeCtCls}, 'before');
+ me.afterCt = target.insertSibling({cls: Ext.layout.container.Box.prototype.innerCls + ' ' + me.afterCtCls}, 'after');
+ me.createWheelListener();
+ }
+ },
+
+ /**
+ * @private
+ * Sets up an listener to scroll on the layout's innerCt mousewheel event
+ */
+ createWheelListener: function() {
+ this.layout.innerCt.on({
+ scope : this,
+ mousewheel: function(e) {
+ e.stopEvent();
+
+ this.scrollBy(e.getWheelDelta() * this.wheelIncrement * -1, false);
+ }
+ });
+ },
+
+ /**
+ * @private
+ */
+ clearOverflow: function() {
+ this.hideScrollers();
+ },
+
+ /**
+ * @private
+ * Shows the scroller elements in the beforeCt and afterCt. Creates the scrollers first if they are not already
+ * present.
+ */
+ showScrollers: function() {
+ this.createScrollers();
+ this.beforeScroller.show();
+ this.afterScroller.show();
+ this.updateScrollButtons();
+
+ this.layout.owner.addClsWithUI('scroller');
+ },
+
+ /**
+ * @private
+ * Hides the scroller elements in the beforeCt and afterCt
+ */
+ hideScrollers: function() {
+ if (this.beforeScroller != undefined) {
+ this.beforeScroller.hide();
+ this.afterScroller.hide();
+
+ this.layout.owner.removeClsWithUI('scroller');
+ }
+ },
+
+ /**
+ * @private
+ * Creates the clickable scroller elements and places them into the beforeCt and afterCt
+ */
+ createScrollers: function() {
+ if (!this.beforeScroller && !this.afterScroller) {
+ var before = this.beforeCt.createChild({
+ cls: Ext.String.format("{0} {1} ", this.scrollerCls, this.beforeScrollerCls)
+ });
+
+ var after = this.afterCt.createChild({
+ cls: Ext.String.format("{0} {1}", this.scrollerCls, this.afterScrollerCls)
+ });
+
+ before.addClsOnOver(this.beforeScrollerCls + '-hover');
+ after.addClsOnOver(this.afterScrollerCls + '-hover');
+
+ before.setVisibilityMode(Ext.Element.DISPLAY);
+ after.setVisibilityMode(Ext.Element.DISPLAY);
+
+ this.beforeRepeater = Ext.create('Ext.util.ClickRepeater', before, {
+ interval: this.scrollRepeatInterval,
+ handler : this.scrollLeft,
+ scope : this
+ });
+
+ this.afterRepeater = Ext.create('Ext.util.ClickRepeater', after, {
+ interval: this.scrollRepeatInterval,
+ handler : this.scrollRight,
+ scope : this
+ });
+
+ /**
+ * @property beforeScroller
+ * @type Ext.Element
+ * The left scroller element. Only created when needed.
+ */
+ this.beforeScroller = before;
+
+ /**
+ * @property afterScroller
+ * @type Ext.Element
+ * The left scroller element. Only created when needed.
+ */
+ this.afterScroller = after;
+ }
+ },
+
+ /**
+ * @private
+ */
+ destroy: function() {
+ Ext.destroy(this.beforeRepeater, this.afterRepeater, this.beforeScroller, this.afterScroller, this.beforeCt, this.afterCt);
+ },
+
+ /**
+ * @private
+ * Scrolls left or right by the number of pixels specified
+ * @param {Number} delta Number of pixels to scroll to the right by. Use a negative number to scroll left
+ */
+ scrollBy: function(delta, animate) {
+ this.scrollTo(this.getScrollPosition() + delta, animate);
+ },
+
+ /**
+ * @private
+ * @return {Object} Object passed to scrollTo when scrolling
+ */
+ getScrollAnim: function() {
+ return {
+ duration: this.scrollDuration,
+ callback: this.updateScrollButtons,
+ scope : this
+ };
+ },
+
+ /**
+ * @private
+ * Enables or disables each scroller button based on the current scroll position
+ */
+ updateScrollButtons: function() {
+ if (this.beforeScroller == undefined || this.afterScroller == undefined) {
+ return;
+ }
+
+ var beforeMeth = this.atExtremeBefore() ? 'addCls' : 'removeCls',
+ afterMeth = this.atExtremeAfter() ? 'addCls' : 'removeCls',
+ beforeCls = this.beforeScrollerCls + '-disabled',
+ afterCls = this.afterScrollerCls + '-disabled';
+
+ this.beforeScroller[beforeMeth](beforeCls);
+ this.afterScroller[afterMeth](afterCls);
+ this.scrolling = false;
+ },
+
+ /**
+ * @private
+ * Returns true if the innerCt scroll is already at its left-most point
+ * @return {Boolean} True if already at furthest left point
+ */
+ atExtremeBefore: function() {
+ return this.getScrollPosition() === 0;
+ },
+
+ /**
+ * @private
+ * Scrolls to the left by the configured amount
+ */
+ scrollLeft: function() {
+ this.scrollBy(-this.scrollIncrement, false);
+ },
+
+ /**
+ * @private
+ * Scrolls to the right by the configured amount
+ */
+ scrollRight: function() {
+ this.scrollBy(this.scrollIncrement, false);
+ },
+
+ /**
+ * Returns the current scroll position of the innerCt element
+ * @return {Number} The current scroll position
+ */
+ getScrollPosition: function(){
+ var layout = this.layout;
+ return parseInt(layout.innerCt.dom['scroll' + layout.parallelBeforeCap], 10) || 0;
+ },
+
+ /**
+ * @private
+ * Returns the maximum value we can scrollTo
+ * @return {Number} The max scroll value
+ */
+ getMaxScrollPosition: function() {
+ var layout = this.layout;
+ return layout.innerCt.dom['scroll' + layout.parallelPrefixCap] - this.layout.innerCt['get' + layout.parallelPrefixCap]();
+ },
+
+ /**
+ * @private
+ * Returns true if the innerCt scroll is already at its right-most point
+ * @return {Boolean} True if already at furthest right point
+ */
+ atExtremeAfter: function() {
+ return this.getScrollPosition() >= this.getMaxScrollPosition();
+ },
+
+ /**
+ * @private
+ * Scrolls to the given position. Performs bounds checking.
+ * @param {Number} position The position to scroll to. This is constrained.
+ * @param {Boolean} animate True to animate. If undefined, falls back to value of this.animateScroll
+ */
+ scrollTo: function(position, animate) {
+ var me = this,
+ layout = me.layout,
+ oldPosition = me.getScrollPosition(),
+ newPosition = Ext.Number.constrain(position, 0, me.getMaxScrollPosition());
+
+ if (newPosition != oldPosition && !me.scrolling) {
+ if (animate == undefined) {
+ animate = me.animateScroll;
+ }
+
+ layout.innerCt.scrollTo(layout.parallelBefore, newPosition, animate ? me.getScrollAnim() : false);
+ if (animate) {
+ me.scrolling = true;
+ } else {
+ me.scrolling = false;
+ me.updateScrollButtons();
+ }
+
+ me.fireEvent('scroll', me, newPosition, animate ? me.getScrollAnim() : false);
+ }
+ },
+
+ /**
+ * Scrolls to the given component.
+ * @param {String/Number/Ext.Component} item The item to scroll to. Can be a numerical index, component id
+ * or a reference to the component itself.
+ * @param {Boolean} animate True to animate the scrolling
+ */
+ scrollToItem: function(item, animate) {
+ var me = this,
+ layout = me.layout,
+ visibility,
+ box,
+ newPos;
+
+ item = me.getItem(item);
+ if (item != undefined) {
+ visibility = this.getItemVisibility(item);
+ if (!visibility.fullyVisible) {
+ box = item.getBox(true, true);
+ newPos = box[layout.parallelPosition];
+ if (visibility.hiddenEnd) {
+ newPos -= (this.layout.innerCt['get' + layout.parallelPrefixCap]() - box[layout.parallelPrefix]);
+ }
+ this.scrollTo(newPos, animate);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * For a given item in the container, return an object with information on whether the item is visible
+ * with the current innerCt scroll value.
+ * @param {Ext.Component} item The item
+ * @return {Object} Values for fullyVisible, hiddenStart and hiddenEnd
+ */
+ getItemVisibility: function(item) {
+ var me = this,
+ box = me.getItem(item).getBox(true, true),
+ layout = me.layout,
+ itemStart = box[layout.parallelPosition],
+ itemEnd = itemStart + box[layout.parallelPrefix],
+ scrollStart = me.getScrollPosition(),
+ scrollEnd = scrollStart + layout.innerCt['get' + layout.parallelPrefixCap]();
+
+ return {
+ hiddenStart : itemStart < scrollStart,
+ hiddenEnd : itemEnd > scrollEnd,
+ fullyVisible: itemStart > scrollStart && itemEnd < scrollEnd
+ };
+ }
+});
+/**
+ * @class Ext.util.Offset
+ * @ignore
+ */
+Ext.define('Ext.util.Offset', {
+
+ /* Begin Definitions */
+
+ statics: {
+ fromObject: function(obj) {
+ return new this(obj.x, obj.y);
+ }
+ },
+
+ /* End Definitions */
+
+ constructor: function(x, y) {
+ this.x = (x != null && !isNaN(x)) ? x : 0;
+ this.y = (y != null && !isNaN(y)) ? y : 0;
+
+ return this;
+ },
+
+ copy: function() {
+ return new Ext.util.Offset(this.x, this.y);
+ },
+
+ copyFrom: function(p) {
+ this.x = p.x;
+ this.y = p.y;
+ },
+
+ toString: function() {
+ return "Offset[" + this.x + "," + this.y + "]";
+ },
+
+ equals: function(offset) {
+
+ return (this.x == offset.x && this.y == offset.y);
+ },
+
+ round: function(to) {
+ if (!isNaN(to)) {
+ var factor = Math.pow(10, to);
+ this.x = Math.round(this.x * factor) / factor;
+ this.y = Math.round(this.y * factor) / factor;
+ } else {
+ this.x = Math.round(this.x);
+ this.y = Math.round(this.y);
+ }
+ },
+
+ isZero: function() {
+ return this.x == 0 && this.y == 0;
+ }
+});
+
+/**
+ * @class Ext.util.KeyNav
+ * <p>Provides a convenient wrapper for normalized keyboard navigation. KeyNav allows you to bind
+ * navigation keys to function calls that will get called when the keys are pressed, providing an easy
+ * way to implement custom navigation schemes for any UI component.</p>
+ * <p>The following are all of the possible keys that can be implemented: enter, space, left, right, up, down, tab, esc,
+ * pageUp, pageDown, del, backspace, home, end. Usage:</p>
+ <pre><code>
+var nav = new Ext.util.KeyNav("my-element", {
+ "left" : function(e){
+ this.moveLeft(e.ctrlKey);
+ },
+ "right" : function(e){
+ this.moveRight(e.ctrlKey);
+ },
+ "enter" : function(e){
+ this.save();
+ },
+ scope : this
+});
+</code></pre>
+ */
+Ext.define('Ext.util.KeyNav', {
+
+ alternateClassName: 'Ext.KeyNav',
+
+ requires: ['Ext.util.KeyMap'],
+
+ statics: {
+ keyOptions: {
+ left: 37,
+ right: 39,
+ up: 38,
+ down: 40,
+ space: 32,
+ pageUp: 33,
+ pageDown: 34,
+ del: 46,
+ backspace: 8,
+ home: 36,
+ end: 35,
+ enter: 13,
+ esc: 27,
+ tab: 9
+ }
+ },
+
+ /**
+ * Creates new KeyNav.
+ * @param {String/HTMLElement/Ext.Element} el The element or its ID to bind to
+ * @param {Object} config The config
+ */
+ constructor: function(el, config){
+ this.setConfig(el, config || {});
+ },
+
+ /**
+ * Sets up a configuration for the KeyNav.
+ * @private
+ * @param {String/HTMLElement/Ext.Element} el The element or its ID to bind to
+ * @param {Object} config A configuration object as specified in the constructor.
+ */
+ setConfig: function(el, config) {
+ if (this.map) {
+ this.map.destroy();
+ }
+
+ var map = Ext.create('Ext.util.KeyMap', el, null, this.getKeyEvent('forceKeyDown' in config ? config.forceKeyDown : this.forceKeyDown)),
+ keys = Ext.util.KeyNav.keyOptions,
+ scope = config.scope || this,
+ key;
+
+ this.map = map;
+ for (key in keys) {
+ if (keys.hasOwnProperty(key)) {
+ if (config[key]) {
+ map.addBinding({
+ scope: scope,
+ key: keys[key],
+ handler: Ext.Function.bind(this.handleEvent, scope, [config[key]], true),
+ defaultEventAction: config.defaultEventAction || this.defaultEventAction
+ });
+ }
+ }
+ }
+
+ map.disable();
+ if (!config.disabled) {
+ map.enable();
+ }
+ },
+
+ /**
+ * Method for filtering out the map argument
+ * @private
+ * @param {Ext.util.KeyMap} map
+ * @param {Ext.EventObject} event
+ * @param {Object} options Contains the handler to call
+ */
+ handleEvent: function(map, event, handler){
+ return handler.call(this, event);
+ },
+
+ /**
+ * @cfg {Boolean} disabled
+ * True to disable this KeyNav instance.
+ */
+ disabled: false,
+
+ /**
+ * @cfg {String} defaultEventAction
+ * The method to call on the {@link Ext.EventObject} after this KeyNav intercepts a key. Valid values are
+ * {@link Ext.EventObject#stopEvent}, {@link Ext.EventObject#preventDefault} and
+ * {@link Ext.EventObject#stopPropagation}.
+ */
+ defaultEventAction: "stopEvent",
+
+ /**
+ * @cfg {Boolean} forceKeyDown
+ * Handle the keydown event instead of keypress. KeyNav automatically does this for IE since
+ * IE does not propagate special keys on keypress, but setting this to true will force other browsers to also
+ * handle keydown instead of keypress.
+ */
+ forceKeyDown: false,
+
+ /**
+ * Destroy this KeyNav (this is the same as calling disable).
+ * @param {Boolean} removeEl True to remove the element associated with this KeyNav.
+ */
+ destroy: function(removeEl){
+ this.map.destroy(removeEl);
+ delete this.map;
+ },
+
+ /**
+ * Enable this KeyNav
+ */
+ enable: function() {
+ this.map.enable();
+ this.disabled = false;
+ },
+
+ /**
+ * Disable this KeyNav
+ */
+ disable: function() {
+ this.map.disable();
+ this.disabled = true;
+ },
+
+ /**
+ * Convenience function for setting disabled/enabled by boolean.
+ * @param {Boolean} disabled
+ */
+ setDisabled : function(disabled){
+ this.map.setDisabled(disabled);
+ this.disabled = disabled;
+ },
+
+ /**
+ * Determines the event to bind to listen for keys. Depends on the {@link #forceKeyDown} setting,
+ * as well as the useKeyDown option on the EventManager.
+ * @return {String} The type of event to listen for.
+ */
+ getKeyEvent: function(forceKeyDown){
+ return (forceKeyDown || Ext.EventManager.useKeyDown) ? 'keydown' : 'keypress';
+ }
+});
+
+/**
+ * @class Ext.fx.Queue
+ * Animation Queue mixin to handle chaining and queueing by target.
+ * @private
+ */
+
+Ext.define('Ext.fx.Queue', {
+
+ requires: ['Ext.util.HashMap'],
+
+ constructor: function() {
+ this.targets = Ext.create('Ext.util.HashMap');
+ this.fxQueue = {};
+ },
+
+ // @private
+ getFxDefaults: function(targetId) {
+ var target = this.targets.get(targetId);
+ if (target) {
+ return target.fxDefaults;
+ }
+ return {};
+ },
+
+ // @private
+ setFxDefaults: function(targetId, obj) {
+ var target = this.targets.get(targetId);
+ if (target) {
+ target.fxDefaults = Ext.apply(target.fxDefaults || {}, obj);
+ }
+ },
+
+ // @private
+ stopAnimation: function(targetId) {
+ var me = this,
+ queue = me.getFxQueue(targetId),
+ ln = queue.length;
+ while (ln) {
+ queue[ln - 1].end();
+ ln--;
+ }
+ },
+
+ /**
+ * @private
+ * Returns current animation object if the element has any effects actively running or queued, else returns false.
+ */
+ getActiveAnimation: function(targetId) {
+ var queue = this.getFxQueue(targetId);
+ return (queue && !!queue.length) ? queue[0] : false;
+ },
+
+ // @private
+ hasFxBlock: function(targetId) {
+ var queue = this.getFxQueue(targetId);
+ return queue && queue[0] && queue[0].block;
+ },
+
+ // @private get fx queue for passed target, create if needed.
+ getFxQueue: function(targetId) {
+ if (!targetId) {
+ return false;
+ }
+ var me = this,
+ queue = me.fxQueue[targetId],
+ target = me.targets.get(targetId);
+
+ if (!target) {
+ return false;
+ }
+
+ if (!queue) {
+ me.fxQueue[targetId] = [];
+ // GarbageCollector will need to clean up Elements since they aren't currently observable
+ if (target.type != 'element') {
+ target.target.on('destroy', function() {
+ me.fxQueue[targetId] = [];
+ });
+ }
+ }
+ return me.fxQueue[targetId];
+ },
+
+ // @private
+ queueFx: function(anim) {
+ var me = this,
+ target = anim.target,
+ queue, ln;
+
+ if (!target) {
+ return;
+ }
+
+ queue = me.getFxQueue(target.getId());
+ ln = queue.length;
+
+ if (ln) {
+ if (anim.concurrent) {
+ anim.paused = false;
+ }
+ else {
+ queue[ln - 1].on('afteranimate', function() {
+ anim.paused = false;
+ });
+ }
+ }
+ else {
+ anim.paused = false;
+ }
+ anim.on('afteranimate', function() {
+ Ext.Array.remove(queue, anim);
+ if (anim.remove) {
+ if (target.type == 'element') {
+ var el = Ext.get(target.id);
+ if (el) {
+ el.remove();
+ }
+ }
+ }
+ }, this);
+ queue.push(anim);
+ }
+});
+/**
+ * @class Ext.fx.target.Target
+
+This class specifies a generic target for an animation. It provides a wrapper around a
+series of different types of objects to allow for a generic animation API.
+A target can be a single object or a Composite object containing other objects that are
+to be animated. This class and it's subclasses are generally not created directly, the
+underlying animation will create the appropriate Ext.fx.target.Target object by passing
+the instance to be animated.
+
+The following types of objects can be animated:
+
+- {@link Ext.fx.target.Component Components}
+- {@link Ext.fx.target.Element Elements}
+- {@link Ext.fx.target.Sprite Sprites}
+
+ * @markdown
+ * @abstract
+ */
+Ext.define('Ext.fx.target.Target', {
+
+ isAnimTarget: true,
+
+ /**
+ * Creates new Target.
+ * @param {Ext.Component/Ext.Element/Ext.draw.Sprite} target The object to be animated
+ */
+ constructor: function(target) {
+ this.target = target;
+ this.id = this.getId();
+ },
+
+ getId: function() {
+ return this.target.id;
+ }
+});
+
+/**
+ * @class Ext.fx.target.Sprite
+ * @extends Ext.fx.target.Target
+
+This class represents a animation target for a {@link Ext.draw.Sprite}. In general this class will not be
+created directly, the {@link Ext.draw.Sprite} will be passed to the animation and
+and the appropriate target will be created.
+
+ * @markdown
+ */
+
+Ext.define('Ext.fx.target.Sprite', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.fx.target.Target',
+
+ /* End Definitions */
+
+ type: 'draw',
+
+ getFromPrim: function(sprite, attr) {
+ var o;
+ if (attr == 'translate') {
+ o = {
+ x: sprite.attr.translation.x || 0,
+ y: sprite.attr.translation.y || 0
+ };
+ }
+ else if (attr == 'rotate') {
+ o = {
+ degrees: sprite.attr.rotation.degrees || 0,
+ x: sprite.attr.rotation.x,
+ y: sprite.attr.rotation.y
+ };
+ }
+ else {
+ o = sprite.attr[attr];
+ }
+ return o;
+ },
+
+ getAttr: function(attr, val) {
+ return [[this.target, val != undefined ? val : this.getFromPrim(this.target, attr)]];
+ },
+
+ setAttr: function(targetData) {
+ var ln = targetData.length,
+ spriteArr = [],
+ attrs, attr, attrArr, attPtr, spritePtr, idx, value, i, j, x, y, ln2;
+ for (i = 0; i < ln; i++) {
+ attrs = targetData[i].attrs;
+ for (attr in attrs) {
+ attrArr = attrs[attr];
+ ln2 = attrArr.length;
+ for (j = 0; j < ln2; j++) {
+ spritePtr = attrArr[j][0];
+ attPtr = attrArr[j][1];
+ if (attr === 'translate') {
+ value = {
+ x: attPtr.x,
+ y: attPtr.y
+ };
+ }
+ else if (attr === 'rotate') {
+ x = attPtr.x;
+ if (isNaN(x)) {
+ x = null;
+ }
+ y = attPtr.y;
+ if (isNaN(y)) {
+ y = null;
+ }
+ value = {
+ degrees: attPtr.degrees,
+ x: x,
+ y: y
+ };
+ }
+ else if (attr === 'width' || attr === 'height' || attr === 'x' || attr === 'y') {
+ value = parseFloat(attPtr);
+ }
+ else {
+ value = attPtr;
+ }
+ idx = Ext.Array.indexOf(spriteArr, spritePtr);
+ if (idx == -1) {
+ spriteArr.push([spritePtr, {}]);
+ idx = spriteArr.length - 1;
+ }
+ spriteArr[idx][1][attr] = value;
+ }
+ }
+ }
+ ln = spriteArr.length;
+ for (i = 0; i < ln; i++) {
+ spritePtr = spriteArr[i];
+ spritePtr[0].setAttributes(spritePtr[1]);
+ }
+ this.target.redraw();
+ }
+});
+
+/**
+ * @class Ext.fx.target.CompositeSprite
+ * @extends Ext.fx.target.Sprite
+
+This class represents a animation target for a {@link Ext.draw.CompositeSprite}. It allows
+each {@link Ext.draw.Sprite} in the group to be animated as a whole. In general this class will not be
+created directly, the {@link Ext.draw.CompositeSprite} will be passed to the animation and
+and the appropriate target will be created.
+
+ * @markdown
+ */
+
+Ext.define('Ext.fx.target.CompositeSprite', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.fx.target.Sprite',
+
+ /* End Definitions */
+
+ getAttr: function(attr, val) {
+ var out = [],
+ target = this.target;
+ target.each(function(sprite) {
+ out.push([sprite, val != undefined ? val : this.getFromPrim(sprite, attr)]);
+ }, this);
+ return out;
+ }
+});
+
+/**
+ * @class Ext.fx.target.Component
+ * @extends Ext.fx.target.Target
+ *
+ * This class represents a animation target for a {@link Ext.Component}. In general this class will not be
+ * created directly, the {@link Ext.Component} will be passed to the animation and
+ * and the appropriate target will be created.
+ */
+Ext.define('Ext.fx.target.Component', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.fx.target.Target',
+
+ /* End Definitions */
+
+ type: 'component',
+
+ // Methods to call to retrieve unspecified "from" values from a target Component
+ getPropMethod: {
+ top: function() {
+ return this.getPosition(true)[1];
+ },
+ left: function() {
+ return this.getPosition(true)[0];
+ },
+ x: function() {
+ return this.getPosition()[0];
+ },
+ y: function() {
+ return this.getPosition()[1];
+ },
+ height: function() {
+ return this.getHeight();
+ },
+ width: function() {
+ return this.getWidth();
+ },
+ opacity: function() {
+ return this.el.getStyle('opacity');
+ }
+ },
+
+ compMethod: {
+ top: 'setPosition',
+ left: 'setPosition',
+ x: 'setPagePosition',
+ y: 'setPagePosition',
+ height: 'setSize',
+ width: 'setSize',
+ opacity: 'setOpacity'
+ },
+
+ // Read the named attribute from the target Component. Use the defined getter for the attribute
+ getAttr: function(attr, val) {
+ return [[this.target, val !== undefined ? val : this.getPropMethod[attr].call(this.target)]];
+ },
+
+ setAttr: function(targetData, isFirstFrame, isLastFrame) {
+ var me = this,
+ target = me.target,
+ ln = targetData.length,
+ attrs, attr, o, i, j, meth, targets, left, top, w, h;
+ for (i = 0; i < ln; i++) {
+ attrs = targetData[i].attrs;
+ for (attr in attrs) {
+ targets = attrs[attr].length;
+ meth = {
+ setPosition: {},
+ setPagePosition: {},
+ setSize: {},
+ setOpacity: {}
+ };
+ for (j = 0; j < targets; j++) {
+ o = attrs[attr][j];
+ // We REALLY want a single function call, so push these down to merge them: eg
+ // meth.setPagePosition.target = <targetComponent>
+ // meth.setPagePosition['x'] = 100
+ // meth.setPagePosition['y'] = 100
+ meth[me.compMethod[attr]].target = o[0];
+ meth[me.compMethod[attr]][attr] = o[1];
+ }
+ if (meth.setPosition.target) {
+ o = meth.setPosition;
+ left = (o.left === undefined) ? undefined : parseInt(o.left, 10);
+ top = (o.top === undefined) ? undefined : parseInt(o.top, 10);
+ o.target.setPosition(left, top);
+ }
+ if (meth.setPagePosition.target) {
+ o = meth.setPagePosition;
+ o.target.setPagePosition(o.x, o.y);
+ }
+ if (meth.setSize.target && meth.setSize.target.el) {
+ o = meth.setSize;
+ // Dimensions not being animated MUST NOT be autosized. They must remain at current value.
+ w = (o.width === undefined) ? o.target.getWidth() : parseInt(o.width, 10);
+ h = (o.height === undefined) ? o.target.getHeight() : parseInt(o.height, 10);
+
+ // Only set the size of the Component on the last frame, or if the animation was
+ // configured with dynamic: true.
+ // In other cases, we just set the target element size.
+ // This will result in either clipping if animating a reduction in size, or the revealing of
+ // the inner elements of the Component if animating an increase in size.
+ // Component's animate function initially resizes to the larger size before resizing the
+ // outer element to clip the contents.
+ if (isLastFrame || me.dynamic) {
+ o.target.componentLayout.childrenChanged = true;
+
+ // Flag if we are being called by an animating layout: use setCalculatedSize
+ if (me.layoutAnimation) {
+ o.target.setCalculatedSize(w, h);
+ } else {
+ o.target.setSize(w, h);
+ }
+ }
+ else {
+ o.target.el.setSize(w, h);
+ }
+ }
+ if (meth.setOpacity.target) {
+ o = meth.setOpacity;
+ o.target.el.setStyle('opacity', o.opacity);
+ }
+ }
+ }
+ }
+});
+
+/**
+ * @class Ext.fx.CubicBezier
+ * @ignore
+ */
+Ext.define('Ext.fx.CubicBezier', {
+
+ /* Begin Definitions */
+
+ singleton: true,
+
+ /* End Definitions */
+
+ cubicBezierAtTime: function(t, p1x, p1y, p2x, p2y, duration) {
+ var cx = 3 * p1x,
+ bx = 3 * (p2x - p1x) - cx,
+ ax = 1 - cx - bx,
+ cy = 3 * p1y,
+ by = 3 * (p2y - p1y) - cy,
+ ay = 1 - cy - by;
+ function sampleCurveX(t) {
+ return ((ax * t + bx) * t + cx) * t;
+ }
+ function solve(x, epsilon) {
+ var t = solveCurveX(x, epsilon);
+ return ((ay * t + by) * t + cy) * t;
+ }
+ function solveCurveX(x, epsilon) {
+ var t0, t1, t2, x2, d2, i;
+ for (t2 = x, i = 0; i < 8; i++) {
+ x2 = sampleCurveX(t2) - x;
+ if (Math.abs(x2) < epsilon) {
+ return t2;
+ }
+ d2 = (3 * ax * t2 + 2 * bx) * t2 + cx;
+ if (Math.abs(d2) < 1e-6) {
+ break;
+ }
+ t2 = t2 - x2 / d2;
+ }
+ t0 = 0;
+ t1 = 1;
+ t2 = x;
+ if (t2 < t0) {
+ return t0;
+ }
+ if (t2 > t1) {
+ return t1;
+ }
+ while (t0 < t1) {
+ x2 = sampleCurveX(t2);
+ if (Math.abs(x2 - x) < epsilon) {
+ return t2;
+ }
+ if (x > x2) {
+ t0 = t2;
+ } else {
+ t1 = t2;
+ }
+ t2 = (t1 - t0) / 2 + t0;
+ }
+ return t2;
+ }
+ return solve(t, 1 / (200 * duration));
+ },
+
+ cubicBezier: function(x1, y1, x2, y2) {
+ var fn = function(pos) {
+ return Ext.fx.CubicBezier.cubicBezierAtTime(pos, x1, y1, x2, y2, 1);
+ };
+ fn.toCSS3 = function() {
+ return 'cubic-bezier(' + [x1, y1, x2, y2].join(',') + ')';
+ };
+ fn.reverse = function() {
+ return Ext.fx.CubicBezier.cubicBezier(1 - x2, 1 - y2, 1 - x1, 1 - y1);
+ };
+ return fn;
+ }
+});
+/**
+ * Represents an RGB color and provides helper functions get
+ * color components in HSL color space.
+ */
+Ext.define('Ext.draw.Color', {
+
+ /* Begin Definitions */
+
+ /* End Definitions */
+
+ colorToHexRe: /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/,
+ rgbRe: /\s*rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)\s*/,
+ hexRe: /\s*#([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)\s*/,
+
+ /**
+ * @cfg {Number} lightnessFactor
+ *
+ * The default factor to compute the lighter or darker color. Defaults to 0.2.
+ */
+ lightnessFactor: 0.2,
+
+ /**
+ * Creates new Color.
+ * @param {Number} red Red component (0..255)
+ * @param {Number} green Green component (0..255)
+ * @param {Number} blue Blue component (0..255)
+ */
+ constructor : function(red, green, blue) {
+ var me = this,
+ clamp = Ext.Number.constrain;
+ me.r = clamp(red, 0, 255);
+ me.g = clamp(green, 0, 255);
+ me.b = clamp(blue, 0, 255);
+ },
+
+ /**
+ * Get the red component of the color, in the range 0..255.
+ * @return {Number}
+ */
+ getRed: function() {
+ return this.r;
+ },
+
+ /**
+ * Get the green component of the color, in the range 0..255.
+ * @return {Number}
+ */
+ getGreen: function() {
+ return this.g;
+ },
+
+ /**
+ * Get the blue component of the color, in the range 0..255.
+ * @return {Number}
+ */
+ getBlue: function() {
+ return this.b;
+ },
+
+ /**
+ * Get the RGB values.
+ * @return {Number[]}
+ */
+ getRGB: function() {
+ var me = this;
+ return [me.r, me.g, me.b];
+ },
+
+ /**
+ * Get the equivalent HSL components of the color.
+ * @return {Number[]}
+ */
+ getHSL: function() {
+ var me = this,
+ r = me.r / 255,
+ g = me.g / 255,
+ b = me.b / 255,
+ max = Math.max(r, g, b),
+ min = Math.min(r, g, b),
+ delta = max - min,
+ h,
+ s = 0,
+ l = 0.5 * (max + min);
+
+ // min==max means achromatic (hue is undefined)
+ if (min != max) {
+ s = (l < 0.5) ? delta / (max + min) : delta / (2 - max - min);
+ if (r == max) {
+ h = 60 * (g - b) / delta;
+ } else if (g == max) {
+ h = 120 + 60 * (b - r) / delta;
+ } else {
+ h = 240 + 60 * (r - g) / delta;
+ }
+ if (h < 0) {
+ h += 360;
+ }
+ if (h >= 360) {
+ h -= 360;
+ }
+ }
+ return [h, s, l];
+ },
+
+ /**
+ * Return a new color that is lighter than this color.
+ * @param {Number} factor Lighter factor (0..1), default to 0.2
+ * @return Ext.draw.Color
+ */
+ getLighter: function(factor) {
+ var hsl = this.getHSL();
+ factor = factor || this.lightnessFactor;
+ hsl[2] = Ext.Number.constrain(hsl[2] + factor, 0, 1);
+ return this.fromHSL(hsl[0], hsl[1], hsl[2]);
+ },
+
+ /**
+ * Return a new color that is darker than this color.
+ * @param {Number} factor Darker factor (0..1), default to 0.2
+ * @return Ext.draw.Color
+ */
+ getDarker: function(factor) {
+ factor = factor || this.lightnessFactor;
+ return this.getLighter(-factor);
+ },
+
+ /**
+ * Return the color in the hex format, i.e. '#rrggbb'.
+ * @return {String}
+ */
+ toString: function() {
+ var me = this,
+ round = Math.round,
+ r = round(me.r).toString(16),
+ g = round(me.g).toString(16),
+ b = round(me.b).toString(16);
+ r = (r.length == 1) ? '0' + r : r;
+ g = (g.length == 1) ? '0' + g : g;
+ b = (b.length == 1) ? '0' + b : b;
+ return ['#', r, g, b].join('');
+ },
+
+ /**
+ * Convert a color to hexadecimal format.
+ *
+ * **Note:** This method is both static and instance.
+ *
+ * @param {String/String[]} color The color value (i.e 'rgb(255, 255, 255)', 'color: #ffffff').
+ * Can also be an Array, in this case the function handles the first member.
+ * @returns {String} The color in hexadecimal format.
+ * @static
+ */
+ toHex: function(color) {
+ if (Ext.isArray(color)) {
+ color = color[0];
+ }
+ if (!Ext.isString(color)) {
+ return '';
+ }
+ if (color.substr(0, 1) === '#') {
+ return color;
+ }
+ var digits = this.colorToHexRe.exec(color);
+
+ if (Ext.isArray(digits)) {
+ var red = parseInt(digits[2], 10),
+ green = parseInt(digits[3], 10),
+ blue = parseInt(digits[4], 10),
+ rgb = blue | (green << 8) | (red << 16);
+ return digits[1] + '#' + ("000000" + rgb.toString(16)).slice(-6);
+ }
+ else {
+ return '';
+ }
+ },
+
+ /**
+ * Parse the string and create a new color.
+ *
+ * Supported formats: '#rrggbb', '#rgb', and 'rgb(r,g,b)'.
+ *
+ * If the string is not recognized, an undefined will be returned instead.
+ *
+ * **Note:** This method is both static and instance.
+ *
+ * @param {String} str Color in string.
+ * @returns Ext.draw.Color
+ * @static
+ */
+ fromString: function(str) {
+ var values, r, g, b,
+ parse = parseInt;
+
+ if ((str.length == 4 || str.length == 7) && str.substr(0, 1) === '#') {
+ values = str.match(this.hexRe);
+ if (values) {
+ r = parse(values[1], 16) >> 0;
+ g = parse(values[2], 16) >> 0;
+ b = parse(values[3], 16) >> 0;
+ if (str.length == 4) {
+ r += (r * 16);
+ g += (g * 16);
+ b += (b * 16);
+ }
+ }
+ }
+ else {
+ values = str.match(this.rgbRe);
+ if (values) {
+ r = values[1];
+ g = values[2];
+ b = values[3];
+ }
+ }
+
+ return (typeof r == 'undefined') ? undefined : Ext.create('Ext.draw.Color', r, g, b);
+ },
+
+ /**
+ * Returns the gray value (0 to 255) of the color.
+ *
+ * The gray value is calculated using the formula r*0.3 + g*0.59 + b*0.11.
+ *
+ * @returns {Number}
+ */
+ getGrayscale: function() {
+ // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
+ return this.r * 0.3 + this.g * 0.59 + this.b * 0.11;
+ },
+
+ /**
+ * Create a new color based on the specified HSL values.
+ *
+ * **Note:** This method is both static and instance.
+ *
+ * @param {Number} h Hue component (0..359)
+ * @param {Number} s Saturation component (0..1)
+ * @param {Number} l Lightness component (0..1)
+ * @returns Ext.draw.Color
+ * @static
+ */
+ fromHSL: function(h, s, l) {
+ var C, X, m, i, rgb = [],
+ abs = Math.abs,
+ floor = Math.floor;
+
+ if (s == 0 || h == null) {
+ // achromatic
+ rgb = [l, l, l];
+ }
+ else {
+ // http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL
+ // C is the chroma
+ // X is the second largest component
+ // m is the lightness adjustment
+ h /= 60;
+ C = s * (1 - abs(2 * l - 1));
+ X = C * (1 - abs(h - 2 * floor(h / 2) - 1));
+ m = l - C / 2;
+ switch (floor(h)) {
+ case 0:
+ rgb = [C, X, 0];
+ break;
+ case 1:
+ rgb = [X, C, 0];
+ break;
+ case 2:
+ rgb = [0, C, X];
+ break;
+ case 3:
+ rgb = [0, X, C];
+ break;
+ case 4:
+ rgb = [X, 0, C];
+ break;
+ case 5:
+ rgb = [C, 0, X];
+ break;
+ }
+ rgb = [rgb[0] + m, rgb[1] + m, rgb[2] + m];
+ }
+ return Ext.create('Ext.draw.Color', rgb[0] * 255, rgb[1] * 255, rgb[2] * 255);
+ }
+}, function() {
+ var prototype = this.prototype;
+
+ //These functions are both static and instance. TODO: find a more elegant way of copying them
+ this.addStatics({
+ fromHSL: function() {
+ return prototype.fromHSL.apply(prototype, arguments);
+ },
+ fromString: function() {
+ return prototype.fromString.apply(prototype, arguments);
+ },
+ toHex: function() {
+ return prototype.toHex.apply(prototype, arguments);
+ }
+ });
+});
+
+/**
+ * @class Ext.dd.StatusProxy
+ * A specialized drag proxy that supports a drop status icon, {@link Ext.Layer} styles and auto-repair. This is the
+ * default drag proxy used by all Ext.dd components.
+ */
+Ext.define('Ext.dd.StatusProxy', {
+ animRepair: false,
+
+ /**
+ * Creates new StatusProxy.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config){
+ Ext.apply(this, config);
+ this.id = this.id || Ext.id();
+ this.proxy = Ext.createWidget('component', {
+ floating: true,
+ stateful: false,
+ id: this.id,
+ html: '<div class="' + Ext.baseCSSPrefix + 'dd-drop-icon"></div>' +
+ '<div class="' + Ext.baseCSSPrefix + 'dd-drag-ghost"></div>',
+ cls: Ext.baseCSSPrefix + 'dd-drag-proxy ' + this.dropNotAllowed,
+ shadow: !config || config.shadow !== false,
+ renderTo: document.body
+ });
+
+ this.el = this.proxy.el;
+ this.el.show();
+ this.el.setVisibilityMode(Ext.Element.VISIBILITY);
+ this.el.hide();
+
+ this.ghost = Ext.get(this.el.dom.childNodes[1]);
+ this.dropStatus = this.dropNotAllowed;
+ },
+ /**
+ * @cfg {String} [dropAllowed="x-dd-drop-ok"]
+ * The CSS class to apply to the status element when drop is allowed.
+ */
+ dropAllowed : Ext.baseCSSPrefix + 'dd-drop-ok',
+ /**
+ * @cfg {String} [dropNotAllowed="x-dd-drop-nodrop"]
+ * The CSS class to apply to the status element when drop is not allowed.
+ */
+ dropNotAllowed : Ext.baseCSSPrefix + 'dd-drop-nodrop',
+
+ /**
+ * Updates the proxy's visual element to indicate the status of whether or not drop is allowed
+ * over the current target element.
+ * @param {String} cssClass The css class for the new drop status indicator image
+ */
+ setStatus : function(cssClass){
+ cssClass = cssClass || this.dropNotAllowed;
+ if(this.dropStatus != cssClass){
+ this.el.replaceCls(this.dropStatus, cssClass);
+ this.dropStatus = cssClass;
+ }
+ },
+
+ /**
+ * Resets the status indicator to the default dropNotAllowed value
+ * @param {Boolean} clearGhost True to also remove all content from the ghost, false to preserve it
+ */
+ reset : function(clearGhost){
+ this.el.dom.className = Ext.baseCSSPrefix + 'dd-drag-proxy ' + this.dropNotAllowed;
+ this.dropStatus = this.dropNotAllowed;
+ if(clearGhost){
+ this.ghost.update("");
+ }
+ },
+
+ /**
+ * Updates the contents of the ghost element
+ * @param {String/HTMLElement} html The html that will replace the current innerHTML of the ghost element, or a
+ * DOM node to append as the child of the ghost element (in which case the innerHTML will be cleared first).
+ */
+ update : function(html){
+ if(typeof html == "string"){
+ this.ghost.update(html);
+ }else{
+ this.ghost.update("");
+ html.style.margin = "0";
+ this.ghost.dom.appendChild(html);
+ }
+ var el = this.ghost.dom.firstChild;
+ if(el){
+ Ext.fly(el).setStyle('float', 'none');
+ }
+ },
+
+ /**
+ * Returns the underlying proxy {@link Ext.Layer}
+ * @return {Ext.Layer} el
+ */
+ getEl : function(){
+ return this.el;
+ },
+
+ /**
+ * Returns the ghost element
+ * @return {Ext.Element} el
+ */
+ getGhost : function(){
+ return this.ghost;
+ },
+
+ /**
+ * Hides the proxy
+ * @param {Boolean} clear True to reset the status and clear the ghost contents, false to preserve them
+ */
+ hide : function(clear) {
+ this.proxy.hide();
+ if (clear) {
+ this.reset(true);
+ }
+ },
+
+ /**
+ * Stops the repair animation if it's currently running
+ */
+ stop : function(){
+ if(this.anim && this.anim.isAnimated && this.anim.isAnimated()){
+ this.anim.stop();
+ }
+ },
+
+ /**
+ * Displays this proxy
+ */
+ show : function() {
+ this.proxy.show();
+ this.proxy.toFront();
+ },
+
+ /**
+ * Force the Layer to sync its shadow and shim positions to the element
+ */
+ sync : function(){
+ this.proxy.el.sync();
+ },
+
+ /**
+ * Causes the proxy to return to its position of origin via an animation. Should be called after an
+ * invalid drop operation by the item being dragged.
+ * @param {Number[]} xy The XY position of the element ([x, y])
+ * @param {Function} callback The function to call after the repair is complete.
+ * @param {Object} scope The scope (<code>this</code> reference) in which the callback function is executed. Defaults to the browser window.
+ */
+ repair : function(xy, callback, scope){
+ this.callback = callback;
+ this.scope = scope;
+ if (xy && this.animRepair !== false) {
+ this.el.addCls(Ext.baseCSSPrefix + 'dd-drag-repair');
+ this.el.hideUnders(true);
+ this.anim = this.el.animate({
+ duration: this.repairDuration || 500,
+ easing: 'ease-out',
+ to: {
+ x: xy[0],
+ y: xy[1]
+ },
+ stopAnimation: true,
+ callback: this.afterRepair,
+ scope: this
+ });
+ } else {
+ this.afterRepair();
+ }
+ },
+
+ // private
+ afterRepair : function(){
+ this.hide(true);
+ if(typeof this.callback == "function"){
+ this.callback.call(this.scope || this);
+ }
+ this.callback = null;
+ this.scope = null;
+ },
+
+ destroy: function(){
+ Ext.destroy(this.ghost, this.proxy, this.el);
+ }
+});
+/**
+ * A custom drag proxy implementation specific to {@link Ext.panel.Panel}s. This class
+ * is primarily used internally for the Panel's drag drop implementation, and
+ * should never need to be created directly.
+ * @private
+ */
+Ext.define('Ext.panel.Proxy', {
+
+ alternateClassName: 'Ext.dd.PanelProxy',
+
+ /**
+ * Creates new panel proxy.
+ * @param {Ext.panel.Panel} panel The {@link Ext.panel.Panel} to proxy for
+ * @param {Object} [config] Config object
+ */
+ constructor: function(panel, config){
+ /**
+ * @property panel
+ * @type Ext.panel.Panel
+ */
+ this.panel = panel;
+ this.id = this.panel.id +'-ddproxy';
+ Ext.apply(this, config);
+ },
+
+ /**
+ * @cfg {Boolean} insertProxy
+ * True to insert a placeholder proxy element while dragging the panel, false to drag with no proxy.
+ * Most Panels are not absolute positioned and therefore we need to reserve this space.
+ */
+ insertProxy: true,
+
+ // private overrides
+ setStatus: Ext.emptyFn,
+ reset: Ext.emptyFn,
+ update: Ext.emptyFn,
+ stop: Ext.emptyFn,
+ sync: Ext.emptyFn,
+
+ /**
+ * Gets the proxy's element
+ * @return {Ext.Element} The proxy's element
+ */
+ getEl: function(){
+ return this.ghost.el;
+ },
+
+ /**
+ * Gets the proxy's ghost Panel
+ * @return {Ext.panel.Panel} The proxy's ghost Panel
+ */
+ getGhost: function(){
+ return this.ghost;
+ },
+
+ /**
+ * Gets the proxy element. This is the element that represents where the
+ * Panel was before we started the drag operation.
+ * @return {Ext.Element} The proxy's element
+ */
+ getProxy: function(){
+ return this.proxy;
+ },
+
+ /**
+ * Hides the proxy
+ */
+ hide : function(){
+ if (this.ghost) {
+ if (this.proxy) {
+ this.proxy.remove();
+ delete this.proxy;
+ }
+
+ // Unghost the Panel, do not move the Panel to where the ghost was
+ this.panel.unghost(null, false);
+ delete this.ghost;
+ }
+ },
+
+ /**
+ * Shows the proxy
+ */
+ show: function(){
+ if (!this.ghost) {
+ var panelSize = this.panel.getSize();
+ this.panel.el.setVisibilityMode(Ext.Element.DISPLAY);
+ this.ghost = this.panel.ghost();
+ if (this.insertProxy) {
+ // bc Panels aren't absolute positioned we need to take up the space
+ // of where the panel previously was
+ this.proxy = this.panel.el.insertSibling({cls: Ext.baseCSSPrefix + 'panel-dd-spacer'});
+ this.proxy.setSize(panelSize);
+ }
+ }
+ },
+
+ // private
+ repair: function(xy, callback, scope) {
+ this.hide();
+ if (typeof callback == "function") {
+ callback.call(scope || this);
+ }
+ },
+
+ /**
+ * Moves the proxy to a different position in the DOM. This is typically
+ * called while dragging the Panel to keep the proxy sync'd to the Panel's
+ * location.
+ * @param {HTMLElement} parentNode The proxy's parent DOM node
+ * @param {HTMLElement} [before] The sibling node before which the
+ * proxy should be inserted (defaults to the parent's last child if not
+ * specified)
+ */
+ moveProxy : function(parentNode, before){
+ if (this.proxy) {
+ parentNode.insertBefore(this.proxy.dom, before);
+ }
+ }
+});
+/**
+ * @class Ext.layout.component.AbstractDock
+ * @extends Ext.layout.component.Component
+ * @private
+ * This ComponentLayout handles docking for Panels. It takes care of panels that are
+ * part of a ContainerLayout that sets this Panel's size and Panels that are part of
+ * an AutoContainerLayout in which this panel get his height based of the CSS or
+ * or its content.
+ */
+
+Ext.define('Ext.layout.component.AbstractDock', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.layout.component.Component',
+
+ /* End Definitions */
+
+ type: 'dock',
+
+ /**
+ * @private
+ * @property autoSizing
+ * @type Boolean
+ * This flag is set to indicate this layout may have an autoHeight/autoWidth.
+ */
+ autoSizing: true,
+
+ beforeLayout: function() {
+ var returnValue = this.callParent(arguments);
+ if (returnValue !== false && (!this.initializedBorders || this.childrenChanged) && (!this.owner.border || this.owner.manageBodyBorders)) {
+ this.handleItemBorders();
+ this.initializedBorders = true;
+ }
+ return returnValue;
+ },
+
+ handleItemBorders: function() {
+ var owner = this.owner,
+ body = owner.body,
+ docked = this.getLayoutItems(),
+ borders = {
+ top: [],
+ right: [],
+ bottom: [],
+ left: []
+ },
+ oldBorders = this.borders,
+ opposites = {
+ top: 'bottom',
+ right: 'left',
+ bottom: 'top',
+ left: 'right'
+ },
+ i, ln, item, dock, side;
+
+ for (i = 0, ln = docked.length; i < ln; i++) {
+ item = docked[i];
+ dock = item.dock;
+
+ if (item.ignoreBorderManagement) {
+ continue;
+ }
+
+ if (!borders[dock].satisfied) {
+ borders[dock].push(item);
+ borders[dock].satisfied = true;
+ }
+
+ if (!borders.top.satisfied && opposites[dock] !== 'top') {
+ borders.top.push(item);
+ }
+ if (!borders.right.satisfied && opposites[dock] !== 'right') {
+ borders.right.push(item);
+ }
+ if (!borders.bottom.satisfied && opposites[dock] !== 'bottom') {
+ borders.bottom.push(item);
+ }
+ if (!borders.left.satisfied && opposites[dock] !== 'left') {
+ borders.left.push(item);
+ }
+ }
+
+ if (oldBorders) {
+ for (side in oldBorders) {
+ if (oldBorders.hasOwnProperty(side)) {
+ ln = oldBorders[side].length;
+ if (!owner.manageBodyBorders) {
+ for (i = 0; i < ln; i++) {
+ oldBorders[side][i].removeCls(Ext.baseCSSPrefix + 'docked-noborder-' + side);
+ }
+ if (!oldBorders[side].satisfied && !owner.bodyBorder) {
+ body.removeCls(Ext.baseCSSPrefix + 'docked-noborder-' + side);
+ }
+ }
+ else if (oldBorders[side].satisfied) {
+ body.setStyle('border-' + side + '-width', '');
+ }
+ }
+ }
+ }
+
+ for (side in borders) {
+ if (borders.hasOwnProperty(side)) {
+ ln = borders[side].length;
+ if (!owner.manageBodyBorders) {
+ for (i = 0; i < ln; i++) {
+ borders[side][i].addCls(Ext.baseCSSPrefix + 'docked-noborder-' + side);
+ }
+ if ((!borders[side].satisfied && !owner.bodyBorder) || owner.bodyBorder === false) {
+ body.addCls(Ext.baseCSSPrefix + 'docked-noborder-' + side);
+ }
+ }
+ else if (borders[side].satisfied) {
+ body.setStyle('border-' + side + '-width', '1px');
+ }
+ }
+ }
+
+ this.borders = borders;
+ },
+
+ /**
+ * @protected
+ * @param {Ext.Component} owner The Panel that owns this DockLayout
+ * @param {Ext.Element} target The target in which we are going to render the docked items
+ * @param {Array} args The arguments passed to the ComponentLayout.layout method
+ */
+ onLayout: function(width, height) {
+ if (this.onLayout_running) {
+ return;
+ }
+ this.onLayout_running = true;
+ var me = this,
+ owner = me.owner,
+ body = owner.body,
+ layout = owner.layout,
+ target = me.getTarget(),
+ autoWidth = false,
+ autoHeight = false,
+ padding, border, frameSize;
+
+ // We start of by resetting all the layouts info
+ var info = me.info = {
+ boxes: [],
+ size: {
+ width: width,
+ height: height
+ },
+ bodyBox: {}
+ };
+ // Clear isAutoDock flag
+ delete layout.isAutoDock;
+
+ Ext.applyIf(info, me.getTargetInfo());
+
+ // We need to bind to the ownerCt whenever we do not have a user set height or width.
+ if (owner && owner.ownerCt && owner.ownerCt.layout && owner.ownerCt.layout.isLayout) {
+ if (!Ext.isNumber(owner.height) || !Ext.isNumber(owner.width)) {
+ owner.ownerCt.layout.bindToOwnerCtComponent = true;
+ }
+ else {
+ owner.ownerCt.layout.bindToOwnerCtComponent = false;
+ }
+ }
+
+ // Determine if we have an autoHeight or autoWidth.
+ if (height == null || width == null) {
+ padding = info.padding;
+ border = info.border;
+ frameSize = me.frameSize;
+
+ // Auto-everything, clear out any style height/width and read from css
+ if ((height == null) && (width == null)) {
+ autoHeight = true;
+ autoWidth = true;
+ me.setTargetSize(null);
+ me.setBodyBox({width: null, height: null});
+ }
+ // Auto-height
+ else if (height == null) {
+ autoHeight = true;
+ // Clear any sizing that we already set in a previous layout
+ me.setTargetSize(width);
+ me.setBodyBox({width: width - padding.left - border.left - padding.right - border.right - frameSize.left - frameSize.right, height: null});
+ // Auto-width
+ }
+ else {
+ autoWidth = true;
+ // Clear any sizing that we already set in a previous layout
+ me.setTargetSize(null, height);
+ me.setBodyBox({width: null, height: height - padding.top - padding.bottom - border.top - border.bottom - frameSize.top - frameSize.bottom});
+ }
+
+ // Run the container
+ if (layout && layout.isLayout) {
+ // Auto-Sized so have the container layout notify the component layout.
+ layout.bindToOwnerCtComponent = true;
+ // Set flag so we don't do a redundant container layout
+ layout.isAutoDock = layout.autoSize !== true;
+ layout.layout();
+
+ // If this is an autosized container layout, then we must compensate for a
+ // body that is being autosized. We do not want to adjust the body's size
+ // to accommodate the dock items, but rather we will want to adjust the
+ // target's size.
+ //
+ // This is necessary because, particularly in a Box layout, all child items
+ // are set with absolute dimensions that are not flexible to the size of its
+ // innerCt/target. So once they are laid out, they are sized for good. By
+ // shrinking the body box to accommodate dock items, we're merely cutting off
+ // parts of the body. Not good. Instead, the target's size should expand
+ // to fit the dock items in. This is valid because the target container is
+ // suppose to be autosized to fit everything accordingly.
+ info.autoSizedCtLayout = layout.autoSize === true;
+ info.autoHeight = autoHeight;
+ info.autoWidth = autoWidth;
+ }
+
+ // The dockItems method will add all the top and bottom docked items height
+ // to the info.panelSize height. That's why we have to call setSize after
+ // we dock all the items to actually set the panel's width and height.
+ // We have to do this because the panel body and docked items will be position
+ // absolute which doesn't stretch the panel.
+ me.dockItems();
+ me.setTargetSize(info.size.width, info.size.height);
+ }
+ else {
+ me.setTargetSize(width, height);
+ me.dockItems();
+ }
+ me.callParent(arguments);
+ this.onLayout_running = false;
+ },
+
+ /**
+ * @protected
+ * This method will first update all the information about the docked items,
+ * body dimensions and position, the panel's total size. It will then
+ * set all these values on the docked items and panel body.
+ * @param {Array} items Array containing all the docked items
+ * @param {Boolean} autoBoxes Set this to true if the Panel is part of an
+ * AutoContainerLayout
+ */
+ dockItems : function() {
+ this.calculateDockBoxes();
+
+ // Both calculateAutoBoxes and calculateSizedBoxes are changing the
+ // information about the body, panel size, and boxes for docked items
+ // inside a property called info.
+ var info = this.info,
+ autoWidth = info.autoWidth,
+ autoHeight = info.autoHeight,
+ boxes = info.boxes,
+ ln = boxes.length,
+ dock, i, item;
+
+ // We are going to loop over all the boxes that were calculated
+ // and set the position of each item the box belongs to.
+ for (i = 0; i < ln; i++) {
+ dock = boxes[i];
+ item = dock.item;
+ item.setPosition(dock.x, dock.y);
+ if ((autoWidth || autoHeight) && item.layout && item.layout.isLayout) {
+ // Auto-Sized so have the container layout notify the component layout.
+ item.layout.bindToOwnerCtComponent = true;
+ }
+ }
+
+ // Don't adjust body width/height if the target is using an auto container layout.
+ // But, we do want to adjust the body size if the container layout is auto sized.
+ if (!info.autoSizedCtLayout) {
+ if (autoWidth) {
+ info.bodyBox.width = null;
+ }
+ if (autoHeight) {
+ info.bodyBox.height = null;
+ }
+ }
+
+ // If the bodyBox has been adjusted because of the docked items
+ // we will update the dimensions and position of the panel's body.
+ this.setBodyBox(info.bodyBox);
+ },
+
+ /**
+ * @protected
+ * This method will set up some initial information about the panel size and bodybox
+ * and then loop over all the items you pass it to take care of stretching, aligning,
+ * dock position and all calculations involved with adjusting the body box.
+ * @param {Array} items Array containing all the docked items we have to layout
+ */
+ calculateDockBoxes : function() {
+ if (this.calculateDockBoxes_running) {
+ // [AbstractDock#calculateDockBoxes] attempted to run again while it was already running
+ return;
+ }
+ this.calculateDockBoxes_running = true;
+ // We want to use the Panel's el width, and the Panel's body height as the initial
+ // size we are going to use in calculateDockBoxes. We also want to account for
+ // the border of the panel.
+ var me = this,
+ target = me.getTarget(),
+ items = me.getLayoutItems(),
+ owner = me.owner,
+ bodyEl = owner.body,
+ info = me.info,
+ autoWidth = info.autoWidth,
+ autoHeight = info.autoHeight,
+ size = info.size,
+ ln = items.length,
+ padding = info.padding,
+ border = info.border,
+ frameSize = me.frameSize,
+ item, i, box, rect;
+
+ // If this Panel is inside an AutoContainerLayout, we will base all the calculations
+ // around the height of the body and the width of the panel.
+ if (autoHeight) {
+ size.height = bodyEl.getHeight() + padding.top + border.top + padding.bottom + border.bottom + frameSize.top + frameSize.bottom;
+ }
+ else {
+ size.height = target.getHeight();
+ }
+ if (autoWidth) {
+ size.width = bodyEl.getWidth() + padding.left + border.left + padding.right + border.right + frameSize.left + frameSize.right;
+ }
+ else {
+ size.width = target.getWidth();
+ }
+
+ info.bodyBox = {
+ x: padding.left + frameSize.left,
+ y: padding.top + frameSize.top,
+ width: size.width - padding.left - border.left - padding.right - border.right - frameSize.left - frameSize.right,
+ height: size.height - border.top - padding.top - border.bottom - padding.bottom - frameSize.top - frameSize.bottom
+ };
+
+ // Loop over all the docked items
+ for (i = 0; i < ln; i++) {
+ item = items[i];
+ // The initBox method will take care of stretching and alignment
+ // In some cases it will also layout the dock items to be able to
+ // get a width or height measurement
+ box = me.initBox(item);
+
+ if (autoHeight === true) {
+ box = me.adjustAutoBox(box, i);
+ }
+ else {
+ box = me.adjustSizedBox(box, i);
+ }
+
+ // Save our box. This allows us to loop over all docked items and do all
+ // calculations first. Then in one loop we will actually size and position
+ // all the docked items that have changed.
+ info.boxes.push(box);
+ }
+ this.calculateDockBoxes_running = false;
+ },
+
+ /**
+ * @protected
+ * This method will adjust the position of the docked item and adjust the body box
+ * accordingly.
+ * @param {Object} box The box containing information about the width and height
+ * of this docked item
+ * @param {Number} index The index position of this docked item
+ * @return {Object} The adjusted box
+ */
+ adjustSizedBox : function(box, index) {
+ var bodyBox = this.info.bodyBox,
+ frameSize = this.frameSize,
+ info = this.info,
+ padding = info.padding,
+ pos = box.type,
+ border = info.border;
+
+ switch (pos) {
+ case 'top':
+ box.y = bodyBox.y;
+ break;
+
+ case 'left':
+ box.x = bodyBox.x;
+ break;
+
+ case 'bottom':
+ box.y = (bodyBox.y + bodyBox.height) - box.height;
+ break;
+
+ case 'right':
+ box.x = (bodyBox.x + bodyBox.width) - box.width;
+ break;
+ }
+
+ if (box.ignoreFrame) {
+ if (pos == 'bottom') {
+ box.y += (frameSize.bottom + padding.bottom + border.bottom);
+ }
+ else {
+ box.y -= (frameSize.top + padding.top + border.top);
+ }
+ if (pos == 'right') {
+ box.x += (frameSize.right + padding.right + border.right);
+ }
+ else {
+ box.x -= (frameSize.left + padding.left + border.left);
+ }
+ }
+
+ // If this is not an overlaying docked item, we have to adjust the body box
+ if (!box.overlay) {
+ switch (pos) {
+ case 'top':
+ bodyBox.y += box.height;
+ bodyBox.height -= box.height;
+ break;
+
+ case 'left':
+ bodyBox.x += box.width;
+ bodyBox.width -= box.width;
+ break;
+
+ case 'bottom':
+ bodyBox.height -= box.height;
+ break;
+
+ case 'right':
+ bodyBox.width -= box.width;
+ break;
+ }
+ }
+ return box;
+ },
+
+ /**
+ * @protected
+ * This method will adjust the position of the docked item inside an AutoContainerLayout
+ * and adjust the body box accordingly.
+ * @param {Object} box The box containing information about the width and height
+ * of this docked item
+ * @param {Number} index The index position of this docked item
+ * @return {Object} The adjusted box
+ */
+ adjustAutoBox : function (box, index) {
+ var info = this.info,
+ owner = this.owner,
+ bodyBox = info.bodyBox,
+ size = info.size,
+ boxes = info.boxes,
+ boxesLn = boxes.length,
+ pos = box.type,
+ frameSize = this.frameSize,
+ padding = info.padding,
+ border = info.border,
+ autoSizedCtLayout = info.autoSizedCtLayout,
+ ln = (boxesLn < index) ? boxesLn : index,
+ i, adjustBox;
+
+ if (pos == 'top' || pos == 'bottom') {
+ // This can affect the previously set left and right and bottom docked items
+ for (i = 0; i < ln; i++) {
+ adjustBox = boxes[i];
+ if (adjustBox.stretched && adjustBox.type == 'left' || adjustBox.type == 'right') {
+ adjustBox.height += box.height;
+ }
+ else if (adjustBox.type == 'bottom') {
+ adjustBox.y += box.height;
+ }
+ }
+ }
+
+ switch (pos) {
+ case 'top':
+ box.y = bodyBox.y;
+ if (!box.overlay) {
+ bodyBox.y += box.height;
+ if (info.autoHeight) {
+ size.height += box.height;
+ } else {
+ bodyBox.height -= box.height;
+ }
+ }
+ break;
+
+ case 'bottom':
+ if (!box.overlay) {
+ if (info.autoHeight) {
+ size.height += box.height;
+ } else {
+ bodyBox.height -= box.height;
+ }
+ }
+ box.y = (bodyBox.y + bodyBox.height);
+ break;
+
+ case 'left':
+ box.x = bodyBox.x;
+ if (!box.overlay) {
+ bodyBox.x += box.width;
+ if (info.autoWidth) {
+ size.width += box.width;
+ } else {
+ bodyBox.width -= box.width;
+ }
+ }
+ break;
+
+ case 'right':
+ if (!box.overlay) {
+ if (info.autoWidth) {
+ size.width += box.width;
+ } else {
+ bodyBox.width -= box.width;
+ }
+ }
+ box.x = (bodyBox.x + bodyBox.width);
+ break;
+ }
+
+ if (box.ignoreFrame) {
+ if (pos == 'bottom') {
+ box.y += (frameSize.bottom + padding.bottom + border.bottom);
+ }
+ else {
+ box.y -= (frameSize.top + padding.top + border.top);
+ }
+ if (pos == 'right') {
+ box.x += (frameSize.right + padding.right + border.right);
+ }
+ else {
+ box.x -= (frameSize.left + padding.left + border.left);
+ }
+ }
+ return box;
+ },
+
+ /**
+ * @protected
+ * This method will create a box object, with a reference to the item, the type of dock
+ * (top, left, bottom, right). It will also take care of stretching and aligning of the
+ * docked items.
+ * @param {Ext.Component} item The docked item we want to initialize the box for
+ * @return {Object} The initial box containing width and height and other useful information
+ */
+ initBox : function(item) {
+ var me = this,
+ bodyBox = me.info.bodyBox,
+ horizontal = (item.dock == 'top' || item.dock == 'bottom'),
+ owner = me.owner,
+ frameSize = me.frameSize,
+ info = me.info,
+ padding = info.padding,
+ border = info.border,
+ box = {
+ item: item,
+ overlay: item.overlay,
+ type: item.dock,
+ offsets: Ext.Element.parseBox(item.offsets || {}),
+ ignoreFrame: item.ignoreParentFrame
+ };
+ // First we are going to take care of stretch and align properties for all four dock scenarios.
+ if (item.stretch !== false) {
+ box.stretched = true;
+ if (horizontal) {
+ box.x = bodyBox.x + box.offsets.left;
+ box.width = bodyBox.width - (box.offsets.left + box.offsets.right);
+ if (box.ignoreFrame) {
+ box.width += (frameSize.left + frameSize.right + border.left + border.right + padding.left + padding.right);
+ }
+ item.setCalculatedSize(box.width - item.el.getMargin('lr'), undefined, owner);
+ }
+ else {
+ box.y = bodyBox.y + box.offsets.top;
+ box.height = bodyBox.height - (box.offsets.bottom + box.offsets.top);
+ if (box.ignoreFrame) {
+ box.height += (frameSize.top + frameSize.bottom + border.top + border.bottom + padding.top + padding.bottom);
+ }
+ item.setCalculatedSize(undefined, box.height - item.el.getMargin('tb'), owner);
+
+ // At this point IE will report the left/right-docked toolbar as having a width equal to the
+ // container's full width. Forcing a repaint kicks it into shape so it reports the correct width.
+ if (!Ext.supports.ComputedStyle) {
+ item.el.repaint();
+ }
+ }
+ }
+ else {
+ item.doComponentLayout();
+ box.width = item.getWidth() - (box.offsets.left + box.offsets.right);
+ box.height = item.getHeight() - (box.offsets.bottom + box.offsets.top);
+ box.y += box.offsets.top;
+ if (horizontal) {
+ box.x = (item.align == 'right') ? bodyBox.width - box.width : bodyBox.x;
+ box.x += box.offsets.left;
+ }
+ }
+
+ // If we haven't calculated the width or height of the docked item yet
+ // do so, since we need this for our upcoming calculations
+ if (box.width === undefined) {
+ box.width = item.getWidth() + item.el.getMargin('lr');
+ }
+ if (box.height === undefined) {
+ box.height = item.getHeight() + item.el.getMargin('tb');
+ }
+
+ return box;
+ },
+
+ /**
+ * @protected
+ * Returns an array containing all the <b>visible</b> docked items inside this layout's owner Panel
+ * @return {Array} An array containing all the <b>visible</b> docked items of the Panel
+ */
+ getLayoutItems : function() {
+ var it = this.owner.getDockedItems(),
+ ln = it.length,
+ i = 0,
+ result = [];
+ for (; i < ln; i++) {
+ if (it[i].isVisible(true)) {
+ result.push(it[i]);
+ }
+ }
+ return result;
+ },
+
+ /**
+ * @protected
+ * Render the top and left docked items before any existing DOM nodes in our render target,
+ * and then render the right and bottom docked items after. This is important, for such things
+ * as tab stops and ARIA readers, that the DOM nodes are in a meaningful order.
+ * Our collection of docked items will already be ordered via Panel.getDockedItems().
+ */
+ renderItems: function(items, target) {
+ var cns = target.dom.childNodes,
+ cnsLn = cns.length,
+ ln = items.length,
+ domLn = 0,
+ i, j, cn, item;
+
+ // Calculate the number of DOM nodes in our target that are not our docked items
+ for (i = 0; i < cnsLn; i++) {
+ cn = Ext.get(cns[i]);
+ for (j = 0; j < ln; j++) {
+ item = items[j];
+ if (item.rendered && (cn.id == item.el.id || cn.contains(item.el.id))) {
+ break;
+ }
+ }
+
+ if (j === ln) {
+ domLn++;
+ }
+ }
+
+ // Now we go through our docked items and render/move them
+ for (i = 0, j = 0; i < ln; i++, j++) {
+ item = items[i];
+
+ // If we're now at the right/bottom docked item, we jump ahead in our
+ // DOM position, just past the existing DOM nodes.
+ //
+ // TODO: This is affected if users provide custom weight values to their
+ // docked items, which puts it out of (t,l,r,b) order. Avoiding a second
+ // sort operation here, for now, in the name of performance. getDockedItems()
+ // needs the sort operation not just for this layout-time rendering, but
+ // also for getRefItems() to return a logical ordering (FocusManager, CQ, et al).
+ if (i === j && (item.dock === 'right' || item.dock === 'bottom')) {
+ j += domLn;
+ }
+
+ // Same logic as Layout.renderItems()
+ if (item && !item.rendered) {
+ this.renderItem(item, target, j);
+ }
+ else if (!this.isValidParent(item, target, j)) {
+ this.moveItem(item, target, j);
+ }
+ }
+ },
+
+ /**
+ * @protected
+ * This function will be called by the dockItems method. Since the body is positioned absolute,
+ * we need to give it dimensions and a position so that it is in the middle surrounded by
+ * docked items
+ * @param {Object} box An object containing new x, y, width and height values for the
+ * Panel's body
+ */
+ setBodyBox : function(box) {
+ var me = this,
+ owner = me.owner,
+ body = owner.body,
+ info = me.info,
+ bodyMargin = info.bodyMargin,
+ padding = info.padding,
+ border = info.border,
+ frameSize = me.frameSize;
+
+ // Panel collapse effectively hides the Panel's body, so this is a no-op.
+ if (owner.collapsed) {
+ return;
+ }
+
+ if (Ext.isNumber(box.width)) {
+ box.width -= bodyMargin.left + bodyMargin.right;
+ }
+
+ if (Ext.isNumber(box.height)) {
+ box.height -= bodyMargin.top + bodyMargin.bottom;
+ }
+
+ me.setElementSize(body, box.width, box.height);
+ if (Ext.isNumber(box.x)) {
+ body.setLeft(box.x - padding.left - frameSize.left);
+ }
+ if (Ext.isNumber(box.y)) {
+ body.setTop(box.y - padding.top - frameSize.top);
+ }
+ },
+
+ /**
+ * @protected
+ * We are overriding the Ext.layout.Layout configureItem method to also add a class that
+ * indicates the position of the docked item. We use the itemCls (x-docked) as a prefix.
+ * An example of a class added to a dock: right item is x-docked-right
+ * @param {Ext.Component} item The item we are configuring
+ */
+ configureItem : function(item, pos) {
+ this.callParent(arguments);
+ if (item.dock == 'top' || item.dock == 'bottom') {
+ item.layoutManagedWidth = 1;
+ item.layoutManagedHeight = 2;
+ } else {
+ item.layoutManagedWidth = 2;
+ item.layoutManagedHeight = 1;
+ }
+
+ item.addCls(Ext.baseCSSPrefix + 'docked');
+ item.addClsWithUI('docked-' + item.dock);
+ },
+
+ afterRemove : function(item) {
+ this.callParent(arguments);
+ if (this.itemCls) {
+ item.el.removeCls(this.itemCls + '-' + item.dock);
+ }
+ var dom = item.el.dom;
+
+ if (!item.destroying && dom) {
+ dom.parentNode.removeChild(dom);
+ }
+ this.childrenChanged = true;
+ }
+});
+/**
+ * @class Ext.util.Memento
+ * This class manages a set of captured properties from an object. These captured properties
+ * can later be restored to an object.
+ */
+Ext.define('Ext.util.Memento', function () {
+
+ function captureOne (src, target, prop) {
+ src[prop] = target[prop];
+ }
+
+ function removeOne (src, target, prop) {
+ delete src[prop];
+ }
+
+ function restoreOne (src, target, prop) {
+ var value = src[prop];
+ if (value || src.hasOwnProperty(prop)) {
+ restoreValue(target, prop, value);
+ }
+ }
+
+ function restoreValue (target, prop, value) {
+ if (Ext.isDefined(value)) {
+ target[prop] = value;
+ } else {
+ delete target[prop];
+ }
+ }
+
+ function doMany (doOne, src, target, props) {
+ if (src) {
+ if (Ext.isArray(props)) {
+ Ext.each(props, function (prop) {
+ doOne(src, target, prop);
+ });
+ } else {
+ doOne(src, target, props);
+ }
+ }
+ }
+
+ return {
+ /**
+ * @property data
+ * The collection of captured properties.
+ * @private
+ */
+ data: null,
+
+ /**
+ * @property target
+ * The default target object for capture/restore (passed to the constructor).
+ */
+ target: null,
+
+ /**
+ * Creates a new memento and optionally captures properties from the target object.
+ * @param {Object} target The target from which to capture properties. If specified in the
+ * constructor, this target becomes the default target for all other operations.
+ * @param {String/String[]} props The property or array of properties to capture.
+ */
+ constructor: function (target, props) {
+ if (target) {
+ this.target = target;
+ if (props) {
+ this.capture(props);
+ }
+ }
+ },
+
+ /**
+ * Captures the specified properties from the target object in this memento.
+ * @param {String/String[]} props The property or array of properties to capture.
+ * @param {Object} target The object from which to capture properties.
+ */
+ capture: function (props, target) {
+ doMany(captureOne, this.data || (this.data = {}), target || this.target, props);
+ },
+
+ /**
+ * Removes the specified properties from this memento. These properties will not be
+ * restored later without re-capturing their values.
+ * @param {String/String[]} props The property or array of properties to remove.
+ */
+ remove: function (props) {
+ doMany(removeOne, this.data, null, props);
+ },
+
+ /**
+ * Restores the specified properties from this memento to the target object.
+ * @param {String/String[]} props The property or array of properties to restore.
+ * @param {Boolean} clear True to remove the restored properties from this memento or
+ * false to keep them (default is true).
+ * @param {Object} target The object to which to restore properties.
+ */
+ restore: function (props, clear, target) {
+ doMany(restoreOne, this.data, target || this.target, props);
+ if (clear !== false) {
+ this.remove(props);
+ }
+ },
+
+ /**
+ * Restores all captured properties in this memento to the target object.
+ * @param {Boolean} clear True to remove the restored properties from this memento or
+ * false to keep them (default is true).
+ * @param {Object} target The object to which to restore properties.
+ */
+ restoreAll: function (clear, target) {
+ var me = this,
+ t = target || this.target;
+
+ Ext.Object.each(me.data, function (prop, value) {
+ restoreValue(t, prop, value);
+ });
+
+ if (clear !== false) {
+ delete me.data;
+ }
+ }
+ };
+}());
+
+/**
+ * @class Ext.app.EventBus
+ * @private
+ */
+Ext.define('Ext.app.EventBus', {
+ requires: [
+ 'Ext.util.Event'
+ ],
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ constructor: function() {
+ this.mixins.observable.constructor.call(this);
+
+ this.bus = {};
+
+ var me = this;
+ Ext.override(Ext.Component, {
+ fireEvent: function(ev) {
+ if (Ext.util.Observable.prototype.fireEvent.apply(this, arguments) !== false) {
+ return me.dispatch.call(me, ev, this, arguments);
+ }
+ return false;
+ }
+ });
+ },
+
+ dispatch: function(ev, target, args) {
+ var bus = this.bus,
+ selectors = bus[ev],
+ selector, controllers, id, events, event, i, ln;
+
+ if (selectors) {
+ // Loop over all the selectors that are bound to this event
+ for (selector in selectors) {
+ // Check if the target matches the selector
+ if (target.is(selector)) {
+ // Loop over all the controllers that are bound to this selector
+ controllers = selectors[selector];
+ for (id in controllers) {
+ // Loop over all the events that are bound to this selector on this controller
+ events = controllers[id];
+ for (i = 0, ln = events.length; i < ln; i++) {
+ event = events[i];
+ // Fire the event!
+ if (event.fire.apply(event, Array.prototype.slice.call(args, 1)) === false) {
+ return false;
+ };
+ }
+ }
+ }
+ }
+ }
+ },
+
+ control: function(selectors, listeners, controller) {
+ var bus = this.bus,
+ selector, fn;
+
+ if (Ext.isString(selectors)) {
+ selector = selectors;
+ selectors = {};
+ selectors[selector] = listeners;
+ this.control(selectors, null, controller);
+ return;
+ }
+
+ Ext.Object.each(selectors, function(selector, listeners) {
+ Ext.Object.each(listeners, function(ev, listener) {
+ var options = {},
+ scope = controller,
+ event = Ext.create('Ext.util.Event', controller, ev);
+
+ // Normalize the listener
+ if (Ext.isObject(listener)) {
+ options = listener;
+ listener = options.fn;
+ scope = options.scope || controller;
+ delete options.fn;
+ delete options.scope;
+ }
+
+ event.addListener(listener, scope, options);
+
+ // Create the bus tree if it is not there yet
+ bus[ev] = bus[ev] || {};
+ bus[ev][selector] = bus[ev][selector] || {};
+ bus[ev][selector][controller.id] = bus[ev][selector][controller.id] || [];
+
+ // Push our listener in our bus
+ bus[ev][selector][controller.id].push(event);
+ });
+ });
+ }
+});
+/**
+ * @class Ext.data.Types
+ * <p>This is a static class containing the system-supplied data types which may be given to a {@link Ext.data.Field Field}.<p/>
+ * <p>The properties in this class are used as type indicators in the {@link Ext.data.Field Field} class, so to
+ * test whether a Field is of a certain type, compare the {@link Ext.data.Field#type type} property against properties
+ * of this class.</p>
+ * <p>Developers may add their own application-specific data types to this class. Definition names must be UPPERCASE.
+ * each type definition must contain three properties:</p>
+ * <div class="mdetail-params"><ul>
+ * <li><code>convert</code> : <i>Function</i><div class="sub-desc">A function to convert raw data values from a data block into the data
+ * to be stored in the Field. The function is passed the collowing parameters:
+ * <div class="mdetail-params"><ul>
+ * <li><b>v</b> : Mixed<div class="sub-desc">The data value as read by the Reader, if undefined will use
+ * the configured <tt>{@link Ext.data.Field#defaultValue defaultValue}</tt>.</div></li>
+ * <li><b>rec</b> : Mixed<div class="sub-desc">The data object containing the row as read by the Reader.
+ * Depending on the Reader type, this could be an Array ({@link Ext.data.reader.Array ArrayReader}), an object
+ * ({@link Ext.data.reader.Json JsonReader}), or an XML element.</div></li>
+ * </ul></div></div></li>
+ * <li><code>sortType</code> : <i>Function</i> <div class="sub-desc">A function to convert the stored data into comparable form, as defined by {@link Ext.data.SortTypes}.</div></li>
+ * <li><code>type</code> : <i>String</i> <div class="sub-desc">A textual data type name.</div></li>
+ * </ul></div>
+ * <p>For example, to create a VELatLong field (See the Microsoft Bing Mapping API) containing the latitude/longitude value of a datapoint on a map from a JsonReader data block
+ * which contained the properties <code>lat</code> and <code>long</code>, you would define a new data type like this:</p>
+ *<pre><code>
+// Add a new Field data type which stores a VELatLong object in the Record.
+Ext.data.Types.VELATLONG = {
+ convert: function(v, data) {
+ return new VELatLong(data.lat, data.long);
+ },
+ sortType: function(v) {
+ return v.Latitude; // When sorting, order by latitude
+ },
+ type: 'VELatLong'
+};
+</code></pre>
+ * <p>Then, when declaring a Model, use: <pre><code>
+var types = Ext.data.Types; // allow shorthand type access
+Ext.define('Unit',
+ extend: 'Ext.data.Model',
+ fields: [
+ { name: 'unitName', mapping: 'UnitName' },
+ { name: 'curSpeed', mapping: 'CurSpeed', type: types.INT },
+ { name: 'latitude', mapping: 'lat', type: types.FLOAT },
+ { name: 'longitude', mapping: 'long', type: types.FLOAT },
+ { name: 'position', type: types.VELATLONG }
+ ]
+});
+</code></pre>
+ * @singleton
+ */
+Ext.define('Ext.data.Types', {
+ singleton: true,
+ requires: ['Ext.data.SortTypes']
+}, function() {
+ var st = Ext.data.SortTypes;
+
+ Ext.apply(Ext.data.Types, {
+ /**
+ * @property {RegExp} stripRe
+ * A regular expression for stripping non-numeric characters from a numeric value. Defaults to <tt>/[\$,%]/g</tt>.
+ * This should be overridden for localization.
+ */
+ stripRe: /[\$,%]/g,
+
+ /**
+ * @property {Object} AUTO
+ * This data type means that no conversion is applied to the raw data before it is placed into a Record.
+ */
+ AUTO: {
+ convert: function(v) {
+ return v;
+ },
+ sortType: st.none,
+ type: 'auto'
+ },
+
+ /**
+ * @property {Object} STRING
+ * This data type means that the raw data is converted into a String before it is placed into a Record.
+ */
+ STRING: {
+ convert: function(v) {
+ var defaultValue = this.useNull ? null : '';
+ return (v === undefined || v === null) ? defaultValue : String(v);
+ },
+ sortType: st.asUCString,
+ type: 'string'
+ },
+
+ /**
+ * @property {Object} INT
+ * This data type means that the raw data is converted into an integer before it is placed into a Record.
+ * <p>The synonym <code>INTEGER</code> is equivalent.</p>
+ */
+ INT: {
+ convert: function(v) {
+ return v !== undefined && v !== null && v !== '' ?
+ parseInt(String(v).replace(Ext.data.Types.stripRe, ''), 10) : (this.useNull ? null : 0);
+ },
+ sortType: st.none,
+ type: 'int'
+ },
+
+ /**
+ * @property {Object} FLOAT
+ * This data type means that the raw data is converted into a number before it is placed into a Record.
+ * <p>The synonym <code>NUMBER</code> is equivalent.</p>
+ */
+ FLOAT: {
+ convert: function(v) {
+ return v !== undefined && v !== null && v !== '' ?
+ parseFloat(String(v).replace(Ext.data.Types.stripRe, ''), 10) : (this.useNull ? null : 0);
+ },
+ sortType: st.none,
+ type: 'float'
+ },
+
+ /**
+ * @property {Object} BOOL
+ * <p>This data type means that the raw data is converted into a boolean before it is placed into
+ * a Record. The string "true" and the number 1 are converted to boolean <code>true</code>.</p>
+ * <p>The synonym <code>BOOLEAN</code> is equivalent.</p>
+ */
+ BOOL: {
+ convert: function(v) {
+ if (this.useNull && (v === undefined || v === null || v === '')) {
+ return null;
+ }
+ return v === true || v === 'true' || v == 1;
+ },
+ sortType: st.none,
+ type: 'bool'
+ },
+
+ /**
+ * @property {Object} DATE
+ * This data type means that the raw data is converted into a Date before it is placed into a Record.
+ * The date format is specified in the constructor of the {@link Ext.data.Field} to which this type is
+ * being applied.
+ */
+ DATE: {
+ convert: function(v) {
+ var df = this.dateFormat,
+ parsed;
+
+ if (!v) {
+ return null;
+ }
+ if (Ext.isDate(v)) {
+ return v;
+ }
+ if (df) {
+ if (df == 'timestamp') {
+ return new Date(v*1000);
+ }
+ if (df == 'time') {
+ return new Date(parseInt(v, 10));
+ }
+ return Ext.Date.parse(v, df);
+ }
+
+ parsed = Date.parse(v);
+ return parsed ? new Date(parsed) : null;
+ },
+ sortType: st.asDate,
+ type: 'date'
+ }
+ });
+
+ Ext.apply(Ext.data.Types, {
+ /**
+ * @property {Object} BOOLEAN
+ * <p>This data type means that the raw data is converted into a boolean before it is placed into
+ * a Record. The string "true" and the number 1 are converted to boolean <code>true</code>.</p>
+ * <p>The synonym <code>BOOL</code> is equivalent.</p>
+ */
+ BOOLEAN: this.BOOL,
+
+ /**
+ * @property {Object} INTEGER
+ * This data type means that the raw data is converted into an integer before it is placed into a Record.
+ * <p>The synonym <code>INT</code> is equivalent.</p>
+ */
+ INTEGER: this.INT,
+
+ /**
+ * @property {Object} NUMBER
+ * This data type means that the raw data is converted into a number before it is placed into a Record.
+ * <p>The synonym <code>FLOAT</code> is equivalent.</p>
+ */
+ NUMBER: this.FLOAT
+ });
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * Fields are used to define what a Model is. They aren't instantiated directly - instead, when we create a class that
+ * extends {@link Ext.data.Model}, it will automatically create a Field instance for each field configured in a {@link
+ * Ext.data.Model Model}. For example, we might set up a model like this:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'name', 'email',
+ * {name: 'age', type: 'int'},
+ * {name: 'gender', type: 'string', defaultValue: 'Unknown'}
+ * ]
+ * });
+ *
+ * Four fields will have been created for the User Model - name, email, age and gender. Note that we specified a couple
+ * of different formats here; if we only pass in the string name of the field (as with name and email), the field is set
+ * up with the 'auto' type. It's as if we'd done this instead:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'name', type: 'auto'},
+ * {name: 'email', type: 'auto'},
+ * {name: 'age', type: 'int'},
+ * {name: 'gender', type: 'string', defaultValue: 'Unknown'}
+ * ]
+ * });
+ *
+ * # Types and conversion
+ *
+ * The {@link #type} is important - it's used to automatically convert data passed to the field into the correct format.
+ * In our example above, the name and email fields used the 'auto' type and will just accept anything that is passed
+ * into them. The 'age' field had an 'int' type however, so if we passed 25.4 this would be rounded to 25.
+ *
+ * Sometimes a simple type isn't enough, or we want to perform some processing when we load a Field's data. We can do
+ * this using a {@link #convert} function. Here, we're going to create a new field based on another:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'name', 'email',
+ * {name: 'age', type: 'int'},
+ * {name: 'gender', type: 'string', defaultValue: 'Unknown'},
+ *
+ * {
+ * name: 'firstName',
+ * convert: function(value, record) {
+ * var fullName = record.get('name'),
+ * splits = fullName.split(" "),
+ * firstName = splits[0];
+ *
+ * return firstName;
+ * }
+ * }
+ * ]
+ * });
+ *
+ * Now when we create a new User, the firstName is populated automatically based on the name:
+ *
+ * var ed = Ext.create('User', {name: 'Ed Spencer'});
+ *
+ * console.log(ed.get('firstName')); //logs 'Ed', based on our convert function
+ *
+ * In fact, if we log out all of the data inside ed, we'll see this:
+ *
+ * console.log(ed.data);
+ *
+ * //outputs this:
+ * {
+ * age: 0,
+ * email: "",
+ * firstName: "Ed",
+ * gender: "Unknown",
+ * name: "Ed Spencer"
+ * }
+ *
+ * The age field has been given a default of zero because we made it an int type. As an auto field, email has defaulted
+ * to an empty string. When we registered the User model we set gender's {@link #defaultValue} to 'Unknown' so we see
+ * that now. Let's correct that and satisfy ourselves that the types work as we expect:
+ *
+ * ed.set('gender', 'Male');
+ * ed.get('gender'); //returns 'Male'
+ *
+ * ed.set('age', 25.4);
+ * ed.get('age'); //returns 25 - we wanted an int, not a float, so no decimal places allowed
+ */
+Ext.define('Ext.data.Field', {
+ requires: ['Ext.data.Types', 'Ext.data.SortTypes'],
+ alias: 'data.field',
+
+ constructor : function(config) {
+ if (Ext.isString(config)) {
+ config = {name: config};
+ }
+ Ext.apply(this, config);
+
+ var types = Ext.data.Types,
+ st = this.sortType,
+ t;
+
+ if (this.type) {
+ if (Ext.isString(this.type)) {
+ this.type = types[this.type.toUpperCase()] || types.AUTO;
+ }
+ } else {
+ this.type = types.AUTO;
+ }
+
+ // named sortTypes are supported, here we look them up
+ if (Ext.isString(st)) {
+ this.sortType = Ext.data.SortTypes[st];
+ } else if(Ext.isEmpty(st)) {
+ this.sortType = this.type.sortType;
+ }
+
+ if (!this.convert) {
+ this.convert = this.type.convert;
+ }
+ },
+
+ /**
+ * @cfg {String} name
+ *
+ * The name by which the field is referenced within the Model. This is referenced by, for example, the `dataIndex`
+ * property in column definition objects passed to {@link Ext.grid.property.HeaderContainer}.
+ *
+ * Note: In the simplest case, if no properties other than `name` are required, a field definition may consist of
+ * just a String for the field name.
+ */
+
+ /**
+ * @cfg {String/Object} type
+ *
+ * The data type for automatic conversion from received data to the *stored* value if
+ * `{@link Ext.data.Field#convert convert}` has not been specified. This may be specified as a string value.
+ * Possible values are
+ *
+ * - auto (Default, implies no conversion)
+ * - string
+ * - int
+ * - float
+ * - boolean
+ * - date
+ *
+ * This may also be specified by referencing a member of the {@link Ext.data.Types} class.
+ *
+ * Developers may create their own application-specific data types by defining new members of the {@link
+ * Ext.data.Types} class.
+ */
+
+ /**
+ * @cfg {Function} convert
+ *
+ * A function which converts the value provided by the Reader into an object that will be stored in the Model.
+ * It is passed the following parameters:
+ *
+ * - **v** : Mixed
+ *
+ * The data value as read by the Reader, if undefined will use the configured `{@link Ext.data.Field#defaultValue
+ * defaultValue}`.
+ *
+ * - **rec** : Ext.data.Model
+ *
+ * The data object containing the Model as read so far by the Reader. Note that the Model may not be fully populated
+ * at this point as the fields are read in the order that they are defined in your
+ * {@link Ext.data.Model#fields fields} array.
+ *
+ * Example of convert functions:
+ *
+ * function fullName(v, record){
+ * return record.name.last + ', ' + record.name.first;
+ * }
+ *
+ * function location(v, record){
+ * return !record.city ? '' : (record.city + ', ' + record.state);
+ * }
+ *
+ * Ext.define('Dude', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'fullname', convert: fullName},
+ * {name: 'firstname', mapping: 'name.first'},
+ * {name: 'lastname', mapping: 'name.last'},
+ * {name: 'city', defaultValue: 'homeless'},
+ * 'state',
+ * {name: 'location', convert: location}
+ * ]
+ * });
+ *
+ * // create the data store
+ * var store = Ext.create('Ext.data.Store', {
+ * reader: {
+ * type: 'json',
+ * model: 'Dude',
+ * idProperty: 'key',
+ * root: 'daRoot',
+ * totalProperty: 'total'
+ * }
+ * });
+ *
+ * var myData = [
+ * { key: 1,
+ * name: { first: 'Fat', last: 'Albert' }
+ * // notice no city, state provided in data object
+ * },
+ * { key: 2,
+ * name: { first: 'Barney', last: 'Rubble' },
+ * city: 'Bedrock', state: 'Stoneridge'
+ * },
+ * { key: 3,
+ * name: { first: 'Cliff', last: 'Claven' },
+ * city: 'Boston', state: 'MA'
+ * }
+ * ];
+ */
+
+ /**
+ * @cfg {String} dateFormat
+ *
+ * Used when converting received data into a Date when the {@link #type} is specified as `"date"`.
+ *
+ * A format string for the {@link Ext.Date#parse Ext.Date.parse} function, or "timestamp" if the value provided by
+ * the Reader is a UNIX timestamp, or "time" if the value provided by the Reader is a javascript millisecond
+ * timestamp. See {@link Ext.Date}.
+ */
+ dateFormat: null,
+
+ /**
+ * @cfg {Boolean} useNull
+ *
+ * Use when converting received data into a Number type (either int or float). If the value cannot be
+ * parsed, null will be used if useNull is true, otherwise the value will be 0. Defaults to false.
+ */
+ useNull: false,
+
+ /**
+ * @cfg {Object} defaultValue
+ *
+ * The default value used **when a Model is being created by a {@link Ext.data.reader.Reader Reader}**
+ * when the item referenced by the `{@link Ext.data.Field#mapping mapping}` does not exist in the data object
+ * (i.e. undefined). Defaults to "".
+ */
+ defaultValue: "",
+
+ /**
+ * @cfg {String/Number} mapping
+ *
+ * (Optional) A path expression for use by the {@link Ext.data.reader.Reader} implementation that is creating the
+ * {@link Ext.data.Model Model} to extract the Field value from the data object. If the path expression is the same
+ * as the field name, the mapping may be omitted.
+ *
+ * The form of the mapping expression depends on the Reader being used.
+ *
+ * - {@link Ext.data.reader.Json}
+ *
+ * The mapping is a string containing the javascript expression to reference the data from an element of the data
+ * item's {@link Ext.data.reader.Json#root root} Array. Defaults to the field name.
+ *
+ * - {@link Ext.data.reader.Xml}
+ *
+ * The mapping is an {@link Ext.DomQuery} path to the data item relative to the DOM element that represents the
+ * {@link Ext.data.reader.Xml#record record}. Defaults to the field name.
+ *
+ * - {@link Ext.data.reader.Array}
+ *
+ * The mapping is a number indicating the Array index of the field's value. Defaults to the field specification's
+ * Array position.
+ *
+ * If a more complex value extraction strategy is required, then configure the Field with a {@link #convert}
+ * function. This is passed the whole row object, and may interrogate it in whatever way is necessary in order to
+ * return the desired data.
+ */
+ mapping: null,
+
+ /**
+ * @cfg {Function} sortType
+ *
+ * A function which converts a Field's value to a comparable value in order to ensure correct sort ordering.
+ * Predefined functions are provided in {@link Ext.data.SortTypes}. A custom sort example:
+ *
+ * // current sort after sort we want
+ * // +-+------+ +-+------+
+ * // |1|First | |1|First |
+ * // |2|Last | |3|Second|
+ * // |3|Second| |2|Last |
+ * // +-+------+ +-+------+
+ *
+ * sortType: function(value) {
+ * switch (value.toLowerCase()) // native toLowerCase():
+ * {
+ * case 'first': return 1;
+ * case 'second': return 2;
+ * default: return 3;
+ * }
+ * }
+ */
+ sortType : null,
+
+ /**
+ * @cfg {String} sortDir
+ *
+ * Initial direction to sort (`"ASC"` or `"DESC"`). Defaults to `"ASC"`.
+ */
+ sortDir : "ASC",
+
+ /**
+ * @cfg {Boolean} allowBlank
+ * @private
+ *
+ * Used for validating a {@link Ext.data.Model model}. Defaults to true. An empty value here will cause
+ * {@link Ext.data.Model}.{@link Ext.data.Model#isValid isValid} to evaluate to false.
+ */
+ allowBlank : true,
+
+ /**
+ * @cfg {Boolean} persist
+ *
+ * False to exclude this field from the {@link Ext.data.Model#modified} fields in a model. This will also exclude
+ * the field from being written using a {@link Ext.data.writer.Writer}. This option is useful when model fields are
+ * used to keep state on the client but do not need to be persisted to the server. Defaults to true.
+ */
+ persist: true
+});
+
+/**
+ * @class Ext.util.AbstractMixedCollection
+ * @private
+ */
+Ext.define('Ext.util.AbstractMixedCollection', {
+ requires: ['Ext.util.Filter'],
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ constructor: function(allowFunctions, keyFn) {
+ var me = this;
+
+ me.items = [];
+ me.map = {};
+ me.keys = [];
+ me.length = 0;
+
+ me.addEvents(
+ /**
+ * @event clear
+ * Fires when the collection is cleared.
+ */
+ 'clear',
+
+ /**
+ * @event add
+ * Fires when an item is added to the collection.
+ * @param {Number} index The index at which the item was added.
+ * @param {Object} o The item added.
+ * @param {String} key The key associated with the added item.
+ */
+ 'add',
+
+ /**
+ * @event replace
+ * Fires when an item is replaced in the collection.
+ * @param {String} key he key associated with the new added.
+ * @param {Object} old The item being replaced.
+ * @param {Object} new The new item.
+ */
+ 'replace',
+
+ /**
+ * @event remove
+ * Fires when an item is removed from the collection.
+ * @param {Object} o The item being removed.
+ * @param {String} key (optional) The key associated with the removed item.
+ */
+ 'remove'
+ );
+
+ me.allowFunctions = allowFunctions === true;
+
+ if (keyFn) {
+ me.getKey = keyFn;
+ }
+
+ me.mixins.observable.constructor.call(me);
+ },
+
+ /**
+ * @cfg {Boolean} allowFunctions Specify <tt>true</tt> if the {@link #addAll}
+ * function should add function references to the collection. Defaults to
+ * <tt>false</tt>.
+ */
+ allowFunctions : false,
+
+ /**
+ * Adds an item to the collection. Fires the {@link #add} event when complete.
+ * @param {String} key <p>The key to associate with the item, or the new item.</p>
+ * <p>If a {@link #getKey} implementation was specified for this MixedCollection,
+ * or if the key of the stored items is in a property called <tt><b>id</b></tt>,
+ * the MixedCollection will be able to <i>derive</i> the key for the new item.
+ * In this case just pass the new item in this parameter.</p>
+ * @param {Object} o The item to add.
+ * @return {Object} The item added.
+ */
+ add : function(key, obj){
+ var me = this,
+ myObj = obj,
+ myKey = key,
+ old;
+
+ if (arguments.length == 1) {
+ myObj = myKey;
+ myKey = me.getKey(myObj);
+ }
+ if (typeof myKey != 'undefined' && myKey !== null) {
+ old = me.map[myKey];
+ if (typeof old != 'undefined') {
+ return me.replace(myKey, myObj);
+ }
+ me.map[myKey] = myObj;
+ }
+ me.length++;
+ me.items.push(myObj);
+ me.keys.push(myKey);
+ me.fireEvent('add', me.length - 1, myObj, myKey);
+ return myObj;
+ },
+
+ /**
+ * MixedCollection has a generic way to fetch keys if you implement getKey. The default implementation
+ * simply returns <b><code>item.id</code></b> but you can provide your own implementation
+ * to return a different value as in the following examples:<pre><code>
+// normal way
+var mc = new Ext.util.MixedCollection();
+mc.add(someEl.dom.id, someEl);
+mc.add(otherEl.dom.id, otherEl);
+//and so on
+
+// using getKey
+var mc = new Ext.util.MixedCollection();
+mc.getKey = function(el){
+ return el.dom.id;
+};
+mc.add(someEl);
+mc.add(otherEl);
+
+// or via the constructor
+var mc = new Ext.util.MixedCollection(false, function(el){
+ return el.dom.id;
+});
+mc.add(someEl);
+mc.add(otherEl);
+ * </code></pre>
+ * @param {Object} item The item for which to find the key.
+ * @return {Object} The key for the passed item.
+ */
+ getKey : function(o){
+ return o.id;
+ },
+
+ /**
+ * Replaces an item in the collection. Fires the {@link #replace} event when complete.
+ * @param {String} key <p>The key associated with the item to replace, or the replacement item.</p>
+ * <p>If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key
+ * of your stored items is in a property called <tt><b>id</b></tt>, then the MixedCollection
+ * will be able to <i>derive</i> the key of the replacement item. If you want to replace an item
+ * with one having the same key value, then just pass the replacement item in this parameter.</p>
+ * @param o {Object} o (optional) If the first parameter passed was a key, the item to associate
+ * with that key.
+ * @return {Object} The new item.
+ */
+ replace : function(key, o){
+ var me = this,
+ old,
+ index;
+
+ if (arguments.length == 1) {
+ o = arguments[0];
+ key = me.getKey(o);
+ }
+ old = me.map[key];
+ if (typeof key == 'undefined' || key === null || typeof old == 'undefined') {
+ return me.add(key, o);
+ }
+ index = me.indexOfKey(key);
+ me.items[index] = o;
+ me.map[key] = o;
+ me.fireEvent('replace', key, old, o);
+ return o;
+ },
+
+ /**
+ * Adds all elements of an Array or an Object to the collection.
+ * @param {Object/Array} objs An Object containing properties which will be added
+ * to the collection, or an Array of values, each of which are added to the collection.
+ * Functions references will be added to the collection if <code>{@link #allowFunctions}</code>
+ * has been set to <tt>true</tt>.
+ */
+ addAll : function(objs){
+ var me = this,
+ i = 0,
+ args,
+ len,
+ key;
+
+ if (arguments.length > 1 || Ext.isArray(objs)) {
+ args = arguments.length > 1 ? arguments : objs;
+ for (len = args.length; i < len; i++) {
+ me.add(args[i]);
+ }
+ } else {
+ for (key in objs) {
+ if (objs.hasOwnProperty(key)) {
+ if (me.allowFunctions || typeof objs[key] != 'function') {
+ me.add(key, objs[key]);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Executes the specified function once for every item in the collection, passing the following arguments:
+ * <div class="mdetail-params"><ul>
+ * <li><b>item</b> : Mixed<p class="sub-desc">The collection item</p></li>
+ * <li><b>index</b> : Number<p class="sub-desc">The item's index</p></li>
+ * <li><b>length</b> : Number<p class="sub-desc">The total number of items in the collection</p></li>
+ * </ul></div>
+ * The function should return a boolean value. 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 (<code>this</code> reference) in which the function is executed. Defaults to the current item in the iteration.
+ */
+ each : function(fn, scope){
+ var items = [].concat(this.items), // each safe for removal
+ i = 0,
+ len = items.length,
+ item;
+
+ for (; i < len; i++) {
+ item = items[i];
+ if (fn.call(scope || item, item, i, len) === false) {
+ break;
+ }
+ }
+ },
+
+ /**
+ * Executes the specified function once for every key in the collection, passing each
+ * key, and its associated item as the first two parameters.
+ * @param {Function} fn The function to execute for each item.
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
+ */
+ eachKey : function(fn, scope){
+ var keys = this.keys,
+ items = this.items,
+ i = 0,
+ len = keys.length;
+
+ for (; i < len; i++) {
+ fn.call(scope || window, keys[i], items[i], i, len);
+ }
+ },
+
+ /**
+ * Returns the first item in the collection which elicits a true return value from the
+ * passed selection function.
+ * @param {Function} fn The selection function to execute for each item.
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
+ * @return {Object} The first item in the collection which returned true from the selection function, or null if none was found
+ */
+ findBy : function(fn, scope) {
+ var keys = this.keys,
+ items = this.items,
+ i = 0,
+ len = items.length;
+
+ for (; i < len; i++) {
+ if (fn.call(scope || window, items[i], keys[i])) {
+ return items[i];
+ }
+ }
+ return null;
+ },
+
+ find : function() {
+ if (Ext.isDefined(Ext.global.console)) {
+ Ext.global.console.warn('Ext.util.MixedCollection: find has been deprecated. Use findBy instead.');
+ }
+ return this.findBy.apply(this, arguments);
+ },
+
+ /**
+ * Inserts an item at the specified index in the collection. Fires the {@link #add} event when complete.
+ * @param {Number} index The index to insert the item at.
+ * @param {String} key The key to associate with the new item, or the item itself.
+ * @param {Object} o (optional) If the second parameter was a key, the new item.
+ * @return {Object} The item inserted.
+ */
+ insert : function(index, key, obj){
+ var me = this,
+ myKey = key,
+ myObj = obj;
+
+ if (arguments.length == 2) {
+ myObj = myKey;
+ myKey = me.getKey(myObj);
+ }
+ if (me.containsKey(myKey)) {
+ me.suspendEvents();
+ me.removeAtKey(myKey);
+ me.resumeEvents();
+ }
+ if (index >= me.length) {
+ return me.add(myKey, myObj);
+ }
+ me.length++;
+ Ext.Array.splice(me.items, index, 0, myObj);
+ if (typeof myKey != 'undefined' && myKey !== null) {
+ me.map[myKey] = myObj;
+ }
+ Ext.Array.splice(me.keys, index, 0, myKey);
+ me.fireEvent('add', index, myObj, myKey);
+ return myObj;
+ },
+
+ /**
+ * Remove an item from the collection.
+ * @param {Object} o The item to remove.
+ * @return {Object} The item removed or false if no item was removed.
+ */
+ remove : function(o){
+ return this.removeAt(this.indexOf(o));
+ },
+
+ /**
+ * Remove all items in the passed array from the collection.
+ * @param {Array} items An array of items to be removed.
+ * @return {Ext.util.MixedCollection} this object
+ */
+ removeAll : function(items){
+ Ext.each(items || [], function(item) {
+ this.remove(item);
+ }, this);
+
+ return this;
+ },
+
+ /**
+ * Remove an item from a specified index in the collection. Fires the {@link #remove} event when complete.
+ * @param {Number} index The index within the collection of the item to remove.
+ * @return {Object} The item removed or false if no item was removed.
+ */
+ removeAt : function(index){
+ var me = this,
+ o,
+ key;
+
+ if (index < me.length && index >= 0) {
+ me.length--;
+ o = me.items[index];
+ Ext.Array.erase(me.items, index, 1);
+ key = me.keys[index];
+ if (typeof key != 'undefined') {
+ delete me.map[key];
+ }
+ Ext.Array.erase(me.keys, index, 1);
+ me.fireEvent('remove', o, key);
+ return o;
+ }
+ return false;
+ },
+
+ /**
+ * Removed an item associated with the passed key fom the collection.
+ * @param {String} key The key of the item to remove.
+ * @return {Object} The item removed or false if no item was removed.
+ */
+ removeAtKey : function(key){
+ return this.removeAt(this.indexOfKey(key));
+ },
+
+ /**
+ * Returns the number of items in the collection.
+ * @return {Number} the number of items in the collection.
+ */
+ getCount : function(){
+ return this.length;
+ },
+
+ /**
+ * Returns index within the collection of the passed Object.
+ * @param {Object} o The item to find the index of.
+ * @return {Number} index of the item. Returns -1 if not found.
+ */
+ indexOf : function(o){
+ return Ext.Array.indexOf(this.items, o);
+ },
+
+ /**
+ * Returns index within the collection of the passed key.
+ * @param {String} key The key to find the index of.
+ * @return {Number} index of the key.
+ */
+ indexOfKey : function(key){
+ return Ext.Array.indexOf(this.keys, key);
+ },
+
+ /**
+ * Returns the item associated with the passed key OR index.
+ * Key has priority over index. This is the equivalent
+ * of calling {@link #getByKey} first, then if nothing matched calling {@link #getAt}.
+ * @param {String/Number} key The key or index of the item.
+ * @return {Object} If the item is found, returns the item. If the item was not found, returns <tt>undefined</tt>.
+ * If an item was found, but is a Class, returns <tt>null</tt>.
+ */
+ get : function(key) {
+ var me = this,
+ mk = me.map[key],
+ item = mk !== undefined ? mk : (typeof key == 'number') ? me.items[key] : undefined;
+ return typeof item != 'function' || me.allowFunctions ? item : null; // for prototype!
+ },
+
+ /**
+ * Returns the item at the specified index.
+ * @param {Number} index The index of the item.
+ * @return {Object} The item at the specified index.
+ */
+ getAt : function(index) {
+ return this.items[index];
+ },
+
+ /**
+ * Returns the item associated with the passed key.
+ * @param {String/Number} key The key of the item.
+ * @return {Object} The item associated with the passed key.
+ */
+ getByKey : function(key) {
+ return this.map[key];
+ },
+
+ /**
+ * Returns true if the collection contains the passed Object as an item.
+ * @param {Object} o The Object to look for in the collection.
+ * @return {Boolean} True if the collection contains the Object as an item.
+ */
+ contains : function(o){
+ return Ext.Array.contains(this.items, o);
+ },
+
+ /**
+ * Returns true if the collection contains the passed Object as a key.
+ * @param {String} key The key to look for in the collection.
+ * @return {Boolean} True if the collection contains the Object as a key.
+ */
+ containsKey : function(key){
+ return typeof this.map[key] != 'undefined';
+ },
+
+ /**
+ * Removes all items from the collection. Fires the {@link #clear} event when complete.
+ */
+ clear : function(){
+ var me = this;
+
+ me.length = 0;
+ me.items = [];
+ me.keys = [];
+ me.map = {};
+ me.fireEvent('clear');
+ },
+
+ /**
+ * Returns the first item in the collection.
+ * @return {Object} the first item in the collection..
+ */
+ first : function() {
+ return this.items[0];
+ },
+
+ /**
+ * Returns the last item in the collection.
+ * @return {Object} the last item in the collection..
+ */
+ last : function() {
+ return this.items[this.length - 1];
+ },
+
+ /**
+ * Collects all of the values of the given property and returns their sum
+ * @param {String} property The property to sum by
+ * @param {String} [root] 'root' property to extract the first argument from. This is used mainly when
+ * summing fields in records, where the fields are all stored inside the 'data' object
+ * @param {Number} [start=0] The record index to start at
+ * @param {Number} [end=-1] The record index to end at
+ * @return {Number} The total
+ */
+ sum: function(property, root, start, end) {
+ var values = this.extractValues(property, root),
+ length = values.length,
+ sum = 0,
+ i;
+
+ start = start || 0;
+ end = (end || end === 0) ? end : length - 1;
+
+ for (i = start; i <= end; i++) {
+ sum += values[i];
+ }
+
+ return sum;
+ },
+
+ /**
+ * Collects unique values of a particular property in this MixedCollection
+ * @param {String} property The property to collect on
+ * @param {String} root (optional) 'root' property to extract the first argument from. This is used mainly when
+ * summing fields in records, where the fields are all stored inside the 'data' object
+ * @param {Boolean} allowBlank (optional) Pass true to allow null, undefined or empty string values
+ * @return {Array} The unique values
+ */
+ collect: function(property, root, allowNull) {
+ var values = this.extractValues(property, root),
+ length = values.length,
+ hits = {},
+ unique = [],
+ value, strValue, i;
+
+ for (i = 0; i < length; i++) {
+ value = values[i];
+ strValue = String(value);
+
+ if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) {
+ hits[strValue] = true;
+ unique.push(value);
+ }
+ }
+
+ return unique;
+ },
+
+ /**
+ * @private
+ * Extracts all of the given property values from the items in the MC. Mainly used as a supporting method for
+ * functions like sum and collect.
+ * @param {String} property The property to extract
+ * @param {String} root (optional) 'root' property to extract the first argument from. This is used mainly when
+ * extracting field data from Model instances, where the fields are stored inside the 'data' object
+ * @return {Array} The extracted values
+ */
+ extractValues: function(property, root) {
+ var values = this.items;
+
+ if (root) {
+ values = Ext.Array.pluck(values, root);
+ }
+
+ return Ext.Array.pluck(values, property);
+ },
+
+ /**
+ * Returns a range of items in this collection
+ * @param {Number} startIndex (optional) The starting index. Defaults to 0.
+ * @param {Number} endIndex (optional) The ending index. Defaults to the last item.
+ * @return {Array} An array of items
+ */
+ getRange : function(start, end){
+ var me = this,
+ items = me.items,
+ range = [],
+ i;
+
+ if (items.length < 1) {
+ return range;
+ }
+
+ start = start || 0;
+ end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1);
+ if (start <= end) {
+ for (i = start; i <= end; i++) {
+ range[range.length] = items[i];
+ }
+ } else {
+ for (i = start; i >= end; i--) {
+ range[range.length] = items[i];
+ }
+ }
+ return range;
+ },
+
+ /**
+ * <p>Filters the objects in this collection by a set of {@link Ext.util.Filter Filter}s, or by a single
+ * property/value pair with optional parameters for substring matching and case sensitivity. See
+ * {@link Ext.util.Filter Filter} for an example of using Filter objects (preferred). Alternatively,
+ * MixedCollection can be easily filtered by property like this:</p>
+<pre><code>
+//create a simple store with a few people defined
+var people = new Ext.util.MixedCollection();
+people.addAll([
+ {id: 1, age: 25, name: 'Ed'},
+ {id: 2, age: 24, name: 'Tommy'},
+ {id: 3, age: 24, name: 'Arne'},
+ {id: 4, age: 26, name: 'Aaron'}
+]);
+
+//a new MixedCollection containing only the items where age == 24
+var middleAged = people.filter('age', 24);
+</code></pre>
+ *
+ *
+ * @param {Ext.util.Filter[]/String} property A property on your objects, or an array of {@link Ext.util.Filter Filter} objects
+ * @param {String/RegExp} value Either string that the property values
+ * should start with or a RegExp to test against the property
+ * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the beginning
+ * @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
+ * @return {Ext.util.MixedCollection} The new filtered collection
+ */
+ filter : function(property, value, anyMatch, caseSensitive) {
+ var filters = [],
+ filterFn;
+
+ //support for the simple case of filtering by property/value
+ if (Ext.isString(property)) {
+ filters.push(Ext.create('Ext.util.Filter', {
+ property : property,
+ value : value,
+ anyMatch : anyMatch,
+ caseSensitive: caseSensitive
+ }));
+ } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) {
+ filters = filters.concat(property);
+ }
+
+ //at this point we have an array of zero or more Ext.util.Filter objects to filter with,
+ //so here we construct a function that combines these filters by ANDing them together
+ filterFn = function(record) {
+ var isMatch = true,
+ length = filters.length,
+ i;
+
+ for (i = 0; i < length; i++) {
+ var filter = filters[i],
+ fn = filter.filterFn,
+ scope = filter.scope;
+
+ isMatch = isMatch && fn.call(scope, record);
+ }
+
+ return isMatch;
+ };
+
+ return this.filterBy(filterFn);
+ },
+
+ /**
+ * Filter by a function. Returns a <i>new</i> collection that has been filtered.
+ * The passed function will be called with each object in the collection.
+ * If the function returns true, the value is included otherwise it is filtered.
+ * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key)
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this MixedCollection.
+ * @return {Ext.util.MixedCollection} The new filtered collection
+ */
+ filterBy : function(fn, scope) {
+ var me = this,
+ newMC = new this.self(),
+ keys = me.keys,
+ items = me.items,
+ length = items.length,
+ i;
+
+ newMC.getKey = me.getKey;
+
+ for (i = 0; i < length; i++) {
+ if (fn.call(scope || me, items[i], keys[i])) {
+ newMC.add(keys[i], items[i]);
+ }
+ }
+
+ return newMC;
+ },
+
+ /**
+ * Finds the index of the first matching object in this collection by a specific property/value.
+ * @param {String} property The name of a property on your objects.
+ * @param {String/RegExp} value A string that the property values
+ * should start with or a RegExp to test against the property.
+ * @param {Number} [start=0] The index to start searching at.
+ * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the beginning.
+ * @param {Boolean} [caseSensitive=false] True for case sensitive comparison.
+ * @return {Number} The matched index or -1
+ */
+ findIndex : function(property, value, start, anyMatch, caseSensitive){
+ if(Ext.isEmpty(value, false)){
+ return -1;
+ }
+ value = this.createValueMatcher(value, anyMatch, caseSensitive);
+ return this.findIndexBy(function(o){
+ return o && value.test(o[property]);
+ }, null, start);
+ },
+
+ /**
+ * Find the index of the first matching object in this collection by a function.
+ * If the function returns <i>true</i> it is considered a match.
+ * @param {Function} fn The function to be called, it will receive the args o (the object), k (the key).
+ * @param {Object} [scope] The scope (<code>this</code> reference) in which the function is executed. Defaults to this MixedCollection.
+ * @param {Number} [start=0] The index to start searching at.
+ * @return {Number} The matched index or -1
+ */
+ findIndexBy : function(fn, scope, start){
+ var me = this,
+ keys = me.keys,
+ items = me.items,
+ i = start || 0,
+ len = items.length;
+
+ for (; i < len; i++) {
+ if (fn.call(scope || me, items[i], keys[i])) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Returns a regular expression based on the given value and matching options. This is used internally for finding and filtering,
+ * and by Ext.data.Store#filter
+ * @private
+ * @param {String} value The value to create the regex for. This is escaped using Ext.escapeRe
+ * @param {Boolean} anyMatch True to allow any match - no regex start/end line anchors will be added. Defaults to false
+ * @param {Boolean} caseSensitive True to make the regex case sensitive (adds 'i' switch to regex). Defaults to false.
+ * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. Ignored if anyMatch is true.
+ */
+ createValueMatcher : function(value, anyMatch, caseSensitive, exactMatch) {
+ if (!value.exec) { // not a regex
+ var er = Ext.String.escapeRegex;
+ value = String(value);
+
+ if (anyMatch === true) {
+ value = er(value);
+ } else {
+ value = '^' + er(value);
+ if (exactMatch === true) {
+ value += '$';
+ }
+ }
+ value = new RegExp(value, caseSensitive ? '' : 'i');
+ }
+ return value;
+ },
+
+ /**
+ * Creates a shallow copy of this collection
+ * @return {Ext.util.MixedCollection}
+ */
+ clone : function() {
+ var me = this,
+ copy = new this.self(),
+ keys = me.keys,
+ items = me.items,
+ i = 0,
+ len = items.length;
+
+ for(; i < len; i++){
+ copy.add(keys[i], items[i]);
+ }
+ copy.getKey = me.getKey;
+ return copy;
+ }
+});
+
+/**
+ * @docauthor Tommy Maintz <tommy@sencha.com>
+ *
+ * A mixin which allows a data component to be sorted. This is used by e.g. {@link Ext.data.Store} and {@link Ext.data.TreeStore}.
+ *
+ * **NOTE**: This mixin is mainly for internal use and most users should not need to use it directly. It
+ * is more likely you will want to use one of the component classes that import this mixin, such as
+ * {@link Ext.data.Store} or {@link Ext.data.TreeStore}.
+ */
+Ext.define("Ext.util.Sortable", {
+ /**
+ * @property {Boolean} isSortable
+ * Flag denoting that this object is sortable. Always true.
+ */
+ isSortable: true,
+
+ /**
+ * @property {String} defaultSortDirection
+ * The default sort direction to use if one is not specified.
+ */
+ defaultSortDirection: "ASC",
+
+ requires: [
+ 'Ext.util.Sorter'
+ ],
+
+ /**
+ * @property {String} sortRoot
+ * The property in each item that contains the data to sort.
+ */
+
+ /**
+ * Performs initialization of this mixin. Component classes using this mixin should call this method during their
+ * own initialization.
+ */
+ initSortable: function() {
+ var me = this,
+ sorters = me.sorters;
+
+ /**
+ * @property {Ext.util.MixedCollection} sorters
+ * The collection of {@link Ext.util.Sorter Sorters} currently applied to this Store
+ */
+ me.sorters = Ext.create('Ext.util.AbstractMixedCollection', false, function(item) {
+ return item.id || item.property;
+ });
+
+ if (sorters) {
+ me.sorters.addAll(me.decodeSorters(sorters));
+ }
+ },
+
+ /**
+ * Sorts the data in the Store by one or more of its properties. Example usage:
+ *
+ * //sort by a single field
+ * myStore.sort('myField', 'DESC');
+ *
+ * //sorting by multiple fields
+ * myStore.sort([
+ * {
+ * property : 'age',
+ * direction: 'ASC'
+ * },
+ * {
+ * property : 'name',
+ * direction: 'DESC'
+ * }
+ * ]);
+ *
+ * Internally, Store converts the passed arguments into an array of {@link Ext.util.Sorter} instances, and delegates
+ * the actual sorting to its internal {@link Ext.util.MixedCollection}.
+ *
+ * When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field, so this code:
+ *
+ * store.sort('myField');
+ * store.sort('myField');
+ *
+ * Is equivalent to this code, because Store handles the toggling automatically:
+ *
+ * store.sort('myField', 'ASC');
+ * store.sort('myField', 'DESC');
+ *
+ * @param {String/Ext.util.Sorter[]} sorters Either a string name of one of the fields in this Store's configured
+ * {@link Ext.data.Model Model}, or an array of sorter configurations.
+ * @param {String} direction The overall direction to sort the data by. Defaults to "ASC".
+ * @return {Ext.util.Sorter[]}
+ */
+ sort: function(sorters, direction, where, doSort) {
+ var me = this,
+ sorter, sorterFn,
+ newSorters;
+
+ if (Ext.isArray(sorters)) {
+ doSort = where;
+ where = direction;
+ newSorters = sorters;
+ }
+ else if (Ext.isObject(sorters)) {
+ doSort = where;
+ where = direction;
+ newSorters = [sorters];
+ }
+ else if (Ext.isString(sorters)) {
+ sorter = me.sorters.get(sorters);
+
+ if (!sorter) {
+ sorter = {
+ property : sorters,
+ direction: direction
+ };
+ newSorters = [sorter];
+ }
+ else if (direction === undefined) {
+ sorter.toggle();
+ }
+ else {
+ sorter.setDirection(direction);
+ }
+ }
+
+ if (newSorters && newSorters.length) {
+ newSorters = me.decodeSorters(newSorters);
+ if (Ext.isString(where)) {
+ if (where === 'prepend') {
+ sorters = me.sorters.clone().items;
+
+ me.sorters.clear();
+ me.sorters.addAll(newSorters);
+ me.sorters.addAll(sorters);
+ }
+ else {
+ me.sorters.addAll(newSorters);
+ }
+ }
+ else {
+ me.sorters.clear();
+ me.sorters.addAll(newSorters);
+ }
+ }
+
+ if (doSort !== false) {
+ me.onBeforeSort(newSorters);
+
+ sorters = me.sorters.items;
+ if (sorters.length) {
+ //construct an amalgamated sorter function which combines all of the Sorters passed
+ sorterFn = function(r1, r2) {
+ var result = sorters[0].sort(r1, r2),
+ length = sorters.length,
+ i;
+
+ //if we have more than one sorter, OR any additional sorter functions together
+ for (i = 1; i < length; i++) {
+ result = result || sorters[i].sort.call(this, r1, r2);
+ }
+
+ return result;
+ };
+
+ me.doSort(sorterFn);
+ }
+ }
+
+ return sorters;
+ },
+
+ onBeforeSort: Ext.emptyFn,
+
+ /**
+ * @private
+ * Normalizes an array of sorter objects, ensuring that they are all Ext.util.Sorter instances
+ * @param {Object[]} sorters The sorters array
+ * @return {Ext.util.Sorter[]} Array of Ext.util.Sorter objects
+ */
+ decodeSorters: function(sorters) {
+ if (!Ext.isArray(sorters)) {
+ if (sorters === undefined) {
+ sorters = [];
+ } else {
+ sorters = [sorters];
+ }
+ }
+
+ var length = sorters.length,
+ Sorter = Ext.util.Sorter,
+ fields = this.model ? this.model.prototype.fields : null,
+ field,
+ config, i;
+
+ for (i = 0; i < length; i++) {
+ config = sorters[i];
+
+ if (!(config instanceof Sorter)) {
+ if (Ext.isString(config)) {
+ config = {
+ property: config
+ };
+ }
+
+ Ext.applyIf(config, {
+ root : this.sortRoot,
+ direction: "ASC"
+ });
+
+ //support for 3.x style sorters where a function can be defined as 'fn'
+ if (config.fn) {
+ config.sorterFn = config.fn;
+ }
+
+ //support a function to be passed as a sorter definition
+ if (typeof config == 'function') {
+ config = {
+ sorterFn: config
+ };
+ }
+
+ // ensure sortType gets pushed on if necessary
+ if (fields && !config.transform) {
+ field = fields.get(config.property);
+ config.transform = field ? field.sortType : undefined;
+ }
+ sorters[i] = Ext.create('Ext.util.Sorter', config);
+ }
+ }
+
+ return sorters;
+ },
+
+ getSorters: function() {
+ return this.sorters.items;
+ }
+});
+/**
+ * @class Ext.util.MixedCollection
+ * <p>
+ * Represents a collection of a set of key and value pairs. Each key in the MixedCollection
+ * must be unique, the same key cannot exist twice. This collection is ordered, items in the
+ * collection can be accessed by index or via the key. Newly added items are added to
+ * the end of the collection. This class is similar to {@link Ext.util.HashMap} however it
+ * is heavier and provides more functionality. Sample usage:
+ * <pre><code>
+var coll = new Ext.util.MixedCollection();
+coll.add('key1', 'val1');
+coll.add('key2', 'val2');
+coll.add('key3', 'val3');
+
+console.log(coll.get('key1')); // prints 'val1'
+console.log(coll.indexOfKey('key3')); // prints 2
+ * </code></pre>
+ *
+ * <p>
+ * The MixedCollection also has support for sorting and filtering of the values in the collection.
+ * <pre><code>
+var coll = new Ext.util.MixedCollection();
+coll.add('key1', 100);
+coll.add('key2', -100);
+coll.add('key3', 17);
+coll.add('key4', 0);
+var biggerThanZero = coll.filterBy(function(value){
+ return value > 0;
+});
+console.log(biggerThanZero.getCount()); // prints 2
+ * </code></pre>
+ * </p>
+ */
+Ext.define('Ext.util.MixedCollection', {
+ extend: 'Ext.util.AbstractMixedCollection',
+ mixins: {
+ sortable: 'Ext.util.Sortable'
+ },
+
+ /**
+ * Creates new MixedCollection.
+ * @param {Boolean} allowFunctions Specify <tt>true</tt> if the {@link #addAll}
+ * function should add function references to the collection. Defaults to
+ * <tt>false</tt>.
+ * @param {Function} keyFn A function that can accept an item of the type(s) stored in this MixedCollection
+ * and return the key value for that item. This is used when available to look up the key on items that
+ * were passed without an explicit key parameter to a MixedCollection method. Passing this parameter is
+ * equivalent to providing an implementation for the {@link #getKey} method.
+ */
+ constructor: function() {
+ var me = this;
+ me.callParent(arguments);
+ me.addEvents('sort');
+ me.mixins.sortable.initSortable.call(me);
+ },
+
+ doSort: function(sorterFn) {
+ this.sortBy(sorterFn);
+ },
+
+ /**
+ * @private
+ * Performs the actual sorting based on a direction and a sorting function. Internally,
+ * this creates a temporary array of all items in the MixedCollection, sorts it and then writes
+ * the sorted array data back into this.items and this.keys
+ * @param {String} property Property to sort by ('key', 'value', or 'index')
+ * @param {String} dir (optional) Direction to sort 'ASC' or 'DESC'. Defaults to 'ASC'.
+ * @param {Function} fn (optional) Comparison function that defines the sort order.
+ * Defaults to sorting by numeric value.
+ */
+ _sort : function(property, dir, fn){
+ var me = this,
+ i, len,
+ dsc = String(dir).toUpperCase() == 'DESC' ? -1 : 1,
+
+ //this is a temporary array used to apply the sorting function
+ c = [],
+ keys = me.keys,
+ items = me.items;
+
+ //default to a simple sorter function if one is not provided
+ fn = fn || function(a, b) {
+ return a - b;
+ };
+
+ //copy all the items into a temporary array, which we will sort
+ for(i = 0, len = items.length; i < len; i++){
+ c[c.length] = {
+ key : keys[i],
+ value: items[i],
+ index: i
+ };
+ }
+
+ //sort the temporary array
+ Ext.Array.sort(c, function(a, b){
+ var v = fn(a[property], b[property]) * dsc;
+ if(v === 0){
+ v = (a.index < b.index ? -1 : 1);
+ }
+ return v;
+ });
+
+ //copy the temporary array back into the main this.items and this.keys objects
+ for(i = 0, len = c.length; i < len; i++){
+ items[i] = c[i].value;
+ keys[i] = c[i].key;
+ }
+
+ me.fireEvent('sort', me);
+ },
+
+ /**
+ * Sorts the collection by a single sorter function
+ * @param {Function} sorterFn The function to sort by
+ */
+ sortBy: function(sorterFn) {
+ var me = this,
+ items = me.items,
+ keys = me.keys,
+ length = items.length,
+ temp = [],
+ i;
+
+ //first we create a copy of the items array so that we can sort it
+ for (i = 0; i < length; i++) {
+ temp[i] = {
+ key : keys[i],
+ value: items[i],
+ index: i
+ };
+ }
+
+ Ext.Array.sort(temp, function(a, b) {
+ var v = sorterFn(a.value, b.value);
+ if (v === 0) {
+ v = (a.index < b.index ? -1 : 1);
+ }
+
+ return v;
+ });
+
+ //copy the temporary array back into the main this.items and this.keys objects
+ for (i = 0; i < length; i++) {
+ items[i] = temp[i].value;
+ keys[i] = temp[i].key;
+ }
+
+ me.fireEvent('sort', me, items, keys);
+ },
+
+ /**
+ * Reorders each of the items based on a mapping from old index to new index. Internally this
+ * just translates into a sort. The 'sort' event is fired whenever reordering has occured.
+ * @param {Object} mapping Mapping from old item index to new item index
+ */
+ reorder: function(mapping) {
+ var me = this,
+ items = me.items,
+ index = 0,
+ length = items.length,
+ order = [],
+ remaining = [],
+ oldIndex;
+
+ me.suspendEvents();
+
+ //object of {oldPosition: newPosition} reversed to {newPosition: oldPosition}
+ for (oldIndex in mapping) {
+ order[mapping[oldIndex]] = items[oldIndex];
+ }
+
+ for (index = 0; index < length; index++) {
+ if (mapping[index] == undefined) {
+ remaining.push(items[index]);
+ }
+ }
+
+ for (index = 0; index < length; index++) {
+ if (order[index] == undefined) {
+ order[index] = remaining.shift();
+ }
+ }
+
+ me.clear();
+ me.addAll(order);
+
+ me.resumeEvents();
+ me.fireEvent('sort', me);
+ },
+
+ /**
+ * Sorts this collection by <b>key</b>s.
+ * @param {String} direction (optional) 'ASC' or 'DESC'. Defaults to 'ASC'.
+ * @param {Function} fn (optional) Comparison function that defines the sort order.
+ * Defaults to sorting by case insensitive string.
+ */
+ sortByKey : function(dir, fn){
+ this._sort('key', dir, fn || function(a, b){
+ var v1 = String(a).toUpperCase(), v2 = String(b).toUpperCase();
+ return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0);
+ });
+ }
+});
+
+/**
+ * @author Ed Spencer
+ * @class Ext.data.Errors
+ * @extends Ext.util.MixedCollection
+ *
+ * <p>Wraps a collection of validation error responses and provides convenient functions for
+ * accessing and errors for specific fields.</p>
+ *
+ * <p>Usually this class does not need to be instantiated directly - instances are instead created
+ * automatically when {@link Ext.data.Model#validate validate} on a model instance:</p>
+ *
+<pre><code>
+//validate some existing model instance - in this case it returned 2 failures messages
+var errors = myModel.validate();
+
+errors.isValid(); //false
+
+errors.length; //2
+errors.getByField('name'); // [{field: 'name', message: 'must be present'}]
+errors.getByField('title'); // [{field: 'title', message: 'is too short'}]
+</code></pre>
+ */
+Ext.define('Ext.data.Errors', {
+ extend: 'Ext.util.MixedCollection',
+
+ /**
+ * Returns true if there are no errors in the collection
+ * @return {Boolean}
+ */
+ isValid: function() {
+ return this.length === 0;
+ },
+
+ /**
+ * Returns all of the errors for the given field
+ * @param {String} fieldName The field to get errors for
+ * @return {Object[]} All errors for the given field
+ */
+ getByField: function(fieldName) {
+ var errors = [],
+ error, field, i;
+
+ for (i = 0; i < this.length; i++) {
+ error = this.items[i];
+
+ if (error.field == fieldName) {
+ errors.push(error);
+ }
+ }
+
+ return errors;
+ }
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * Readers are used to interpret data to be loaded into a {@link Ext.data.Model Model} instance or a {@link
+ * Ext.data.Store Store} - often in response to an AJAX request. In general there is usually no need to create
+ * a Reader instance directly, since a Reader is almost always used together with a {@link Ext.data.proxy.Proxy Proxy},
+ * and is configured using the Proxy's {@link Ext.data.proxy.Proxy#cfg-reader reader} configuration property:
+ *
+ * Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'ajax',
+ * url : 'users.json',
+ * reader: {
+ * type: 'json',
+ * root: 'users'
+ * }
+ * },
+ * });
+ *
+ * The above reader is configured to consume a JSON string that looks something like this:
+ *
+ * {
+ * "success": true,
+ * "users": [
+ * { "name": "User 1" },
+ * { "name": "User 2" }
+ * ]
+ * }
+ *
+ *
+ * # Loading Nested Data
+ *
+ * Readers have the ability to automatically load deeply-nested data objects based on the {@link Ext.data.Association
+ * associations} configured on each Model. Below is an example demonstrating the flexibility of these associations in a
+ * fictional CRM system which manages a User, their Orders, OrderItems and Products. First we'll define the models:
+ *
+ * Ext.define("User", {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'id', 'name'
+ * ],
+ *
+ * hasMany: {model: 'Order', name: 'orders'},
+ *
+ * proxy: {
+ * type: 'rest',
+ * url : 'users.json',
+ * reader: {
+ * type: 'json',
+ * root: 'users'
+ * }
+ * }
+ * });
+ *
+ * Ext.define("Order", {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'id', 'total'
+ * ],
+ *
+ * hasMany : {model: 'OrderItem', name: 'orderItems', associationKey: 'order_items'},
+ * belongsTo: 'User'
+ * });
+ *
+ * Ext.define("OrderItem", {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'id', 'price', 'quantity', 'order_id', 'product_id'
+ * ],
+ *
+ * belongsTo: ['Order', {model: 'Product', associationKey: 'product'}]
+ * });
+ *
+ * Ext.define("Product", {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * 'id', 'name'
+ * ],
+ *
+ * hasMany: 'OrderItem'
+ * });
+ *
+ * This may be a lot to take in - basically a User has many Orders, each of which is composed of several OrderItems.
+ * Finally, each OrderItem has a single Product. This allows us to consume data like this:
+ *
+ * {
+ * "users": [
+ * {
+ * "id": 123,
+ * "name": "Ed",
+ * "orders": [
+ * {
+ * "id": 50,
+ * "total": 100,
+ * "order_items": [
+ * {
+ * "id" : 20,
+ * "price" : 40,
+ * "quantity": 2,
+ * "product" : {
+ * "id": 1000,
+ * "name": "MacBook Pro"
+ * }
+ * },
+ * {
+ * "id" : 21,
+ * "price" : 20,
+ * "quantity": 3,
+ * "product" : {
+ * "id": 1001,
+ * "name": "iPhone"
+ * }
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ * ]
+ * }
+ *
+ * The JSON response is deeply nested - it returns all Users (in this case just 1 for simplicity's sake), all of the
+ * Orders for each User (again just 1 in this case), all of the OrderItems for each Order (2 order items in this case),
+ * and finally the Product associated with each OrderItem. Now we can read the data and use it as follows:
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * model: "User"
+ * });
+ *
+ * store.load({
+ * callback: function() {
+ * //the user that was loaded
+ * var user = store.first();
+ *
+ * console.log("Orders for " + user.get('name') + ":")
+ *
+ * //iterate over the Orders for each User
+ * user.orders().each(function(order) {
+ * console.log("Order ID: " + order.getId() + ", which contains items:");
+ *
+ * //iterate over the OrderItems for each Order
+ * order.orderItems().each(function(orderItem) {
+ * //we know that the Product data is already loaded, so we can use the synchronous getProduct
+ * //usually, we would use the asynchronous version (see {@link Ext.data.BelongsToAssociation})
+ * var product = orderItem.getProduct();
+ *
+ * console.log(orderItem.get('quantity') + ' orders of ' + product.get('name'));
+ * });
+ * });
+ * }
+ * });
+ *
+ * Running the code above results in the following:
+ *
+ * Orders for Ed:
+ * Order ID: 50, which contains items:
+ * 2 orders of MacBook Pro
+ * 3 orders of iPhone
+ */
+Ext.define('Ext.data.reader.Reader', {
+ requires: ['Ext.data.ResultSet'],
+ alternateClassName: ['Ext.data.Reader', 'Ext.data.DataReader'],
+
+ /**
+ * @cfg {String} idProperty
+ * Name of the property within a row object that contains a record identifier value. Defaults to The id of the
+ * model. If an idProperty is explicitly specified it will override that of the one specified on the model
+ */
+
+ /**
+ * @cfg {String} totalProperty
+ * Name of the property from which to retrieve the total number of records in the dataset. This is only needed if
+ * the whole dataset is not passed in one go, but is being paged from the remote server. Defaults to total.
+ */
+ totalProperty: 'total',
+
+ /**
+ * @cfg {String} successProperty
+ * Name of the property from which to retrieve the success attribute. Defaults to success. See
+ * {@link Ext.data.proxy.Server}.{@link Ext.data.proxy.Server#exception exception} for additional information.
+ */
+ successProperty: 'success',
+
+ /**
+ * @cfg {String} root
+ * The name of the property which contains the Array of row objects. For JSON reader it's dot-separated list
+ * of property names. For XML reader it's a CSS selector. For array reader it's not applicable.
+ *
+ * By default the natural root of the data will be used. The root Json array, the root XML element, or the array.
+ *
+ * The data packet value for this property should be an empty array to clear the data or show no data.
+ */
+ root: '',
+
+ /**
+ * @cfg {String} messageProperty
+ * The name of the property which contains a response message. This property is optional.
+ */
+
+ /**
+ * @cfg {Boolean} implicitIncludes
+ * True to automatically parse models nested within other models in a response object. See the
+ * Ext.data.reader.Reader intro docs for full explanation. Defaults to true.
+ */
+ implicitIncludes: true,
+
+ isReader: true,
+
+ /**
+ * Creates new Reader.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ var me = this;
+
+ Ext.apply(me, config || {});
+ me.fieldCount = 0;
+ me.model = Ext.ModelManager.getModel(config.model);
+ if (me.model) {
+ me.buildExtractors();
+ }
+ },
+
+ /**
+ * Sets a new model for the reader.
+ * @private
+ * @param {Object} model The model to set.
+ * @param {Boolean} setOnProxy True to also set on the Proxy, if one is configured
+ */
+ setModel: function(model, setOnProxy) {
+ var me = this;
+
+ me.model = Ext.ModelManager.getModel(model);
+ me.buildExtractors(true);
+
+ if (setOnProxy && me.proxy) {
+ me.proxy.setModel(me.model, true);
+ }
+ },
+
+ /**
+ * Reads the given response object. This method normalizes the different types of response object that may be passed
+ * to it, before handing off the reading of records to the {@link #readRecords} function.
+ * @param {Object} response The response object. This may be either an XMLHttpRequest object or a plain JS object
+ * @return {Ext.data.ResultSet} The parsed ResultSet object
+ */
+ read: function(response) {
+ var data = response;
+
+ if (response && response.responseText) {
+ data = this.getResponseData(response);
+ }
+
+ if (data) {
+ return this.readRecords(data);
+ } else {
+ return this.nullResultSet;
+ }
+ },
+
+ /**
+ * Abstracts common functionality used by all Reader subclasses. Each subclass is expected to call this function
+ * before running its own logic and returning the Ext.data.ResultSet instance. For most Readers additional
+ * processing should not be needed.
+ * @param {Object} data The raw data object
+ * @return {Ext.data.ResultSet} A ResultSet object
+ */
+ readRecords: function(data) {
+ var me = this;
+
+ /*
+ * We check here whether the number of fields has changed since the last read.
+ * This works around an issue when a Model is used for both a Tree and another
+ * source, because the tree decorates the model with extra fields and it causes
+ * issues because the readers aren't notified.
+ */
+ if (me.fieldCount !== me.getFields().length) {
+ me.buildExtractors(true);
+ }
+
+ /**
+ * @property {Object} rawData
+ * The raw data object that was last passed to readRecords. Stored for further processing if needed
+ */
+ me.rawData = data;
+
+ data = me.getData(data);
+
+ // If we pass an array as the data, we dont use getRoot on the data.
+ // Instead the root equals to the data.
+ var root = Ext.isArray(data) ? data : me.getRoot(data),
+ success = true,
+ recordCount = 0,
+ total, value, records, message;
+
+ if (root) {
+ total = root.length;
+ }
+
+ if (me.totalProperty) {
+ value = parseInt(me.getTotal(data), 10);
+ if (!isNaN(value)) {
+ total = value;
+ }
+ }
+
+ if (me.successProperty) {
+ value = me.getSuccess(data);
+ if (value === false || value === 'false') {
+ success = false;
+ }
+ }
+
+ if (me.messageProperty) {
+ message = me.getMessage(data);
+ }
+
+ if (root) {
+ records = me.extractData(root);
+ recordCount = records.length;
+ } else {
+ recordCount = 0;
+ records = [];
+ }
+
+ return Ext.create('Ext.data.ResultSet', {
+ total : total || recordCount,
+ count : recordCount,
+ records: records,
+ success: success,
+ message: message
+ });
+ },
+
+ /**
+ * Returns extracted, type-cast rows of data. Iterates to call #extractValues for each row
+ * @param {Object[]/Object} root from server response
+ * @private
+ */
+ extractData : function(root) {
+ var me = this,
+ values = [],
+ records = [],
+ Model = me.model,
+ i = 0,
+ length = root.length,
+ idProp = me.getIdProperty(),
+ node, id, record;
+
+ if (!root.length && Ext.isObject(root)) {
+ root = [root];
+ length = 1;
+ }
+
+ for (; i < length; i++) {
+ node = root[i];
+ values = me.extractValues(node);
+ id = me.getId(node);
+
+
+ record = new Model(values, id, node);
+ records.push(record);
+
+ if (me.implicitIncludes) {
+ me.readAssociated(record, node);
+ }
+ }
+
+ return records;
+ },
+
+ /**
+ * @private
+ * Loads a record's associations from the data object. This prepopulates hasMany and belongsTo associations
+ * on the record provided.
+ * @param {Ext.data.Model} record The record to load associations for
+ * @param {Object} data The data object
+ * @return {String} Return value description
+ */
+ readAssociated: function(record, data) {
+ var associations = record.associations.items,
+ i = 0,
+ length = associations.length,
+ association, associationData, proxy, reader;
+
+ for (; i < length; i++) {
+ association = associations[i];
+ associationData = this.getAssociatedDataRoot(data, association.associationKey || association.name);
+
+ if (associationData) {
+ reader = association.getReader();
+ if (!reader) {
+ proxy = association.associatedModel.proxy;
+ // if the associated model has a Reader already, use that, otherwise attempt to create a sensible one
+ if (proxy) {
+ reader = proxy.getReader();
+ } else {
+ reader = new this.constructor({
+ model: association.associatedName
+ });
+ }
+ }
+ association.read(record, reader, associationData);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Used internally by {@link #readAssociated}. Given a data object (which could be json, xml etc) for a specific
+ * record, this should return the relevant part of that data for the given association name. This is only really
+ * needed to support the XML Reader, which has to do a query to get the associated data object
+ * @param {Object} data The raw data object
+ * @param {String} associationName The name of the association to get data for (uses associationKey if present)
+ * @return {Object} The root
+ */
+ getAssociatedDataRoot: function(data, associationName) {
+ return data[associationName];
+ },
+
+ getFields: function() {
+ return this.model.prototype.fields.items;
+ },
+
+ /**
+ * @private
+ * Given an object representing a single model instance's data, iterates over the model's fields and
+ * builds an object with the value for each field.
+ * @param {Object} data The data object to convert
+ * @return {Object} Data object suitable for use with a model constructor
+ */
+ extractValues: function(data) {
+ var fields = this.getFields(),
+ i = 0,
+ length = fields.length,
+ output = {},
+ field, value;
+
+ for (; i < length; i++) {
+ field = fields[i];
+ value = this.extractorFunctions[i](data);
+
+ output[field.name] = value;
+ }
+
+ return output;
+ },
+
+ /**
+ * @private
+ * By default this function just returns what is passed to it. It can be overridden in a subclass
+ * to return something else. See XmlReader for an example.
+ * @param {Object} data The data object
+ * @return {Object} The normalized data object
+ */
+ getData: function(data) {
+ return data;
+ },
+
+ /**
+ * @private
+ * This will usually need to be implemented in a subclass. Given a generic data object (the type depends on the type
+ * of data we are reading), this function should return the object as configured by the Reader's 'root' meta data config.
+ * See XmlReader's getRoot implementation for an example. By default the same data object will simply be returned.
+ * @param {Object} data The data object
+ * @return {Object} The same data object
+ */
+ getRoot: function(data) {
+ return data;
+ },
+
+ /**
+ * Takes a raw response object (as passed to this.read) and returns the useful data segment of it. This must be
+ * implemented by each subclass
+ * @param {Object} response The responce object
+ * @return {Object} The useful data from the response
+ */
+ getResponseData: function(response) {
+ },
+
+ /**
+ * @private
+ * Reconfigures the meta data tied to this Reader
+ */
+ onMetaChange : function(meta) {
+ var fields = meta.fields,
+ newModel;
+
+ Ext.apply(this, meta);
+
+ if (fields) {
+ newModel = Ext.define("Ext.data.reader.Json-Model" + Ext.id(), {
+ extend: 'Ext.data.Model',
+ fields: fields
+ });
+ this.setModel(newModel, true);
+ } else {
+ this.buildExtractors(true);
+ }
+ },
+
+ /**
+ * Get the idProperty to use for extracting data
+ * @private
+ * @return {String} The id property
+ */
+ getIdProperty: function(){
+ var prop = this.idProperty;
+ if (Ext.isEmpty(prop)) {
+ prop = this.model.prototype.idProperty;
+ }
+ return prop;
+ },
+
+ /**
+ * @private
+ * This builds optimized functions for retrieving record data and meta data from an object.
+ * Subclasses may need to implement their own getRoot function.
+ * @param {Boolean} [force=false] True to automatically remove existing extractor functions first
+ */
+ buildExtractors: function(force) {
+ var me = this,
+ idProp = me.getIdProperty(),
+ totalProp = me.totalProperty,
+ successProp = me.successProperty,
+ messageProp = me.messageProperty,
+ accessor;
+
+ if (force === true) {
+ delete me.extractorFunctions;
+ }
+
+ if (me.extractorFunctions) {
+ return;
+ }
+
+ //build the extractors for all the meta data
+ if (totalProp) {
+ me.getTotal = me.createAccessor(totalProp);
+ }
+
+ if (successProp) {
+ me.getSuccess = me.createAccessor(successProp);
+ }
+
+ if (messageProp) {
+ me.getMessage = me.createAccessor(messageProp);
+ }
+
+ if (idProp) {
+ accessor = me.createAccessor(idProp);
+
+ me.getId = function(record) {
+ var id = accessor.call(me, record);
+ return (id === undefined || id === '') ? null : id;
+ };
+ } else {
+ me.getId = function() {
+ return null;
+ };
+ }
+ me.buildFieldExtractors();
+ },
+
+ /**
+ * @private
+ */
+ buildFieldExtractors: function() {
+ //now build the extractors for all the fields
+ var me = this,
+ fields = me.getFields(),
+ ln = fields.length,
+ i = 0,
+ extractorFunctions = [],
+ field, map;
+
+ for (; i < ln; i++) {
+ field = fields[i];
+ map = (field.mapping !== undefined && field.mapping !== null) ? field.mapping : field.name;
+
+ extractorFunctions.push(me.createAccessor(map));
+ }
+ me.fieldCount = ln;
+
+ me.extractorFunctions = extractorFunctions;
+ }
+}, function() {
+ Ext.apply(this, {
+ // Private. Empty ResultSet to return when response is falsy (null|undefined|empty string)
+ nullResultSet: Ext.create('Ext.data.ResultSet', {
+ total : 0,
+ count : 0,
+ records: [],
+ success: true
+ })
+ });
+});
+/**
+ * @author Ed Spencer
+ * @class Ext.data.reader.Json
+ * @extends Ext.data.reader.Reader
+ *
+ * <p>The JSON Reader is used by a Proxy to read a server response that is sent back in JSON format. This usually
+ * happens as a result of loading a Store - for example we might create something like this:</p>
+ *
+<pre><code>
+Ext.define('User', {
+ extend: 'Ext.data.Model',
+ fields: ['id', 'name', 'email']
+});
+
+var store = Ext.create('Ext.data.Store', {
+ model: 'User',
+ proxy: {
+ type: 'ajax',
+ url : 'users.json',
+ reader: {
+ type: 'json'
+ }
+ }
+});
+</code></pre>
+ *
+ * <p>The example above creates a 'User' model. Models are explained in the {@link Ext.data.Model Model} docs if you're
+ * not already familiar with them.</p>
+ *
+ * <p>We created the simplest type of JSON Reader possible by simply telling our {@link Ext.data.Store Store}'s
+ * {@link Ext.data.proxy.Proxy Proxy} that we want a JSON Reader. The Store automatically passes the configured model to the
+ * Store, so it is as if we passed this instead:
+ *
+<pre><code>
+reader: {
+ type : 'json',
+ model: 'User'
+}
+</code></pre>
+ *
+ * <p>The reader we set up is ready to read data from our server - at the moment it will accept a response like this:</p>
+ *
+<pre><code>
+[
+ {
+ "id": 1,
+ "name": "Ed Spencer",
+ "email": "ed@sencha.com"
+ },
+ {
+ "id": 2,
+ "name": "Abe Elias",
+ "email": "abe@sencha.com"
+ }
+]
+</code></pre>
+ *
+ * <p><u>Reading other JSON formats</u></p>
+ *
+ * <p>If you already have your JSON format defined and it doesn't look quite like what we have above, you can usually
+ * pass JsonReader a couple of configuration options to make it parse your format. For example, we can use the
+ * {@link #root} configuration to parse data that comes back like this:</p>
+ *
+<pre><code>
+{
+ "users": [
+ {
+ "id": 1,
+ "name": "Ed Spencer",
+ "email": "ed@sencha.com"
+ },
+ {
+ "id": 2,
+ "name": "Abe Elias",
+ "email": "abe@sencha.com"
+ }
+ ]
+}
+</code></pre>
+ *
+ * <p>To parse this we just pass in a {@link #root} configuration that matches the 'users' above:</p>
+ *
+<pre><code>
+reader: {
+ type: 'json',
+ root: 'users'
+}
+</code></pre>
+ *
+ * <p>Sometimes the JSON structure is even more complicated. Document databases like CouchDB often provide metadata
+ * around each record inside a nested structure like this:</p>
+ *
+<pre><code>
+{
+ "total": 122,
+ "offset": 0,
+ "users": [
+ {
+ "id": "ed-spencer-1",
+ "value": 1,
+ "user": {
+ "id": 1,
+ "name": "Ed Spencer",
+ "email": "ed@sencha.com"
+ }
+ }
+ ]
+}
+</code></pre>
+ *
+ * <p>In the case above the record data is nested an additional level inside the "users" array as each "user" item has
+ * additional metadata surrounding it ('id' and 'value' in this case). To parse data out of each "user" item in the
+ * JSON above we need to specify the {@link #record} configuration like this:</p>
+ *
+<pre><code>
+reader: {
+ type : 'json',
+ root : 'users',
+ record: 'user'
+}
+</code></pre>
+ *
+ * <p><u>Response metadata</u></p>
+ *
+ * <p>The server can return additional data in its response, such as the {@link #totalProperty total number of records}
+ * and the {@link #successProperty success status of the response}. These are typically included in the JSON response
+ * like this:</p>
+ *
+<pre><code>
+{
+ "total": 100,
+ "success": true,
+ "users": [
+ {
+ "id": 1,
+ "name": "Ed Spencer",
+ "email": "ed@sencha.com"
+ }
+ ]
+}
+</code></pre>
+ *
+ * <p>If these properties are present in the JSON response they can be parsed out by the JsonReader and used by the
+ * Store that loaded it. We can set up the names of these properties by specifying a final pair of configuration
+ * options:</p>
+ *
+<pre><code>
+reader: {
+ type : 'json',
+ root : 'users',
+ totalProperty : 'total',
+ successProperty: 'success'
+}
+</code></pre>
+ *
+ * <p>These final options are not necessary to make the Reader work, but can be useful when the server needs to report
+ * an error or if it needs to indicate that there is a lot of data available of which only a subset is currently being
+ * returned.</p>
+ */
+Ext.define('Ext.data.reader.Json', {
+ extend: 'Ext.data.reader.Reader',
+ alternateClassName: 'Ext.data.JsonReader',
+ alias : 'reader.json',
+
+ root: '',
+
+ /**
+ * @cfg {String} record The optional location within the JSON response that the record data itself can be found at.
+ * See the JsonReader intro docs for more details. This is not often needed.
+ */
+
+ /**
+ * @cfg {Boolean} useSimpleAccessors True to ensure that field names/mappings are treated as literals when
+ * reading values. Defalts to <tt>false</tt>.
+ * For example, by default, using the mapping "foo.bar.baz" will try and read a property foo from the root, then a property bar
+ * from foo, then a property baz from bar. Setting the simple accessors to true will read the property with the name
+ * "foo.bar.baz" direct from the root object.
+ */
+ useSimpleAccessors: false,
+
+ /**
+ * Reads a JSON object and returns a ResultSet. Uses the internal getTotal and getSuccess extractors to
+ * retrieve meta data from the response, and extractData to turn the JSON data into model instances.
+ * @param {Object} data The raw JSON data
+ * @return {Ext.data.ResultSet} A ResultSet containing model instances and meta data about the results
+ */
+ readRecords: function(data) {
+ //this has to be before the call to super because we use the meta data in the superclass readRecords
+ if (data.metaData) {
+ this.onMetaChange(data.metaData);
+ }
+
+ /**
+ * @deprecated will be removed in Ext JS 5.0. This is just a copy of this.rawData - use that instead
+ * @property {Object} jsonData
+ */
+ this.jsonData = data;
+ return this.callParent([data]);
+ },
+
+ //inherit docs
+ getResponseData: function(response) {
+ var data;
+ try {
+ data = Ext.decode(response.responseText);
+ }
+ catch (ex) {
+ Ext.Error.raise({
+ response: response,
+ json: response.responseText,
+ parseError: ex,
+ msg: 'Unable to parse the JSON returned by the server: ' + ex.toString()
+ });
+ }
+
+ return data;
+ },
+
+ //inherit docs
+ buildExtractors : function() {
+ var me = this;
+
+ me.callParent(arguments);
+
+ if (me.root) {
+ me.getRoot = me.createAccessor(me.root);
+ } else {
+ me.getRoot = function(root) {
+ return root;
+ };
+ }
+ },
+
+ /**
+ * @private
+ * We're just preparing the data for the superclass by pulling out the record objects we want. If a {@link #record}
+ * was specified we have to pull those out of the larger JSON object, which is most of what this function is doing
+ * @param {Object} root The JSON root node
+ * @return {Ext.data.Model[]} The records
+ */
+ extractData: function(root) {
+ var recordName = this.record,
+ data = [],
+ length, i;
+
+ if (recordName) {
+ length = root.length;
+
+ if (!length && Ext.isObject(root)) {
+ length = 1;
+ root = [root];
+ }
+
+ for (i = 0; i < length; i++) {
+ data[i] = root[i][recordName];
+ }
+ } else {
+ data = root;
+ }
+ return this.callParent([data]);
+ },
+
+ /**
+ * @private
+ * Returns an accessor function for the given property string. Gives support for properties such as the following:
+ * 'someProperty'
+ * 'some.property'
+ * 'some["property"]'
+ * This is used by buildExtractors to create optimized extractor functions when casting raw data into model instances.
+ */
+ createAccessor: function() {
+ var re = /[\[\.]/;
+
+ return function(expr) {
+ if (Ext.isEmpty(expr)) {
+ return Ext.emptyFn;
+ }
+ if (Ext.isFunction(expr)) {
+ return expr;
+ }
+ if (this.useSimpleAccessors !== true) {
+ var i = String(expr).search(re);
+ if (i >= 0) {
+ return Ext.functionFactory('obj', 'return obj' + (i > 0 ? '.' : '') + expr);
+ }
+ }
+ return function(obj) {
+ return obj[expr];
+ };
+ };
+ }()
+});
+/**
+ * @class Ext.data.writer.Json
+ * @extends Ext.data.writer.Writer
+
+This class is used to write {@link Ext.data.Model} data to the server in a JSON format.
+The {@link #allowSingle} configuration can be set to false to force the records to always be
+encoded in an array, even if there is only a single record being sent.
+
+ * @markdown
+ */
+Ext.define('Ext.data.writer.Json', {
+ extend: 'Ext.data.writer.Writer',
+ alternateClassName: 'Ext.data.JsonWriter',
+ alias: 'writer.json',
+
+ /**
+ * @cfg {String} root The key under which the records in this Writer will be placed. Defaults to <tt>undefined</tt>.
+ * Example generated request, using root: 'records':
+<pre><code>
+{'records': [{name: 'my record'}, {name: 'another record'}]}
+</code></pre>
+ */
+ root: undefined,
+
+ /**
+ * @cfg {Boolean} encode True to use Ext.encode() on the data before sending. Defaults to <tt>false</tt>.
+ * The encode option should only be set to true when a {@link #root} is defined, because the values will be
+ * sent as part of the request parameters as opposed to a raw post. The root will be the name of the parameter
+ * sent to the server.
+ */
+ encode: false,
+
+ /**
+ * @cfg {Boolean} allowSingle False to ensure that records are always wrapped in an array, even if there is only
+ * one record being sent. When there is more than one record, they will always be encoded into an array.
+ * Defaults to <tt>true</tt>. Example:
+ * <pre><code>
+// with allowSingle: true
+"root": {
+ "first": "Mark",
+ "last": "Corrigan"
+}
+
+// with allowSingle: false
+"root": [{
+ "first": "Mark",
+ "last": "Corrigan"
+}]
+ * </code></pre>
+ */
+ allowSingle: true,
+
+ //inherit docs
+ writeRecords: function(request, data) {
+ var root = this.root;
+
+ if (this.allowSingle && data.length == 1) {
+ // convert to single object format
+ data = data[0];
+ }
+
+ if (this.encode) {
+ if (root) {
+ // sending as a param, need to encode
+ request.params[root] = Ext.encode(data);
+ } else {
+ }
+ } else {
+ // send as jsonData
+ request.jsonData = request.jsonData || {};
+ if (root) {
+ request.jsonData[root] = data;
+ } else {
+ request.jsonData = data;
+ }
+ }
+ return request;
+ }
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * Proxies are used by {@link Ext.data.Store Stores} to handle the loading and saving of {@link Ext.data.Model Model}
+ * data. Usually developers will not need to create or interact with proxies directly.
+ *
+ * # Types of Proxy
+ *
+ * There are two main types of Proxy - {@link Ext.data.proxy.Client Client} and {@link Ext.data.proxy.Server Server}.
+ * The Client proxies save their data locally and include the following subclasses:
+ *
+ * - {@link Ext.data.proxy.LocalStorage LocalStorageProxy} - saves its data to localStorage if the browser supports it
+ * - {@link Ext.data.proxy.SessionStorage SessionStorageProxy} - saves its data to sessionStorage if the browsers supports it
+ * - {@link Ext.data.proxy.Memory MemoryProxy} - holds data in memory only, any data is lost when the page is refreshed
+ *
+ * The Server proxies save their data by sending requests to some remote server. These proxies include:
+ *
+ * - {@link Ext.data.proxy.Ajax Ajax} - sends requests to a server on the same domain
+ * - {@link Ext.data.proxy.JsonP JsonP} - uses JSON-P to send requests to a server on a different domain
+ * - {@link Ext.data.proxy.Direct Direct} - uses {@link Ext.direct.Manager} to send requests
+ *
+ * Proxies operate on the principle that all operations performed are either Create, Read, Update or Delete. These four
+ * operations are mapped to the methods {@link #create}, {@link #read}, {@link #update} and {@link #destroy}
+ * respectively. Each Proxy subclass implements these functions.
+ *
+ * The CRUD methods each expect an {@link Ext.data.Operation Operation} object as the sole argument. The Operation
+ * encapsulates information about the action the Store wishes to perform, the {@link Ext.data.Model model} instances
+ * that are to be modified, etc. See the {@link Ext.data.Operation Operation} documentation for more details. Each CRUD
+ * method also accepts a callback function to be called asynchronously on completion.
+ *
+ * Proxies also support batching of Operations via a {@link Ext.data.Batch batch} object, invoked by the {@link #batch}
+ * method.
+ */
+Ext.define('Ext.data.proxy.Proxy', {
+ alias: 'proxy.proxy',
+ alternateClassName: ['Ext.data.DataProxy', 'Ext.data.Proxy'],
+ requires: [
+ 'Ext.data.reader.Json',
+ 'Ext.data.writer.Json'
+ ],
+ uses: [
+ 'Ext.data.Batch',
+ 'Ext.data.Operation',
+ 'Ext.data.Model'
+ ],
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ /**
+ * @cfg {String} batchOrder
+ * Comma-separated ordering 'create', 'update' and 'destroy' actions when batching. Override this to set a different
+ * order for the batched CRUD actions to be executed in. Defaults to 'create,update,destroy'.
+ */
+ batchOrder: 'create,update,destroy',
+
+ /**
+ * @cfg {Boolean} batchActions
+ * True to batch actions of a particular type when synchronizing the store. Defaults to true.
+ */
+ batchActions: true,
+
+ /**
+ * @cfg {String} defaultReaderType
+ * The default registered reader type. Defaults to 'json'.
+ * @private
+ */
+ defaultReaderType: 'json',
+
+ /**
+ * @cfg {String} defaultWriterType
+ * The default registered writer type. Defaults to 'json'.
+ * @private
+ */
+ defaultWriterType: 'json',
+
+ /**
+ * @cfg {String/Ext.data.Model} model
+ * The name of the Model to tie to this Proxy. Can be either the string name of the Model, or a reference to the
+ * Model constructor. Required.
+ */
+
+ /**
+ * @cfg {Object/String/Ext.data.reader.Reader} reader
+ * The Ext.data.reader.Reader to use to decode the server's response or data read from client. This can either be a
+ * Reader instance, a config object or just a valid Reader type name (e.g. 'json', 'xml').
+ */
+
+ /**
+ * @cfg {Object/String/Ext.data.writer.Writer} writer
+ * The Ext.data.writer.Writer to use to encode any request sent to the server or saved to client. This can either be
+ * a Writer instance, a config object or just a valid Writer type name (e.g. 'json', 'xml').
+ */
+
+ isProxy: true,
+
+ /**
+ * Creates the Proxy
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ config = config || {};
+
+ if (config.model === undefined) {
+ delete config.model;
+ }
+
+ this.mixins.observable.constructor.call(this, config);
+
+ if (this.model !== undefined && !(this.model instanceof Ext.data.Model)) {
+ this.setModel(this.model);
+ }
+ },
+
+ /**
+ * Sets the model associated with this proxy. This will only usually be called by a Store
+ *
+ * @param {String/Ext.data.Model} model The new model. Can be either the model name string,
+ * or a reference to the model's constructor
+ * @param {Boolean} setOnStore Sets the new model on the associated Store, if one is present
+ */
+ setModel: function(model, setOnStore) {
+ this.model = Ext.ModelManager.getModel(model);
+
+ var reader = this.reader,
+ writer = this.writer;
+
+ this.setReader(reader);
+ this.setWriter(writer);
+
+ if (setOnStore && this.store) {
+ this.store.setModel(this.model);
+ }
+ },
+
+ /**
+ * Returns the model attached to this Proxy
+ * @return {Ext.data.Model} The model
+ */
+ getModel: function() {
+ return this.model;
+ },
+
+ /**
+ * Sets the Proxy's Reader by string, config object or Reader instance
+ *
+ * @param {String/Object/Ext.data.reader.Reader} reader The new Reader, which can be either a type string,
+ * a configuration object or an Ext.data.reader.Reader instance
+ * @return {Ext.data.reader.Reader} The attached Reader object
+ */
+ setReader: function(reader) {
+ var me = this;
+
+ if (reader === undefined || typeof reader == 'string') {
+ reader = {
+ type: reader
+ };
+ }
+
+ if (reader.isReader) {
+ reader.setModel(me.model);
+ } else {
+ Ext.applyIf(reader, {
+ proxy: me,
+ model: me.model,
+ type : me.defaultReaderType
+ });
+
+ reader = Ext.createByAlias('reader.' + reader.type, reader);
+ }
+
+ me.reader = reader;
+ return me.reader;
+ },
+
+ /**
+ * Returns the reader currently attached to this proxy instance
+ * @return {Ext.data.reader.Reader} The Reader instance
+ */
+ getReader: function() {
+ return this.reader;
+ },
+
+ /**
+ * Sets the Proxy's Writer by string, config object or Writer instance
+ *
+ * @param {String/Object/Ext.data.writer.Writer} writer The new Writer, which can be either a type string,
+ * a configuration object or an Ext.data.writer.Writer instance
+ * @return {Ext.data.writer.Writer} The attached Writer object
+ */
+ setWriter: function(writer) {
+ if (writer === undefined || typeof writer == 'string') {
+ writer = {
+ type: writer
+ };
+ }
+
+ if (!(writer instanceof Ext.data.writer.Writer)) {
+ Ext.applyIf(writer, {
+ model: this.model,
+ type : this.defaultWriterType
+ });
+
+ writer = Ext.createByAlias('writer.' + writer.type, writer);
+ }
+
+ this.writer = writer;
+
+ return this.writer;
+ },
+
+ /**
+ * Returns the writer currently attached to this proxy instance
+ * @return {Ext.data.writer.Writer} The Writer instance
+ */
+ getWriter: function() {
+ return this.writer;
+ },
+
+ /**
+ * Performs the given create operation.
+ * @param {Ext.data.Operation} operation The Operation to perform
+ * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
+ * @param {Object} scope Scope to execute the callback function in
+ * @method
+ */
+ create: Ext.emptyFn,
+
+ /**
+ * Performs the given read operation.
+ * @param {Ext.data.Operation} operation The Operation to perform
+ * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
+ * @param {Object} scope Scope to execute the callback function in
+ * @method
+ */
+ read: Ext.emptyFn,
+
+ /**
+ * Performs the given update operation.
+ * @param {Ext.data.Operation} operation The Operation to perform
+ * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
+ * @param {Object} scope Scope to execute the callback function in
+ * @method
+ */
+ update: Ext.emptyFn,
+
+ /**
+ * Performs the given destroy operation.
+ * @param {Ext.data.Operation} operation The Operation to perform
+ * @param {Function} callback Callback function to be called when the Operation has completed (whether successful or not)
+ * @param {Object} scope Scope to execute the callback function in
+ * @method
+ */
+ destroy: Ext.emptyFn,
+
+ /**
+ * Performs a batch of {@link Ext.data.Operation Operations}, in the order specified by {@link #batchOrder}. Used
+ * internally by {@link Ext.data.Store}'s {@link Ext.data.Store#sync sync} method. Example usage:
+ *
+ * myProxy.batch({
+ * create : [myModel1, myModel2],
+ * update : [myModel3],
+ * destroy: [myModel4, myModel5]
+ * });
+ *
+ * Where the myModel* above are {@link Ext.data.Model Model} instances - in this case 1 and 2 are new instances and
+ * have not been saved before, 3 has been saved previously but needs to be updated, and 4 and 5 have already been
+ * saved but should now be destroyed.
+ *
+ * @param {Object} operations Object containing the Model instances to act upon, keyed by action name
+ * @param {Object} listeners (optional) listeners object passed straight through to the Batch -
+ * see {@link Ext.data.Batch}
+ * @return {Ext.data.Batch} The newly created Ext.data.Batch object
+ */
+ batch: function(operations, listeners) {
+ var me = this,
+ batch = Ext.create('Ext.data.Batch', {
+ proxy: me,
+ listeners: listeners || {}
+ }),
+ useBatch = me.batchActions,
+ records;
+
+ Ext.each(me.batchOrder.split(','), function(action) {
+ records = operations[action];
+ if (records) {
+ if (useBatch) {
+ batch.add(Ext.create('Ext.data.Operation', {
+ action: action,
+ records: records
+ }));
+ } else {
+ Ext.each(records, function(record){
+ batch.add(Ext.create('Ext.data.Operation', {
+ action : action,
+ records: [record]
+ }));
+ });
+ }
+ }
+ }, me);
+
+ batch.start();
+ return batch;
+ }
+}, function() {
+ // Ext.data.proxy.ProxyMgr.registerType('proxy', this);
+
+ //backwards compatibility
+ Ext.data.DataProxy = this;
+ // Ext.deprecate('platform', '2.0', function() {
+ // Ext.data.DataProxy = this;
+ // }, this);
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * ServerProxy is a superclass of {@link Ext.data.proxy.JsonP JsonPProxy} and {@link Ext.data.proxy.Ajax AjaxProxy}, and
+ * would not usually be used directly.
+ *
+ * ServerProxy should ideally be named HttpProxy as it is a superclass for all HTTP proxies - for Ext JS 4.x it has been
+ * called ServerProxy to enable any 3.x applications that reference the HttpProxy to continue to work (HttpProxy is now
+ * an alias of AjaxProxy).
+ * @private
+ */
+Ext.define('Ext.data.proxy.Server', {
+ extend: 'Ext.data.proxy.Proxy',
+ alias : 'proxy.server',
+ alternateClassName: 'Ext.data.ServerProxy',
+ uses : ['Ext.data.Request'],
+
+ /**
+ * @cfg {String} url
+ * The URL from which to request the data object.
+ */
+
+ /**
+ * @cfg {String} pageParam
+ * The name of the 'page' parameter to send in a request. Defaults to 'page'. Set this to undefined if you don't
+ * want to send a page parameter.
+ */
+ pageParam: 'page',
+
+ /**
+ * @cfg {String} startParam
+ * The name of the 'start' parameter to send in a request. Defaults to 'start'. Set this to undefined if you don't
+ * want to send a start parameter.
+ */
+ startParam: 'start',
+
+ /**
+ * @cfg {String} limitParam
+ * The name of the 'limit' parameter to send in a request. Defaults to 'limit'. Set this to undefined if you don't
+ * want to send a limit parameter.
+ */
+ limitParam: 'limit',
+
+ /**
+ * @cfg {String} groupParam
+ * The name of the 'group' parameter to send in a request. Defaults to 'group'. Set this to undefined if you don't
+ * want to send a group parameter.
+ */
+ groupParam: 'group',
+
+ /**
+ * @cfg {String} sortParam
+ * The name of the 'sort' parameter to send in a request. Defaults to 'sort'. Set this to undefined if you don't
+ * want to send a sort parameter.
+ */
+ sortParam: 'sort',
+
+ /**
+ * @cfg {String} filterParam
+ * The name of the 'filter' parameter to send in a request. Defaults to 'filter'. Set this to undefined if you don't
+ * want to send a filter parameter.
+ */
+ filterParam: 'filter',
+
+ /**
+ * @cfg {String} directionParam
+ * The name of the direction parameter to send in a request. **This is only used when simpleSortMode is set to
+ * true.** Defaults to 'dir'.
+ */
+ directionParam: 'dir',
+
+ /**
+ * @cfg {Boolean} simpleSortMode
+ * Enabling simpleSortMode in conjunction with remoteSort will only send one sort property and a direction when a
+ * remote sort is requested. The directionParam and sortParam will be sent with the property name and either 'ASC'
+ * or 'DESC'.
+ */
+ simpleSortMode: false,
+
+ /**
+ * @cfg {Boolean} noCache
+ * Disable caching by adding a unique parameter name to the request. Set to false to allow caching. Defaults to true.
+ */
+ noCache : true,
+
+ /**
+ * @cfg {String} cacheString
+ * The name of the cache param added to the url when using noCache. Defaults to "_dc".
+ */
+ cacheString: "_dc",
+
+ /**
+ * @cfg {Number} timeout
+ * The number of milliseconds to wait for a response. Defaults to 30000 milliseconds (30 seconds).
+ */
+ timeout : 30000,
+
+ /**
+ * @cfg {Object} api
+ * Specific urls to call on CRUD action methods "create", "read", "update" and "destroy". Defaults to:
+ *
+ * api: {
+ * create : undefined,
+ * read : undefined,
+ * update : undefined,
+ * destroy : undefined
+ * }
+ *
+ * The url is built based upon the action being executed [create|read|update|destroy] using the commensurate
+ * {@link #api} property, or if undefined default to the configured
+ * {@link Ext.data.Store}.{@link Ext.data.proxy.Server#url url}.
+ *
+ * For example:
+ *
+ * api: {
+ * create : '/controller/new',
+ * read : '/controller/load',
+ * update : '/controller/update',
+ * destroy : '/controller/destroy_action'
+ * }
+ *
+ * If the specific URL for a given CRUD action is undefined, the CRUD action request will be directed to the
+ * configured {@link Ext.data.proxy.Server#url url}.
+ */
+
+ constructor: function(config) {
+ var me = this;
+
+ config = config || {};
+ this.addEvents(
+ /**
+ * @event exception
+ * Fires when the server returns an exception
+ * @param {Ext.data.proxy.Proxy} this
+ * @param {Object} response The response from the AJAX request
+ * @param {Ext.data.Operation} operation The operation that triggered request
+ */
+ 'exception'
+ );
+ me.callParent([config]);
+
+ /**
+ * @cfg {Object} extraParams
+ * Extra parameters that will be included on every request. Individual requests with params of the same name
+ * will override these params when they are in conflict.
+ */
+ me.extraParams = config.extraParams || {};
+
+ me.api = config.api || {};
+
+ //backwards compatibility, will be deprecated in 5.0
+ me.nocache = me.noCache;
+ },
+
+ //in a ServerProxy all four CRUD operations are executed in the same manner, so we delegate to doRequest in each case
+ create: function() {
+ return this.doRequest.apply(this, arguments);
+ },
+
+ read: function() {
+ return this.doRequest.apply(this, arguments);
+ },
+
+ update: function() {
+ return this.doRequest.apply(this, arguments);
+ },
+
+ destroy: function() {
+ return this.doRequest.apply(this, arguments);
+ },
+
+ /**
+ * Creates and returns an Ext.data.Request object based on the options passed by the {@link Ext.data.Store Store}
+ * that this Proxy is attached to.
+ * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object to execute
+ * @return {Ext.data.Request} The request object
+ */
+ buildRequest: function(operation) {
+ var params = Ext.applyIf(operation.params || {}, this.extraParams || {}),
+ request;
+
+ //copy any sorters, filters etc into the params so they can be sent over the wire
+ params = Ext.applyIf(params, this.getParams(operation));
+
+ if (operation.id && !params.id) {
+ params.id = operation.id;
+ }
+
+ request = Ext.create('Ext.data.Request', {
+ params : params,
+ action : operation.action,
+ records : operation.records,
+ operation: operation,
+ url : operation.url
+ });
+
+ request.url = this.buildUrl(request);
+
+ /*
+ * Save the request on the Operation. Operations don't usually care about Request and Response data, but in the
+ * ServerProxy and any of its subclasses we add both request and response as they may be useful for further processing
+ */
+ operation.request = request;
+
+ return request;
+ },
+
+ // Should this be documented as protected method?
+ processResponse: function(success, operation, request, response, callback, scope){
+ var me = this,
+ reader,
+ result;
+
+ if (success === true) {
+ reader = me.getReader();
+ result = reader.read(me.extractResponseData(response));
+
+ if (result.success !== false) {
+ //see comment in buildRequest for why we include the response object here
+ Ext.apply(operation, {
+ response: response,
+ resultSet: result
+ });
+
+ operation.commitRecords(result.records);
+ operation.setCompleted();
+ operation.setSuccessful();
+ } else {
+ operation.setException(result.message);
+ me.fireEvent('exception', this, response, operation);
+ }
+ } else {
+ me.setException(operation, response);
+ me.fireEvent('exception', this, response, operation);
+ }
+
+ //this callback is the one that was passed to the 'read' or 'write' function above
+ if (typeof callback == 'function') {
+ callback.call(scope || me, operation);
+ }
+
+ me.afterRequest(request, success);
+ },
+
+ /**
+ * Sets up an exception on the operation
+ * @private
+ * @param {Ext.data.Operation} operation The operation
+ * @param {Object} response The response
+ */
+ setException: function(operation, response){
+ operation.setException({
+ status: response.status,
+ statusText: response.statusText
+ });
+ },
+
+ /**
+ * Template method to allow subclasses to specify how to get the response for the reader.
+ * @template
+ * @private
+ * @param {Object} response The server response
+ * @return {Object} The response data to be used by the reader
+ */
+ extractResponseData: function(response){
+ return response;
+ },
+
+ /**
+ * Encode any values being sent to the server. Can be overridden in subclasses.
+ * @private
+ * @param {Array} An array of sorters/filters.
+ * @return {Object} The encoded value
+ */
+ applyEncoding: function(value){
+ return Ext.encode(value);
+ },
+
+ /**
+ * Encodes the array of {@link Ext.util.Sorter} objects into a string to be sent in the request url. By default,
+ * this simply JSON-encodes the sorter data
+ * @param {Ext.util.Sorter[]} sorters The array of {@link Ext.util.Sorter Sorter} objects
+ * @return {String} The encoded sorters
+ */
+ encodeSorters: function(sorters) {
+ var min = [],
+ length = sorters.length,
+ i = 0;
+
+ for (; i < length; i++) {
+ min[i] = {
+ property : sorters[i].property,
+ direction: sorters[i].direction
+ };
+ }
+ return this.applyEncoding(min);
+
+ },
+
+ /**
+ * Encodes the array of {@link Ext.util.Filter} objects into a string to be sent in the request url. By default,
+ * this simply JSON-encodes the filter data
+ * @param {Ext.util.Filter[]} filters The array of {@link Ext.util.Filter Filter} objects
+ * @return {String} The encoded filters
+ */
+ encodeFilters: function(filters) {
+ var min = [],
+ length = filters.length,
+ i = 0;
+
+ for (; i < length; i++) {
+ min[i] = {
+ property: filters[i].property,
+ value : filters[i].value
+ };
+ }
+ return this.applyEncoding(min);
+ },
+
+ /**
+ * @private
+ * Copy any sorters, filters etc into the params so they can be sent over the wire
+ */
+ getParams: function(operation) {
+ var me = this,
+ params = {},
+ isDef = Ext.isDefined,
+ groupers = operation.groupers,
+ sorters = operation.sorters,
+ filters = operation.filters,
+ page = operation.page,
+ start = operation.start,
+ limit = operation.limit,
+
+ simpleSortMode = me.simpleSortMode,
+
+ pageParam = me.pageParam,
+ startParam = me.startParam,
+ limitParam = me.limitParam,
+ groupParam = me.groupParam,
+ sortParam = me.sortParam,
+ filterParam = me.filterParam,
+ directionParam = me.directionParam;
+
+ if (pageParam && isDef(page)) {
+ params[pageParam] = page;
+ }
+
+ if (startParam && isDef(start)) {
+ params[startParam] = start;
+ }
+
+ if (limitParam && isDef(limit)) {
+ params[limitParam] = limit;
+ }
+
+ if (groupParam && groupers && groupers.length > 0) {
+ // Grouper is a subclass of sorter, so we can just use the sorter method
+ params[groupParam] = me.encodeSorters(groupers);
+ }
+
+ if (sortParam && sorters && sorters.length > 0) {
+ if (simpleSortMode) {
+ params[sortParam] = sorters[0].property;
+ params[directionParam] = sorters[0].direction;
+ } else {
+ params[sortParam] = me.encodeSorters(sorters);
+ }
+
+ }
+
+ if (filterParam && filters && filters.length > 0) {
+ params[filterParam] = me.encodeFilters(filters);
+ }
+
+ return params;
+ },
+
+ /**
+ * Generates a url based on a given Ext.data.Request object. By default, ServerProxy's buildUrl will add the
+ * cache-buster param to the end of the url. Subclasses may need to perform additional modifications to the url.
+ * @param {Ext.data.Request} request The request object
+ * @return {String} The url
+ */
+ buildUrl: function(request) {
+ var me = this,
+ url = me.getUrl(request);
+
+
+ if (me.noCache) {
+ url = Ext.urlAppend(url, Ext.String.format("{0}={1}", me.cacheString, Ext.Date.now()));
+ }
+
+ return url;
+ },
+
+ /**
+ * Get the url for the request taking into account the order of priority,
+ * - The request
+ * - The api
+ * - The url
+ * @private
+ * @param {Ext.data.Request} request The request
+ * @return {String} The url
+ */
+ getUrl: function(request){
+ return request.url || this.api[request.action] || this.url;
+ },
+
+ /**
+ * In ServerProxy subclasses, the {@link #create}, {@link #read}, {@link #update} and {@link #destroy} methods all
+ * pass through to doRequest. Each ServerProxy subclass must implement the doRequest method - see {@link
+ * Ext.data.proxy.JsonP} and {@link Ext.data.proxy.Ajax} for examples. This method carries the same signature as
+ * each of the methods that delegate to it.
+ *
+ * @param {Ext.data.Operation} operation The Ext.data.Operation object
+ * @param {Function} callback The callback function to call when the Operation has completed
+ * @param {Object} scope The scope in which to execute the callback
+ */
+ doRequest: function(operation, callback, scope) {
+ },
+
+ /**
+ * Optional callback function which can be used to clean up after a request has been completed.
+ * @param {Ext.data.Request} request The Request object
+ * @param {Boolean} success True if the request was successful
+ * @method
+ */
+ afterRequest: Ext.emptyFn,
+
+ onDestroy: function() {
+ Ext.destroy(this.reader, this.writer);
+ }
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * AjaxProxy is one of the most widely-used ways of getting data into your application. It uses AJAX requests to load
+ * data from the server, usually to be placed into a {@link Ext.data.Store Store}. Let's take a look at a typical setup.
+ * Here we're going to set up a Store that has an AjaxProxy. To prepare, we'll also set up a {@link Ext.data.Model
+ * Model}:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', 'email']
+ * });
+ *
+ * //The Store contains the AjaxProxy as an inline configuration
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'ajax',
+ * url : 'users.json'
+ * }
+ * });
+ *
+ * store.load();
+ *
+ * Our example is going to load user data into a Store, so we start off by defining a {@link Ext.data.Model Model} with
+ * the fields that we expect the server to return. Next we set up the Store itself, along with a
+ * {@link Ext.data.Store#proxy proxy} configuration. This configuration was automatically turned into an
+ * Ext.data.proxy.Ajax instance, with the url we specified being passed into AjaxProxy's constructor.
+ * It's as if we'd done this:
+ *
+ * new Ext.data.proxy.Ajax({
+ * url: 'users.json',
+ * model: 'User',
+ * reader: 'json'
+ * });
+ *
+ * A couple of extra configurations appeared here - {@link #model} and {@link #reader}. These are set by default when we
+ * create the proxy via the Store - the Store already knows about the Model, and Proxy's default {@link
+ * Ext.data.reader.Reader Reader} is {@link Ext.data.reader.Json JsonReader}.
+ *
+ * Now when we call store.load(), the AjaxProxy springs into action, making a request to the url we configured
+ * ('users.json' in this case). As we're performing a read, it sends a GET request to that url (see
+ * {@link #actionMethods} to customize this - by default any kind of read will be sent as a GET request and any kind of write
+ * will be sent as a POST request).
+ *
+ * # Limitations
+ *
+ * AjaxProxy cannot be used to retrieve data from other domains. If your application is running on http://domainA.com it
+ * cannot load data from http://domainB.com because browsers have a built-in security policy that prohibits domains
+ * talking to each other via AJAX.
+ *
+ * If you need to read data from another domain and can't set up a proxy server (some software that runs on your own
+ * domain's web server and transparently forwards requests to http://domainB.com, making it look like they actually came
+ * from http://domainA.com), you can use {@link Ext.data.proxy.JsonP} and a technique known as JSON-P (JSON with
+ * Padding), which can help you get around the problem so long as the server on http://domainB.com is set up to support
+ * JSON-P responses. See {@link Ext.data.proxy.JsonP JsonPProxy}'s introduction docs for more details.
+ *
+ * # Readers and Writers
+ *
+ * AjaxProxy can be configured to use any type of {@link Ext.data.reader.Reader Reader} to decode the server's response.
+ * If no Reader is supplied, AjaxProxy will default to using a {@link Ext.data.reader.Json JsonReader}. Reader
+ * configuration can be passed in as a simple object, which the Proxy automatically turns into a {@link
+ * Ext.data.reader.Reader Reader} instance:
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * model: 'User',
+ * reader: {
+ * type: 'xml',
+ * root: 'users'
+ * }
+ * });
+ *
+ * proxy.getReader(); //returns an {@link Ext.data.reader.Xml XmlReader} instance based on the config we supplied
+ *
+ * # Url generation
+ *
+ * AjaxProxy automatically inserts any sorting, filtering, paging and grouping options into the url it generates for
+ * each request. These are controlled with the following configuration options:
+ *
+ * - {@link #pageParam} - controls how the page number is sent to the server (see also {@link #startParam} and {@link #limitParam})
+ * - {@link #sortParam} - controls how sort information is sent to the server
+ * - {@link #groupParam} - controls how grouping information is sent to the server
+ * - {@link #filterParam} - controls how filter information is sent to the server
+ *
+ * Each request sent by AjaxProxy is described by an {@link Ext.data.Operation Operation}. To see how we can customize
+ * the generated urls, let's say we're loading the Proxy with the following Operation:
+ *
+ * var operation = new Ext.data.Operation({
+ * action: 'read',
+ * page : 2
+ * });
+ *
+ * Now we'll issue the request for this Operation by calling {@link #read}:
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users'
+ * });
+ *
+ * proxy.read(operation); //GET /users?page=2
+ *
+ * Easy enough - the Proxy just copied the page property from the Operation. We can customize how this page data is sent
+ * to the server:
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users',
+ * pagePage: 'pageNumber'
+ * });
+ *
+ * proxy.read(operation); //GET /users?pageNumber=2
+ *
+ * Alternatively, our Operation could have been configured to send start and limit parameters instead of page:
+ *
+ * var operation = new Ext.data.Operation({
+ * action: 'read',
+ * start : 50,
+ * limit : 25
+ * });
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users'
+ * });
+ *
+ * proxy.read(operation); //GET /users?start=50&limit;=25
+ *
+ * Again we can customize this url:
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users',
+ * startParam: 'startIndex',
+ * limitParam: 'limitIndex'
+ * });
+ *
+ * proxy.read(operation); //GET /users?startIndex=50&limitIndex;=25
+ *
+ * AjaxProxy will also send sort and filter information to the server. Let's take a look at how this looks with a more
+ * expressive Operation object:
+ *
+ * var operation = new Ext.data.Operation({
+ * action: 'read',
+ * sorters: [
+ * new Ext.util.Sorter({
+ * property : 'name',
+ * direction: 'ASC'
+ * }),
+ * new Ext.util.Sorter({
+ * property : 'age',
+ * direction: 'DESC'
+ * })
+ * ],
+ * filters: [
+ * new Ext.util.Filter({
+ * property: 'eyeColor',
+ * value : 'brown'
+ * })
+ * ]
+ * });
+ *
+ * This is the type of object that is generated internally when loading a {@link Ext.data.Store Store} with sorters and
+ * filters defined. By default the AjaxProxy will JSON encode the sorters and filters, resulting in something like this
+ * (note that the url is escaped before sending the request, but is left unescaped here for clarity):
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users'
+ * });
+ *
+ * proxy.read(operation); //GET /users?sort=[{"property":"name","direction":"ASC"},{"property":"age","direction":"DESC"}]&filter;=[{"property":"eyeColor","value":"brown"}]
+ *
+ * We can again customize how this is created by supplying a few configuration options. Let's say our server is set up
+ * to receive sorting information is a format like "sortBy=name#ASC,age#DESC". We can configure AjaxProxy to provide
+ * that format like this:
+ *
+ * var proxy = new Ext.data.proxy.Ajax({
+ * url: '/users',
+ * sortParam: 'sortBy',
+ * filterParam: 'filterBy',
+ *
+ * //our custom implementation of sorter encoding - turns our sorters into "name#ASC,age#DESC"
+ * encodeSorters: function(sorters) {
+ * var length = sorters.length,
+ * sortStrs = [],
+ * sorter, i;
+ *
+ * for (i = 0; i < length; i++) {
+ * sorter = sorters[i];
+ *
+ * sortStrs[i] = sorter.property + '#' + sorter.direction
+ * }
+ *
+ * return sortStrs.join(",");
+ * }
+ * });
+ *
+ * proxy.read(operation); //GET /users?sortBy=name#ASC,age#DESC&filterBy;=[{"property":"eyeColor","value":"brown"}]
+ *
+ * We can also provide a custom {@link #encodeFilters} function to encode our filters.
+ *
+ * @constructor
+ * Note that if this HttpProxy is being used by a {@link Ext.data.Store Store}, then the Store's call to
+ * {@link Ext.data.Store#load load} will override any specified callback and params options. In this case, use the
+ * {@link Ext.data.Store Store}'s events to modify parameters, or react to loading events.
+ *
+ * @param {Object} config (optional) Config object.
+ * If an options parameter is passed, the singleton {@link Ext.Ajax} object will be used to make the request.
+ */
+Ext.define('Ext.data.proxy.Ajax', {
+ requires: ['Ext.util.MixedCollection', 'Ext.Ajax'],
+ extend: 'Ext.data.proxy.Server',
+ alias: 'proxy.ajax',
+ alternateClassName: ['Ext.data.HttpProxy', 'Ext.data.AjaxProxy'],
+
+ /**
+ * @property {Object} actionMethods
+ * Mapping of action name to HTTP request method. In the basic AjaxProxy these are set to 'GET' for 'read' actions
+ * and 'POST' for 'create', 'update' and 'destroy' actions. The {@link Ext.data.proxy.Rest} maps these to the
+ * correct RESTful methods.
+ */
+ actionMethods: {
+ create : 'POST',
+ read : 'GET',
+ update : 'POST',
+ destroy: 'POST'
+ },
+
+ /**
+ * @cfg {Object} headers
+ * Any headers to add to the Ajax request. Defaults to undefined.
+ */
+
+ /**
+ * @ignore
+ */
+ doRequest: function(operation, callback, scope) {
+ var writer = this.getWriter(),
+ request = this.buildRequest(operation, callback, scope);
+
+ if (operation.allowWrite()) {
+ request = writer.write(request);
+ }
+
+ Ext.apply(request, {
+ headers : this.headers,
+ timeout : this.timeout,
+ scope : this,
+ callback : this.createRequestCallback(request, operation, callback, scope),
+ method : this.getMethod(request),
+ disableCaching: false // explicitly set it to false, ServerProxy handles caching
+ });
+
+ Ext.Ajax.request(request);
+
+ return request;
+ },
+
+ /**
+ * Returns the HTTP method name for a given request. By default this returns based on a lookup on
+ * {@link #actionMethods}.
+ * @param {Ext.data.Request} request The request object
+ * @return {String} The HTTP method to use (should be one of 'GET', 'POST', 'PUT' or 'DELETE')
+ */
+ getMethod: function(request) {
+ return this.actionMethods[request.action];
+ },
+
+ /**
+ * @private
+ * TODO: This is currently identical to the JsonPProxy version except for the return function's signature. There is a lot
+ * of code duplication inside the returned function so we need to find a way to DRY this up.
+ * @param {Ext.data.Request} request The Request object
+ * @param {Ext.data.Operation} operation The Operation being executed
+ * @param {Function} callback The callback function to be called when the request completes. This is usually the callback
+ * passed to doRequest
+ * @param {Object} scope The scope in which to execute the callback function
+ * @return {Function} The callback function
+ */
+ createRequestCallback: function(request, operation, callback, scope) {
+ var me = this;
+
+ return function(options, success, response) {
+ me.processResponse(success, operation, request, response, callback, scope);
+ };
+ }
+}, function() {
+ //backwards compatibility, remove in Ext JS 5.0
+ Ext.data.HttpProxy = this;
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * A Model represents some object that your application manages. For example, one might define a Model for Users,
+ * Products, Cars, or any other real-world object that we want to model in the system. Models are registered via the
+ * {@link Ext.ModelManager model manager}, and are used by {@link Ext.data.Store stores}, which are in turn used by many
+ * of the data-bound components in Ext.
+ *
+ * Models are defined as a set of fields and any arbitrary methods and properties relevant to the model. For example:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'name', type: 'string'},
+ * {name: 'age', type: 'int'},
+ * {name: 'phone', type: 'string'},
+ * {name: 'alive', type: 'boolean', defaultValue: true}
+ * ],
+ *
+ * changeName: function() {
+ * var oldName = this.get('name'),
+ * newName = oldName + " The Barbarian";
+ *
+ * this.set('name', newName);
+ * }
+ * });
+ *
+ * The fields array is turned into a {@link Ext.util.MixedCollection MixedCollection} automatically by the {@link
+ * Ext.ModelManager ModelManager}, and all other functions and properties are copied to the new Model's prototype.
+ *
+ * Now we can create instances of our User model and call any model logic we defined:
+ *
+ * var user = Ext.create('User', {
+ * name : 'Conan',
+ * age : 24,
+ * phone: '555-555-5555'
+ * });
+ *
+ * user.changeName();
+ * user.get('name'); //returns "Conan The Barbarian"
+ *
+ * # Validations
+ *
+ * Models have built-in support for validations, which are executed against the validator functions in {@link
+ * Ext.data.validations} ({@link Ext.data.validations see all validation functions}). Validations are easy to add to
+ * models:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'name', type: 'string'},
+ * {name: 'age', type: 'int'},
+ * {name: 'phone', type: 'string'},
+ * {name: 'gender', type: 'string'},
+ * {name: 'username', type: 'string'},
+ * {name: 'alive', type: 'boolean', defaultValue: true}
+ * ],
+ *
+ * validations: [
+ * {type: 'presence', field: 'age'},
+ * {type: 'length', field: 'name', min: 2},
+ * {type: 'inclusion', field: 'gender', list: ['Male', 'Female']},
+ * {type: 'exclusion', field: 'username', list: ['Admin', 'Operator']},
+ * {type: 'format', field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}
+ * ]
+ * });
+ *
+ * The validations can be run by simply calling the {@link #validate} function, which returns a {@link Ext.data.Errors}
+ * object:
+ *
+ * var instance = Ext.create('User', {
+ * name: 'Ed',
+ * gender: 'Male',
+ * username: 'edspencer'
+ * });
+ *
+ * var errors = instance.validate();
+ *
+ * # Associations
+ *
+ * Models can have associations with other Models via {@link Ext.data.BelongsToAssociation belongsTo} and {@link
+ * Ext.data.HasManyAssociation hasMany} associations. For example, let's say we're writing a blog administration
+ * application which deals with Users, Posts and Comments. We can express the relationships between these models like
+ * this:
+ *
+ * Ext.define('Post', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'user_id'],
+ *
+ * belongsTo: 'User',
+ * hasMany : {model: 'Comment', name: 'comments'}
+ * });
+ *
+ * Ext.define('Comment', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'user_id', 'post_id'],
+ *
+ * belongsTo: 'Post'
+ * });
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id'],
+ *
+ * hasMany: [
+ * 'Post',
+ * {model: 'Comment', name: 'comments'}
+ * ]
+ * });
+ *
+ * See the docs for {@link Ext.data.BelongsToAssociation} and {@link Ext.data.HasManyAssociation} for details on the
+ * usage and configuration of associations. Note that associations can also be specified like this:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id'],
+ *
+ * associations: [
+ * {type: 'hasMany', model: 'Post', name: 'posts'},
+ * {type: 'hasMany', model: 'Comment', name: 'comments'}
+ * ]
+ * });
+ *
+ * # Using a Proxy
+ *
+ * Models are great for representing types of data and relationships, but sooner or later we're going to want to load or
+ * save that data somewhere. All loading and saving of data is handled via a {@link Ext.data.proxy.Proxy Proxy}, which
+ * can be set directly on the Model:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', 'email'],
+ *
+ * proxy: {
+ * type: 'rest',
+ * url : '/users'
+ * }
+ * });
+ *
+ * Here we've set up a {@link Ext.data.proxy.Rest Rest Proxy}, which knows how to load and save data to and from a
+ * RESTful backend. Let's see how this works:
+ *
+ * var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
+ *
+ * user.save(); //POST /users
+ *
+ * Calling {@link #save} on the new Model instance tells the configured RestProxy that we wish to persist this Model's
+ * data onto our server. RestProxy figures out that this Model hasn't been saved before because it doesn't have an id,
+ * and performs the appropriate action - in this case issuing a POST request to the url we configured (/users). We
+ * configure any Proxy on any Model and always follow this API - see {@link Ext.data.proxy.Proxy} for a full list.
+ *
+ * Loading data via the Proxy is equally easy:
+ *
+ * //get a reference to the User model class
+ * var User = Ext.ModelManager.getModel('User');
+ *
+ * //Uses the configured RestProxy to make a GET request to /users/123
+ * User.load(123, {
+ * success: function(user) {
+ * console.log(user.getId()); //logs 123
+ * }
+ * });
+ *
+ * Models can also be updated and destroyed easily:
+ *
+ * //the user Model we loaded in the last snippet:
+ * user.set('name', 'Edward Spencer');
+ *
+ * //tells the Proxy to save the Model. In this case it will perform a PUT request to /users/123 as this Model already has an id
+ * user.save({
+ * success: function() {
+ * console.log('The User was updated');
+ * }
+ * });
+ *
+ * //tells the Proxy to destroy the Model. Performs a DELETE request to /users/123
+ * user.destroy({
+ * success: function() {
+ * console.log('The User was destroyed!');
+ * }
+ * });
+ *
+ * # Usage in Stores
+ *
+ * It is very common to want to load a set of Model instances to be displayed and manipulated in the UI. We do this by
+ * creating a {@link Ext.data.Store Store}:
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'User'
+ * });
+ *
+ * //uses the Proxy we set up on Model to load the Store data
+ * store.load();
+ *
+ * A Store is just a collection of Model instances - usually loaded from a server somewhere. Store can also maintain a
+ * set of added, updated and removed Model instances to be synchronized with the server via the Proxy. See the {@link
+ * Ext.data.Store Store docs} for more information on Stores.
+ *
+ * @constructor
+ * Creates new Model instance.
+ * @param {Object} data An object containing keys corresponding to this model's fields, and their associated values
+ * @param {Number} id (optional) Unique ID to assign to this model instance
+ */
+Ext.define('Ext.data.Model', {
+ alternateClassName: 'Ext.data.Record',
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ requires: [
+ 'Ext.ModelManager',
+ 'Ext.data.IdGenerator',
+ 'Ext.data.Field',
+ 'Ext.data.Errors',
+ 'Ext.data.Operation',
+ 'Ext.data.validations',
+ 'Ext.data.proxy.Ajax',
+ 'Ext.util.MixedCollection'
+ ],
+
+ onClassExtended: function(cls, data) {
+ var onBeforeClassCreated = data.onBeforeClassCreated;
+
+ data.onBeforeClassCreated = function(cls, data) {
+ var me = this,
+ name = Ext.getClassName(cls),
+ prototype = cls.prototype,
+ superCls = cls.prototype.superclass,
+
+ validations = data.validations || [],
+ fields = data.fields || [],
+ associations = data.associations || [],
+ belongsTo = data.belongsTo,
+ hasMany = data.hasMany,
+ idgen = data.idgen,
+
+ fieldsMixedCollection = new Ext.util.MixedCollection(false, function(field) {
+ return field.name;
+ }),
+
+ associationsMixedCollection = new Ext.util.MixedCollection(false, function(association) {
+ return association.name;
+ }),
+
+ superValidations = superCls.validations,
+ superFields = superCls.fields,
+ superAssociations = superCls.associations,
+
+ association, i, ln,
+ dependencies = [];
+
+ // Save modelName on class and its prototype
+ cls.modelName = name;
+ prototype.modelName = name;
+
+ // Merge the validations of the superclass and the new subclass
+ if (superValidations) {
+ validations = superValidations.concat(validations);
+ }
+
+ data.validations = validations;
+
+ // Merge the fields of the superclass and the new subclass
+ if (superFields) {
+ fields = superFields.items.concat(fields);
+ }
+
+ for (i = 0, ln = fields.length; i < ln; ++i) {
+ fieldsMixedCollection.add(new Ext.data.Field(fields[i]));
+ }
+
+ data.fields = fieldsMixedCollection;
+
+ if (idgen) {
+ data.idgen = Ext.data.IdGenerator.get(idgen);
+ }
+
+ //associations can be specified in the more convenient format (e.g. not inside an 'associations' array).
+ //we support that here
+ if (belongsTo) {
+ belongsTo = Ext.Array.from(belongsTo);
+
+ for (i = 0, ln = belongsTo.length; i < ln; ++i) {
+ association = belongsTo[i];
+
+ if (!Ext.isObject(association)) {
+ association = {model: association};
+ }
+
+ association.type = 'belongsTo';
+ associations.push(association);
+ }
+
+ delete data.belongsTo;
+ }
+
+ if (hasMany) {
+ hasMany = Ext.Array.from(hasMany);
+ for (i = 0, ln = hasMany.length; i < ln; ++i) {
+ association = hasMany[i];
+
+ if (!Ext.isObject(association)) {
+ association = {model: association};
+ }
+
+ association.type = 'hasMany';
+ associations.push(association);
+ }
+
+ delete data.hasMany;
+ }
+
+ if (superAssociations) {
+ associations = superAssociations.items.concat(associations);
+ }
+
+ for (i = 0, ln = associations.length; i < ln; ++i) {
+ dependencies.push('association.' + associations[i].type.toLowerCase());
+ }
+
+ if (data.proxy) {
+ if (typeof data.proxy === 'string') {
+ dependencies.push('proxy.' + data.proxy);
+ }
+ else if (typeof data.proxy.type === 'string') {
+ dependencies.push('proxy.' + data.proxy.type);
+ }
+ }
+
+ Ext.require(dependencies, function() {
+ Ext.ModelManager.registerType(name, cls);
+
+ for (i = 0, ln = associations.length; i < ln; ++i) {
+ association = associations[i];
+
+ Ext.apply(association, {
+ ownerModel: name,
+ associatedModel: association.model
+ });
+
+ if (Ext.ModelManager.getModel(association.model) === undefined) {
+ Ext.ModelManager.registerDeferredAssociation(association);
+ } else {
+ associationsMixedCollection.add(Ext.data.Association.create(association));
+ }
+ }
+
+ data.associations = associationsMixedCollection;
+
+ onBeforeClassCreated.call(me, cls, data);
+
+ cls.setProxy(cls.prototype.proxy || cls.prototype.defaultProxyType);
+
+ // Fire the onModelDefined template method on ModelManager
+ Ext.ModelManager.onModelDefined(cls);
+ });
+ };
+ },
+
+ inheritableStatics: {
+ /**
+ * Sets the Proxy to use for this model. Accepts any options that can be accepted by
+ * {@link Ext#createByAlias Ext.createByAlias}.
+ * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy
+ * @return {Ext.data.proxy.Proxy}
+ * @static
+ * @inheritable
+ */
+ setProxy: function(proxy) {
+ //make sure we have an Ext.data.proxy.Proxy object
+ if (!proxy.isProxy) {
+ if (typeof proxy == "string") {
+ proxy = {
+ type: proxy
+ };
+ }
+ proxy = Ext.createByAlias("proxy." + proxy.type, proxy);
+ }
+ proxy.setModel(this);
+ this.proxy = this.prototype.proxy = proxy;
+
+ return proxy;
+ },
+
+ /**
+ * Returns the configured Proxy for this Model
+ * @return {Ext.data.proxy.Proxy} The proxy
+ * @static
+ * @inheritable
+ */
+ getProxy: function() {
+ return this.proxy;
+ },
+
+ /**
+ * Asynchronously loads a model instance by id. Sample usage:
+ *
+ * MyApp.User = Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'id', type: 'int'},
+ * {name: 'name', type: 'string'}
+ * ]
+ * });
+ *
+ * MyApp.User.load(10, {
+ * scope: this,
+ * failure: function(record, operation) {
+ * //do something if the load failed
+ * },
+ * success: function(record, operation) {
+ * //do something if the load succeeded
+ * },
+ * callback: function(record, operation) {
+ * //do something whether the load succeeded or failed
+ * }
+ * });
+ *
+ * @param {Number} id The id of the model to load
+ * @param {Object} config (optional) config object containing success, failure and callback functions, plus
+ * optional scope
+ * @static
+ * @inheritable
+ */
+ load: function(id, config) {
+ config = Ext.apply({}, config);
+ config = Ext.applyIf(config, {
+ action: 'read',
+ id : id
+ });
+
+ var operation = Ext.create('Ext.data.Operation', config),
+ scope = config.scope || this,
+ record = null,
+ callback;
+
+ callback = function(operation) {
+ if (operation.wasSuccessful()) {
+ record = operation.getRecords()[0];
+ Ext.callback(config.success, scope, [record, operation]);
+ } else {
+ Ext.callback(config.failure, scope, [record, operation]);
+ }
+ Ext.callback(config.callback, scope, [record, operation]);
+ };
+
+ this.proxy.read(operation, callback, this);
+ }
+ },
+
+ statics: {
+ PREFIX : 'ext-record',
+ AUTO_ID: 1,
+ EDIT : 'edit',
+ REJECT : 'reject',
+ COMMIT : 'commit',
+
+ /**
+ * Generates a sequential id. This method is typically called when a record is {@link Ext#create
+ * create}d and {@link #constructor no id has been specified}. The id will automatically be assigned to the
+ * record. The returned id takes the form: {PREFIX}-{AUTO_ID}.
+ *
+ * - **PREFIX** : String - Ext.data.Model.PREFIX (defaults to 'ext-record')
+ * - **AUTO_ID** : String - Ext.data.Model.AUTO_ID (defaults to 1 initially)
+ *
+ * @param {Ext.data.Model} rec The record being created. The record does not exist, it's a {@link #phantom}.
+ * @return {String} auto-generated string id, `"ext-record-i++"`;
+ * @static
+ */
+ id: function(rec) {
+ var id = [this.PREFIX, '-', this.AUTO_ID++].join('');
+ rec.phantom = true;
+ rec.internalId = id;
+ return id;
+ }
+ },
+
+ /**
+ * @cfg {String/Object} idgen
+ * The id generator to use for this model. The default id generator does not generate
+ * values for the {@link #idProperty}.
+ *
+ * This can be overridden at the model level to provide a custom generator for a model.
+ * The simplest form of this would be:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * requires: ['Ext.data.SequentialIdGenerator'],
+ * idgen: 'sequential',
+ * ...
+ * });
+ *
+ * The above would generate {@link Ext.data.SequentialIdGenerator sequential} id's such
+ * as 1, 2, 3 etc..
+ *
+ * Another useful id generator is {@link Ext.data.UuidGenerator}:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * requires: ['Ext.data.UuidGenerator'],
+ * idgen: 'uuid',
+ * ...
+ * });
+ *
+ * An id generation can also be further configured:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * idgen: {
+ * type: 'sequential',
+ * seed: 1000,
+ * prefix: 'ID_'
+ * }
+ * });
+ *
+ * The above would generate id's such as ID_1000, ID_1001, ID_1002 etc..
+ *
+ * If multiple models share an id space, a single generator can be shared:
+ *
+ * Ext.define('MyApp.data.MyModelX', {
+ * extend: 'Ext.data.Model',
+ * idgen: {
+ * type: 'sequential',
+ * id: 'xy'
+ * }
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelY', {
+ * extend: 'Ext.data.Model',
+ * idgen: {
+ * type: 'sequential',
+ * id: 'xy'
+ * }
+ * });
+ *
+ * For more complex, shared id generators, a custom generator is the best approach.
+ * See {@link Ext.data.IdGenerator} for details on creating custom id generators.
+ *
+ * @markdown
+ */
+ idgen: {
+ isGenerator: true,
+ type: 'default',
+
+ generate: function () {
+ return null;
+ },
+ getRecId: function (rec) {
+ return rec.modelName + '-' + rec.internalId;
+ }
+ },
+
+ /**
+ * @property {Boolean} editing
+ * Internal flag used to track whether or not the model instance is currently being edited. Read-only.
+ */
+ editing : false,
+
+ /**
+ * @property {Boolean} dirty
+ * True if this Record has been modified. Read-only.
+ */
+ dirty : false,
+
+ /**
+ * @cfg {String} persistenceProperty
+ * The property on this Persistable object that its data is saved to. Defaults to 'data'
+ * (e.g. all persistable data resides in this.data.)
+ */
+ persistenceProperty: 'data',
+
+ evented: false,
+ isModel: true,
+
+ /**
+ * @property {Boolean} phantom
+ * True when the record does not yet exist in a server-side database (see {@link #setDirty}).
+ * Any record which has a real database pk set as its id property is NOT a phantom -- it's real.
+ */
+ phantom : false,
+
+ /**
+ * @cfg {String} idProperty
+ * The name of the field treated as this Model's unique id. Defaults to 'id'.
+ */
+ idProperty: 'id',
+
+ /**
+ * @cfg {String} defaultProxyType
+ * The string type of the default Model Proxy. Defaults to 'ajax'.
+ */
+ defaultProxyType: 'ajax',
+
+ // Fields config and property
+ /**
+ * @cfg {Object[]/String[]} fields
+ * The fields for this model.
+ */
+ /**
+ * @property {Ext.util.MixedCollection} fields
+ * The fields defined on this model.
+ */
+
+ /**
+ * @cfg {Object[]} validations
+ * An array of {@link Ext.data.validations validations} for this model.
+ */
+
+ // Associations configs and properties
+ /**
+ * @cfg {Object[]} associations
+ * An array of {@link Ext.data.Association associations} for this model.
+ */
+ /**
+ * @cfg {String/Object/String[]/Object[]} hasMany
+ * One or more {@link Ext.data.HasManyAssociation HasMany associations} for this model.
+ */
+ /**
+ * @cfg {String/Object/String[]/Object[]} belongsTo
+ * One or more {@link Ext.data.BelongsToAssociation BelongsTo associations} for this model.
+ */
+ /**
+ * @property {Ext.util.MixedCollection} associations
+ * {@link Ext.data.Association Associations} defined on this model.
+ */
+
+ /**
+ * @cfg {String/Object/Ext.data.proxy.Proxy} proxy
+ * The {@link Ext.data.proxy.Proxy proxy} to use for this model.
+ */
+
+ // raw not documented intentionally, meant to be used internally.
+ constructor: function(data, id, raw) {
+ data = data || {};
+
+ var me = this,
+ fields,
+ length,
+ field,
+ name,
+ i,
+ newId,
+ isArray = Ext.isArray(data),
+ newData = isArray ? {} : null; // to hold mapped array data if needed
+
+ /**
+ * An internal unique ID for each Model instance, used to identify Models that don't have an ID yet
+ * @property internalId
+ * @type String
+ * @private
+ */
+ me.internalId = (id || id === 0) ? id : Ext.data.Model.id(me);
+
+ /**
+ * @property {Object} raw The raw data used to create this model if created via a reader.
+ */
+ me.raw = raw;
+
+ Ext.applyIf(me, {
+ data: {}
+ });
+
+ /**
+ * @property {Object} modified Key: value pairs of all fields whose values have changed
+ */
+ me.modified = {};
+
+ // Deal with spelling error in previous releases
+ if (me.persistanceProperty) {
+ me.persistenceProperty = me.persistanceProperty;
+ }
+ me[me.persistenceProperty] = {};
+
+ me.mixins.observable.constructor.call(me);
+
+ //add default field values if present
+ fields = me.fields.items;
+ length = fields.length;
+
+ for (i = 0; i < length; i++) {
+ field = fields[i];
+ name = field.name;
+
+ if (isArray){
+ // Have to map array data so the values get assigned to the named fields
+ // rather than getting set as the field names with undefined values.
+ newData[name] = data[i];
+ }
+ else if (data[name] === undefined) {
+ data[name] = field.defaultValue;
+ }
+ }
+
+ me.set(newData || data);
+
+ if (me.getId()) {
+ me.phantom = false;
+ } else if (me.phantom) {
+ newId = me.idgen.generate();
+ if (newId !== null) {
+ me.setId(newId);
+ }
+ }
+
+ // clear any dirty/modified since we're initializing
+ me.dirty = false;
+ me.modified = {};
+
+ if (typeof me.init == 'function') {
+ me.init();
+ }
+
+ me.id = me.idgen.getRecId(me);
+ },
+
+ /**
+ * Returns the value of the given field
+ * @param {String} fieldName The field to fetch the value for
+ * @return {Object} The value
+ */
+ get: function(field) {
+ return this[this.persistenceProperty][field];
+ },
+
+ /**
+ * Sets the given field to the given value, marks the instance as dirty
+ * @param {String/Object} fieldName The field to set, or an object containing key/value pairs
+ * @param {Object} value The value to set
+ */
+ set: function(fieldName, value) {
+ var me = this,
+ fields = me.fields,
+ modified = me.modified,
+ convertFields = [],
+ field, key, i, currentValue, notEditing, count, length;
+
+ /*
+ * If we're passed an object, iterate over that object. NOTE: we pull out fields with a convert function and
+ * set those last so that all other possible data is set before the convert function is called
+ */
+ if (arguments.length == 1 && Ext.isObject(fieldName)) {
+ notEditing = !me.editing;
+ count = 0;
+ for (key in fieldName) {
+ if (fieldName.hasOwnProperty(key)) {
+
+ //here we check for the custom convert function. Note that if a field doesn't have a convert function,
+ //we default it to its type's convert function, so we have to check that here. This feels rather dirty.
+ field = fields.get(key);
+ if (field && field.convert !== field.type.convert) {
+ convertFields.push(key);
+ continue;
+ }
+
+ if (!count && notEditing) {
+ me.beginEdit();
+ }
+ ++count;
+ me.set(key, fieldName[key]);
+ }
+ }
+
+ length = convertFields.length;
+ if (length) {
+ if (!count && notEditing) {
+ me.beginEdit();
+ }
+ count += length;
+ for (i = 0; i < length; i++) {
+ field = convertFields[i];
+ me.set(field, fieldName[field]);
+ }
+ }
+
+ if (notEditing && count) {
+ me.endEdit();
+ }
+ } else {
+ if (fields) {
+ field = fields.get(fieldName);
+
+ if (field && field.convert) {
+ value = field.convert(value, me);
+ }
+ }
+ currentValue = me.get(fieldName);
+ me[me.persistenceProperty][fieldName] = value;
+
+ if (field && field.persist && !me.isEqual(currentValue, value)) {
+ if (me.isModified(fieldName)) {
+ if (me.isEqual(modified[fieldName], value)) {
+ // the original value in me.modified equals the new value, so the
+ // field is no longer modified
+ delete modified[fieldName];
+ // we might have removed the last modified field, so check to see if
+ // there are any modified fields remaining and correct me.dirty:
+ me.dirty = false;
+ for (key in modified) {
+ if (modified.hasOwnProperty(key)){
+ me.dirty = true;
+ break;
+ }
+ }
+ }
+ } else {
+ me.dirty = true;
+ modified[fieldName] = currentValue;
+ }
+ }
+
+ if (!me.editing) {
+ me.afterEdit();
+ }
+ }
+ },
+
+ /**
+ * Checks if two values are equal, taking into account certain
+ * special factors, for example dates.
+ * @private
+ * @param {Object} a The first value
+ * @param {Object} b The second value
+ * @return {Boolean} True if the values are equal
+ */
+ isEqual: function(a, b){
+ if (Ext.isDate(a) && Ext.isDate(b)) {
+ return a.getTime() === b.getTime();
+ }
+ return a === b;
+ },
+
+ /**
+ * Begins an edit. While in edit mode, no events (e.g.. the `update` event) are relayed to the containing store.
+ * When an edit has begun, it must be followed by either {@link #endEdit} or {@link #cancelEdit}.
+ */
+ beginEdit : function(){
+ var me = this;
+ if (!me.editing) {
+ me.editing = true;
+ me.dirtySave = me.dirty;
+ me.dataSave = Ext.apply({}, me[me.persistenceProperty]);
+ me.modifiedSave = Ext.apply({}, me.modified);
+ }
+ },
+
+ /**
+ * Cancels all changes made in the current edit operation.
+ */
+ cancelEdit : function(){
+ var me = this;
+ if (me.editing) {
+ me.editing = false;
+ // reset the modified state, nothing changed since the edit began
+ me.modified = me.modifiedSave;
+ me[me.persistenceProperty] = me.dataSave;
+ me.dirty = me.dirtySave;
+ delete me.modifiedSave;
+ delete me.dataSave;
+ delete me.dirtySave;
+ }
+ },
+
+ /**
+ * Ends an edit. If any data was modified, the containing store is notified (ie, the store's `update` event will
+ * fire).
+ * @param {Boolean} silent True to not notify the store of the change
+ */
+ endEdit : function(silent){
+ var me = this,
+ didChange;
+
+ if (me.editing) {
+ me.editing = false;
+ didChange = me.dirty || me.changedWhileEditing();
+ delete me.modifiedSave;
+ delete me.dataSave;
+ delete me.dirtySave;
+ if (silent !== true && didChange) {
+ me.afterEdit();
+ }
+ }
+ },
+
+ /**
+ * Checks if the underlying data has changed during an edit. This doesn't necessarily
+ * mean the record is dirty, however we still need to notify the store since it may need
+ * to update any views.
+ * @private
+ * @return {Boolean} True if the underlying data has changed during an edit.
+ */
+ changedWhileEditing: function(){
+ var me = this,
+ saved = me.dataSave,
+ data = me[me.persistenceProperty],
+ key;
+
+ for (key in data) {
+ if (data.hasOwnProperty(key)) {
+ if (!me.isEqual(data[key], saved[key])) {
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Gets a hash of only the fields that have been modified since this Model was created or commited.
+ * @return {Object}
+ */
+ getChanges : function(){
+ var modified = this.modified,
+ changes = {},
+ field;
+
+ for (field in modified) {
+ if (modified.hasOwnProperty(field)){
+ changes[field] = this.get(field);
+ }
+ }
+
+ return changes;
+ },
+
+ /**
+ * Returns true if the passed field name has been `{@link #modified}` since the load or last commit.
+ * @param {String} fieldName {@link Ext.data.Field#name}
+ * @return {Boolean}
+ */
+ isModified : function(fieldName) {
+ return this.modified.hasOwnProperty(fieldName);
+ },
+
+ /**
+ * Marks this **Record** as `{@link #dirty}`. This method is used interally when adding `{@link #phantom}` records
+ * to a {@link Ext.data.proxy.Server#writer writer enabled store}.
+ *
+ * Marking a record `{@link #dirty}` causes the phantom to be returned by {@link Ext.data.Store#getUpdatedRecords}
+ * where it will have a create action composed for it during {@link Ext.data.Model#save model save} operations.
+ */
+ setDirty : function() {
+ var me = this,
+ name;
+
+ me.dirty = true;
+
+ me.fields.each(function(field) {
+ if (field.persist) {
+ name = field.name;
+ me.modified[name] = me.get(name);
+ }
+ }, me);
+ },
+
+
+ /**
+ * Usually called by the {@link Ext.data.Store} to which this model instance has been {@link #join joined}. Rejects
+ * all changes made to the model instance since either creation, or the last commit operation. Modified fields are
+ * reverted to their original values.
+ *
+ * Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of reject
+ * operations.
+ *
+ * @param {Boolean} silent (optional) True to skip notification of the owning store of the change.
+ * Defaults to false.
+ */
+ reject : function(silent) {
+ var me = this,
+ modified = me.modified,
+ field;
+
+ for (field in modified) {
+ if (modified.hasOwnProperty(field)) {
+ if (typeof modified[field] != "function") {
+ me[me.persistenceProperty][field] = modified[field];
+ }
+ }
+ }
+
+ me.dirty = false;
+ me.editing = false;
+ me.modified = {};
+
+ if (silent !== true) {
+ me.afterReject();
+ }
+ },
+
+ /**
+ * Usually called by the {@link Ext.data.Store} which owns the model instance. Commits all changes made to the
+ * instance since either creation or the last commit operation.
+ *
+ * Developers should subscribe to the {@link Ext.data.Store#update} event to have their code notified of commit
+ * operations.
+ *
+ * @param {Boolean} silent (optional) True to skip notification of the owning store of the change.
+ * Defaults to false.
+ */
+ commit : function(silent) {
+ var me = this;
+
+ me.phantom = me.dirty = me.editing = false;
+ me.modified = {};
+
+ if (silent !== true) {
+ me.afterCommit();
+ }
+ },
+
+ /**
+ * Creates a copy (clone) of this Model instance.
+ *
+ * @param {String} [id] A new id, defaults to the id of the instance being copied.
+ * See `{@link Ext.data.Model#id id}`. To generate a phantom instance with a new id use:
+ *
+ * var rec = record.copy(); // clone the record
+ * Ext.data.Model.id(rec); // automatically generate a unique sequential id
+ *
+ * @return {Ext.data.Model}
+ */
+ copy : function(newId) {
+ var me = this;
+
+ return new me.self(Ext.apply({}, me[me.persistenceProperty]), newId || me.internalId);
+ },
+
+ /**
+ * Sets the Proxy to use for this model. Accepts any options that can be accepted by
+ * {@link Ext#createByAlias Ext.createByAlias}.
+ *
+ * @param {String/Object/Ext.data.proxy.Proxy} proxy The proxy
+ * @return {Ext.data.proxy.Proxy}
+ */
+ setProxy: function(proxy) {
+ //make sure we have an Ext.data.proxy.Proxy object
+ if (!proxy.isProxy) {
+ if (typeof proxy === "string") {
+ proxy = {
+ type: proxy
+ };
+ }
+ proxy = Ext.createByAlias("proxy." + proxy.type, proxy);
+ }
+ proxy.setModel(this.self);
+ this.proxy = proxy;
+
+ return proxy;
+ },
+
+ /**
+ * Returns the configured Proxy for this Model.
+ * @return {Ext.data.proxy.Proxy} The proxy
+ */
+ getProxy: function() {
+ return this.proxy;
+ },
+
+ /**
+ * Validates the current data against all of its configured {@link #validations}.
+ * @return {Ext.data.Errors} The errors object
+ */
+ validate: function() {
+ var errors = Ext.create('Ext.data.Errors'),
+ validations = this.validations,
+ validators = Ext.data.validations,
+ length, validation, field, valid, type, i;
+
+ if (validations) {
+ length = validations.length;
+
+ for (i = 0; i < length; i++) {
+ validation = validations[i];
+ field = validation.field || validation.name;
+ type = validation.type;
+ valid = validators[type](validation, this.get(field));
+
+ if (!valid) {
+ errors.add({
+ field : field,
+ message: validation.message || validators[type + 'Message']
+ });
+ }
+ }
+ }
+
+ return errors;
+ },
+
+ /**
+ * Checks if the model is valid. See {@link #validate}.
+ * @return {Boolean} True if the model is valid.
+ */
+ isValid: function(){
+ return this.validate().isValid();
+ },
+
+ /**
+ * Saves the model instance using the configured proxy.
+ * @param {Object} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
+ * @return {Ext.data.Model} The Model instance
+ */
+ save: function(options) {
+ options = Ext.apply({}, options);
+
+ var me = this,
+ action = me.phantom ? 'create' : 'update',
+ record = null,
+ scope = options.scope || me,
+ operation,
+ callback;
+
+ Ext.apply(options, {
+ records: [me],
+ action : action
+ });
+
+ operation = Ext.create('Ext.data.Operation', options);
+
+ callback = function(operation) {
+ if (operation.wasSuccessful()) {
+ record = operation.getRecords()[0];
+ //we need to make sure we've set the updated data here. Ideally this will be redundant once the
+ //ModelCache is in place
+ me.set(record.data);
+ record.dirty = false;
+
+ Ext.callback(options.success, scope, [record, operation]);
+ } else {
+ Ext.callback(options.failure, scope, [record, operation]);
+ }
+
+ Ext.callback(options.callback, scope, [record, operation]);
+ };
+
+ me.getProxy()[action](operation, callback, me);
+
+ return me;
+ },
+
+ /**
+ * Destroys the model using the configured proxy.
+ * @param {Object} options Options to pass to the proxy. Config object for {@link Ext.data.Operation}.
+ * @return {Ext.data.Model} The Model instance
+ */
+ destroy: function(options){
+ options = Ext.apply({}, options);
+
+ var me = this,
+ record = null,
+ scope = options.scope || me,
+ operation,
+ callback;
+
+ Ext.apply(options, {
+ records: [me],
+ action : 'destroy'
+ });
+
+ operation = Ext.create('Ext.data.Operation', options);
+ callback = function(operation) {
+ if (operation.wasSuccessful()) {
+ Ext.callback(options.success, scope, [record, operation]);
+ } else {
+ Ext.callback(options.failure, scope, [record, operation]);
+ }
+ Ext.callback(options.callback, scope, [record, operation]);
+ };
+
+ me.getProxy().destroy(operation, callback, me);
+ return me;
+ },
+
+ /**
+ * Returns the unique ID allocated to this model instance as defined by {@link #idProperty}.
+ * @return {Number} The id
+ */
+ getId: function() {
+ return this.get(this.idProperty);
+ },
+
+ /**
+ * Sets the model instance's id field to the given id.
+ * @param {Number} id The new id
+ */
+ setId: function(id) {
+ this.set(this.idProperty, id);
+ },
+
+ /**
+ * Tells this model instance that it has been added to a store.
+ * @param {Ext.data.Store} store The store to which this model has been added.
+ */
+ join : function(store) {
+ /**
+ * @property {Ext.data.Store} store
+ * The {@link Ext.data.Store Store} to which this Record belongs.
+ */
+ this.store = store;
+ },
+
+ /**
+ * Tells this model instance that it has been removed from the store.
+ * @param {Ext.data.Store} store The store from which this model has been removed.
+ */
+ unjoin: function(store) {
+ delete this.store;
+ },
+
+ /**
+ * @private
+ * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
+ * afterEdit method is called
+ */
+ afterEdit : function() {
+ this.callStore('afterEdit');
+ },
+
+ /**
+ * @private
+ * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
+ * afterReject method is called
+ */
+ afterReject : function() {
+ this.callStore("afterReject");
+ },
+
+ /**
+ * @private
+ * If this Model instance has been {@link #join joined} to a {@link Ext.data.Store store}, the store's
+ * afterCommit method is called
+ */
+ afterCommit: function() {
+ this.callStore('afterCommit');
+ },
+
+ /**
+ * @private
+ * Helper function used by afterEdit, afterReject and afterCommit. Calls the given method on the
+ * {@link Ext.data.Store store} that this instance has {@link #join joined}, if any. The store function
+ * will always be called with the model instance as its single argument.
+ * @param {String} fn The function to call on the store
+ */
+ callStore: function(fn) {
+ var store = this.store;
+
+ if (store !== undefined && typeof store[fn] == "function") {
+ store[fn](this);
+ }
+ },
+
+ /**
+ * Gets all of the data from this Models *loaded* associations. It does this recursively - for example if we have a
+ * User which hasMany Orders, and each Order hasMany OrderItems, it will return an object like this:
+ *
+ * {
+ * orders: [
+ * {
+ * id: 123,
+ * status: 'shipped',
+ * orderItems: [
+ * ...
+ * ]
+ * }
+ * ]
+ * }
+ *
+ * @return {Object} The nested data set for the Model's loaded associations
+ */
+ getAssociatedData: function(){
+ return this.prepareAssociatedData(this, [], null);
+ },
+
+ /**
+ * @private
+ * This complex-looking method takes a given Model instance and returns an object containing all data from
+ * all of that Model's *loaded* associations. See (@link #getAssociatedData}
+ * @param {Ext.data.Model} record The Model instance
+ * @param {String[]} ids PRIVATE. The set of Model instance internalIds that have already been loaded
+ * @param {String} associationType (optional) The name of the type of association to limit to.
+ * @return {Object} The nested data set for the Model's loaded associations
+ */
+ prepareAssociatedData: function(record, ids, associationType) {
+ //we keep track of all of the internalIds of the models that we have loaded so far in here
+ var associations = record.associations.items,
+ associationCount = associations.length,
+ associationData = {},
+ associatedStore, associatedName, associatedRecords, associatedRecord,
+ associatedRecordCount, association, id, i, j, type, allow;
+
+ for (i = 0; i < associationCount; i++) {
+ association = associations[i];
+ type = association.type;
+ allow = true;
+ if (associationType) {
+ allow = type == associationType;
+ }
+ if (allow && type == 'hasMany') {
+
+ //this is the hasMany store filled with the associated data
+ associatedStore = record[association.storeName];
+
+ //we will use this to contain each associated record's data
+ associationData[association.name] = [];
+
+ //if it's loaded, put it into the association data
+ if (associatedStore && associatedStore.data.length > 0) {
+ associatedRecords = associatedStore.data.items;
+ associatedRecordCount = associatedRecords.length;
+
+ //now we're finally iterating over the records in the association. We do this recursively
+ for (j = 0; j < associatedRecordCount; j++) {
+ associatedRecord = associatedRecords[j];
+ // Use the id, since it is prefixed with the model name, guaranteed to be unique
+ id = associatedRecord.id;
+
+ //when we load the associations for a specific model instance we add it to the set of loaded ids so that
+ //we don't load it twice. If we don't do this, we can fall into endless recursive loading failures.
+ if (Ext.Array.indexOf(ids, id) == -1) {
+ ids.push(id);
+
+ associationData[association.name][j] = associatedRecord.data;
+ Ext.apply(associationData[association.name][j], this.prepareAssociatedData(associatedRecord, ids, type));
+ }
+ }
+ }
+ } else if (allow && type == 'belongsTo') {
+ associatedRecord = record[association.instanceName];
+ if (associatedRecord !== undefined) {
+ id = associatedRecord.id;
+ if (Ext.Array.indexOf(ids, id) == -1) {
+ ids.push(id);
+ associationData[association.name] = associatedRecord.data;
+ Ext.apply(associationData[association.name], this.prepareAssociatedData(associatedRecord, ids, type));
+ }
+ }
+ }
+ }
+
+ return associationData;
+ }
+});
+
+/**
+ * @docauthor Evan Trimboli <evan@sencha.com>
+ *
+ * Contains a collection of all stores that are created that have an identifier. An identifier can be assigned by
+ * setting the {@link Ext.data.AbstractStore#storeId storeId} property. When a store is in the StoreManager, it can be
+ * referred to via it's identifier:
+ *
+ * Ext.create('Ext.data.Store', {
+ * model: 'SomeModel',
+ * storeId: 'myStore'
+ * });
+ *
+ * var store = Ext.data.StoreManager.lookup('myStore');
+ *
+ * Also note that the {@link #lookup} method is aliased to {@link Ext#getStore} for convenience.
+ *
+ * If a store is registered with the StoreManager, you can also refer to the store by it's identifier when registering
+ * it with any Component that consumes data from a store:
+ *
+ * Ext.create('Ext.data.Store', {
+ * model: 'SomeModel',
+ * storeId: 'myStore'
+ * });
+ *
+ * Ext.create('Ext.view.View', {
+ * store: 'myStore',
+ * // other configuration here
+ * });
+ *
+ */
+Ext.define('Ext.data.StoreManager', {
+ extend: 'Ext.util.MixedCollection',
+ alternateClassName: ['Ext.StoreMgr', 'Ext.data.StoreMgr', 'Ext.StoreManager'],
+ singleton: true,
+ uses: ['Ext.data.ArrayStore'],
+
+ /**
+ * @cfg {Object} listeners @hide
+ */
+
+ /**
+ * Registers one or more Stores with the StoreManager. You do not normally need to register stores manually. Any
+ * store initialized with a {@link Ext.data.Store#storeId} will be auto-registered.
+ * @param {Ext.data.Store...} stores Any number of Store instances
+ */
+ register : function() {
+ for (var i = 0, s; (s = arguments[i]); i++) {
+ this.add(s);
+ }
+ },
+
+ /**
+ * Unregisters one or more Stores with the StoreManager
+ * @param {String/Object...} stores Any number of Store instances or ID-s
+ */
+ unregister : function() {
+ for (var i = 0, s; (s = arguments[i]); i++) {
+ this.remove(this.lookup(s));
+ }
+ },
+
+ /**
+ * Gets a registered Store by id
+ * @param {String/Object} store The id of the Store, or a Store instance, or a store configuration
+ * @return {Ext.data.Store}
+ */
+ lookup : function(store) {
+ // handle the case when we are given an array or an array of arrays.
+ if (Ext.isArray(store)) {
+ var fields = ['field1'],
+ expand = !Ext.isArray(store[0]),
+ data = store,
+ i,
+ len;
+
+ if(expand){
+ data = [];
+ for (i = 0, len = store.length; i < len; ++i) {
+ data.push([store[i]]);
+ }
+ } else {
+ for(i = 2, len = store[0].length; i <= len; ++i){
+ fields.push('field' + i);
+ }
+ }
+ return Ext.create('Ext.data.ArrayStore', {
+ data : data,
+ fields: fields,
+ autoDestroy: true,
+ autoCreated: true,
+ expanded: expand
+ });
+ }
+
+ if (Ext.isString(store)) {
+ // store id
+ return this.get(store);
+ } else {
+ // store instance or store config
+ return Ext.data.AbstractStore.create(store);
+ }
+ },
+
+ // getKey implementation for MixedCollection
+ getKey : function(o) {
+ return o.storeId;
+ }
+}, function() {
+ /**
+ * Creates a new store for the given id and config, then registers it with the {@link Ext.data.StoreManager Store Mananger}.
+ * Sample usage:
+ *
+ * Ext.regStore('AllUsers', {
+ * model: 'User'
+ * });
+ *
+ * // the store can now easily be used throughout the application
+ * new Ext.List({
+ * store: 'AllUsers',
+ * ... other config
+ * });
+ *
+ * @param {String} id The id to set on the new store
+ * @param {Object} config The store config
+ * @member Ext
+ * @method regStore
+ */
+ Ext.regStore = function(name, config) {
+ var store;
+
+ if (Ext.isObject(name)) {
+ config = name;
+ } else {
+ config.storeId = name;
+ }
+
+ if (config instanceof Ext.data.Store) {
+ store = config;
+ } else {
+ store = Ext.create('Ext.data.Store', config);
+ }
+
+ return Ext.data.StoreManager.register(store);
+ };
+
+ /**
+ * Shortcut to {@link Ext.data.StoreManager#lookup}.
+ * @member Ext
+ * @method getStore
+ * @alias Ext.data.StoreManager#lookup
+ */
+ Ext.getStore = function(name) {
+ return Ext.data.StoreManager.lookup(name);
+ };
+});
+
+/**
+ * Base class for all Ext components. All subclasses of Component may participate in the automated Ext component
+ * lifecycle of creation, rendering and destruction which is provided by the {@link Ext.container.Container Container}
+ * class. Components may be added to a Container through the {@link Ext.container.Container#items items} config option
+ * at the time the Container is created, or they may be added dynamically via the
+ * {@link Ext.container.Container#add add} method.
+ *
+ * The Component base class has built-in support for basic hide/show and enable/disable and size control behavior.
+ *
+ * All Components are registered with the {@link Ext.ComponentManager} on construction so that they can be referenced at
+ * any time via {@link Ext#getCmp Ext.getCmp}, passing the {@link #id}.
+ *
+ * All user-developed visual widgets that are required to participate in automated lifecycle and size management should
+ * subclass Component.
+ *
+ * See the [Creating new UI controls][1] tutorial for details on how and to either extend or augment ExtJs base classes
+ * to create custom Components.
+ *
+ * Every component has a specific xtype, which is its Ext-specific type name, along with methods for checking the xtype
+ * like {@link #getXType} and {@link #isXType}. See the [Component Guide][2] for more information on xtypes and the
+ * Component hierarchy.
+ *
+ * This is the list of all valid xtypes:
+ *
+ * xtype Class
+ * ------------- ------------------
+ * button {@link Ext.button.Button}
+ * buttongroup {@link Ext.container.ButtonGroup}
+ * colorpalette {@link Ext.picker.Color}
+ * component {@link Ext.Component}
+ * container {@link Ext.container.Container}
+ * cycle {@link Ext.button.Cycle}
+ * dataview {@link Ext.view.View}
+ * datepicker {@link Ext.picker.Date}
+ * editor {@link Ext.Editor}
+ * editorgrid {@link Ext.grid.plugin.Editing}
+ * grid {@link Ext.grid.Panel}
+ * multislider {@link Ext.slider.Multi}
+ * panel {@link Ext.panel.Panel}
+ * progressbar {@link Ext.ProgressBar}
+ * slider {@link Ext.slider.Single}
+ * splitbutton {@link Ext.button.Split}
+ * tabpanel {@link Ext.tab.Panel}
+ * treepanel {@link Ext.tree.Panel}
+ * viewport {@link Ext.container.Viewport}
+ * window {@link Ext.window.Window}
+ *
+ * Toolbar components
+ * ---------------------------------------
+ * pagingtoolbar {@link Ext.toolbar.Paging}
+ * toolbar {@link Ext.toolbar.Toolbar}
+ * tbfill {@link Ext.toolbar.Fill}
+ * tbitem {@link Ext.toolbar.Item}
+ * tbseparator {@link Ext.toolbar.Separator}
+ * tbspacer {@link Ext.toolbar.Spacer}
+ * tbtext {@link Ext.toolbar.TextItem}
+ *
+ * Menu components
+ * ---------------------------------------
+ * menu {@link Ext.menu.Menu}
+ * menucheckitem {@link Ext.menu.CheckItem}
+ * menuitem {@link Ext.menu.Item}
+ * menuseparator {@link Ext.menu.Separator}
+ * menutextitem {@link Ext.menu.Item}
+ *
+ * Form components
+ * ---------------------------------------
+ * form {@link Ext.form.Panel}
+ * checkbox {@link Ext.form.field.Checkbox}
+ * combo {@link Ext.form.field.ComboBox}
+ * datefield {@link Ext.form.field.Date}
+ * displayfield {@link Ext.form.field.Display}
+ * field {@link Ext.form.field.Base}
+ * fieldset {@link Ext.form.FieldSet}
+ * hidden {@link Ext.form.field.Hidden}
+ * htmleditor {@link Ext.form.field.HtmlEditor}
+ * label {@link Ext.form.Label}
+ * numberfield {@link Ext.form.field.Number}
+ * radio {@link Ext.form.field.Radio}
+ * radiogroup {@link Ext.form.RadioGroup}
+ * textarea {@link Ext.form.field.TextArea}
+ * textfield {@link Ext.form.field.Text}
+ * timefield {@link Ext.form.field.Time}
+ * trigger {@link Ext.form.field.Trigger}
+ *
+ * Chart components
+ * ---------------------------------------
+ * chart {@link Ext.chart.Chart}
+ * barchart {@link Ext.chart.series.Bar}
+ * columnchart {@link Ext.chart.series.Column}
+ * linechart {@link Ext.chart.series.Line}
+ * piechart {@link Ext.chart.series.Pie}
+ *
+ * It should not usually be necessary to instantiate a Component because there are provided subclasses which implement
+ * specialized Component use cases which cover most application needs. However it is possible to instantiate a base
+ * Component, and it will be renderable, or will particpate in layouts as the child item of a Container:
+ *
+ * @example
+ * Ext.create('Ext.Component', {
+ * html: 'Hello world!',
+ * width: 300,
+ * height: 200,
+ * padding: 20,
+ * style: {
+ * color: '#FFFFFF',
+ * backgroundColor:'#000000'
+ * },
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * The Component above creates its encapsulating `div` upon render, and use the configured HTML as content. More complex
+ * internal structure may be created using the {@link #renderTpl} configuration, although to display database-derived
+ * mass data, it is recommended that an ExtJS data-backed Component such as a {@link Ext.view.View View}, or {@link
+ * Ext.grid.Panel GridPanel}, or {@link Ext.tree.Panel TreePanel} be used.
+ *
+ * [1]: http://sencha.com/learn/Tutorial:Creating_new_UI_controls
+ */
+Ext.define('Ext.Component', {
+
+ /* Begin Definitions */
+
+ alias: ['widget.component', 'widget.box'],
+
+ extend: 'Ext.AbstractComponent',
+
+ requires: [
+ 'Ext.util.DelayedTask'
+ ],
+
+ uses: [
+ 'Ext.Layer',
+ 'Ext.resizer.Resizer',
+ 'Ext.util.ComponentDragger'
+ ],
+
+ mixins: {
+ floating: 'Ext.util.Floating'
+ },
+
+ statics: {
+ // Collapse/expand directions
+ DIRECTION_TOP: 'top',
+ DIRECTION_RIGHT: 'right',
+ DIRECTION_BOTTOM: 'bottom',
+ DIRECTION_LEFT: 'left',
+
+ VERTICAL_DIRECTION_Re: /^(?:top|bottom)$/,
+
+ // RegExp whih specifies characters in an xtype which must be translated to '-' when generating auto IDs.
+ // This includes dot, comma and whitespace
+ INVALID_ID_CHARS_Re: /[\.,\s]/g
+ },
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Boolean/Object} resizable
+ * Specify as `true` to apply a {@link Ext.resizer.Resizer Resizer} to this Component after rendering.
+ *
+ * May also be specified as a config object to be passed to the constructor of {@link Ext.resizer.Resizer Resizer}
+ * to override any defaults. By default the Component passes its minimum and maximum size, and uses
+ * `{@link Ext.resizer.Resizer#dynamic}: false`
+ */
+
+ /**
+ * @cfg {String} resizeHandles
+ * A valid {@link Ext.resizer.Resizer} handles config string. Only applies when resizable = true.
+ */
+ resizeHandles: 'all',
+
+ /**
+ * @cfg {Boolean} [autoScroll=false]
+ * `true` to use overflow:'auto' on the components layout element and show scroll bars automatically when necessary,
+ * `false` to clip any overflowing content.
+ */
+
+ /**
+ * @cfg {Boolean} floating
+ * Specify as true to float the Component outside of the document flow using CSS absolute positioning.
+ *
+ * Components such as {@link Ext.window.Window Window}s and {@link Ext.menu.Menu Menu}s are floating by default.
+ *
+ * Floating Components that are programatically {@link Ext.Component#render rendered} will register themselves with
+ * the global {@link Ext.WindowManager ZIndexManager}
+ *
+ * ### Floating Components as child items of a Container
+ *
+ * A floating Component may be used as a child item of a Container. This just allows the floating Component to seek
+ * a ZIndexManager by examining the ownerCt chain.
+ *
+ * When configured as floating, Components acquire, at render time, a {@link Ext.ZIndexManager ZIndexManager} which
+ * manages a stack of related floating Components. The ZIndexManager brings a single floating Component to the top
+ * of its stack when the Component's {@link #toFront} method is called.
+ *
+ * The ZIndexManager is found by traversing up the {@link #ownerCt} chain to find an ancestor which itself is
+ * floating. This is so that descendant floating Components of floating _Containers_ (Such as a ComboBox dropdown
+ * within a Window) can have its zIndex managed relative to any siblings, but always **above** that floating
+ * ancestor Container.
+ *
+ * If no floating ancestor is found, a floating Component registers itself with the default {@link Ext.WindowManager
+ * ZIndexManager}.
+ *
+ * Floating components _do not participate in the Container's layout_. Because of this, they are not rendered until
+ * you explicitly {@link #show} them.
+ *
+ * After rendering, the ownerCt reference is deleted, and the {@link #floatParent} property is set to the found
+ * floating ancestor Container. If no floating ancestor Container was found the {@link #floatParent} property will
+ * not be set.
+ */
+ floating: false,
+
+ /**
+ * @cfg {Boolean} toFrontOnShow
+ * True to automatically call {@link #toFront} when the {@link #show} method is called on an already visible,
+ * floating component.
+ */
+ toFrontOnShow: true,
+
+ /**
+ * @property {Ext.ZIndexManager} zIndexManager
+ * Only present for {@link #floating} Components after they have been rendered.
+ *
+ * A reference to the ZIndexManager which is managing this Component's z-index.
+ *
+ * The {@link Ext.ZIndexManager ZIndexManager} maintains a stack of floating Component z-indices, and also provides
+ * a single modal mask which is insert just beneath the topmost visible modal floating Component.
+ *
+ * Floating Components may be {@link #toFront brought to the front} or {@link #toBack sent to the back} of the
+ * z-index stack.
+ *
+ * This defaults to the global {@link Ext.WindowManager ZIndexManager} for floating Components that are
+ * programatically {@link Ext.Component#render rendered}.
+ *
+ * For {@link #floating} Components which are added to a Container, the ZIndexManager is acquired from the first
+ * ancestor Container found which is floating, or if not found the global {@link Ext.WindowManager ZIndexManager} is
+ * used.
+ *
+ * See {@link #floating} and {@link #floatParent}
+ */
+
+ /**
+ * @property {Ext.Container} floatParent
+ * Only present for {@link #floating} Components which were inserted as descendant items of floating Containers.
+ *
+ * Floating Components that are programatically {@link Ext.Component#render rendered} will not have a `floatParent`
+ * property.
+ *
+ * For {@link #floating} Components which are child items of a Container, the floatParent will be the floating
+ * ancestor Container which is responsible for the base z-index value of all its floating descendants. It provides
+ * a {@link Ext.ZIndexManager ZIndexManager} which provides z-indexing services for all its descendant floating
+ * Components.
+ *
+ * For example, the dropdown {@link Ext.view.BoundList BoundList} of a ComboBox which is in a Window will have the
+ * Window as its `floatParent`
+ *
+ * See {@link #floating} and {@link #zIndexManager}
+ */
+
+ /**
+ * @cfg {Boolean/Object} [draggable=false]
+ * Specify as true to make a {@link #floating} Component draggable using the Component's encapsulating element as
+ * the drag handle.
+ *
+ * This may also be specified as a config object for the {@link Ext.util.ComponentDragger ComponentDragger} which is
+ * instantiated to perform dragging.
+ *
+ * For example to create a Component which may only be dragged around using a certain internal element as the drag
+ * handle, use the delegate option:
+ *
+ * new Ext.Component({
+ * constrain: true,
+ * floating: true,
+ * style: {
+ * backgroundColor: '#fff',
+ * border: '1px solid black'
+ * },
+ * html: '<h1 style="cursor:move">The title</h1><p>The content</p>',
+ * draggable: {
+ * delegate: 'h1'
+ * }
+ * }).show();
+ */
+
+ /**
+ * @cfg {Boolean} [maintainFlex=false]
+ * **Only valid when a sibling element of a {@link Ext.resizer.Splitter Splitter} within a
+ * {@link Ext.layout.container.VBox VBox} or {@link Ext.layout.container.HBox HBox} layout.**
+ *
+ * Specifies that if an immediate sibling Splitter is moved, the Component on the *other* side is resized, and this
+ * Component maintains its configured {@link Ext.layout.container.Box#flex flex} value.
+ */
+
+ hideMode: 'display',
+ // Deprecate 5.0
+ hideParent: false,
+
+ ariaRole: 'presentation',
+
+ bubbleEvents: [],
+
+ actionMode: 'el',
+ monPropRe: /^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate)$/,
+
+ //renderTpl: new Ext.XTemplate(
+ // '<div id="{id}" class="{baseCls} {cls} {cmpCls}<tpl if="typeof ui !== \'undefined\'"> {uiBase}-{ui}</tpl>"<tpl if="typeof style !== \'undefined\'"> style="{style}"</tpl>></div>', {
+ // compiled: true,
+ // disableFormats: true
+ // }
+ //),
+
+ /**
+ * Creates new Component.
+ * @param {Ext.Element/String/Object} config The configuration options may be specified as either:
+ *
+ * - **an element** : it is set as the internal element and its id used as the component id
+ * - **a string** : it is assumed to be the id of an existing element and is used as the component id
+ * - **anything else** : it is assumed to be a standard config object and is applied to the component
+ */
+ constructor: function(config) {
+ var me = this;
+
+ config = config || {};
+ if (config.initialConfig) {
+
+ // Being initialized from an Ext.Action instance...
+ if (config.isAction) {
+ me.baseAction = config;
+ }
+ config = config.initialConfig;
+ // component cloning / action set up
+ }
+ else if (config.tagName || config.dom || Ext.isString(config)) {
+ // element object
+ config = {
+ applyTo: config,
+ id: config.id || config
+ };
+ }
+
+ me.callParent([config]);
+
+ // If we were configured from an instance of Ext.Action, (or configured with a baseAction option),
+ // register this Component as one of its items
+ if (me.baseAction){
+ me.baseAction.addComponent(me);
+ }
+ },
+
+ /**
+ * The initComponent template method is an important initialization step for a Component. It is intended to be
+ * implemented by each subclass of Ext.Component to provide any needed constructor logic. The
+ * initComponent method of the class being created is called first, with each initComponent method
+ * up the hierarchy to Ext.Component being called thereafter. This makes it easy to implement and,
+ * if needed, override the constructor logic of the Component at any step in the hierarchy.
+ *
+ * The initComponent method **must** contain a call to {@link Ext.Base#callParent callParent} in order
+ * to ensure that the parent class' initComponent method is also called.
+ *
+ * The following example demonstrates using a dynamic string for the text of a button at the time of
+ * instantiation of the class.
+ *
+ * Ext.define('DynamicButtonText', {
+ * extend: 'Ext.button.Button',
+ *
+ * initComponent: function() {
+ * this.text = new Date();
+ * this.renderTo = Ext.getBody();
+ * this.callParent();
+ * }
+ * });
+ *
+ * Ext.onReady(function() {
+ * Ext.create('DynamicButtonText');
+ * });
+ *
+ * @template
+ */
+ initComponent: function() {
+ var me = this;
+
+ me.callParent();
+
+ if (me.listeners) {
+ me.on(me.listeners);
+ delete me.listeners;
+ }
+ me.enableBubble(me.bubbleEvents);
+ me.mons = [];
+ },
+
+ // private
+ afterRender: function() {
+ var me = this,
+ resizable = me.resizable;
+
+ if (me.floating) {
+ me.makeFloating(me.floating);
+ } else {
+ me.el.setVisibilityMode(Ext.Element[me.hideMode.toUpperCase()]);
+ }
+
+ if (Ext.isDefined(me.autoScroll)) {
+ me.setAutoScroll(me.autoScroll);
+ }
+ me.callParent();
+
+ if (!(me.x && me.y) && (me.pageX || me.pageY)) {
+ me.setPagePosition(me.pageX, me.pageY);
+ }
+
+ if (resizable) {
+ me.initResizable(resizable);
+ }
+
+ if (me.draggable) {
+ me.initDraggable();
+ }
+
+ me.initAria();
+ },
+
+ initAria: function() {
+ var actionEl = this.getActionEl(),
+ role = this.ariaRole;
+ if (role) {
+ actionEl.dom.setAttribute('role', role);
+ }
+ },
+
+ /**
+ * Sets the overflow on the content element of the component.
+ * @param {Boolean} scroll True to allow the Component to auto scroll.
+ * @return {Ext.Component} this
+ */
+ setAutoScroll : function(scroll){
+ var me = this,
+ targetEl;
+ scroll = !!scroll;
+ if (me.rendered) {
+ targetEl = me.getTargetEl();
+ targetEl.setStyle('overflow', scroll ? 'auto' : '');
+ if (scroll && (Ext.isIE6 || Ext.isIE7)) {
+ // The scrollable container element must be non-statically positioned or IE6/7 will make
+ // positioned children stay in place rather than scrolling with the rest of the content
+ targetEl.position();
+ }
+ }
+ me.autoScroll = scroll;
+ return me;
+ },
+
+ // private
+ makeFloating : function(cfg){
+ this.mixins.floating.constructor.call(this, cfg);
+ },
+
+ initResizable: function(resizable) {
+ var me = this;
+
+ resizable = Ext.apply({
+ target: me,
+ dynamic: false,
+ constrainTo: me.constrainTo || (me.floatParent ? me.floatParent.getTargetEl() : me.el.getScopeParent()),
+ handles: me.resizeHandles
+ }, resizable);
+ resizable.target = me;
+ me.resizer = Ext.create('Ext.resizer.Resizer', resizable);
+ },
+
+ getDragEl: function() {
+ return this.el;
+ },
+
+ initDraggable: function() {
+ var me = this,
+ ddConfig = Ext.applyIf({
+ el: me.getDragEl(),
+ constrainTo: me.constrain ? (me.constrainTo || (me.floatParent ? me.floatParent.getTargetEl() : me.el.getScopeParent())) : undefined
+ }, me.draggable);
+
+ // Add extra configs if Component is specified to be constrained
+ if (me.constrain || me.constrainDelegate) {
+ ddConfig.constrain = me.constrain;
+ ddConfig.constrainDelegate = me.constrainDelegate;
+ }
+
+ me.dd = Ext.create('Ext.util.ComponentDragger', me, ddConfig);
+ },
+
+ /**
+ * Sets the left and top of the component. To set the page XY position instead, use {@link #setPagePosition}. This
+ * method fires the {@link #move} event.
+ * @param {Number} left The new left
+ * @param {Number} top The new top
+ * @param {Boolean/Object} [animate] If true, the Component is _animated_ into its new position. You may also pass an
+ * animation configuration.
+ * @return {Ext.Component} this
+ */
+ setPosition: function(x, y, animate) {
+ var me = this,
+ el = me.el,
+ to = {},
+ adj, adjX, adjY, xIsNumber, yIsNumber;
+
+ if (Ext.isArray(x)) {
+ animate = y;
+ y = x[1];
+ x = x[0];
+ }
+ me.x = x;
+ me.y = y;
+
+ if (!me.rendered) {
+ return me;
+ }
+
+ adj = me.adjustPosition(x, y);
+ adjX = adj.x;
+ adjY = adj.y;
+ xIsNumber = Ext.isNumber(adjX);
+ yIsNumber = Ext.isNumber(adjY);
+
+ if (xIsNumber || yIsNumber) {
+ if (animate) {
+ if (xIsNumber) {
+ to.left = adjX;
+ }
+ if (yIsNumber) {
+ to.top = adjY;
+ }
+
+ me.stopAnimation();
+ me.animate(Ext.apply({
+ duration: 1000,
+ listeners: {
+ afteranimate: Ext.Function.bind(me.afterSetPosition, me, [adjX, adjY])
+ },
+ to: to
+ }, animate));
+ }
+ else {
+ if (!xIsNumber) {
+ el.setTop(adjY);
+ }
+ else if (!yIsNumber) {
+ el.setLeft(adjX);
+ }
+ else {
+ el.setLeftTop(adjX, adjY);
+ }
+ me.afterSetPosition(adjX, adjY);
+ }
+ }
+ return me;
+ },
+
+ /**
+ * @private
+ * @template
+ * Template method called after a Component has been positioned.
+ */
+ afterSetPosition: function(ax, ay) {
+ this.onPosition(ax, ay);
+ this.fireEvent('move', this, ax, ay);
+ },
+
+ /**
+ * Displays component at specific xy position.
+ * A floating component (like a menu) is positioned relative to its ownerCt if any.
+ * Useful for popping up a context menu:
+ *
+ * listeners: {
+ * itemcontextmenu: function(view, record, item, index, event, options) {
+ * Ext.create('Ext.menu.Menu', {
+ * width: 100,
+ * height: 100,
+ * margin: '0 0 10 0',
+ * items: [{
+ * text: 'regular item 1'
+ * },{
+ * text: 'regular item 2'
+ * },{
+ * text: 'regular item 3'
+ * }]
+ * }).showAt(event.getXY());
+ * }
+ * }
+ *
+ * @param {Number} x The new x position
+ * @param {Number} y The new y position
+ * @param {Boolean/Object} [animate] True to animate the Component into its new position. You may also pass an
+ * animation configuration.
+ */
+ showAt: function(x, y, animate) {
+ var me = this;
+
+ if (me.floating) {
+ me.setPosition(x, y, animate);
+ } else {
+ me.setPagePosition(x, y, animate);
+ }
+ me.show();
+ },
+
+ /**
+ * Sets the page XY position of the component. To set the left and top instead, use {@link #setPosition}.
+ * This method fires the {@link #move} event.
+ * @param {Number} x The new x position
+ * @param {Number} y The new y position
+ * @param {Boolean/Object} [animate] True to animate the Component into its new position. You may also pass an
+ * animation configuration.
+ * @return {Ext.Component} this
+ */
+ setPagePosition: function(x, y, animate) {
+ var me = this,
+ p;
+
+ if (Ext.isArray(x)) {
+ y = x[1];
+ x = x[0];
+ }
+ me.pageX = x;
+ me.pageY = y;
+ if (me.floating && me.floatParent) {
+ // Floating Components being positioned in their ownerCt have to be made absolute
+ p = me.floatParent.getTargetEl().getViewRegion();
+ if (Ext.isNumber(x) && Ext.isNumber(p.left)) {
+ x -= p.left;
+ }
+ if (Ext.isNumber(y) && Ext.isNumber(p.top)) {
+ y -= p.top;
+ }
+ me.setPosition(x, y, animate);
+ }
+ else {
+ p = me.el.translatePoints(x, y);
+ me.setPosition(p.left, p.top, animate);
+ }
+ return me;
+ },
+
+ /**
+ * Gets the current box measurements of the component's underlying element.
+ * @param {Boolean} [local=false] If true the element's left and top are returned instead of page XY.
+ * @return {Object} box An object in the format {x, y, width, height}
+ */
+ getBox : function(local){
+ var pos = this.getPosition(local),
+ size = this.getSize();
+
+ size.x = pos[0];
+ size.y = pos[1];
+ return size;
+ },
+
+ /**
+ * Sets the current box measurements of the component's underlying element.
+ * @param {Object} box An object in the format {x, y, width, height}
+ * @return {Ext.Component} this
+ */
+ updateBox : function(box){
+ this.setSize(box.width, box.height);
+ this.setPagePosition(box.x, box.y);
+ return this;
+ },
+
+ // Include margins
+ getOuterSize: function() {
+ var el = this.el;
+ return {
+ width: el.getWidth() + el.getMargin('lr'),
+ height: el.getHeight() + el.getMargin('tb')
+ };
+ },
+
+ // private
+ adjustPosition: function(x, y) {
+
+ // Floating Components being positioned in their ownerCt have to be made absolute
+ if (this.floating && this.floatParent) {
+ var o = this.floatParent.getTargetEl().getViewRegion();
+ x += o.left;
+ y += o.top;
+ }
+
+ return {
+ x: x,
+ y: y
+ };
+ },
+
+ /**
+ * Gets the current XY position of the component's underlying element.
+ * @param {Boolean} [local=false] If true the element's left and top are returned instead of page XY.
+ * @return {Number[]} The XY position of the element (e.g., [100, 200])
+ */
+ getPosition: function(local) {
+ var me = this,
+ el = me.el,
+ xy,
+ o;
+
+ // Floating Components which were just rendered with no ownerCt return local position.
+ if ((local === true) || (me.floating && !me.floatParent)) {
+ return [el.getLeft(true), el.getTop(true)];
+ }
+ xy = me.xy || el.getXY();
+
+ // Floating Components in an ownerCt have to have their positions made relative
+ if (me.floating) {
+ o = me.floatParent.getTargetEl().getViewRegion();
+ xy[0] -= o.left;
+ xy[1] -= o.top;
+ }
+ return xy;
+ },
+
+ getId: function() {
+ var me = this,
+ xtype;
+
+ if (!me.id) {
+ xtype = me.getXType();
+ xtype = xtype ? xtype.replace(Ext.Component.INVALID_ID_CHARS_Re, '-') : 'ext-comp';
+ me.id = xtype + '-' + me.getAutoId();
+ }
+ return me.id;
+ },
+
+ onEnable: function() {
+ var actionEl = this.getActionEl();
+ actionEl.dom.removeAttribute('aria-disabled');
+ actionEl.dom.disabled = false;
+ this.callParent();
+ },
+
+ onDisable: function() {
+ var actionEl = this.getActionEl();
+ actionEl.dom.setAttribute('aria-disabled', true);
+ actionEl.dom.disabled = true;
+ this.callParent();
+ },
+
+ /**
+ * Shows this Component, rendering it first if {@link #autoRender} or {@link #floating} are `true`.
+ *
+ * After being shown, a {@link #floating} Component (such as a {@link Ext.window.Window}), is activated it and
+ * brought to the front of its {@link #zIndexManager z-index stack}.
+ *
+ * @param {String/Ext.Element} [animateTarget=null] **only valid for {@link #floating} Components such as {@link
+ * Ext.window.Window Window}s or {@link Ext.tip.ToolTip ToolTip}s, or regular Components which have been configured
+ * with `floating: true`.** The target from which the Component should animate from while opening.
+ * @param {Function} [callback] A callback function to call after the Component is displayed.
+ * Only necessary if animation was specified.
+ * @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
+ * Defaults to this Component.
+ * @return {Ext.Component} this
+ */
+ show: function(animateTarget, cb, scope) {
+ var me = this;
+
+ if (me.rendered && me.isVisible()) {
+ if (me.toFrontOnShow && me.floating) {
+ me.toFront();
+ }
+ } else if (me.fireEvent('beforeshow', me) !== false) {
+ me.hidden = false;
+
+ // Render on first show if there is an autoRender config, or if this is a floater (Window, Menu, BoundList etc).
+ if (!me.rendered && (me.autoRender || me.floating)) {
+ me.doAutoRender();
+ }
+ if (me.rendered) {
+ me.beforeShow();
+ me.onShow.apply(me, arguments);
+
+ // Notify any owning Container unless it's suspended.
+ // Floating Components do not participate in layouts.
+ if (me.ownerCt && !me.floating && !(me.ownerCt.suspendLayout || me.ownerCt.layout.layoutBusy)) {
+ me.ownerCt.doLayout();
+ }
+ me.afterShow.apply(me, arguments);
+ }
+ }
+ return me;
+ },
+
+ beforeShow: Ext.emptyFn,
+
+ // Private. Override in subclasses where more complex behaviour is needed.
+ onShow: function() {
+ var me = this;
+
+ me.el.show();
+ me.callParent(arguments);
+ if (me.floating && me.constrain) {
+ me.doConstrain();
+ }
+ },
+
+ afterShow: function(animateTarget, cb, scope) {
+ var me = this,
+ fromBox,
+ toBox,
+ ghostPanel;
+
+ // Default to configured animate target if none passed
+ animateTarget = animateTarget || me.animateTarget;
+
+ // Need to be able to ghost the Component
+ if (!me.ghost) {
+ animateTarget = null;
+ }
+ // If we're animating, kick of an animation of the ghost from the target to the *Element* current box
+ if (animateTarget) {
+ animateTarget = animateTarget.el ? animateTarget.el : Ext.get(animateTarget);
+ toBox = me.el.getBox();
+ fromBox = animateTarget.getBox();
+ me.el.addCls(Ext.baseCSSPrefix + 'hide-offsets');
+ ghostPanel = me.ghost();
+ ghostPanel.el.stopAnimation();
+
+ // Shunting it offscreen immediately, *before* the Animation class grabs it ensure no flicker.
+ ghostPanel.el.setX(-10000);
+
+ ghostPanel.el.animate({
+ from: fromBox,
+ to: toBox,
+ listeners: {
+ afteranimate: function() {
+ delete ghostPanel.componentLayout.lastComponentSize;
+ me.unghost();
+ me.el.removeCls(Ext.baseCSSPrefix + 'hide-offsets');
+ me.onShowComplete(cb, scope);
+ }
+ }
+ });
+ }
+ else {
+ me.onShowComplete(cb, scope);
+ }
+ },
+
+ onShowComplete: function(cb, scope) {
+ var me = this;
+ if (me.floating) {
+ me.toFront();
+ }
+ Ext.callback(cb, scope || me);
+ me.fireEvent('show', me);
+ },
+
+ /**
+ * Hides this Component, setting it to invisible using the configured {@link #hideMode}.
+ * @param {String/Ext.Element/Ext.Component} [animateTarget=null] **only valid for {@link #floating} Components
+ * such as {@link Ext.window.Window Window}s or {@link Ext.tip.ToolTip ToolTip}s, or regular Components which have
+ * been configured with `floating: true`.**. The target to which the Component should animate while hiding.
+ * @param {Function} [callback] A callback function to call after the Component is hidden.
+ * @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
+ * Defaults to this Component.
+ * @return {Ext.Component} this
+ */
+ hide: function() {
+ var me = this;
+
+ // Clear the flag which is set if a floatParent was hidden while this is visible.
+ // If a hide operation was subsequently called, that pending show must be hidden.
+ me.showOnParentShow = false;
+
+ if (!(me.rendered && !me.isVisible()) && me.fireEvent('beforehide', me) !== false) {
+ me.hidden = true;
+ if (me.rendered) {
+ me.onHide.apply(me, arguments);
+
+ // Notify any owning Container unless it's suspended.
+ // Floating Components do not participate in layouts.
+ if (me.ownerCt && !me.floating && !(me.ownerCt.suspendLayout || me.ownerCt.layout.layoutBusy)) {
+ me.ownerCt.doLayout();
+ }
+ }
+ }
+ return me;
+ },
+
+ // Possibly animate down to a target element.
+ onHide: function(animateTarget, cb, scope) {
+ var me = this,
+ ghostPanel,
+ toBox;
+
+ // Default to configured animate target if none passed
+ animateTarget = animateTarget || me.animateTarget;
+
+ // Need to be able to ghost the Component
+ if (!me.ghost) {
+ animateTarget = null;
+ }
+ // If we're animating, kick off an animation of the ghost down to the target
+ if (animateTarget) {
+ animateTarget = animateTarget.el ? animateTarget.el : Ext.get(animateTarget);
+ ghostPanel = me.ghost();
+ ghostPanel.el.stopAnimation();
+ toBox = animateTarget.getBox();
+ toBox.width += 'px';
+ toBox.height += 'px';
+ ghostPanel.el.animate({
+ to: toBox,
+ listeners: {
+ afteranimate: function() {
+ delete ghostPanel.componentLayout.lastComponentSize;
+ ghostPanel.el.hide();
+ me.afterHide(cb, scope);
+ }
+ }
+ });
+ }
+ me.el.hide();
+ if (!animateTarget) {
+ me.afterHide(cb, scope);
+ }
+ },
+
+ afterHide: function(cb, scope) {
+ Ext.callback(cb, scope || this);
+ this.fireEvent('hide', this);
+ },
+
+ /**
+ * @private
+ * @template
+ * Template method to contribute functionality at destroy time.
+ */
+ onDestroy: function() {
+ var me = this;
+
+ // Ensure that any ancillary components are destroyed.
+ if (me.rendered) {
+ Ext.destroy(
+ me.proxy,
+ me.proxyWrap,
+ me.resizer
+ );
+ // Different from AbstractComponent
+ if (me.actionMode == 'container' || me.removeMode == 'container') {
+ me.container.remove();
+ }
+ }
+ delete me.focusTask;
+ me.callParent();
+ },
+
+ deleteMembers: function() {
+ var args = arguments,
+ len = args.length,
+ i = 0;
+ for (; i < len; ++i) {
+ delete this[args[i]];
+ }
+ },
+
+ /**
+ * Try to focus this component.
+ * @param {Boolean} [selectText] If applicable, true to also select the text in this component
+ * @param {Boolean/Number} [delay] Delay the focus this number of milliseconds (true for 10 milliseconds).
+ * @return {Ext.Component} this
+ */
+ focus: function(selectText, delay) {
+ var me = this,
+ focusEl;
+
+ if (delay) {
+ if (!me.focusTask) {
+ me.focusTask = Ext.create('Ext.util.DelayedTask', me.focus);
+ }
+ me.focusTask.delay(Ext.isNumber(delay) ? delay : 10, null, me, [selectText, false]);
+ return me;
+ }
+
+ if (me.rendered && !me.isDestroyed) {
+ // getFocusEl could return a Component.
+ focusEl = me.getFocusEl();
+ focusEl.focus();
+ if (focusEl.dom && selectText === true) {
+ focusEl.dom.select();
+ }
+
+ // Focusing a floating Component brings it to the front of its stack.
+ // this is performed by its zIndexManager. Pass preventFocus true to avoid recursion.
+ if (me.floating) {
+ me.toFront(true);
+ }
+ }
+ return me;
+ },
+
+ /**
+ * @private
+ * Returns the focus holder element associated with this Component. By default, this is the Component's encapsulating
+ * element. Subclasses which use embedded focusable elements (such as Window and Button) should override this for use
+ * by the {@link #focus} method.
+ * @returns {Ext.Element} the focus holing element.
+ */
+ getFocusEl: function() {
+ return this.el;
+ },
+
+ // private
+ blur: function() {
+ if (this.rendered) {
+ this.getFocusEl().blur();
+ }
+ return this;
+ },
+
+ getEl: function() {
+ return this.el;
+ },
+
+ // Deprecate 5.0
+ getResizeEl: function() {
+ return this.el;
+ },
+
+ // Deprecate 5.0
+ getPositionEl: function() {
+ return this.el;
+ },
+
+ // Deprecate 5.0
+ getActionEl: function() {
+ return this.el;
+ },
+
+ // Deprecate 5.0
+ getVisibilityEl: function() {
+ return this.el;
+ },
+
+ // Deprecate 5.0
+ onResize: Ext.emptyFn,
+
+ // private
+ getBubbleTarget: function() {
+ return this.ownerCt;
+ },
+
+ // private
+ getContentTarget: function() {
+ return this.el;
+ },
+
+ /**
+ * Clone the current component using the original config values passed into this instance by default.
+ * @param {Object} overrides A new config containing any properties to override in the cloned version.
+ * An id property can be passed on this object, otherwise one will be generated to avoid duplicates.
+ * @return {Ext.Component} clone The cloned copy of this component
+ */
+ cloneConfig: function(overrides) {
+ overrides = overrides || {};
+ var id = overrides.id || Ext.id(),
+ cfg = Ext.applyIf(overrides, this.initialConfig),
+ self;
+
+ cfg.id = id;
+
+ self = Ext.getClass(this);
+
+ // prevent dup id
+ return new self(cfg);
+ },
+
+ /**
+ * Gets the xtype for this component as registered with {@link Ext.ComponentManager}. For a list of all available
+ * xtypes, see the {@link Ext.Component} header. Example usage:
+ *
+ * var t = new Ext.form.field.Text();
+ * alert(t.getXType()); // alerts 'textfield'
+ *
+ * @return {String} The xtype
+ */
+ getXType: function() {
+ return this.self.xtype;
+ },
+
+ /**
+ * Find a container above this component at any level by a custom function. If the passed function returns true, the
+ * container will be returned.
+ * @param {Function} fn The custom function to call with the arguments (container, this component).
+ * @return {Ext.container.Container} The first Container for which the custom function returns true
+ */
+ findParentBy: function(fn) {
+ var p;
+
+ // Iterate up the ownerCt chain until there's no ownerCt, or we find an ancestor which matches using the selector function.
+ for (p = this.ownerCt; p && !fn(p, this); p = p.ownerCt);
+ return p || null;
+ },
+
+ /**
+ * Find a container above this component at any level by xtype or class
+ *
+ * See also the {@link Ext.Component#up up} method.
+ *
+ * @param {String/Ext.Class} xtype The xtype string for a component, or the class of the component directly
+ * @return {Ext.container.Container} The first Container which matches the given xtype or class
+ */
+ findParentByType: function(xtype) {
+ return Ext.isFunction(xtype) ?
+ this.findParentBy(function(p) {
+ return p.constructor === xtype;
+ })
+ :
+ this.up(xtype);
+ },
+
+ /**
+ * Bubbles up the component/container heirarchy, calling the specified function with each component. The scope
+ * (*this*) of function call will be the scope provided or the current component. The arguments to the function will
+ * be the args provided or the current component. If the function returns false at any point, the bubble is stopped.
+ *
+ * @param {Function} fn The function to call
+ * @param {Object} [scope] The scope of the function. Defaults to current node.
+ * @param {Array} [args] The args to call the function with. Defaults to passing the current component.
+ * @return {Ext.Component} this
+ */
+ bubble: function(fn, scope, args) {
+ var p = this;
+ while (p) {
+ if (fn.apply(scope || p, args || [p]) === false) {
+ break;
+ }
+ p = p.ownerCt;
+ }
+ return this;
+ },
+
+ getProxy: function() {
+ var me = this,
+ target;
+
+ if (!me.proxy) {
+ target = Ext.getBody();
+ if (Ext.scopeResetCSS) {
+ me.proxyWrap = target = Ext.getBody().createChild({
+ cls: Ext.baseCSSPrefix + 'reset'
+ });
+ }
+ me.proxy = me.el.createProxy(Ext.baseCSSPrefix + 'proxy-el', target, true);
+ }
+ return me.proxy;
+ }
+
+});
+
+/**
+ * @class Ext.layout.container.AbstractContainer
+ * @extends Ext.layout.Layout
+ * Please refer to sub classes documentation
+ * @private
+ */
+Ext.define('Ext.layout.container.AbstractContainer', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.layout.Layout',
+
+ /* End Definitions */
+
+ type: 'container',
+
+ /**
+ * @cfg {Boolean} bindToOwnerCtComponent
+ * Flag to notify the ownerCt Component on afterLayout of a change
+ */
+ bindToOwnerCtComponent: false,
+
+ /**
+ * @cfg {Boolean} bindToOwnerCtContainer
+ * Flag to notify the ownerCt Container on afterLayout of a change
+ */
+ bindToOwnerCtContainer: false,
+
+ /**
+ * @cfg {String} itemCls
+ * <p>An optional extra CSS class that will be added to the container. This can be useful for adding
+ * customized styles to the container or any of its children using standard CSS rules. See
+ * {@link Ext.Component}.{@link Ext.Component#componentCls componentCls} also.</p>
+ * </p>
+ */
+
+ /**
+ * Set the size of an item within the Container. We should always use setCalculatedSize.
+ * @private
+ */
+ setItemSize: function(item, width, height) {
+ if (Ext.isObject(width)) {
+ height = width.height;
+ width = width.width;
+ }
+ item.setCalculatedSize(width, height, this.owner);
+ },
+
+ /**
+ * <p>Returns an array of child components either for a render phase (Performed in the beforeLayout method of the layout's
+ * base class), or the layout phase (onLayout).</p>
+ * @return {Ext.Component[]} of child components
+ */
+ getLayoutItems: function() {
+ return this.owner && this.owner.items && this.owner.items.items || [];
+ },
+
+ /**
+ * Containers should not lay out child components when collapsed.
+ */
+ beforeLayout: function() {
+ return !this.owner.collapsed && this.callParent(arguments);
+ },
+
+ afterLayout: function() {
+ this.owner.afterLayout(this);
+ },
+ /**
+ * Returns the owner component's resize element.
+ * @return {Ext.Element}
+ */
+ getTarget: function() {
+ return this.owner.getTargetEl();
+ },
+ /**
+ * <p>Returns the element into which rendering must take place. Defaults to the owner Container's target element.</p>
+ * May be overridden in layout managers which implement an inner element.
+ * @return {Ext.Element}
+ */
+ getRenderTarget: function() {
+ return this.owner.getTargetEl();
+ }
+});
+
+/**
+* @class Ext.layout.container.Container
+* @extends Ext.layout.container.AbstractContainer
+* <p>This class is intended to be extended or created via the {@link Ext.container.Container#layout layout}
+* configuration property. See {@link Ext.container.Container#layout} for additional details.</p>
+*/
+Ext.define('Ext.layout.container.Container', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.layout.container.AbstractContainer',
+ alternateClassName: 'Ext.layout.ContainerLayout',
+
+ /* End Definitions */
+
+ layoutItem: function(item, box) {
+ if (box) {
+ item.doComponentLayout(box.width, box.height);
+ } else {
+ item.doComponentLayout();
+ }
+ },
+
+ getLayoutTargetSize : function() {
+ var target = this.getTarget(),
+ ret;
+
+ if (target) {
+ ret = target.getViewSize();
+
+ // IE in will sometimes return a width of 0 on the 1st pass of getViewSize.
+ // Use getStyleSize to verify the 0 width, the adjustment pass will then work properly
+ // with getViewSize
+ if (Ext.isIE && ret.width == 0){
+ ret = target.getStyleSize();
+ }
+
+ ret.width -= target.getPadding('lr');
+ ret.height -= target.getPadding('tb');
+ }
+ return ret;
+ },
+
+ beforeLayout: function() {
+ if (this.owner.beforeLayout(arguments) !== false) {
+ return this.callParent(arguments);
+ }
+ else {
+ return false;
+ }
+ },
+
+ /**
+ * @protected
+ * Returns all items that are rendered
+ * @return {Array} All matching items
+ */
+ getRenderedItems: function() {
+ var me = this,
+ target = me.getTarget(),
+ items = me.getLayoutItems(),
+ ln = items.length,
+ renderedItems = [],
+ i, item;
+
+ for (i = 0; i < ln; i++) {
+ item = items[i];
+ if (item.rendered && me.isValidParent(item, target, i)) {
+ renderedItems.push(item);
+ }
+ }
+
+ return renderedItems;
+ },
+
+ /**
+ * @protected
+ * Returns all items that are both rendered and visible
+ * @return {Array} All matching items
+ */
+ getVisibleItems: function() {
+ var target = this.getTarget(),
+ items = this.getLayoutItems(),
+ ln = items.length,
+ visibleItems = [],
+ i, item;
+
+ for (i = 0; i < ln; i++) {
+ item = items[i];
+ if (item.rendered && this.isValidParent(item, target, i) && item.hidden !== true) {
+ visibleItems.push(item);
+ }
+ }
+
+ return visibleItems;
+ }
+});
+/**
+ * @class Ext.layout.container.Auto
+ * @extends Ext.layout.container.Container
+ *
+ * The AutoLayout is the default layout manager delegated by {@link Ext.container.Container} to
+ * render any child Components when no `{@link Ext.container.Container#layout layout}` is configured into
+ * a `{@link Ext.container.Container Container}.` AutoLayout provides only a passthrough of any layout calls
+ * to any child containers.
+ *
+ * @example
+ * Ext.create('Ext.Panel', {
+ * width: 500,
+ * height: 280,
+ * title: "AutoLayout Panel",
+ * layout: 'auto',
+ * renderTo: document.body,
+ * items: [{
+ * xtype: 'panel',
+ * title: 'Top Inner Panel',
+ * width: '75%',
+ * height: 90
+ * },
+ * {
+ * xtype: 'panel',
+ * title: 'Bottom Inner Panel',
+ * width: '75%',
+ * height: 90
+ * }]
+ * });
+ */
+Ext.define('Ext.layout.container.Auto', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.auto', 'layout.autocontainer'],
+
+ extend: 'Ext.layout.container.Container',
+
+ /* End Definitions */
+
+ type: 'autocontainer',
+
+ bindToOwnerCtComponent: true,
+
+ // @private
+ onLayout : function(owner, target) {
+ var me = this,
+ items = me.getLayoutItems(),
+ ln = items.length,
+ i;
+
+ // Ensure the Container is only primed with the clear element if there are child items.
+ if (ln) {
+ // Auto layout uses natural HTML flow to arrange the child items.
+ // To ensure that all browsers (I'm looking at you IE!) add the bottom margin of the last child to the
+ // containing element height, we create a zero-sized element with style clear:both to force a "new line"
+ if (!me.clearEl) {
+ me.clearEl = me.getRenderTarget().createChild({
+ cls: Ext.baseCSSPrefix + 'clear',
+ role: 'presentation'
+ });
+ }
+
+ // Auto layout allows CSS to size its child items.
+ for (i = 0; i < ln; i++) {
+ me.setItemSize(items[i]);
+ }
+ }
+ },
+
+ configureItem: function(item) {
+ this.callParent(arguments);
+
+ // Auto layout does not manage any dimensions.
+ item.layoutManagedHeight = 2;
+ item.layoutManagedWidth = 2;
+ }
+});
+/**
+ * @class Ext.container.AbstractContainer
+ * @extends Ext.Component
+ * An abstract base class which provides shared methods for Containers across the Sencha product line.
+ * @private
+ */
+Ext.define('Ext.container.AbstractContainer', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.Component',
+
+ requires: [
+ 'Ext.util.MixedCollection',
+ 'Ext.layout.container.Auto',
+ 'Ext.ZIndexManager'
+ ],
+
+ /* End Definitions */
+ /**
+ * @cfg {String/Object} layout
+ * <p><b>Important</b>: In order for child items to be correctly sized and
+ * positioned, typically a layout manager <b>must</b> be specified through
+ * the <code>layout</code> configuration option.</p>
+ * <p>The sizing and positioning of child {@link #items} is the responsibility of
+ * the Container's layout manager which creates and manages the type of layout
+ * you have in mind. For example:</p>
+ * <p>If the {@link #layout} configuration is not explicitly specified for
+ * a general purpose container (e.g. Container or Panel) the
+ * {@link Ext.layout.container.Auto default layout manager} will be used
+ * which does nothing but render child components sequentially into the
+ * Container (no sizing or positioning will be performed in this situation).</p>
+ * <p><b><code>layout</code></b> may be specified as either as an Object or as a String:</p>
+ * <div><ul class="mdetail-params">
+ * <li><u>Specify as an Object</u></li>
+ * <div><ul class="mdetail-params">
+ * <li>Example usage:</li>
+ * <pre><code>
+layout: {
+ type: 'vbox',
+ align: 'left'
+}
+ </code></pre>
+ *
+ * <li><code><b>type</b></code></li>
+ * <br/><p>The layout type to be used for this container. If not specified,
+ * a default {@link Ext.layout.container.Auto} will be created and used.</p>
+ * <p>Valid layout <code>type</code> values are:</p>
+ * <div class="sub-desc"><ul class="mdetail-params">
+ * <li><code><b>{@link Ext.layout.container.Auto Auto}</b></code> <b>Default</b></li>
+ * <li><code><b>{@link Ext.layout.container.Card card}</b></code></li>
+ * <li><code><b>{@link Ext.layout.container.Fit fit}</b></code></li>
+ * <li><code><b>{@link Ext.layout.container.HBox hbox}</b></code></li>
+ * <li><code><b>{@link Ext.layout.container.VBox vbox}</b></code></li>
+ * <li><code><b>{@link Ext.layout.container.Anchor anchor}</b></code></li>
+ * <li><code><b>{@link Ext.layout.container.Table table}</b></code></li>
+ * </ul></div>
+ *
+ * <li>Layout specific configuration properties</li>
+ * <p>Additional layout specific configuration properties may also be
+ * specified. For complete details regarding the valid config options for
+ * each layout type, see the layout class corresponding to the <code>type</code>
+ * specified.</p>
+ *
+ * </ul></div>
+ *
+ * <li><u>Specify as a String</u></li>
+ * <div><ul class="mdetail-params">
+ * <li>Example usage:</li>
+ * <pre><code>
+layout: 'vbox'
+ </code></pre>
+ * <li><code><b>layout</b></code></li>
+ * <p>The layout <code>type</code> to be used for this container (see list
+ * of valid layout type values above).</p>
+ * <p>Additional layout specific configuration properties. For complete
+ * details regarding the valid config options for each layout type, see the
+ * layout class corresponding to the <code>layout</code> specified.</p>
+ * </ul></div></ul></div>
+ */
+
+ /**
+ * @cfg {String/Number} activeItem
+ * A string component id or the numeric index of the component that should be initially activated within the
+ * container's layout on render. For example, activeItem: 'item-1' or activeItem: 0 (index 0 = the first
+ * item in the container's collection). activeItem only applies to layout styles that can display
+ * items one at a time (like {@link Ext.layout.container.Card} and {@link Ext.layout.container.Fit}).
+ */
+ /**
+ * @cfg {Object/Object[]} items
+ * <p>A single item, or an array of child Components to be added to this container</p>
+ * <p><b>Unless configured with a {@link #layout}, a Container simply renders child Components serially into
+ * its encapsulating element and performs no sizing or positioning upon them.</b><p>
+ * <p>Example:</p>
+ * <pre><code>
+// specifying a single item
+items: {...},
+layout: 'fit', // The single items is sized to fit
+
+// specifying multiple items
+items: [{...}, {...}],
+layout: 'hbox', // The items are arranged horizontally
+ </code></pre>
+ * <p>Each item may be:</p>
+ * <ul>
+ * <li>A {@link Ext.Component Component}</li>
+ * <li>A Component configuration object</li>
+ * </ul>
+ * <p>If a configuration object is specified, the actual type of Component to be
+ * instantiated my be indicated by using the {@link Ext.Component#xtype xtype} option.</p>
+ * <p>Every Component class has its own {@link Ext.Component#xtype xtype}.</p>
+ * <p>If an {@link Ext.Component#xtype xtype} is not explicitly
+ * specified, the {@link #defaultType} for the Container is used, which by default is usually <code>panel</code>.</p>
+ * <p><b>Notes</b>:</p>
+ * <p>Ext uses lazy rendering. Child Components will only be rendered
+ * should it become necessary. Items are automatically laid out when they are first
+ * shown (no sizing is done while hidden), or in response to a {@link #doLayout} call.</p>
+ * <p>Do not specify <code>{@link Ext.panel.Panel#contentEl contentEl}</code> or
+ * <code>{@link Ext.panel.Panel#html html}</code> with <code>items</code>.</p>
+ */
+ /**
+ * @cfg {Object/Function} defaults
+ * This option is a means of applying default settings to all added items whether added through the {@link #items}
+ * config or via the {@link #add} or {@link #insert} methods.
+ *
+ * Defaults are applied to both config objects and instantiated components conditionally so as not to override
+ * existing properties in the item (see {@link Ext#applyIf}).
+ *
+ * If the defaults option is specified as a function, then the function will be called using this Container as the
+ * scope (`this` reference) and passing the added item as the first parameter. Any resulting object
+ * from that call is then applied to the item as default properties.
+ *
+ * For example, to automatically apply padding to the body of each of a set of
+ * contained {@link Ext.panel.Panel} items, you could pass: `defaults: {bodyStyle:'padding:15px'}`.
+ *
+ * Usage:
+ *
+ * defaults: { // defaults are applied to items, not the container
+ * autoScroll: true
+ * },
+ * items: [
+ * // default will not be applied here, panel1 will be autoScroll: false
+ * {
+ * xtype: 'panel',
+ * id: 'panel1',
+ * autoScroll: false
+ * },
+ * // this component will have autoScroll: true
+ * new Ext.panel.Panel({
+ * id: 'panel2'
+ * })
+ * ]
+ */
+
+ /** @cfg {Boolean} suspendLayout
+ * If true, suspend calls to doLayout. Useful when batching multiple adds to a container and not passing them
+ * as multiple arguments or an array.
+ */
+ suspendLayout : false,
+
+ /** @cfg {Boolean} autoDestroy
+ * If true the container will automatically destroy any contained component that is removed from it, else
+ * destruction must be handled manually.
+ * Defaults to true.
+ */
+ autoDestroy : true,
+
+ /** @cfg {String} defaultType
+ * <p>The default {@link Ext.Component xtype} of child Components to create in this Container when
+ * a child item is specified as a raw configuration object, rather than as an instantiated Component.</p>
+ * <p>Defaults to <code>'panel'</code>.</p>
+ */
+ defaultType: 'panel',
+
+ isContainer : true,
+
+ /**
+ * The number of container layout calls made on this object.
+ * @property layoutCounter
+ * @type {Number}
+ * @private
+ */
+ layoutCounter : 0,
+
+ baseCls: Ext.baseCSSPrefix + 'container',
+
+ /**
+ * @cfg {String[]} bubbleEvents
+ * <p>An array of events that, when fired, should be bubbled to any parent container.
+ * See {@link Ext.util.Observable#enableBubble}.
+ * Defaults to <code>['add', 'remove']</code>.
+ */
+ bubbleEvents: ['add', 'remove'],
+
+ // @private
+ initComponent : function(){
+ var me = this;
+ me.addEvents(
+ /**
+ * @event afterlayout
+ * Fires when the components in this container are arranged by the associated layout manager.
+ * @param {Ext.container.Container} this
+ * @param {Ext.layout.container.Container} layout The ContainerLayout implementation for this container
+ */
+ 'afterlayout',
+ /**
+ * @event beforeadd
+ * Fires before any {@link Ext.Component} is added or inserted into the container.
+ * A handler can return false to cancel the add.
+ * @param {Ext.container.Container} this
+ * @param {Ext.Component} component The component being added
+ * @param {Number} index The index at which the component will be added to the container's items collection
+ */
+ 'beforeadd',
+ /**
+ * @event beforeremove
+ * Fires before any {@link Ext.Component} is removed from the container. A handler can return
+ * false to cancel the remove.
+ * @param {Ext.container.Container} this
+ * @param {Ext.Component} component The component being removed
+ */
+ 'beforeremove',
+ /**
+ * @event add
+ * @bubbles
+ * Fires after any {@link Ext.Component} is added or inserted into the container.
+ * @param {Ext.container.Container} this
+ * @param {Ext.Component} component The component that was added
+ * @param {Number} index The index at which the component was added to the container's items collection
+ */
+ 'add',
+ /**
+ * @event remove
+ * @bubbles
+ * Fires after any {@link Ext.Component} is removed from the container.
+ * @param {Ext.container.Container} this
+ * @param {Ext.Component} component The component that was removed
+ */
+ 'remove'
+ );
+
+ // layoutOnShow stack
+ me.layoutOnShow = Ext.create('Ext.util.MixedCollection');
+ me.callParent();
+ me.initItems();
+ },
+
+ // @private
+ initItems : function() {
+ var me = this,
+ items = me.items;
+
+ /**
+ * The MixedCollection containing all the child items of this container.
+ * @property items
+ * @type Ext.util.MixedCollection
+ */
+ me.items = Ext.create('Ext.util.MixedCollection', false, me.getComponentId);
+
+ if (items) {
+ if (!Ext.isArray(items)) {
+ items = [items];
+ }
+
+ me.add(items);
+ }
+ },
+
+ // @private
+ afterRender : function() {
+ this.getLayout();
+ this.callParent();
+ },
+
+ renderChildren: function () {
+ var me = this,
+ layout = me.getLayout();
+
+ me.callParent();
+ // this component's elements exist, so now create the child components' elements
+
+ if (layout) {
+ me.suspendLayout = true;
+ layout.renderChildren();
+ delete me.suspendLayout;
+ }
+ },
+
+ // @private
+ setLayout : function(layout) {
+ var currentLayout = this.layout;
+
+ if (currentLayout && currentLayout.isLayout && currentLayout != layout) {
+ currentLayout.setOwner(null);
+ }
+
+ this.layout = layout;
+ layout.setOwner(this);
+ },
+
+ /**
+ * Returns the {@link Ext.layout.container.AbstractContainer layout} instance currently associated with this Container.
+ * If a layout has not been instantiated yet, that is done first
+ * @return {Ext.layout.container.AbstractContainer} The layout
+ */
+ getLayout : function() {
+ var me = this;
+ if (!me.layout || !me.layout.isLayout) {
+ me.setLayout(Ext.layout.Layout.create(me.layout, 'autocontainer'));
+ }
+
+ return me.layout;
+ },
+
+ /**
+ * Manually force this container's layout to be recalculated. The framework uses this internally to refresh layouts
+ * form most cases.
+ * @return {Ext.container.Container} this
+ */
+ doLayout : function() {
+ var me = this,
+ layout = me.getLayout();
+
+ if (me.rendered && layout && !me.suspendLayout) {
+ // If either dimension is being auto-set, then it requires a ComponentLayout to be run.
+ if (!me.isFixedWidth() || !me.isFixedHeight()) {
+ // Only run the ComponentLayout if it is not already in progress
+ if (me.componentLayout.layoutBusy !== true) {
+ me.doComponentLayout();
+ if (me.componentLayout.layoutCancelled === true) {
+ layout.layout();
+ }
+ }
+ }
+ // Both dimensions set, either by configuration, or by an owning layout, run a ContainerLayout
+ else {
+ // Only run the ContainerLayout if it is not already in progress
+ if (layout.layoutBusy !== true) {
+ layout.layout();
+ }
+ }
+ }
+
+ return me;
+ },
+
+ // @private
+ afterLayout : function(layout) {
+ ++this.layoutCounter;
+ this.fireEvent('afterlayout', this, layout);
+ },
+
+ // @private
+ prepareItems : function(items, applyDefaults) {
+ if (!Ext.isArray(items)) {
+ items = [items];
+ }
+
+ // Make sure defaults are applied and item is initialized
+ var i = 0,
+ len = items.length,
+ item;
+
+ for (; i < len; i++) {
+ item = items[i];
+ if (applyDefaults) {
+ item = this.applyDefaults(item);
+ }
+ items[i] = this.lookupComponent(item);
+ }
+ return items;
+ },
+
+ // @private
+ applyDefaults : function(config) {
+ var defaults = this.defaults;
+
+ if (defaults) {
+ if (Ext.isFunction(defaults)) {
+ defaults = defaults.call(this, config);
+ }
+
+ if (Ext.isString(config)) {
+ config = Ext.ComponentManager.get(config);
+ }
+ Ext.applyIf(config, defaults);
+ }
+
+ return config;
+ },
+
+ // @private
+ lookupComponent : function(comp) {
+ return Ext.isString(comp) ? Ext.ComponentManager.get(comp) : this.createComponent(comp);
+ },
+
+ // @private
+ createComponent : function(config, defaultType) {
+ // // add in ownerCt at creation time but then immediately
+ // // remove so that onBeforeAdd can handle it
+ // var component = Ext.create(Ext.apply({ownerCt: this}, config), defaultType || this.defaultType);
+ //
+ // delete component.initialConfig.ownerCt;
+ // delete component.ownerCt;
+
+ return Ext.ComponentManager.create(config, defaultType || this.defaultType);
+ },
+
+ // @private - used as the key lookup function for the items collection
+ getComponentId : function(comp) {
+ return comp.getItemId();
+ },
+
+ /**
+
+Adds {@link Ext.Component Component}(s) to this Container.
+
+##Description:##
+
+- Fires the {@link #beforeadd} event before adding.
+- The Container's {@link #defaults default config values} will be applied
+ accordingly (see `{@link #defaults}` for details).
+- Fires the `{@link #add}` event after the component has been added.
+
+##Notes:##
+
+If the Container is __already rendered__ when `add`
+is called, it will render the newly added Component into its content area.
+
+__**If**__ the Container was configured with a size-managing {@link #layout} manager, the Container
+will recalculate its internal layout at this time too.
+
+Note that the default layout manager simply renders child Components sequentially into the content area and thereafter performs no sizing.
+
+If adding multiple new child Components, pass them as an array to the `add` method, so that only one layout recalculation is performed.
+
+ tb = new {@link Ext.toolbar.Toolbar}({
+ renderTo: document.body
+ }); // toolbar is rendered
+ tb.add([{text:'Button 1'}, {text:'Button 2'}]); // add multiple items. ({@link #defaultType} for {@link Ext.toolbar.Toolbar Toolbar} is 'button')
+
+##Warning:##
+
+Components directly managed by the BorderLayout layout manager
+may not be removed or added. See the Notes for {@link Ext.layout.container.Border BorderLayout}
+for more details.
+
+ * @param {Ext.Component[]/Ext.Component...} component
+ * Either one or more Components to add or an Array of Components to add.
+ * See `{@link #items}` for additional information.
+ *
+ * @return {Ext.Component[]/Ext.Component} The Components that were added.
+ * @markdown
+ */
+ add : function() {
+ var me = this,
+ args = Array.prototype.slice.call(arguments),
+ hasMultipleArgs,
+ items,
+ results = [],
+ i,
+ ln,
+ item,
+ index = -1,
+ cmp;
+
+ if (typeof args[0] == 'number') {
+ index = args.shift();
+ }
+
+ hasMultipleArgs = args.length > 1;
+ if (hasMultipleArgs || Ext.isArray(args[0])) {
+
+ items = hasMultipleArgs ? args : args[0];
+ // Suspend Layouts while we add multiple items to the container
+ me.suspendLayout = true;
+ for (i = 0, ln = items.length; i < ln; i++) {
+ item = items[i];
+
+
+ if (index != -1) {
+ item = me.add(index + i, item);
+ } else {
+ item = me.add(item);
+ }
+ results.push(item);
+ }
+ // Resume Layouts now that all items have been added and do a single layout for all the items just added
+ me.suspendLayout = false;
+ me.doLayout();
+ return results;
+ }
+
+ cmp = me.prepareItems(args[0], true)[0];
+
+ // Floating Components are not added into the items collection
+ // But they do get an upward ownerCt link so that they can traverse
+ // up to their z-index parent.
+ if (cmp.floating) {
+ cmp.onAdded(me, index);
+ } else {
+ index = (index !== -1) ? index : me.items.length;
+ if (me.fireEvent('beforeadd', me, cmp, index) !== false && me.onBeforeAdd(cmp) !== false) {
+ me.items.insert(index, cmp);
+ cmp.onAdded(me, index);
+ me.onAdd(cmp, index);
+ me.fireEvent('add', me, cmp, index);
+ }
+ me.doLayout();
+ }
+ return cmp;
+ },
+
+ onAdd : Ext.emptyFn,
+ onRemove : Ext.emptyFn,
+
+ /**
+ * Inserts a Component into this Container at a specified index. Fires the
+ * {@link #beforeadd} event before inserting, then fires the {@link #add} event after the
+ * Component has been inserted.
+ * @param {Number} index The index at which the Component will be inserted
+ * into the Container's items collection
+ * @param {Ext.Component} component The child Component to insert.<br><br>
+ * Ext uses lazy rendering, and will only render the inserted Component should
+ * it become necessary.<br><br>
+ * A Component config object may be passed in order to avoid the overhead of
+ * constructing a real Component object if lazy rendering might mean that the
+ * inserted Component will not be rendered immediately. To take advantage of
+ * this 'lazy instantiation', set the {@link Ext.Component#xtype} config
+ * property to the registered type of the Component wanted.<br><br>
+ * For a list of all available xtypes, see {@link Ext.Component}.
+ * @return {Ext.Component} component The Component (or config object) that was
+ * inserted with the Container's default config values applied.
+ */
+ insert : function(index, comp) {
+ return this.add(index, comp);
+ },
+
+ /**
+ * Moves a Component within the Container
+ * @param {Number} fromIdx The index the Component you wish to move is currently at.
+ * @param {Number} toIdx The new index for the Component.
+ * @return {Ext.Component} component The Component (or config object) that was moved.
+ */
+ move : function(fromIdx, toIdx) {
+ var items = this.items,
+ item;
+ item = items.removeAt(fromIdx);
+ if (item === false) {
+ return false;
+ }
+ items.insert(toIdx, item);
+ this.doLayout();
+ return item;
+ },
+
+ // @private
+ onBeforeAdd : function(item) {
+ var me = this;
+
+ if (item.ownerCt) {
+ item.ownerCt.remove(item, false);
+ }
+
+ if (me.border === false || me.border === 0) {
+ item.border = (item.border === true);
+ }
+ },
+
+ /**
+ * Removes a component from this container. Fires the {@link #beforeremove} event before removing, then fires
+ * the {@link #remove} event after the component has been removed.
+ * @param {Ext.Component/String} component The component reference or id to remove.
+ * @param {Boolean} autoDestroy (optional) True to automatically invoke the removed Component's {@link Ext.Component#destroy} function.
+ * Defaults to the value of this Container's {@link #autoDestroy} config.
+ * @return {Ext.Component} component The Component that was removed.
+ */
+ remove : function(comp, autoDestroy) {
+ var me = this,
+ c = me.getComponent(comp);
+
+ if (c && me.fireEvent('beforeremove', me, c) !== false) {
+ me.doRemove(c, autoDestroy);
+ me.fireEvent('remove', me, c);
+ }
+
+ return c;
+ },
+
+ // @private
+ doRemove : function(component, autoDestroy) {
+ var me = this,
+ layout = me.layout,
+ hasLayout = layout && me.rendered;
+
+ me.items.remove(component);
+ component.onRemoved();
+
+ if (hasLayout) {
+ layout.onRemove(component);
+ }
+
+ me.onRemove(component, autoDestroy);
+
+ if (autoDestroy === true || (autoDestroy !== false && me.autoDestroy)) {
+ component.destroy();
+ }
+
+ if (hasLayout && !autoDestroy) {
+ layout.afterRemove(component);
+ }
+
+ if (!me.destroying) {
+ me.doLayout();
+ }
+ },
+
+ /**
+ * Removes all components from this container.
+ * @param {Boolean} autoDestroy (optional) True to automatically invoke the removed Component's {@link Ext.Component#destroy} function.
+ * Defaults to the value of this Container's {@link #autoDestroy} config.
+ * @return {Ext.Component[]} Array of the destroyed components
+ */
+ removeAll : function(autoDestroy) {
+ var me = this,
+ removeItems = me.items.items.slice(),
+ items = [],
+ i = 0,
+ len = removeItems.length,
+ item;
+
+ // Suspend Layouts while we remove multiple items from the container
+ me.suspendLayout = true;
+ for (; i < len; i++) {
+ item = removeItems[i];
+ me.remove(item, autoDestroy);
+
+ if (item.ownerCt !== me) {
+ items.push(item);
+ }
+ }
+
+ // Resume Layouts now that all items have been removed and do a single layout (if we removed anything!)
+ me.suspendLayout = false;
+ if (len) {
+ me.doLayout();
+ }
+ return items;
+ },
+
+ // Used by ComponentQuery to retrieve all of the items
+ // which can potentially be considered a child of this Container.
+ // This should be overriden by components which have child items
+ // that are not contained in items. For example dockedItems, menu, etc
+ // IMPORTANT note for maintainers:
+ // Items are returned in tree traversal order. Each item is appended to the result array
+ // followed by the results of that child's getRefItems call.
+ // Floating child items are appended after internal child items.
+ getRefItems : function(deep) {
+ var me = this,
+ items = me.items.items,
+ len = items.length,
+ i = 0,
+ item,
+ result = [];
+
+ for (; i < len; i++) {
+ item = items[i];
+ result.push(item);
+ if (deep && item.getRefItems) {
+ result.push.apply(result, item.getRefItems(true));
+ }
+ }
+
+ // Append floating items to the list.
+ // These will only be present after they are rendered.
+ if (me.floatingItems && me.floatingItems.accessList) {
+ result.push.apply(result, me.floatingItems.accessList);
+ }
+
+ return result;
+ },
+
+ /**
+ * Cascades down the component/container heirarchy from this component (passed in the first call), calling the specified function with
+ * each component. The scope (<code>this</code> reference) of the
+ * function call will be the scope provided or the current component. The arguments to the function
+ * will be the args provided or the current component. 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] The scope of the function (defaults to current component)
+ * @param {Array} [args] The args to call the function with. The current component always passed as the last argument.
+ * @return {Ext.Container} this
+ */
+ cascade : function(fn, scope, origArgs){
+ var me = this,
+ cs = me.items ? me.items.items : [],
+ len = cs.length,
+ i = 0,
+ c,
+ args = origArgs ? origArgs.concat(me) : [me],
+ componentIndex = args.length - 1;
+
+ if (fn.apply(scope || me, args) !== false) {
+ for(; i < len; i++){
+ c = cs[i];
+ if (c.cascade) {
+ c.cascade(fn, scope, origArgs);
+ } else {
+ args[componentIndex] = c;
+ fn.apply(scope || cs, args);
+ }
+ }
+ }
+ return this;
+ },
+
+ /**
+ * Examines this container's <code>{@link #items}</code> <b>property</b>
+ * and gets a direct child component of this container.
+ * @param {String/Number} comp This parameter may be any of the following:
+ * <div><ul class="mdetail-params">
+ * <li>a <b><code>String</code></b> : representing the <code>{@link Ext.Component#itemId itemId}</code>
+ * or <code>{@link Ext.Component#id id}</code> of the child component </li>
+ * <li>a <b><code>Number</code></b> : representing the position of the child component
+ * within the <code>{@link #items}</code> <b>property</b></li>
+ * </ul></div>
+ * <p>For additional information see {@link Ext.util.MixedCollection#get}.
+ * @return Ext.Component The component (if found).
+ */
+ getComponent : function(comp) {
+ if (Ext.isObject(comp)) {
+ comp = comp.getItemId();
+ }
+
+ return this.items.get(comp);
+ },
+
+ /**
+ * Retrieves all descendant components which match the passed selector.
+ * Executes an Ext.ComponentQuery.query using this container as its root.
+ * @param {String} selector (optional) Selector complying to an Ext.ComponentQuery selector.
+ * If no selector is specified all items will be returned.
+ * @return {Ext.Component[]} Components which matched the selector
+ */
+ query : function(selector) {
+ selector = selector || '*';
+ return Ext.ComponentQuery.query(selector, this);
+ },
+
+ /**
+ * Retrieves the first direct child of this container which matches the passed selector.
+ * The passed in selector must comply with an Ext.ComponentQuery selector.
+ * @param {String} selector (optional) An Ext.ComponentQuery selector. If no selector is
+ * specified, the first child will be returned.
+ * @return Ext.Component
+ */
+ child : function(selector) {
+ selector = selector || '';
+ return this.query('> ' + selector)[0] || null;
+ },
+
+ /**
+ * Retrieves the first descendant of this container which matches the passed selector.
+ * The passed in selector must comply with an Ext.ComponentQuery selector.
+ * @param {String} selector (optional) An Ext.ComponentQuery selector. If no selector is
+ * specified, the first child will be returned.
+ * @return Ext.Component
+ */
+ down : function(selector) {
+ return this.query(selector)[0] || null;
+ },
+
+ // inherit docs
+ show : function() {
+ this.callParent(arguments);
+ this.performDeferredLayouts();
+ return this;
+ },
+
+ // Lay out any descendant containers who queued a layout operation during the time this was hidden
+ // This is also called by Panel after it expands because descendants of a collapsed Panel allso queue any layout ops.
+ performDeferredLayouts: function() {
+ var layoutCollection = this.layoutOnShow,
+ ln = layoutCollection.getCount(),
+ i = 0,
+ needsLayout,
+ item;
+
+ for (; i < ln; i++) {
+ item = layoutCollection.get(i);
+ needsLayout = item.needsLayout;
+
+ if (Ext.isObject(needsLayout)) {
+ item.doComponentLayout(needsLayout.width, needsLayout.height, needsLayout.isSetSize, needsLayout.ownerCt);
+ }
+ }
+ layoutCollection.clear();
+ },
+
+ //@private
+ // Enable all immediate children that was previously disabled
+ onEnable: function() {
+ Ext.Array.each(this.query('[isFormField]'), function(item) {
+ if (item.resetDisable) {
+ item.enable();
+ delete item.resetDisable;
+ }
+ });
+ this.callParent();
+ },
+
+ // @private
+ // Disable all immediate children that was previously disabled
+ onDisable: function() {
+ Ext.Array.each(this.query('[isFormField]'), function(item) {
+ if (item.resetDisable !== false && !item.disabled) {
+ item.disable();
+ item.resetDisable = true;
+ }
+ });
+ this.callParent();
+ },
+
+ /**
+ * Occurs before componentLayout is run. Returning false from this method will prevent the containerLayout
+ * from being executed.
+ */
+ beforeLayout: function() {
+ return true;
+ },
+
+ // @private
+ beforeDestroy : function() {
+ var me = this,
+ items = me.items,
+ c;
+
+ if (items) {
+ while ((c = items.first())) {
+ me.doRemove(c, true);
+ }
+ }
+
+ Ext.destroy(
+ me.layout
+ );
+ me.callParent();
+ }
+});
+
+/**
+ * Base class for any Ext.Component that may contain other Components. Containers handle the basic behavior of
+ * containing items, namely adding, inserting and removing items.
+ *
+ * The most commonly used Container classes are Ext.panel.Panel, Ext.window.Window and
+ * Ext.tab.Panel. If you do not need the capabilities offered by the aforementioned classes you can create a
+ * lightweight Container to be encapsulated by an HTML element to your specifications by using the
+ * {@link Ext.Component#autoEl autoEl} config option.
+ *
+ * The code below illustrates how to explicitly create a Container:
+ *
+ * @example
+ * // Explicitly create a Container
+ * Ext.create('Ext.container.Container', {
+ * layout: {
+ * type: 'hbox'
+ * },
+ * width: 400,
+ * renderTo: Ext.getBody(),
+ * border: 1,
+ * style: {borderColor:'#000000', borderStyle:'solid', borderWidth:'1px'},
+ * defaults: {
+ * labelWidth: 80,
+ * // implicitly create Container by specifying xtype
+ * xtype: 'datefield',
+ * flex: 1,
+ * style: {
+ * padding: '10px'
+ * }
+ * },
+ * items: [{
+ * xtype: 'datefield',
+ * name: 'startDate',
+ * fieldLabel: 'Start date'
+ * },{
+ * xtype: 'datefield',
+ * name: 'endDate',
+ * fieldLabel: 'End date'
+ * }]
+ * });
+ *
+ * ## Layout
+ *
+ * Container classes delegate the rendering of child Components to a layout manager class which must be configured into
+ * the Container using the `{@link #layout}` configuration property.
+ *
+ * When either specifying child `{@link #items}` of a Container, or dynamically {@link #add adding} Components to a
+ * Container, remember to consider how you wish the Container to arrange those child elements, and whether those child
+ * elements need to be sized using one of Ext's built-in `{@link #layout}` schemes. By default, Containers use the
+ * {@link Ext.layout.container.Auto Auto} scheme which only renders child components, appending them one after the other
+ * inside the Container, and **does not apply any sizing** at all.
+ *
+ * A common mistake is when a developer neglects to specify a `{@link #layout}` (e.g. widgets like GridPanels or
+ * TreePanels are added to Containers for which no `{@link #layout}` has been specified). If a Container is left to
+ * use the default {@link Ext.layout.container.Auto Auto} scheme, none of its child components will be resized, or changed in
+ * any way when the Container is resized.
+ *
+ * Certain layout managers allow dynamic addition of child components. Those that do include
+ * Ext.layout.container.Card, Ext.layout.container.Anchor, Ext.layout.container.VBox,
+ * Ext.layout.container.HBox, and Ext.layout.container.Table. For example:
+ *
+ * // Create the GridPanel.
+ * var myNewGrid = new Ext.grid.Panel({
+ * store: myStore,
+ * headers: myHeaders,
+ * title: 'Results', // the title becomes the title of the tab
+ * });
+ *
+ * myTabPanel.add(myNewGrid); // {@link Ext.tab.Panel} implicitly uses {@link Ext.layout.container.Card Card}
+ * myTabPanel.{@link Ext.tab.Panel#setActiveTab setActiveTab}(myNewGrid);
+ *
+ * The example above adds a newly created GridPanel to a TabPanel. Note that a TabPanel uses {@link
+ * Ext.layout.container.Card} as its layout manager which means all its child items are sized to {@link
+ * Ext.layout.container.Fit fit} exactly into its client area.
+ *
+ * **_Overnesting is a common problem_**. An example of overnesting occurs when a GridPanel is added to a TabPanel by
+ * wrapping the GridPanel _inside_ a wrapping Panel (that has no `{@link #layout}` specified) and then add that
+ * wrapping Panel to the TabPanel. The point to realize is that a GridPanel **is** a Component which can be added
+ * directly to a Container. If the wrapping Panel has no `{@link #layout}` configuration, then the overnested
+ * GridPanel will not be sized as expected.
+ *
+ * ## Adding via remote configuration
+ *
+ * A server side script can be used to add Components which are generated dynamically on the server. An example of
+ * adding a GridPanel to a TabPanel where the GridPanel is generated by the server based on certain parameters:
+ *
+ * // execute an Ajax request to invoke server side script:
+ * Ext.Ajax.request({
+ * url: 'gen-invoice-grid.php',
+ * // send additional parameters to instruct server script
+ * params: {
+ * startDate: Ext.getCmp('start-date').getValue(),
+ * endDate: Ext.getCmp('end-date').getValue()
+ * },
+ * // process the response object to add it to the TabPanel:
+ * success: function(xhr) {
+ * var newComponent = eval(xhr.responseText); // see discussion below
+ * myTabPanel.add(newComponent); // add the component to the TabPanel
+ * myTabPanel.setActiveTab(newComponent);
+ * },
+ * failure: function() {
+ * Ext.Msg.alert("Grid create failed", "Server communication failure");
+ * }
+ * });
+ *
+ * The server script needs to return a JSON representation of a configuration object, which, when decoded will return a
+ * config object with an {@link Ext.Component#xtype xtype}. The server might return the following JSON:
+ *
+ * {
+ * "xtype": 'grid',
+ * "title": 'Invoice Report',
+ * "store": {
+ * "model": 'Invoice',
+ * "proxy": {
+ * "type": 'ajax',
+ * "url": 'get-invoice-data.php',
+ * "reader": {
+ * "type": 'json'
+ * "record": 'transaction',
+ * "idProperty": 'id',
+ * "totalRecords": 'total'
+ * })
+ * },
+ * "autoLoad": {
+ * "params": {
+ * "startDate": '01/01/2008',
+ * "endDate": '01/31/2008'
+ * }
+ * }
+ * },
+ * "headers": [
+ * {"header": "Customer", "width": 250, "dataIndex": 'customer', "sortable": true},
+ * {"header": "Invoice Number", "width": 120, "dataIndex": 'invNo', "sortable": true},
+ * {"header": "Invoice Date", "width": 100, "dataIndex": 'date', "renderer": Ext.util.Format.dateRenderer('M d, y'), "sortable": true},
+ * {"header": "Value", "width": 120, "dataIndex": 'value', "renderer": 'usMoney', "sortable": true}
+ * ]
+ * }
+ *
+ * When the above code fragment is passed through the `eval` function in the success handler of the Ajax request, the
+ * result will be a config object which, when added to a Container, will cause instantiation of a GridPanel. **Be sure
+ * that the Container is configured with a layout which sizes and positions the child items to your requirements.**
+ *
+ * **Note:** since the code above is _generated_ by a server script, the `autoLoad` params for the Store, the user's
+ * preferred date format, the metadata to allow generation of the Model layout, and the ColumnModel can all be generated
+ * into the code since these are all known on the server.
+ */
+Ext.define('Ext.container.Container', {
+ extend: 'Ext.container.AbstractContainer',
+ alias: 'widget.container',
+ alternateClassName: 'Ext.Container',
+
+ /**
+ * Return the immediate child Component in which the passed element is located.
+ * @param {Ext.Element/HTMLElement/String} el The element to test (or ID of element).
+ * @return {Ext.Component} The child item which contains the passed element.
+ */
+ getChildByElement: function(el) {
+ var item,
+ itemEl,
+ i = 0,
+ it = this.items.items,
+ ln = it.length;
+
+ el = Ext.getDom(el);
+ for (; i < ln; i++) {
+ item = it[i];
+ itemEl = item.getEl();
+ if ((itemEl.dom === el) || itemEl.contains(el)) {
+ return item;
+ }
+ }
+ return null;
+ }
+});
+
+/**
+ * A non-rendering placeholder item which instructs the Toolbar's Layout to begin using
+ * the right-justified button container.
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Toolbar Fill Example',
+ * width: 300,
+ * height: 200,
+ * tbar : [
+ * 'Item 1',
+ * { xtype: 'tbfill' },
+ * 'Item 2'
+ * ],
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.toolbar.Fill', {
+ extend: 'Ext.Component',
+ alias: 'widget.tbfill',
+ alternateClassName: 'Ext.Toolbar.Fill',
+ isFill : true,
+ flex: 1
+});
+/**
+ * @class Ext.toolbar.Item
+ * @extends Ext.Component
+ * The base class that other non-interacting Toolbar Item classes should extend in order to
+ * get some basic common toolbar item functionality.
+ */
+Ext.define('Ext.toolbar.Item', {
+ extend: 'Ext.Component',
+ alias: 'widget.tbitem',
+ alternateClassName: 'Ext.Toolbar.Item',
+ enable:Ext.emptyFn,
+ disable:Ext.emptyFn,
+ focus:Ext.emptyFn
+ /**
+ * @cfg {String} overflowText Text to be used for the menu if the item is overflowed.
+ */
+});
+/**
+ * @class Ext.toolbar.Separator
+ * @extends Ext.toolbar.Item
+ * A simple class that adds a vertical separator bar between toolbar items (css class: 'x-toolbar-separator').
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Toolbar Seperator Example',
+ * width: 300,
+ * height: 200,
+ * tbar : [
+ * 'Item 1',
+ * { xtype: 'tbseparator' },
+ * 'Item 2'
+ * ],
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.toolbar.Separator', {
+ extend: 'Ext.toolbar.Item',
+ alias: 'widget.tbseparator',
+ alternateClassName: 'Ext.Toolbar.Separator',
+ baseCls: Ext.baseCSSPrefix + 'toolbar-separator',
+ focusable: false
+});
+/**
+ * @class Ext.menu.Manager
+ * Provides a common registry of all menus on a page.
+ * @singleton
+ */
+Ext.define('Ext.menu.Manager', {
+ singleton: true,
+ requires: [
+ 'Ext.util.MixedCollection',
+ 'Ext.util.KeyMap'
+ ],
+ alternateClassName: 'Ext.menu.MenuMgr',
+
+ uses: ['Ext.menu.Menu'],
+
+ menus: {},
+ groups: {},
+ attached: false,
+ lastShow: new Date(),
+
+ init: function() {
+ var me = this;
+
+ me.active = Ext.create('Ext.util.MixedCollection');
+ Ext.getDoc().addKeyListener(27, function() {
+ if (me.active.length > 0) {
+ me.hideAll();
+ }
+ }, me);
+ },
+
+ /**
+ * Hides all menus that are currently visible
+ * @return {Boolean} success True if any active menus were hidden.
+ */
+ hideAll: function() {
+ var active = this.active,
+ c;
+ if (active && active.length > 0) {
+ c = active.clone();
+ c.each(function(m) {
+ m.hide();
+ });
+ return true;
+ }
+ return false;
+ },
+
+ onHide: function(m) {
+ var me = this,
+ active = me.active;
+ active.remove(m);
+ if (active.length < 1) {
+ Ext.getDoc().un('mousedown', me.onMouseDown, me);
+ me.attached = false;
+ }
+ },
+
+ onShow: function(m) {
+ var me = this,
+ active = me.active,
+ last = active.last(),
+ attached = me.attached,
+ menuEl = m.getEl(),
+ zIndex;
+
+ me.lastShow = new Date();
+ active.add(m);
+ if (!attached) {
+ Ext.getDoc().on('mousedown', me.onMouseDown, me);
+ me.attached = true;
+ }
+ m.toFront();
+ },
+
+ onBeforeHide: function(m) {
+ if (m.activeChild) {
+ m.activeChild.hide();
+ }
+ if (m.autoHideTimer) {
+ clearTimeout(m.autoHideTimer);
+ delete m.autoHideTimer;
+ }
+ },
+
+ onBeforeShow: function(m) {
+ var active = this.active,
+ parentMenu = m.parentMenu;
+
+ active.remove(m);
+ if (!parentMenu && !m.allowOtherMenus) {
+ this.hideAll();
+ }
+ else if (parentMenu && parentMenu.activeChild && m != parentMenu.activeChild) {
+ parentMenu.activeChild.hide();
+ }
+ },
+
+ // private
+ onMouseDown: function(e) {
+ var me = this,
+ active = me.active,
+ lastShow = me.lastShow,
+ target = e.target;
+
+ if (Ext.Date.getElapsed(lastShow) > 50 && active.length > 0 && !e.getTarget('.' + Ext.baseCSSPrefix + 'menu')) {
+ me.hideAll();
+ // in IE, if we mousedown on a focusable element, the focus gets cancelled and the focus event is never
+ // fired on the element, so we'll focus it here
+ if (Ext.isIE && Ext.fly(target).focusable()) {
+ target.focus();
+ }
+ }
+ },
+
+ // private
+ register: function(menu) {
+ var me = this;
+
+ if (!me.active) {
+ me.init();
+ }
+
+ if (menu.floating) {
+ me.menus[menu.id] = menu;
+ menu.on({
+ beforehide: me.onBeforeHide,
+ hide: me.onHide,
+ beforeshow: me.onBeforeShow,
+ show: me.onShow,
+ scope: me
+ });
+ }
+ },
+
+ /**
+ * Returns a {@link Ext.menu.Menu} object
+ * @param {String/Object} menu The string menu id, an existing menu object reference, or a Menu config that will
+ * be used to generate and return a new Menu this.
+ * @return {Ext.menu.Menu} The specified menu, or null if none are found
+ */
+ get: function(menu) {
+ var menus = this.menus;
+
+ if (typeof menu == 'string') { // menu id
+ if (!menus) { // not initialized, no menus to return
+ return null;
+ }
+ return menus[menu];
+ } else if (menu.isMenu) { // menu instance
+ return menu;
+ } else if (Ext.isArray(menu)) { // array of menu items
+ return Ext.create('Ext.menu.Menu', {items:menu});
+ } else { // otherwise, must be a config
+ return Ext.ComponentManager.create(menu, 'menu');
+ }
+ },
+
+ // private
+ unregister: function(menu) {
+ var me = this,
+ menus = me.menus,
+ active = me.active;
+
+ delete menus[menu.id];
+ active.remove(menu);
+ menu.un({
+ beforehide: me.onBeforeHide,
+ hide: me.onHide,
+ beforeshow: me.onBeforeShow,
+ show: me.onShow,
+ scope: me
+ });
+ },
+
+ // private
+ registerCheckable: function(menuItem) {
+ var groups = this.groups,
+ groupId = menuItem.group;
+
+ if (groupId) {
+ if (!groups[groupId]) {
+ groups[groupId] = [];
+ }
+
+ groups[groupId].push(menuItem);
+ }
+ },
+
+ // private
+ unregisterCheckable: function(menuItem) {
+ var groups = this.groups,
+ groupId = menuItem.group;
+
+ if (groupId) {
+ Ext.Array.remove(groups[groupId], menuItem);
+ }
+ },
+
+ onCheckChange: function(menuItem, state) {
+ var groups = this.groups,
+ groupId = menuItem.group,
+ i = 0,
+ group, ln, curr;
+
+ if (groupId && state) {
+ group = groups[groupId];
+ ln = group.length;
+ for (; i < ln; i++) {
+ curr = group[i];
+ if (curr != menuItem) {
+ curr.setChecked(false);
+ }
+ }
+ }
+ }
+});
+/**
+ * Component layout for buttons
+ * @class Ext.layout.component.Button
+ * @extends Ext.layout.component.Component
+ * @private
+ */
+Ext.define('Ext.layout.component.Button', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.button'],
+
+ extend: 'Ext.layout.component.Component',
+
+ /* End Definitions */
+
+ type: 'button',
+
+ cellClsRE: /-btn-(tl|br)\b/,
+ htmlRE: /<.*>/,
+
+ beforeLayout: function() {
+ return this.callParent(arguments) || this.lastText !== this.owner.text;
+ },
+
+ /**
+ * Set the dimensions of the inner <button> element to match the
+ * component dimensions.
+ */
+ onLayout: function(width, height) {
+ var me = this,
+ isNum = Ext.isNumber,
+ owner = me.owner,
+ ownerEl = owner.el,
+ btnEl = owner.btnEl,
+ btnInnerEl = owner.btnInnerEl,
+ btnIconEl = owner.btnIconEl,
+ sizeIconEl = (owner.icon || owner.iconCls) && (owner.iconAlign == "top" || owner.iconAlign == "bottom"),
+ minWidth = owner.minWidth,
+ maxWidth = owner.maxWidth,
+ ownerWidth, btnFrameWidth, metrics;
+
+ me.getTargetInfo();
+ me.callParent(arguments);
+
+ btnInnerEl.unclip();
+ me.setTargetSize(width, height);
+
+ if (!isNum(width)) {
+ // In IE7 strict mode button elements with width:auto get strange extra side margins within
+ // the wrapping table cell, but they go away if the width is explicitly set. So we measure
+ // the size of the text and set the width to match.
+ if (owner.text && (Ext.isIE6 || Ext.isIE7) && Ext.isStrict && btnEl && btnEl.getWidth() > 20) {
+ btnFrameWidth = me.btnFrameWidth;
+ metrics = Ext.util.TextMetrics.measure(btnInnerEl, owner.text);
+ ownerEl.setWidth(metrics.width + btnFrameWidth + me.adjWidth);
+ btnEl.setWidth(metrics.width + btnFrameWidth);
+ btnInnerEl.setWidth(metrics.width + btnFrameWidth);
+
+ if (sizeIconEl) {
+ btnIconEl.setWidth(metrics.width + btnFrameWidth);
+ }
+ } else {
+ // Remove any previous fixed widths
+ ownerEl.setWidth(null);
+ btnEl.setWidth(null);
+ btnInnerEl.setWidth(null);
+ btnIconEl.setWidth(null);
+ }
+
+ // Handle maxWidth/minWidth config
+ if (minWidth || maxWidth) {
+ ownerWidth = ownerEl.getWidth();
+ if (minWidth && (ownerWidth < minWidth)) {
+ me.setTargetSize(minWidth, height);
+ }
+ else if (maxWidth && (ownerWidth > maxWidth)) {
+ btnInnerEl.clip();
+ me.setTargetSize(maxWidth, height);
+ }
+ }
+ }
+
+ this.lastText = owner.text;
+ },
+
+ setTargetSize: function(width, height) {
+ var me = this,
+ owner = me.owner,
+ isNum = Ext.isNumber,
+ btnInnerEl = owner.btnInnerEl,
+ btnWidth = (isNum(width) ? width - me.adjWidth : width),
+ btnHeight = (isNum(height) ? height - me.adjHeight : height),
+ btnFrameHeight = me.btnFrameHeight,
+ text = owner.getText(),
+ textHeight;
+
+ me.callParent(arguments);
+ me.setElementSize(owner.btnEl, btnWidth, btnHeight);
+ me.setElementSize(btnInnerEl, btnWidth, btnHeight);
+ if (btnHeight >= 0) {
+ btnInnerEl.setStyle('line-height', btnHeight - btnFrameHeight + 'px');
+ }
+
+ // Button text may contain markup that would force it to wrap to more than one line (e.g. 'Button<br>Label').
+ // When this happens, we cannot use the line-height set above for vertical centering; we instead reset the
+ // line-height to normal, measure the rendered text height, and add padding-top to center the text block
+ // vertically within the button's height. This is more expensive than the basic line-height approach so
+ // we only do it if the text contains markup.
+ if (text && this.htmlRE.test(text)) {
+ btnInnerEl.setStyle('line-height', 'normal');
+ textHeight = Ext.util.TextMetrics.measure(btnInnerEl, text).height;
+ btnInnerEl.setStyle('padding-top', me.btnFrameTop + Math.max(btnInnerEl.getHeight() - btnFrameHeight - textHeight, 0) / 2 + 'px');
+ me.setElementSize(btnInnerEl, btnWidth, btnHeight);
+ }
+ },
+
+ getTargetInfo: function() {
+ var me = this,
+ owner = me.owner,
+ ownerEl = owner.el,
+ frameSize = me.frameSize,
+ frameBody = owner.frameBody,
+ btnWrap = owner.btnWrap,
+ innerEl = owner.btnInnerEl;
+
+ if (!('adjWidth' in me)) {
+ Ext.apply(me, {
+ // Width adjustment must take into account the arrow area. The btnWrap is the <em> which has padding to accommodate the arrow.
+ adjWidth: frameSize.left + frameSize.right + ownerEl.getBorderWidth('lr') + ownerEl.getPadding('lr') +
+ btnWrap.getPadding('lr') + (frameBody ? frameBody.getFrameWidth('lr') : 0),
+ adjHeight: frameSize.top + frameSize.bottom + ownerEl.getBorderWidth('tb') + ownerEl.getPadding('tb') +
+ btnWrap.getPadding('tb') + (frameBody ? frameBody.getFrameWidth('tb') : 0),
+ btnFrameWidth: innerEl.getFrameWidth('lr'),
+ btnFrameHeight: innerEl.getFrameWidth('tb'),
+ btnFrameTop: innerEl.getFrameWidth('t')
+ });
+ }
+
+ return me.callParent();
+ }
+});
+/**
+ * @docauthor Robert Dougan <rob@sencha.com>
+ *
+ * Create simple buttons with this component. Customisations include {@link #iconAlign aligned}
+ * {@link #iconCls icons}, {@link #menu dropdown menus}, {@link #tooltip tooltips}
+ * and {@link #scale sizing options}. Specify a {@link #handler handler} to run code when
+ * a user clicks the button, or use {@link #listeners listeners} for other events such as
+ * {@link #mouseover mouseover}. Example usage:
+ *
+ * @example
+ * Ext.create('Ext.Button', {
+ * text: 'Click me',
+ * renderTo: Ext.getBody(),
+ * handler: function() {
+ * alert('You clicked the button!')
+ * }
+ * });
+ *
+ * The {@link #handler} configuration can also be updated dynamically using the {@link #setHandler}
+ * method. Example usage:
+ *
+ * @example
+ * Ext.create('Ext.Button', {
+ * text : 'Dynamic Handler Button',
+ * renderTo: Ext.getBody(),
+ * handler : function() {
+ * // this button will spit out a different number every time you click it.
+ * // so firstly we must check if that number is already set:
+ * if (this.clickCount) {
+ * // looks like the property is already set, so lets just add 1 to that number and alert the user
+ * this.clickCount++;
+ * alert('You have clicked the button "' + this.clickCount + '" times.\n\nTry clicking it again..');
+ * } else {
+ * // if the clickCount property is not set, we will set it and alert the user
+ * this.clickCount = 1;
+ * alert('You just clicked the button for the first time!\n\nTry pressing it again..');
+ * }
+ * }
+ * });
+ *
+ * A button within a container:
+ *
+ * @example
+ * Ext.create('Ext.Container', {
+ * renderTo: Ext.getBody(),
+ * items : [
+ * {
+ * xtype: 'button',
+ * text : 'My Button'
+ * }
+ * ]
+ * });
+ *
+ * A useful option of Button is the {@link #scale} configuration. This configuration has three different options:
+ *
+ * - `'small'`
+ * - `'medium'`
+ * - `'large'`
+ *
+ * Example usage:
+ *
+ * @example
+ * Ext.create('Ext.Button', {
+ * renderTo: document.body,
+ * text : 'Click me',
+ * scale : 'large'
+ * });
+ *
+ * Buttons can also be toggled. To enable this, you simple set the {@link #enableToggle} property to `true`.
+ * Example usage:
+ *
+ * @example
+ * Ext.create('Ext.Button', {
+ * renderTo: Ext.getBody(),
+ * text: 'Click Me',
+ * enableToggle: true
+ * });
+ *
+ * You can assign a menu to a button by using the {@link #menu} configuration. This standard configuration
+ * can either be a reference to a {@link Ext.menu.Menu menu} object, a {@link Ext.menu.Menu menu} id or a
+ * {@link Ext.menu.Menu menu} config blob. When assigning a menu to a button, an arrow is automatically
+ * added to the button. You can change the alignment of the arrow using the {@link #arrowAlign} configuration
+ * on button. Example usage:
+ *
+ * @example
+ * Ext.create('Ext.Button', {
+ * text : 'Menu button',
+ * renderTo : Ext.getBody(),
+ * arrowAlign: 'bottom',
+ * menu : [
+ * {text: 'Item 1'},
+ * {text: 'Item 2'},
+ * {text: 'Item 3'},
+ * {text: 'Item 4'}
+ * ]
+ * });
+ *
+ * Using listeners, you can easily listen to events fired by any component, using the {@link #listeners}
+ * configuration or using the {@link #addListener} method. Button has a variety of different listeners:
+ *
+ * - `click`
+ * - `toggle`
+ * - `mouseover`
+ * - `mouseout`
+ * - `mouseshow`
+ * - `menuhide`
+ * - `menutriggerover`
+ * - `menutriggerout`
+ *
+ * Example usage:
+ *
+ * @example
+ * Ext.create('Ext.Button', {
+ * text : 'Button',
+ * renderTo : Ext.getBody(),
+ * listeners: {
+ * click: function() {
+ * // this == the button, as we are in the local scope
+ * this.setText('I was clicked!');
+ * },
+ * mouseover: function() {
+ * // set a new config which says we moused over, if not already set
+ * if (!this.mousedOver) {
+ * this.mousedOver = true;
+ * alert('You moused over a button!\n\nI wont do this again.');
+ * }
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.button.Button', {
+
+ /* Begin Definitions */
+ alias: 'widget.button',
+ extend: 'Ext.Component',
+
+ requires: [
+ 'Ext.menu.Manager',
+ 'Ext.util.ClickRepeater',
+ 'Ext.layout.component.Button',
+ 'Ext.util.TextMetrics',
+ 'Ext.util.KeyMap'
+ ],
+
+ alternateClassName: 'Ext.Button',
+ /* End Definitions */
+
+ isButton: true,
+ componentLayout: 'button',
+
+ /**
+ * @property {Boolean} hidden
+ * True if this button is hidden. Read-only.
+ */
+ hidden: false,
+
+ /**
+ * @property {Boolean} disabled
+ * True if this button is disabled. Read-only.
+ */
+ disabled: false,
+
+ /**
+ * @property {Boolean} pressed
+ * True if this button is pressed (only if enableToggle = true). Read-only.
+ */
+ pressed: false,
+
+ /**
+ * @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).
+ * @cfg {Ext.button.Button} handler.button This button.
+ * @cfg {Ext.EventObject} handler.e 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.Panel}.{@link Ext.panel.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=false]
+ * True to start hidden.
+ */
+
+ /**
+ * @cfg {Boolean} [disabled=true]
+ * True to start disabled.
+ */
+
+ /**
+ * @cfg {Boolean} [pressed=false]
+ * 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=false]
+ * True to repeat fire the click event while the mouse is down. This can also be a
+ * {@link Ext.util.ClickRepeater ClickRepeater} config object.
+ */
+
+ /**
+ * @cfg {Number} tabIndex
+ * Set a DOM tabIndex for this button.
+ */
+
+ /**
+ * @cfg {Boolean} [allowDepress=true]
+ * False to not allow a pressed Button to be depressed. Only valid when {@link #enableToggle} is true.
+ */
+
+ /**
+ * @cfg {Boolean} [enableToggle=false]
+ * True to enable pressed/not pressed toggling.
+ */
+ enableToggle: false,
+
+ /**
+ * @cfg {Function} toggleHandler
+ * Function called when a Button with {@link #enableToggle} set to true is clicked.
+ * @cfg {Ext.button.Button} toggleHandler.button This button.
+ * @cfg {Boolean} toggleHandler.state The next state of the Button, true means pressed.
+ */
+
+ /**
+ * @cfg {Ext.menu.Menu/String/Object} menu
+ * Standard menu attribute consisting of a reference to a menu object, a menu id or a menu config blob.
+ */
+
+ /**
+ * @cfg {String} menuAlign
+ * The position to align the menu to (see {@link Ext.Element#alignTo} for more details).
+ */
+ menuAlign: 'tl-bl?',
+
+ /**
+ * @cfg {String} textAlign
+ * The text alignment for this button (center, left, right).
+ */
+ textAlign: 'center',
+
+ /**
+ * @cfg {String} overflowText
+ * If used in a {@link Ext.toolbar.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
+ * The type of `<input>` to create: submit, reset or button.
+ */
+ type: 'button',
+
+ /**
+ * @cfg {String} clickEvent
+ * The DOM event that will fire the handler of the button. This can be any valid event name (dblclick, contextmenu).
+ */
+ clickEvent: 'click',
+
+ /**
+ * @cfg {Boolean} preventDefault
+ * True to prevent the default action when the {@link #clickEvent} is processed.
+ */
+ preventDefault: true,
+
+ /**
+ * @cfg {Boolean} handleMouseEvents
+ * False to disable visual cues on mouseover, mouseout and mousedown.
+ */
+ handleMouseEvents: true,
+
+ /**
+ * @cfg {String} tooltipType
+ * The type of tooltip to use. Either 'qtip' for QuickTips or 'title' for title attribute.
+ */
+ tooltipType: 'qtip',
+
+ /**
+ * @cfg {String} [baseCls='x-btn']
+ * The base CSS class to add to all buttons.
+ */
+ baseCls: Ext.baseCSSPrefix + 'btn',
+
+ /**
+ * @cfg {String} pressedCls
+ * The CSS class to add to a button when it is in the pressed state.
+ */
+ pressedCls: 'pressed',
+
+ /**
+ * @cfg {String} overCls
+ * The CSS class to add to a button when it is in the over (hovered) state.
+ */
+ overCls: 'over',
+
+ /**
+ * @cfg {String} focusCls
+ * The CSS class to add to a button when it is in the focussed state.
+ */
+ focusCls: 'focus',
+
+ /**
+ * @cfg {String} menuActiveCls
+ * The CSS class to add to a button when it's menu is active.
+ */
+ menuActiveCls: 'menu-active',
+
+ /**
+ * @cfg {String} href
+ * The URL to visit when the button is clicked. Specifying this config is equivalent to specifying:
+ *
+ * handler: function() { window.location = "http://www.sencha.com" }
+ */
+
+ /**
+ * @cfg {Object} baseParams
+ * An object literal of parameters to pass to the url when the {@link #href} property is specified.
+ */
+
+ /**
+ * @cfg {Object} params
+ * An object literal of parameters to pass to the url when the {@link #href} property is specified. Any params
+ * override {@link #baseParams}. New params can be set using the {@link #setParams} method.
+ */
+
+ ariaRole: 'button',
+
+ // inherited
+ renderTpl:
+ '<em id="{id}-btnWrap" class="{splitCls}">' +
+ '<tpl if="href">' +
+ '<a id="{id}-btnEl" href="{href}" target="{target}"<tpl if="tabIndex"> tabIndex="{tabIndex}"</tpl> role="link">' +
+ '<span id="{id}-btnInnerEl" class="{baseCls}-inner">' +
+ '{text}' +
+ '</span>' +
+ '<span id="{id}-btnIconEl" class="{baseCls}-icon"></span>' +
+ '</a>' +
+ '</tpl>' +
+ '<tpl if="!href">' +
+ '<button id="{id}-btnEl" type="{type}" hidefocus="true"' +
+ // the autocomplete="off" is required to prevent Firefox from remembering
+ // the button's disabled state between page reloads.
+ '<tpl if="tabIndex"> tabIndex="{tabIndex}"</tpl> role="button" autocomplete="off">' +
+ '<span id="{id}-btnInnerEl" class="{baseCls}-inner" style="{innerSpanStyle}">' +
+ '{text}' +
+ '</span>' +
+ '<span id="{id}-btnIconEl" class="{baseCls}-icon {iconCls}"> </span>' +
+ '</button>' +
+ '</tpl>' +
+ '</em>' ,
+
+ /**
+ * @cfg {String} scale
+ * 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.
+ */
+ scale: 'small',
+
+ /**
+ * @private
+ * An array of allowed scales.
+ */
+ allowedScales: ['small', 'medium', 'large'],
+
+ /**
+ * @cfg {Object} scope
+ * The scope (**this** reference) in which the `{@link #handler}` and `{@link #toggleHandler}` is executed.
+ * Defaults to this Button.
+ */
+
+ /**
+ * @cfg {String} iconAlign
+ * The side of the Button box to render the icon. Four values are allowed:
+ *
+ * - 'top'
+ * - 'right'
+ * - 'bottom'
+ * - 'left'
+ */
+ iconAlign: 'left',
+
+ /**
+ * @cfg {String} arrowAlign
+ * The side of the Button box to render the arrow if the button has an associated {@link #menu}. Two
+ * values are allowed:
+ *
+ * - 'right'
+ * - 'bottom'
+ */
+ arrowAlign: 'right',
+
+ /**
+ * @cfg {String} arrowCls
+ * The className used for the inner arrow element if the button has a menu.
+ */
+ arrowCls: 'arrow',
+
+ /**
+ * @property {Ext.Template} template
+ * 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}.
+ */
+
+ /**
+ * @cfg {String} cls
+ * A CSS class string to apply to the button's main element.
+ */
+
+ /**
+ * @property {Ext.menu.Menu} menu
+ * The {@link Ext.menu.Menu Menu} object associated with this Button when configured with the {@link #menu} config
+ * option.
+ */
+
+ /**
+ * @cfg {Boolean} autoWidth
+ * By default, if a width is not specified the button will attempt to stretch horizontally to fit its content. If
+ * the button is being managed by a width sizing layout (hbox, fit, anchor), set this to false to prevent the button
+ * from doing this automatic sizing.
+ */
+
+ maskOnDisable: false,
+
+ // inherit docs
+ initComponent: function() {
+ var me = this;
+ me.callParent(arguments);
+
+ me.addEvents(
+ /**
+ * @event click
+ * Fires when this button is clicked
+ * @param {Ext.button.Button} this
+ * @param {Event} e The click event
+ */
+ 'click',
+
+ /**
+ * @event toggle
+ * Fires when the 'pressed' state of this button changes (only if enableToggle = true)
+ * @param {Ext.button.Button} this
+ * @param {Boolean} pressed
+ */
+ 'toggle',
+
+ /**
+ * @event mouseover
+ * Fires when the mouse hovers over the button
+ * @param {Ext.button.Button} this
+ * @param {Event} e The event object
+ */
+ 'mouseover',
+
+ /**
+ * @event mouseout
+ * Fires when the mouse exits the button
+ * @param {Ext.button.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 {Ext.button.Button} this
+ * @param {Ext.menu.Menu} menu
+ */
+ 'menushow',
+
+ /**
+ * @event menuhide
+ * If this button has a menu, this event fires when it is hidden
+ * @param {Ext.button.Button} this
+ * @param {Ext.menu.Menu} menu
+ */
+ 'menuhide',
+
+ /**
+ * @event menutriggerover
+ * If this button has a menu, this event fires when the mouse enters the menu triggering element
+ * @param {Ext.button.Button} this
+ * @param {Ext.menu.Menu} menu
+ * @param {Event} e
+ */
+ 'menutriggerover',
+
+ /**
+ * @event menutriggerout
+ * If this button has a menu, this event fires when the mouse leaves the menu triggering element
+ * @param {Ext.button.Button} this
+ * @param {Ext.menu.Menu} menu
+ * @param {Event} e
+ */
+ 'menutriggerout'
+ );
+
+ if (me.menu) {
+ // Flag that we'll have a splitCls
+ me.split = true;
+
+ // retrieve menu by id or instantiate instance if needed
+ me.menu = Ext.menu.Manager.get(me.menu);
+ me.menu.ownerCt = me;
+ }
+
+ // Accept url as a synonym for href
+ if (me.url) {
+ me.href = me.url;
+ }
+
+ // preventDefault defaults to false for links
+ if (me.href && !me.hasOwnProperty('preventDefault')) {
+ me.preventDefault = false;
+ }
+
+ if (Ext.isString(me.toggleGroup)) {
+ me.enableToggle = true;
+ }
+
+ },
+
+ // private
+ initAria: function() {
+ this.callParent();
+ var actionEl = this.getActionEl();
+ if (this.menu) {
+ actionEl.dom.setAttribute('aria-haspopup', true);
+ }
+ },
+
+ // inherit docs
+ getActionEl: function() {
+ return this.btnEl;
+ },
+
+ // inherit docs
+ getFocusEl: function() {
+ return this.btnEl;
+ },
+
+ // private
+ setButtonCls: function() {
+ var me = this,
+ cls = [],
+ btnIconEl = me.btnIconEl,
+ hide = 'x-hide-display';
+
+ if (me.useSetClass) {
+ if (!Ext.isEmpty(me.oldCls)) {
+ me.removeClsWithUI(me.oldCls);
+ me.removeClsWithUI(me.pressedCls);
+ }
+
+ // Check whether the button has an icon or not, and if it has an icon, what is th alignment
+ if (me.iconCls || me.icon) {
+ if (me.text) {
+ cls.push('icon-text-' + me.iconAlign);
+ } else {
+ cls.push('icon');
+ }
+ if (btnIconEl) {
+ btnIconEl.removeCls(hide);
+ }
+ } else {
+ if (me.text) {
+ cls.push('noicon');
+ }
+ if (btnIconEl) {
+ btnIconEl.addCls(hide);
+ }
+ }
+
+ me.oldCls = cls;
+ me.addClsWithUI(cls);
+ me.addClsWithUI(me.pressed ? me.pressedCls : null);
+ }
+ },
+
+ // private
+ onRender: function(ct, position) {
+ // classNames for the button
+ var me = this,
+ repeater, btn;
+
+ // Apply the renderData to the template args
+ Ext.applyIf(me.renderData, me.getTemplateArgs());
+
+ me.addChildEls('btnEl', 'btnWrap', 'btnInnerEl', 'btnIconEl');
+
+ if (me.scale) {
+ me.ui = me.ui + '-' + me.scale;
+ }
+
+ // Render internal structure
+ me.callParent(arguments);
+
+ // If it is a split button + has a toolip for the arrow
+ if (me.split && me.arrowTooltip) {
+ me.arrowEl.dom.setAttribute(me.getTipAttr(), me.arrowTooltip);
+ }
+
+ // Add listeners to the focus and blur events on the element
+ me.mon(me.btnEl, {
+ scope: me,
+ focus: me.onFocus,
+ blur : me.onBlur
+ });
+
+ // Set btn as a local variable for easy access
+ btn = me.el;
+
+ if (me.icon) {
+ me.setIcon(me.icon);
+ }
+
+ if (me.iconCls) {
+ me.setIconCls(me.iconCls);
+ }
+
+ if (me.tooltip) {
+ me.setTooltip(me.tooltip, true);
+ }
+
+ if (me.textAlign) {
+ me.setTextAlign(me.textAlign);
+ }
+
+ // Add the mouse events to the button
+ if (me.handleMouseEvents) {
+ me.mon(btn, {
+ scope: me,
+ mouseover: me.onMouseOver,
+ mouseout: me.onMouseOut,
+ mousedown: me.onMouseDown
+ });
+
+ if (me.split) {
+ me.mon(btn, {
+ mousemove: me.onMouseMove,
+ scope: me
+ });
+ }
+ }
+
+ // Check if the button has a menu
+ if (me.menu) {
+ me.mon(me.menu, {
+ scope: me,
+ show: me.onMenuShow,
+ hide: me.onMenuHide
+ });
+
+ me.keyMap = Ext.create('Ext.util.KeyMap', me.el, {
+ key: Ext.EventObject.DOWN,
+ handler: me.onDownKey,
+ scope: me
+ });
+ }
+
+ // Check if it is a repeat button
+ if (me.repeat) {
+ repeater = Ext.create('Ext.util.ClickRepeater', btn, Ext.isObject(me.repeat) ? me.repeat: {});
+ me.mon(repeater, 'click', me.onRepeatClick, me);
+ } else {
+ me.mon(btn, me.clickEvent, me.onClick, me);
+ }
+
+ // Register the button in the toggle manager
+ Ext.ButtonToggleManager.register(me);
+ },
+
+ /**
+ * This method returns an object which provides substitution parameters for the {@link #renderTpl XTemplate} 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.
+ *
+ * @return {Object} Substitution data for a Template. The default implementation which provides data for the default
+ * {@link #template} returns an Object containing the following properties:
+ * @return {String} return.type The `<button>`'s {@link #type}
+ * @return {String} return.splitCls A CSS class to determine the presence and position of an arrow icon.
+ * (`'x-btn-arrow'` or `'x-btn-arrow-bottom'` or `''`)
+ * @return {String} return.cls A CSS class name applied to the Button's main `<tbody>` element which determines the
+ * button's scale and icon alignment.
+ * @return {String} return.text The {@link #text} to display ion the Button.
+ * @return {Number} return.tabIndex The tab index within the input flow.
+ */
+ getTemplateArgs: function() {
+ var me = this,
+ persistentPadding = me.getPersistentBtnPadding(),
+ innerSpanStyle = '';
+
+ // Create negative margin offsets to counteract persistent button padding if needed
+ if (Math.max.apply(Math, persistentPadding) > 0) {
+ innerSpanStyle = 'margin:' + Ext.Array.map(persistentPadding, function(pad) {
+ return -pad + 'px';
+ }).join(' ');
+ }
+
+ return {
+ href : me.getHref(),
+ target : me.target || '_blank',
+ type : me.type,
+ splitCls : me.getSplitCls(),
+ cls : me.cls,
+ iconCls : me.iconCls || '',
+ text : me.text || ' ',
+ tabIndex : me.tabIndex,
+ innerSpanStyle: innerSpanStyle
+ };
+ },
+
+ /**
+ * @private
+ * If there is a configured href for this Button, returns the href with parameters appended.
+ * @returns The href string with parameters appended.
+ */
+ getHref: function() {
+ var me = this,
+ params = Ext.apply({}, me.baseParams);
+
+ // write baseParams first, then write any params
+ params = Ext.apply(params, me.params);
+ return me.href ? Ext.urlAppend(me.href, Ext.Object.toQueryString(params)) : false;
+ },
+
+ /**
+ * Sets the href of the link dynamically according to the params passed, and any {@link #baseParams} configured.
+ *
+ * **Only valid if the Button was originally configured with a {@link #href}**
+ *
+ * @param {Object} params Parameters to use in the href URL.
+ */
+ setParams: function(params) {
+ this.params = params;
+ this.btnEl.dom.href = this.getHref();
+ },
+
+ getSplitCls: function() {
+ var me = this;
+ return me.split ? (me.baseCls + '-' + me.arrowCls) + ' ' + (me.baseCls + '-' + me.arrowCls + '-' + me.arrowAlign) : '';
+ },
+
+ // private
+ afterRender: function() {
+ var me = this;
+ me.useSetClass = true;
+ me.setButtonCls();
+ me.doc = Ext.getDoc();
+ this.callParent(arguments);
+ },
+
+ /**
+ * 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.Button} this
+ */
+ setIconCls: function(cls) {
+ var me = this,
+ btnIconEl = me.btnIconEl,
+ oldCls = me.iconCls;
+
+ me.iconCls = cls;
+ if (btnIconEl) {
+ // Remove the previous iconCls from the button
+ btnIconEl.removeCls(oldCls);
+ btnIconEl.addCls(cls || '');
+ me.setButtonCls();
+ }
+ return me;
+ },
+
+ /**
+ * 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.tip.QuickTipManager#register}.
+ *
+ * @return {Ext.button.Button} this
+ */
+ setTooltip: function(tooltip, initial) {
+ var me = this;
+
+ if (me.rendered) {
+ if (!initial) {
+ me.clearTip();
+ }
+ if (Ext.isObject(tooltip)) {
+ Ext.tip.QuickTipManager.register(Ext.apply({
+ target: me.btnEl.id
+ },
+ tooltip));
+ me.tooltip = tooltip;
+ } else {
+ me.btnEl.dom.setAttribute(me.getTipAttr(), tooltip);
+ }
+ } else {
+ me.tooltip = tooltip;
+ }
+ return me;
+ },
+
+ /**
+ * Sets the text alignment for this button.
+ * @param {String} align The new alignment of the button text. See {@link #textAlign}.
+ */
+ setTextAlign: function(align) {
+ var me = this,
+ btnEl = me.btnEl;
+
+ if (btnEl) {
+ btnEl.removeCls(me.baseCls + '-' + me.textAlign);
+ btnEl.addCls(me.baseCls + '-' + align);
+ }
+ me.textAlign = align;
+ return me;
+ },
+
+ getTipAttr: function(){
+ return this.tooltipType == 'qtip' ? 'data-qtip' : 'title';
+ },
+
+ // private
+ getRefItems: function(deep){
+ var menu = this.menu,
+ items;
+
+ if (menu) {
+ items = menu.getRefItems(deep);
+ items.unshift(menu);
+ }
+ return items || [];
+ },
+
+ // private
+ clearTip: function() {
+ if (Ext.isObject(this.tooltip)) {
+ Ext.tip.QuickTipManager.unregister(this.btnEl);
+ }
+ },
+
+ // private
+ beforeDestroy: function() {
+ var me = this;
+ if (me.rendered) {
+ me.clearTip();
+ }
+ if (me.menu && me.destroyMenu !== false) {
+ Ext.destroy(me.menu);
+ }
+ Ext.destroy(me.btnInnerEl, me.repeater);
+ me.callParent();
+ },
+
+ // private
+ onDestroy: function() {
+ var me = this;
+ if (me.rendered) {
+ me.doc.un('mouseover', me.monitorMouseOver, me);
+ me.doc.un('mouseup', me.onMouseUp, me);
+ delete me.doc;
+ Ext.ButtonToggleManager.unregister(me);
+
+ Ext.destroy(me.keyMap);
+ delete me.keyMap;
+ }
+ me.callParent();
+ },
+
+ /**
+ * Assigns this Button's click handler
+ * @param {Function} handler The function to call when the button is clicked
+ * @param {Object} [scope] The scope (`this` reference) in which the handler function is executed.
+ * Defaults to this Button.
+ * @return {Ext.button.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.Button} this
+ */
+ setText: function(text) {
+ var me = this;
+ me.text = text;
+ if (me.el) {
+ me.btnInnerEl.update(text || ' ');
+ me.setButtonCls();
+ }
+ me.doComponentLayout();
+ return me;
+ },
+
+ /**
+ * 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.Button} this
+ */
+ setIcon: function(icon) {
+ var me = this,
+ iconEl = me.btnIconEl;
+
+ me.icon = icon;
+ if (iconEl) {
+ iconEl.setStyle('background-image', icon ? 'url(' + icon + ')': '');
+ me.setButtonCls();
+ }
+ return me;
+ },
+
+ /**
+ * 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] Force a particular state
+ * @param {Boolean} [suppressEvent=false] True to stop events being fired when calling this method.
+ * @return {Ext.button.Button} this
+ */
+ toggle: function(state, suppressEvent) {
+ var me = this;
+ state = state === undefined ? !me.pressed : !!state;
+ if (state !== me.pressed) {
+ if (me.rendered) {
+ me[state ? 'addClsWithUI': 'removeClsWithUI'](me.pressedCls);
+ }
+ me.btnEl.dom.setAttribute('aria-pressed', state);
+ me.pressed = state;
+ if (!suppressEvent) {
+ me.fireEvent('toggle', me, state);
+ Ext.callback(me.toggleHandler, me.scope || me, [me, state]);
+ }
+ }
+ return me;
+ },
+
+ maybeShowMenu: function(){
+ var me = this;
+ if (me.menu && !me.hasVisibleMenu() && !me.ignoreNextClick) {
+ me.showMenu();
+ }
+ },
+
+ /**
+ * Shows this button's menu (if it has one)
+ */
+ showMenu: function() {
+ var me = this;
+ if (me.rendered && me.menu) {
+ if (me.tooltip && me.getTipAttr() != 'title') {
+ Ext.tip.QuickTipManager.getQuickTip().cancelShow(me.btnEl);
+ }
+ if (me.menu.isVisible()) {
+ me.menu.hide();
+ }
+
+ me.menu.showBy(me.el, me.menuAlign);
+ }
+ return me;
+ },
+
+ /**
+ * Hides 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() {
+ var menu = this.menu;
+ return menu && menu.rendered && menu.isVisible();
+ },
+
+ // private
+ onRepeatClick: function(repeat, e) {
+ this.onClick(e);
+ },
+
+ // private
+ onClick: function(e) {
+ var me = this;
+ if (me.preventDefault || (me.disabled && me.getHref()) && e) {
+ e.preventDefault();
+ }
+ if (e.button !== 0) {
+ return;
+ }
+ if (!me.disabled) {
+ me.doToggle();
+ me.maybeShowMenu();
+ me.fireHandler(e);
+ }
+ },
+
+ fireHandler: function(e){
+ var me = this,
+ handler = me.handler;
+
+ me.fireEvent('click', me, e);
+ if (handler) {
+ handler.call(me.scope || me, me, e);
+ }
+ me.onBlur();
+ },
+
+ doToggle: function(){
+ var me = this;
+ if (me.enableToggle && (me.allowDepress !== false || !me.pressed)) {
+ me.toggle();
+ }
+ },
+
+ /**
+ * @private mouseover handler called when a mouseover event occurs anywhere within the encapsulating element.
+ * The targets are interrogated to see what is being entered from where.
+ * @param e
+ */
+ onMouseOver: function(e) {
+ var me = this;
+ if (!me.disabled && !e.within(me.el, true, true)) {
+ me.onMouseEnter(e);
+ }
+ },
+
+ /**
+ * @private
+ * mouseout handler called when a mouseout event occurs anywhere within the encapsulating element -
+ * or the mouse leaves the encapsulating element.
+ * The targets are interrogated to see what is being exited to where.
+ * @param e
+ */
+ onMouseOut: function(e) {
+ var me = this;
+ if (!e.within(me.el, true, true)) {
+ if (me.overMenuTrigger) {
+ me.onMenuTriggerOut(e);
+ }
+ me.onMouseLeave(e);
+ }
+ },
+
+ /**
+ * @private
+ * mousemove handler called when the mouse moves anywhere within the encapsulating element.
+ * The position is checked to determine if the mouse is entering or leaving the trigger area. Using
+ * mousemove to check this is more resource intensive than we'd like, but it is necessary because
+ * the trigger area does not line up exactly with sub-elements so we don't always get mouseover/out
+ * events when needed. In the future we should consider making the trigger a separate element that
+ * is absolutely positioned and sized over the trigger area.
+ */
+ onMouseMove: function(e) {
+ var me = this,
+ el = me.el,
+ over = me.overMenuTrigger,
+ overlap, btnSize;
+
+ if (me.split) {
+ if (me.arrowAlign === 'right') {
+ overlap = e.getX() - el.getX();
+ btnSize = el.getWidth();
+ } else {
+ overlap = e.getY() - el.getY();
+ btnSize = el.getHeight();
+ }
+
+ if (overlap > (btnSize - me.getTriggerSize())) {
+ if (!over) {
+ me.onMenuTriggerOver(e);
+ }
+ } else {
+ if (over) {
+ me.onMenuTriggerOut(e);
+ }
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Measures the size of the trigger area for menu and split buttons. Will be a width for
+ * a right-aligned trigger and a height for a bottom-aligned trigger. Cached after first measurement.
+ */
+ getTriggerSize: function() {
+ var me = this,
+ size = me.triggerSize,
+ side, sideFirstLetter, undef;
+
+ if (size === undef) {
+ side = me.arrowAlign;
+ sideFirstLetter = side.charAt(0);
+ size = me.triggerSize = me.el.getFrameWidth(sideFirstLetter) + me.btnWrap.getFrameWidth(sideFirstLetter) + (me.frameSize && me.frameSize[side] || 0);
+ }
+ return size;
+ },
+
+ /**
+ * @private
+ * virtual mouseenter handler called when it is detected that the mouseout event
+ * signified the mouse entering the encapsulating element.
+ * @param e
+ */
+ onMouseEnter: function(e) {
+ var me = this;
+ me.addClsWithUI(me.overCls);
+ me.fireEvent('mouseover', me, e);
+ },
+
+ /**
+ * @private
+ * virtual mouseleave handler called when it is detected that the mouseover event
+ * signified the mouse entering the encapsulating element.
+ * @param e
+ */
+ onMouseLeave: function(e) {
+ var me = this;
+ me.removeClsWithUI(me.overCls);
+ me.fireEvent('mouseout', me, e);
+ },
+
+ /**
+ * @private
+ * virtual mouseenter handler called when it is detected that the mouseover event
+ * signified the mouse entering the arrow area of the button - the <em>.
+ * @param e
+ */
+ onMenuTriggerOver: function(e) {
+ var me = this;
+ me.overMenuTrigger = true;
+ me.fireEvent('menutriggerover', me, me.menu, e);
+ },
+
+ /**
+ * @private
+ * virtual mouseleave handler called when it is detected that the mouseout event
+ * signified the mouse leaving the arrow area of the button - the <em>.
+ * @param e
+ */
+ onMenuTriggerOut: function(e) {
+ var me = this;
+ delete me.overMenuTrigger;
+ me.fireEvent('menutriggerout', me, me.menu, e);
+ },
+
+ // inherit docs
+ enable : function(silent) {
+ var me = this;
+
+ me.callParent(arguments);
+
+ me.removeClsWithUI('disabled');
+
+ return me;
+ },
+
+ // inherit docs
+ disable : function(silent) {
+ var me = this;
+
+ me.callParent(arguments);
+
+ me.addClsWithUI('disabled');
+ me.removeClsWithUI(me.overCls);
+
+ return me;
+ },
+
+ /**
+ * Method to change the scale of the button. See {@link #scale} for allowed configurations.
+ * @param {String} scale The scale to change to.
+ */
+ setScale: function(scale) {
+ var me = this,
+ ui = me.ui.replace('-' + me.scale, '');
+
+ //check if it is an allowed scale
+ if (!Ext.Array.contains(me.allowedScales, scale)) {
+ throw('#setScale: scale must be an allowed scale (' + me.allowedScales.join(', ') + ')');
+ }
+
+ me.scale = scale;
+ me.setUI(ui);
+ },
+
+ // inherit docs
+ setUI: function(ui) {
+ var me = this;
+
+ //we need to append the scale to the UI, if not already done
+ if (me.scale && !ui.match(me.scale)) {
+ ui = ui + '-' + me.scale;
+ }
+
+ me.callParent([ui]);
+
+ // Set all the state classNames, as they need to include the UI
+ // me.disabledCls += ' ' + me.baseCls + '-' + me.ui + '-disabled';
+ },
+
+ // private
+ onFocus: function(e) {
+ var me = this;
+ if (!me.disabled) {
+ me.addClsWithUI(me.focusCls);
+ }
+ },
+
+ // private
+ onBlur: function(e) {
+ var me = this;
+ me.removeClsWithUI(me.focusCls);
+ },
+
+ // private
+ onMouseDown: function(e) {
+ var me = this;
+ if (!me.disabled && e.button === 0) {
+ me.addClsWithUI(me.pressedCls);
+ me.doc.on('mouseup', me.onMouseUp, me);
+ }
+ },
+ // private
+ onMouseUp: function(e) {
+ var me = this;
+ if (e.button === 0) {
+ if (!me.pressed) {
+ me.removeClsWithUI(me.pressedCls);
+ }
+ me.doc.un('mouseup', me.onMouseUp, me);
+ }
+ },
+ // private
+ onMenuShow: function(e) {
+ var me = this;
+ me.ignoreNextClick = 0;
+ me.addClsWithUI(me.menuActiveCls);
+ me.fireEvent('menushow', me, me.menu);
+ },
+
+ // private
+ onMenuHide: function(e) {
+ var me = this;
+ me.removeClsWithUI(me.menuActiveCls);
+ me.ignoreNextClick = Ext.defer(me.restoreClick, 250, me);
+ me.fireEvent('menuhide', me, me.menu);
+ },
+
+ // private
+ restoreClick: function() {
+ this.ignoreNextClick = 0;
+ },
+
+ // private
+ onDownKey: function() {
+ var me = this;
+
+ if (!me.disabled) {
+ if (me.menu) {
+ me.showMenu();
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Some browsers (notably Safari and older Chromes on Windows) add extra "padding" inside the button
+ * element that cannot be removed. This method returns the size of that padding with a one-time detection.
+ * @return {Number[]} [top, right, bottom, left]
+ */
+ getPersistentBtnPadding: function() {
+ var cls = Ext.button.Button,
+ padding = cls.persistentPadding,
+ btn, leftTop, btnEl, btnInnerEl;
+
+ if (!padding) {
+ padding = cls.persistentPadding = [0, 0, 0, 0]; //set early to prevent recursion
+
+ if (!Ext.isIE) { //short-circuit IE as it sometimes gives false positive for padding
+ // Create auto-size button offscreen and measure its insides
+ btn = Ext.create('Ext.button.Button', {
+ renderTo: Ext.getBody(),
+ text: 'test',
+ style: 'position:absolute;top:-999px;'
+ });
+ btnEl = btn.btnEl;
+ btnInnerEl = btn.btnInnerEl;
+ btnEl.setSize(null, null); //clear any hard dimensions on the button el to see what it does naturally
+
+ leftTop = btnInnerEl.getOffsetsTo(btnEl);
+ padding[0] = leftTop[1];
+ padding[1] = btnEl.getWidth() - btnInnerEl.getWidth() - leftTop[0];
+ padding[2] = btnEl.getHeight() - btnInnerEl.getHeight() - leftTop[1];
+ padding[3] = leftTop[0];
+
+ btn.destroy();
+ }
+ }
+
+ return padding;
+ }
+
+}, function() {
+ var groups = {};
+
+ function toggleGroup(btn, state) {
+ var g, i, l;
+ if (state) {
+ g = groups[btn.toggleGroup];
+ for (i = 0, l = g.length; i < l; i++) {
+ if (g[i] !== btn) {
+ g[i].toggle(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Private utility class used by Button
+ * @hide
+ */
+ Ext.ButtonToggleManager = {
+ register: function(btn) {
+ if (!btn.toggleGroup) {
+ return;
+ }
+ var group = groups[btn.toggleGroup];
+ if (!group) {
+ group = groups[btn.toggleGroup] = [];
+ }
+ group.push(btn);
+ btn.on('toggle', toggleGroup);
+ },
+
+ unregister: function(btn) {
+ if (!btn.toggleGroup) {
+ return;
+ }
+ var group = groups[btn.toggleGroup];
+ if (group) {
+ Ext.Array.remove(group, btn);
+ btn.un('toggle', toggleGroup);
+ }
+ },
+
+ /**
+ * Gets the pressed button in the passed group or null
+ * @param {String} group
+ * @return {Ext.button.Button}
+ */
+ getPressed: function(group) {
+ var g = groups[group],
+ i = 0,
+ len;
+ if (g) {
+ for (len = g.length; i < len; i++) {
+ if (g[i].pressed === true) {
+ return g[i];
+ }
+ }
+ }
+ return null;
+ }
+ };
+});
+
+/**
+ * @class Ext.layout.container.boxOverflow.Menu
+ * @extends Ext.layout.container.boxOverflow.None
+ * @private
+ */
+Ext.define('Ext.layout.container.boxOverflow.Menu', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.layout.container.boxOverflow.None',
+ requires: ['Ext.toolbar.Separator', 'Ext.button.Button'],
+ alternateClassName: 'Ext.layout.boxOverflow.Menu',
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String} afterCtCls
+ * CSS class added to the afterCt element. This is the element that holds any special items such as scrollers,
+ * which must always be present at the rightmost edge of the Container
+ */
+
+ /**
+ * @property noItemsMenuText
+ * @type String
+ * HTML fragment to render into the toolbar overflow menu if there are no items to display
+ */
+ noItemsMenuText : '<div class="' + Ext.baseCSSPrefix + 'toolbar-no-items">(None)</div>',
+
+ constructor: function(layout) {
+ var me = this;
+
+ me.callParent(arguments);
+
+ // Before layout, we need to re-show all items which we may have hidden due to a previous overflow.
+ layout.beforeLayout = Ext.Function.createInterceptor(layout.beforeLayout, this.clearOverflow, this);
+
+ me.afterCtCls = me.afterCtCls || Ext.baseCSSPrefix + 'box-menu-' + layout.parallelAfter;
+ /**
+ * @property menuItems
+ * @type Array
+ * Array of all items that are currently hidden and should go into the dropdown menu
+ */
+ me.menuItems = [];
+ },
+
+ onRemove: function(comp){
+ Ext.Array.remove(this.menuItems, comp);
+ },
+
+ handleOverflow: function(calculations, targetSize) {
+ var me = this,
+ layout = me.layout,
+ methodName = 'get' + layout.parallelPrefixCap,
+ newSize = {},
+ posArgs = [null, null];
+
+ me.callParent(arguments);
+ this.createMenu(calculations, targetSize);
+ newSize[layout.perpendicularPrefix] = targetSize[layout.perpendicularPrefix];
+ newSize[layout.parallelPrefix] = targetSize[layout.parallelPrefix] - me.afterCt[methodName]();
+
+ // Center the menuTrigger button.
+ // TODO: Should we emulate align: 'middle' like this, or should we 'stretchmax' the menuTrigger?
+ posArgs[layout.perpendicularSizeIndex] = (calculations.meta.maxSize - me.menuTrigger['get' + layout.perpendicularPrefixCap]()) / 2;
+ me.menuTrigger.setPosition.apply(me.menuTrigger, posArgs);
+
+ return { targetSize: newSize };
+ },
+
+ /**
+ * @private
+ * Called by the layout, when it determines that there is no overflow.
+ * Also called as an interceptor to the layout's onLayout method to reshow
+ * previously hidden overflowing items.
+ */
+ clearOverflow: function(calculations, targetSize) {
+ var me = this,
+ newWidth = targetSize ? targetSize.width + (me.afterCt ? me.afterCt.getWidth() : 0) : 0,
+ items = me.menuItems,
+ i = 0,
+ length = items.length,
+ item;
+
+ me.hideTrigger();
+ for (; i < length; i++) {
+ items[i].show();
+ }
+ items.length = 0;
+
+ return targetSize ? {
+ targetSize: {
+ height: targetSize.height,
+ width : newWidth
+ }
+ } : null;
+ },
+
+ /**
+ * @private
+ */
+ showTrigger: function() {
+ this.menuTrigger.show();
+ },
+
+ /**
+ * @private
+ */
+ hideTrigger: function() {
+ if (this.menuTrigger !== undefined) {
+ this.menuTrigger.hide();
+ }
+ },
+
+ /**
+ * @private
+ * Called before the overflow menu is shown. This constructs the menu's items, caching them for as long as it can.
+ */
+ beforeMenuShow: function(menu) {
+ var me = this,
+ items = me.menuItems,
+ i = 0,
+ len = items.length,
+ item,
+ prev;
+
+ var needsSep = function(group, prev){
+ return group.isXType('buttongroup') && !(prev instanceof Ext.toolbar.Separator);
+ };
+
+ me.clearMenu();
+ menu.removeAll();
+
+ for (; i < len; i++) {
+ item = items[i];
+
+ // Do not show a separator as a first item
+ if (!i && (item instanceof Ext.toolbar.Separator)) {
+ continue;
+ }
+ if (prev && (needsSep(item, prev) || needsSep(prev, item))) {
+ menu.add('-');
+ }
+
+ me.addComponentToMenu(menu, item);
+ prev = item;
+ }
+
+ // put something so the menu isn't empty if no compatible items found
+ if (menu.items.length < 1) {
+ menu.add(me.noItemsMenuText);
+ }
+ },
+
+ /**
+ * @private
+ * Returns a menu config for a given component. This config is used to create a menu item
+ * to be added to the expander menu
+ * @param {Ext.Component} component The component to create the config for
+ * @param {Boolean} hideOnClick Passed through to the menu item
+ */
+ createMenuConfig : function(component, hideOnClick) {
+ var config = Ext.apply({}, component.initialConfig),
+ group = component.toggleGroup;
+
+ Ext.copyTo(config, component, [
+ 'iconCls', 'icon', 'itemId', 'disabled', 'handler', 'scope', 'menu'
+ ]);
+
+ Ext.apply(config, {
+ text : component.overflowText || component.text,
+ hideOnClick: hideOnClick,
+ destroyMenu: false
+ });
+
+ if (group || component.enableToggle) {
+ Ext.apply(config, {
+ group : group,
+ checked: component.pressed,
+ listeners: {
+ checkchange: function(item, checked){
+ component.toggle(checked);
+ }
+ }
+ });
+ }
+
+ delete config.ownerCt;
+ delete config.xtype;
+ delete config.id;
+ return config;
+ },
+
+ /**
+ * @private
+ * Adds the given Toolbar item to the given menu. Buttons inside a buttongroup are added individually.
+ * @param {Ext.menu.Menu} menu The menu to add to
+ * @param {Ext.Component} component The component to add
+ */
+ addComponentToMenu : function(menu, component) {
+ var me = this;
+ if (component instanceof Ext.toolbar.Separator) {
+ menu.add('-');
+ } else if (component.isComponent) {
+ if (component.isXType('splitbutton')) {
+ menu.add(me.createMenuConfig(component, true));
+
+ } else if (component.isXType('button')) {
+ menu.add(me.createMenuConfig(component, !component.menu));
+
+ } else if (component.isXType('buttongroup')) {
+ component.items.each(function(item){
+ me.addComponentToMenu(menu, item);
+ });
+ } else {
+ menu.add(Ext.create(Ext.getClassName(component), me.createMenuConfig(component)));
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Deletes the sub-menu of each item in the expander menu. Submenus are created for items such as
+ * splitbuttons and buttongroups, where the Toolbar item cannot be represented by a single menu item
+ */
+ clearMenu : function() {
+ var menu = this.moreMenu;
+ if (menu && menu.items) {
+ menu.items.each(function(item) {
+ if (item.menu) {
+ delete item.menu;
+ }
+ });
+ }
+ },
+
+ /**
+ * @private
+ * Creates the overflow trigger and menu used when enableOverflow is set to true and the items
+ * in the layout are too wide to fit in the space available
+ */
+ createMenu: function(calculations, targetSize) {
+ var me = this,
+ layout = me.layout,
+ startProp = layout.parallelBefore,
+ sizeProp = layout.parallelPrefix,
+ available = targetSize[sizeProp],
+ boxes = calculations.boxes,
+ i = 0,
+ len = boxes.length,
+ box;
+
+ if (!me.menuTrigger) {
+ me.createInnerElements();
+
+ /**
+ * @private
+ * @property menu
+ * @type Ext.menu.Menu
+ * The expand menu - holds items for every item that cannot be shown
+ * because the container is currently not large enough.
+ */
+ me.menu = Ext.create('Ext.menu.Menu', {
+ listeners: {
+ scope: me,
+ beforeshow: me.beforeMenuShow
+ }
+ });
+
+ /**
+ * @private
+ * @property menuTrigger
+ * @type Ext.button.Button
+ * The expand button which triggers the overflow menu to be shown
+ */
+ me.menuTrigger = Ext.create('Ext.button.Button', {
+ ownerCt : me.layout.owner, // To enable the Menu to ascertain a valid zIndexManager owner in the same tree
+ iconCls : me.layout.owner.menuTriggerCls,
+ ui : layout.owner instanceof Ext.toolbar.Toolbar ? 'default-toolbar' : 'default',
+ menu : me.menu,
+ getSplitCls: function() { return '';},
+ renderTo: me.afterCt
+ });
+ }
+ me.showTrigger();
+ available -= me.afterCt.getWidth();
+
+ // Hide all items which are off the end, and store them to allow them to be restored
+ // before each layout operation.
+ me.menuItems.length = 0;
+ for (; i < len; i++) {
+ box = boxes[i];
+ if (box[startProp] + box[sizeProp] > available) {
+ me.menuItems.push(box.component);
+ box.component.hide();
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Creates the beforeCt, innerCt and afterCt elements if they have not already been created
+ * @param {Ext.container.Container} container The Container attached to this Layout instance
+ * @param {Ext.Element} target The target Element
+ */
+ createInnerElements: function() {
+ var me = this,
+ target = me.layout.getRenderTarget();
+
+ if (!this.afterCt) {
+ target.addCls(Ext.baseCSSPrefix + me.layout.direction + '-box-overflow-body');
+ this.afterCt = target.insertSibling({cls: Ext.layout.container.Box.prototype.innerCls + ' ' + this.afterCtCls}, 'before');
+ }
+ },
+
+ /**
+ * @private
+ */
+ destroy: function() {
+ Ext.destroy(this.menu, this.menuTrigger);
+ }
+});
+/**
+ * This class represents a rectangular region in X,Y space, and performs geometric
+ * transformations or tests upon the region.
+ *
+ * This class may be used to compare the document regions occupied by elements.
+ */
+Ext.define('Ext.util.Region', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.util.Offset'],
+
+ statics: {
+ /**
+ * @static
+ * Retrieves an Ext.util.Region for a particular element.
+ * @param {String/HTMLElement/Ext.Element} el An element ID, htmlElement or Ext.Element representing an element in the document.
+ * @returns {Ext.util.Region} region
+ */
+ getRegion: function(el) {
+ return Ext.fly(el).getPageBox(true);
+ },
+
+ /**
+ * @static
+ * Creates a Region from a "box" Object which contains four numeric properties `top`, `right`, `bottom` and `left`.
+ * @param {Object} o An object with `top`, `right`, `bottom` and `left` properties.
+ * @return {Ext.util.Region} region The Region constructed based on the passed object
+ */
+ from: function(o) {
+ return new this(o.top, o.right, o.bottom, o.left);
+ }
+ },
+
+ /* End Definitions */
+
+ /**
+ * Creates a region from the bounding sides.
+ * @param {Number} top Top The topmost pixel of the Region.
+ * @param {Number} right Right The rightmost pixel of the Region.
+ * @param {Number} bottom Bottom The bottom pixel of the Region.
+ * @param {Number} left Left The leftmost pixel of the Region.
+ */
+ constructor : function(t, r, b, l) {
+ var me = this;
+ me.y = me.top = me[1] = t;
+ me.right = r;
+ me.bottom = b;
+ me.x = me.left = me[0] = l;
+ },
+
+ /**
+ * Checks if this region completely contains the region that is passed in.
+ * @param {Ext.util.Region} region
+ * @return {Boolean}
+ */
+ contains : function(region) {
+ var me = this;
+ return (region.x >= me.x &&
+ region.right <= me.right &&
+ region.y >= me.y &&
+ region.bottom <= me.bottom);
+
+ },
+
+ /**
+ * Checks if this region intersects the region passed in.
+ * @param {Ext.util.Region} region
+ * @return {Ext.util.Region/Boolean} Returns the intersected region or false if there is no intersection.
+ */
+ intersect : function(region) {
+ var me = this,
+ t = Math.max(me.y, region.y),
+ r = Math.min(me.right, region.right),
+ b = Math.min(me.bottom, region.bottom),
+ l = Math.max(me.x, region.x);
+
+ if (b > t && r > l) {
+ return new this.self(t, r, b, l);
+ }
+ else {
+ return false;
+ }
+ },
+
+ /**
+ * Returns the smallest region that contains the current AND targetRegion.
+ * @param {Ext.util.Region} region
+ * @return {Ext.util.Region} a new region
+ */
+ union : function(region) {
+ var me = this,
+ t = Math.min(me.y, region.y),
+ r = Math.max(me.right, region.right),
+ b = Math.max(me.bottom, region.bottom),
+ l = Math.min(me.x, region.x);
+
+ return new this.self(t, r, b, l);
+ },
+
+ /**
+ * Modifies the current region to be constrained to the targetRegion.
+ * @param {Ext.util.Region} targetRegion
+ * @return {Ext.util.Region} this
+ */
+ constrainTo : function(r) {
+ var me = this,
+ constrain = Ext.Number.constrain;
+ me.top = me.y = constrain(me.top, r.y, r.bottom);
+ me.bottom = constrain(me.bottom, r.y, r.bottom);
+ me.left = me.x = constrain(me.left, r.x, r.right);
+ me.right = constrain(me.right, r.x, r.right);
+ return me;
+ },
+
+ /**
+ * Modifies the current region to be adjusted by offsets.
+ * @param {Number} top top offset
+ * @param {Number} right right offset
+ * @param {Number} bottom bottom offset
+ * @param {Number} left left offset
+ * @return {Ext.util.Region} this
+ */
+ adjust : function(t, r, b, l) {
+ var me = this;
+ me.top = me.y += t;
+ me.left = me.x += l;
+ me.right += r;
+ me.bottom += b;
+ return me;
+ },
+
+ /**
+ * Get the offset amount of a point outside the region
+ * @param {String} [axis]
+ * @param {Ext.util.Point} [p] the point
+ * @return {Ext.util.Offset}
+ */
+ getOutOfBoundOffset: function(axis, p) {
+ if (!Ext.isObject(axis)) {
+ if (axis == 'x') {
+ return this.getOutOfBoundOffsetX(p);
+ } else {
+ return this.getOutOfBoundOffsetY(p);
+ }
+ } else {
+ p = axis;
+ var d = Ext.create('Ext.util.Offset');
+ d.x = this.getOutOfBoundOffsetX(p.x);
+ d.y = this.getOutOfBoundOffsetY(p.y);
+ return d;
+ }
+
+ },
+
+ /**
+ * Get the offset amount on the x-axis
+ * @param {Number} p the offset
+ * @return {Number}
+ */
+ getOutOfBoundOffsetX: function(p) {
+ if (p <= this.x) {
+ return this.x - p;
+ } else if (p >= this.right) {
+ return this.right - p;
+ }
+
+ return 0;
+ },
+
+ /**
+ * Get the offset amount on the y-axis
+ * @param {Number} p the offset
+ * @return {Number}
+ */
+ getOutOfBoundOffsetY: function(p) {
+ if (p <= this.y) {
+ return this.y - p;
+ } else if (p >= this.bottom) {
+ return this.bottom - p;
+ }
+
+ return 0;
+ },
+
+ /**
+ * Check whether the point / offset is out of bound
+ * @param {String} [axis]
+ * @param {Ext.util.Point/Number} [p] the point / offset
+ * @return {Boolean}
+ */
+ isOutOfBound: function(axis, p) {
+ if (!Ext.isObject(axis)) {
+ if (axis == 'x') {
+ return this.isOutOfBoundX(p);
+ } else {
+ return this.isOutOfBoundY(p);
+ }
+ } else {
+ p = axis;
+ return (this.isOutOfBoundX(p.x) || this.isOutOfBoundY(p.y));
+ }
+ },
+
+ /**
+ * Check whether the offset is out of bound in the x-axis
+ * @param {Number} p the offset
+ * @return {Boolean}
+ */
+ isOutOfBoundX: function(p) {
+ return (p < this.x || p > this.right);
+ },
+
+ /**
+ * Check whether the offset is out of bound in the y-axis
+ * @param {Number} p the offset
+ * @return {Boolean}
+ */
+ isOutOfBoundY: function(p) {
+ return (p < this.y || p > this.bottom);
+ },
+
+ /**
+ * Restrict a point within the region by a certain factor.
+ * @param {String} [axis]
+ * @param {Ext.util.Point/Ext.util.Offset/Object} [p]
+ * @param {Number} [factor]
+ * @return {Ext.util.Point/Ext.util.Offset/Object/Number}
+ * @private
+ */
+ restrict: function(axis, p, factor) {
+ if (Ext.isObject(axis)) {
+ var newP;
+
+ factor = p;
+ p = axis;
+
+ if (p.copy) {
+ newP = p.copy();
+ }
+ else {
+ newP = {
+ x: p.x,
+ y: p.y
+ };
+ }
+
+ newP.x = this.restrictX(p.x, factor);
+ newP.y = this.restrictY(p.y, factor);
+ return newP;
+ } else {
+ if (axis == 'x') {
+ return this.restrictX(p, factor);
+ } else {
+ return this.restrictY(p, factor);
+ }
+ }
+ },
+
+ /**
+ * Restrict an offset within the region by a certain factor, on the x-axis
+ * @param {Number} p
+ * @param {Number} [factor=1] The factor.
+ * @return {Number}
+ * @private
+ */
+ restrictX : function(p, factor) {
+ if (!factor) {
+ factor = 1;
+ }
+
+ if (p <= this.x) {
+ p -= (p - this.x) * factor;
+ }
+ else if (p >= this.right) {
+ p -= (p - this.right) * factor;
+ }
+ return p;
+ },
+
+ /**
+ * Restrict an offset within the region by a certain factor, on the y-axis
+ * @param {Number} p
+ * @param {Number} [factor] The factor, defaults to 1
+ * @return {Number}
+ * @private
+ */
+ restrictY : function(p, factor) {
+ if (!factor) {
+ factor = 1;
+ }
+
+ if (p <= this.y) {
+ p -= (p - this.y) * factor;
+ }
+ else if (p >= this.bottom) {
+ p -= (p - this.bottom) * factor;
+ }
+ return p;
+ },
+
+ /**
+ * Get the width / height of this region
+ * @return {Object} an object with width and height properties
+ * @private
+ */
+ getSize: function() {
+ return {
+ width: this.right - this.x,
+ height: this.bottom - this.y
+ };
+ },
+
+ /**
+ * Create a copy of this Region.
+ * @return {Ext.util.Region}
+ */
+ copy: function() {
+ return new this.self(this.y, this.right, this.bottom, this.x);
+ },
+
+ /**
+ * Copy the values of another Region to this Region
+ * @param {Ext.util.Region} p The region to copy from.
+ * @return {Ext.util.Region} This Region
+ */
+ copyFrom: function(p) {
+ var me = this;
+ me.top = me.y = me[1] = p.y;
+ me.right = p.right;
+ me.bottom = p.bottom;
+ me.left = me.x = me[0] = p.x;
+
+ return this;
+ },
+
+ /*
+ * Dump this to an eye-friendly string, great for debugging
+ * @return {String}
+ */
+ toString: function() {
+ return "Region[" + this.top + "," + this.right + "," + this.bottom + "," + this.left + "]";
+ },
+
+ /**
+ * Translate this region by the given offset amount
+ * @param {Ext.util.Offset/Object} x Object containing the `x` and `y` properties.
+ * Or the x value is using the two argument form.
+ * @param {Number} y The y value unless using an Offset object.
+ * @return {Ext.util.Region} this This Region
+ */
+ translateBy: function(x, y) {
+ if (arguments.length == 1) {
+ y = x.y;
+ x = x.x;
+ }
+ var me = this;
+ me.top = me.y += y;
+ me.right += x;
+ me.bottom += y;
+ me.left = me.x += x;
+
+ return me;
+ },
+
+ /**
+ * Round all the properties of this region
+ * @return {Ext.util.Region} this This Region
+ */
+ round: function() {
+ var me = this;
+ me.top = me.y = Math.round(me.y);
+ me.right = Math.round(me.right);
+ me.bottom = Math.round(me.bottom);
+ me.left = me.x = Math.round(me.x);
+
+ return me;
+ },
+
+ /**
+ * Check whether this region is equivalent to the given region
+ * @param {Ext.util.Region} region The region to compare with
+ * @return {Boolean}
+ */
+ equals: function(region) {
+ return (this.top == region.top && this.right == region.right && this.bottom == region.bottom && this.left == region.left);
+ }
+});
+
+/*
+ * This is a derivative of the similarly named class in the YUI Library.
+ * The original license:
+ * Copyright (c) 2006, Yahoo! Inc. All rights reserved.
+ * Code licensed under the BSD License:
+ * http://developer.yahoo.net/yui/license.txt
+ */
+
+
+/**
+ * @class Ext.dd.DragDropManager
+ * DragDropManager is a singleton that tracks the element interaction for
+ * all DragDrop items in the window. Generally, you will not call
+ * this class directly, but it does have helper methods that could
+ * be useful in your DragDrop implementations.
+ * @singleton
+ */
+Ext.define('Ext.dd.DragDropManager', {
+ singleton: true,
+
+ requires: ['Ext.util.Region'],
+
+ uses: ['Ext.tip.QuickTipManager'],
+
+ // shorter ClassName, to save bytes and use internally
+ alternateClassName: ['Ext.dd.DragDropMgr', 'Ext.dd.DDM'],
+
+ /**
+ * Two dimensional Array of registered DragDrop objects. The first
+ * dimension is the DragDrop item group, the second the DragDrop
+ * object.
+ * @property ids
+ * @type String[]
+ * @private
+ */
+ ids: {},
+
+ /**
+ * Array of element ids defined as drag handles. Used to determine
+ * if the element that generated the mousedown event is actually the
+ * handle and not the html element itself.
+ * @property handleIds
+ * @type String[]
+ * @private
+ */
+ handleIds: {},
+
+ /**
+ * the DragDrop object that is currently being dragged
+ * @property {Ext.dd.DragDrop} dragCurrent
+ * @private
+ **/
+ dragCurrent: null,
+
+ /**
+ * the DragDrop object(s) that are being hovered over
+ * @property {Ext.dd.DragDrop[]} dragOvers
+ * @private
+ */
+ dragOvers: {},
+
+ /**
+ * the X distance between the cursor and the object being dragged
+ * @property deltaX
+ * @type Number
+ * @private
+ */
+ deltaX: 0,
+
+ /**
+ * the Y distance between the cursor and the object being dragged
+ * @property deltaY
+ * @type Number
+ * @private
+ */
+ deltaY: 0,
+
+ /**
+ * Flag to determine if we should prevent the default behavior of the
+ * events we define. By default this is true, but this can be set to
+ * false if you need the default behavior (not recommended)
+ * @property preventDefault
+ * @type Boolean
+ */
+ preventDefault: true,
+
+ /**
+ * Flag to determine if we should stop the propagation of the events
+ * we generate. This is true by default but you may want to set it to
+ * false if the html element contains other features that require the
+ * mouse click.
+ * @property stopPropagation
+ * @type Boolean
+ */
+ stopPropagation: true,
+
+ /**
+ * Internal flag that is set to true when drag and drop has been
+ * intialized
+ * @property initialized
+ * @private
+ */
+ initialized: false,
+
+ /**
+ * All drag and drop can be disabled.
+ * @property locked
+ * @private
+ */
+ locked: false,
+
+ /**
+ * Called the first time an element is registered.
+ * @method init
+ * @private
+ */
+ init: function() {
+ this.initialized = true;
+ },
+
+ /**
+ * In point mode, drag and drop interaction is defined by the
+ * location of the cursor during the drag/drop
+ * @property POINT
+ * @type Number
+ */
+ POINT: 0,
+
+ /**
+ * In intersect mode, drag and drop interaction is defined by the
+ * overlap of two or more drag and drop objects.
+ * @property INTERSECT
+ * @type Number
+ */
+ INTERSECT: 1,
+
+ /**
+ * The current drag and drop mode. Default: POINT
+ * @property mode
+ * @type Number
+ */
+ mode: 0,
+
+ /**
+ * Runs method on all drag and drop objects
+ * @method _execOnAll
+ * @private
+ */
+ _execOnAll: function(sMethod, args) {
+ for (var i in this.ids) {
+ for (var j in this.ids[i]) {
+ var oDD = this.ids[i][j];
+ if (! this.isTypeOfDD(oDD)) {
+ continue;
+ }
+ oDD[sMethod].apply(oDD, args);
+ }
+ }
+ },
+
+ /**
+ * Drag and drop initialization. Sets up the global event handlers
+ * @method _onLoad
+ * @private
+ */
+ _onLoad: function() {
+
+ this.init();
+
+ var Event = Ext.EventManager;
+ Event.on(document, "mouseup", this.handleMouseUp, this, true);
+ Event.on(document, "mousemove", this.handleMouseMove, this, true);
+ Event.on(window, "unload", this._onUnload, this, true);
+ Event.on(window, "resize", this._onResize, this, true);
+ // Event.on(window, "mouseout", this._test);
+
+ },
+
+ /**
+ * Reset constraints on all drag and drop objs
+ * @method _onResize
+ * @private
+ */
+ _onResize: function(e) {
+ this._execOnAll("resetConstraints", []);
+ },
+
+ /**
+ * Lock all drag and drop functionality
+ * @method lock
+ */
+ lock: function() { this.locked = true; },
+
+ /**
+ * Unlock all drag and drop functionality
+ * @method unlock
+ */
+ unlock: function() { this.locked = false; },
+
+ /**
+ * Is drag and drop locked?
+ * @method isLocked
+ * @return {Boolean} True if drag and drop is locked, false otherwise.
+ */
+ isLocked: function() { return this.locked; },
+
+ /**
+ * Location cache that is set for all drag drop objects when a drag is
+ * initiated, cleared when the drag is finished.
+ * @property locationCache
+ * @private
+ */
+ locationCache: {},
+
+ /**
+ * Set useCache to false if you want to force object the lookup of each
+ * drag and drop linked element constantly during a drag.
+ * @property useCache
+ * @type Boolean
+ */
+ useCache: true,
+
+ /**
+ * The number of pixels that the mouse needs to move after the
+ * mousedown before the drag is initiated. Default=3;
+ * @property clickPixelThresh
+ * @type Number
+ */
+ clickPixelThresh: 3,
+
+ /**
+ * The number of milliseconds after the mousedown event to initiate the
+ * drag if we don't get a mouseup event. Default=350
+ * @property clickTimeThresh
+ * @type Number
+ */
+ clickTimeThresh: 350,
+
+ /**
+ * Flag that indicates that either the drag pixel threshold or the
+ * mousdown time threshold has been met
+ * @property dragThreshMet
+ * @type Boolean
+ * @private
+ */
+ dragThreshMet: false,
+
+ /**
+ * Timeout used for the click time threshold
+ * @property clickTimeout
+ * @type Object
+ * @private
+ */
+ clickTimeout: null,
+
+ /**
+ * The X position of the mousedown event stored for later use when a
+ * drag threshold is met.
+ * @property startX
+ * @type Number
+ * @private
+ */
+ startX: 0,
+
+ /**
+ * The Y position of the mousedown event stored for later use when a
+ * drag threshold is met.
+ * @property startY
+ * @type Number
+ * @private
+ */
+ startY: 0,
+
+ /**
+ * Each DragDrop instance must be registered with the DragDropManager.
+ * This is executed in DragDrop.init()
+ * @method regDragDrop
+ * @param {Ext.dd.DragDrop} oDD the DragDrop object to register
+ * @param {String} sGroup the name of the group this element belongs to
+ */
+ regDragDrop: function(oDD, sGroup) {
+ if (!this.initialized) { this.init(); }
+
+ if (!this.ids[sGroup]) {
+ this.ids[sGroup] = {};
+ }
+ this.ids[sGroup][oDD.id] = oDD;
+ },
+
+ /**
+ * Removes the supplied dd instance from the supplied group. Executed
+ * by DragDrop.removeFromGroup, so don't call this function directly.
+ * @method removeDDFromGroup
+ * @private
+ */
+ removeDDFromGroup: function(oDD, sGroup) {
+ if (!this.ids[sGroup]) {
+ this.ids[sGroup] = {};
+ }
+
+ var obj = this.ids[sGroup];
+ if (obj && obj[oDD.id]) {
+ delete obj[oDD.id];
+ }
+ },
+
+ /**
+ * Unregisters a drag and drop item. This is executed in
+ * DragDrop.unreg, use that method instead of calling this directly.
+ * @method _remove
+ * @private
+ */
+ _remove: function(oDD) {
+ for (var g in oDD.groups) {
+ if (g && this.ids[g] && this.ids[g][oDD.id]) {
+ delete this.ids[g][oDD.id];
+ }
+ }
+ delete this.handleIds[oDD.id];
+ },
+
+ /**
+ * Each DragDrop handle element must be registered. This is done
+ * automatically when executing DragDrop.setHandleElId()
+ * @method regHandle
+ * @param {String} sDDId the DragDrop id this element is a handle for
+ * @param {String} sHandleId the id of the element that is the drag
+ * handle
+ */
+ regHandle: function(sDDId, sHandleId) {
+ if (!this.handleIds[sDDId]) {
+ this.handleIds[sDDId] = {};
+ }
+ this.handleIds[sDDId][sHandleId] = sHandleId;
+ },
+
+ /**
+ * Utility function to determine if a given element has been
+ * registered as a drag drop item.
+ * @method isDragDrop
+ * @param {String} id the element id to check
+ * @return {Boolean} true if this element is a DragDrop item,
+ * false otherwise
+ */
+ isDragDrop: function(id) {
+ return ( this.getDDById(id) ) ? true : false;
+ },
+
+ /**
+ * Returns the drag and drop instances that are in all groups the
+ * passed in instance belongs to.
+ * @method getRelated
+ * @param {Ext.dd.DragDrop} p_oDD the obj to get related data for
+ * @param {Boolean} bTargetsOnly if true, only return targetable objs
+ * @return {Ext.dd.DragDrop[]} the related instances
+ */
+ getRelated: function(p_oDD, bTargetsOnly) {
+ var oDDs = [];
+ for (var i in p_oDD.groups) {
+ for (var j in this.ids[i]) {
+ var dd = this.ids[i][j];
+ if (! this.isTypeOfDD(dd)) {
+ continue;
+ }
+ if (!bTargetsOnly || dd.isTarget) {
+ oDDs[oDDs.length] = dd;
+ }
+ }
+ }
+
+ return oDDs;
+ },
+
+ /**
+ * Returns true if the specified dd target is a legal target for
+ * the specifice drag obj
+ * @method isLegalTarget
+ * @param {Ext.dd.DragDrop} oDD the drag obj
+ * @param {Ext.dd.DragDrop} oTargetDD the target
+ * @return {Boolean} true if the target is a legal target for the
+ * dd obj
+ */
+ isLegalTarget: function (oDD, oTargetDD) {
+ var targets = this.getRelated(oDD, true);
+ for (var i=0, len=targets.length;i<len;++i) {
+ if (targets[i].id == oTargetDD.id) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * My goal is to be able to transparently determine if an object is
+ * typeof DragDrop, and the exact subclass of DragDrop. typeof
+ * returns "object", oDD.constructor.toString() always returns
+ * "DragDrop" and not the name of the subclass. So for now it just
+ * evaluates a well-known variable in DragDrop.
+ * @method isTypeOfDD
+ * @param {Object} the object to evaluate
+ * @return {Boolean} true if typeof oDD = DragDrop
+ */
+ isTypeOfDD: function (oDD) {
+ return (oDD && oDD.__ygDragDrop);
+ },
+
+ /**
+ * Utility function to determine if a given element has been
+ * registered as a drag drop handle for the given Drag Drop object.
+ * @method isHandle
+ * @param {String} id the element id to check
+ * @return {Boolean} true if this element is a DragDrop handle, false
+ * otherwise
+ */
+ isHandle: function(sDDId, sHandleId) {
+ return ( this.handleIds[sDDId] &&
+ this.handleIds[sDDId][sHandleId] );
+ },
+
+ /**
+ * Returns the DragDrop instance for a given id
+ * @method getDDById
+ * @param {String} id the id of the DragDrop object
+ * @return {Ext.dd.DragDrop} the drag drop object, null if it is not found
+ */
+ getDDById: function(id) {
+ for (var i in this.ids) {
+ if (this.ids[i][id]) {
+ return this.ids[i][id];
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Fired after a registered DragDrop object gets the mousedown event.
+ * Sets up the events required to track the object being dragged
+ * @method handleMouseDown
+ * @param {Event} e the event
+ * @param {Ext.dd.DragDrop} oDD the DragDrop object being dragged
+ * @private
+ */
+ handleMouseDown: function(e, oDD) {
+ if(Ext.tip.QuickTipManager){
+ Ext.tip.QuickTipManager.ddDisable();
+ }
+ if(this.dragCurrent){
+ // the original browser mouseup wasn't handled (e.g. outside FF browser window)
+ // so clean up first to avoid breaking the next drag
+ this.handleMouseUp(e);
+ }
+
+ this.currentTarget = e.getTarget();
+ this.dragCurrent = oDD;
+
+ var el = oDD.getEl();
+
+ // track start position
+ this.startX = e.getPageX();
+ this.startY = e.getPageY();
+
+ this.deltaX = this.startX - el.offsetLeft;
+ this.deltaY = this.startY - el.offsetTop;
+
+ this.dragThreshMet = false;
+
+ this.clickTimeout = setTimeout(
+ function() {
+ var DDM = Ext.dd.DragDropManager;
+ DDM.startDrag(DDM.startX, DDM.startY);
+ },
+ this.clickTimeThresh );
+ },
+
+ /**
+ * Fired when either the drag pixel threshol or the mousedown hold
+ * time threshold has been met.
+ * @method startDrag
+ * @param {Number} x the X position of the original mousedown
+ * @param {Number} y the Y position of the original mousedown
+ */
+ startDrag: function(x, y) {
+ clearTimeout(this.clickTimeout);
+ if (this.dragCurrent) {
+ this.dragCurrent.b4StartDrag(x, y);
+ this.dragCurrent.startDrag(x, y);
+ }
+ this.dragThreshMet = true;
+ },
+
+ /**
+ * Internal function to handle the mouseup event. Will be invoked
+ * from the context of the document.
+ * @method handleMouseUp
+ * @param {Event} e the event
+ * @private
+ */
+ handleMouseUp: function(e) {
+
+ if(Ext.tip && Ext.tip.QuickTipManager){
+ Ext.tip.QuickTipManager.ddEnable();
+ }
+ if (! this.dragCurrent) {
+ return;
+ }
+
+ clearTimeout(this.clickTimeout);
+
+ if (this.dragThreshMet) {
+ this.fireEvents(e, true);
+ } else {
+ }
+
+ this.stopDrag(e);
+
+ this.stopEvent(e);
+ },
+
+ /**
+ * Utility to stop event propagation and event default, if these
+ * features are turned on.
+ * @method stopEvent
+ * @param {Event} e the event as returned by this.getEvent()
+ */
+ stopEvent: function(e){
+ if(this.stopPropagation) {
+ e.stopPropagation();
+ }
+
+ if (this.preventDefault) {
+ e.preventDefault();
+ }
+ },
+
+ /**
+ * Internal function to clean up event handlers after the drag
+ * operation is complete
+ * @method stopDrag
+ * @param {Event} e the event
+ * @private
+ */
+ stopDrag: function(e) {
+ // Fire the drag end event for the item that was dragged
+ if (this.dragCurrent) {
+ if (this.dragThreshMet) {
+ this.dragCurrent.b4EndDrag(e);
+ this.dragCurrent.endDrag(e);
+ }
+
+ this.dragCurrent.onMouseUp(e);
+ }
+
+ this.dragCurrent = null;
+ this.dragOvers = {};
+ },
+
+ /**
+ * Internal function to handle the mousemove event. Will be invoked
+ * from the context of the html element.
+ *
+ * @TODO figure out what we can do about mouse events lost when the
+ * user drags objects beyond the window boundary. Currently we can
+ * detect this in internet explorer by verifying that the mouse is
+ * down during the mousemove event. Firefox doesn't give us the
+ * button state on the mousemove event.
+ * @method handleMouseMove
+ * @param {Event} e the event
+ * @private
+ */
+ handleMouseMove: function(e) {
+ if (! this.dragCurrent) {
+ return true;
+ }
+ // var button = e.which || e.button;
+
+ // check for IE mouseup outside of page boundary
+ if (Ext.isIE && (e.button !== 0 && e.button !== 1 && e.button !== 2)) {
+ this.stopEvent(e);
+ return this.handleMouseUp(e);
+ }
+
+ if (!this.dragThreshMet) {
+ var diffX = Math.abs(this.startX - e.getPageX());
+ var diffY = Math.abs(this.startY - e.getPageY());
+ if (diffX > this.clickPixelThresh ||
+ diffY > this.clickPixelThresh) {
+ this.startDrag(this.startX, this.startY);
+ }
+ }
+
+ if (this.dragThreshMet) {
+ this.dragCurrent.b4Drag(e);
+ this.dragCurrent.onDrag(e);
+ if(!this.dragCurrent.moveOnly){
+ this.fireEvents(e, false);
+ }
+ }
+
+ this.stopEvent(e);
+
+ return true;
+ },
+
+ /**
+ * Iterates over all of the DragDrop elements to find ones we are
+ * hovering over or dropping on
+ * @method fireEvents
+ * @param {Event} e the event
+ * @param {Boolean} isDrop is this a drop op or a mouseover op?
+ * @private
+ */
+ fireEvents: function(e, isDrop) {
+ var dc = this.dragCurrent;
+
+ // If the user did the mouse up outside of the window, we could
+ // get here even though we have ended the drag.
+ if (!dc || dc.isLocked()) {
+ return;
+ }
+
+ var pt = e.getPoint();
+
+ // cache the previous dragOver array
+ var oldOvers = [];
+
+ var outEvts = [];
+ var overEvts = [];
+ var dropEvts = [];
+ var enterEvts = [];
+
+ // Check to see if the object(s) we were hovering over is no longer
+ // being hovered over so we can fire the onDragOut event
+ for (var i in this.dragOvers) {
+
+ var ddo = this.dragOvers[i];
+
+ if (! this.isTypeOfDD(ddo)) {
+ continue;
+ }
+
+ if (! this.isOverTarget(pt, ddo, this.mode)) {
+ outEvts.push( ddo );
+ }
+
+ oldOvers[i] = true;
+ delete this.dragOvers[i];
+ }
+
+ for (var sGroup in dc.groups) {
+
+ if ("string" != typeof sGroup) {
+ continue;
+ }
+
+ for (i in this.ids[sGroup]) {
+ var oDD = this.ids[sGroup][i];
+ if (! this.isTypeOfDD(oDD)) {
+ continue;
+ }
+
+ if (oDD.isTarget && !oDD.isLocked() && ((oDD != dc) || (dc.ignoreSelf === false))) {
+ if (this.isOverTarget(pt, oDD, this.mode)) {
+ // look for drop interactions
+ if (isDrop) {
+ dropEvts.push( oDD );
+ // look for drag enter and drag over interactions
+ } else {
+
+ // initial drag over: dragEnter fires
+ if (!oldOvers[oDD.id]) {
+ enterEvts.push( oDD );
+ // subsequent drag overs: dragOver fires
+ } else {
+ overEvts.push( oDD );
+ }
+
+ this.dragOvers[oDD.id] = oDD;
+ }
+ }
+ }
+ }
+ }
+
+ if (this.mode) {
+ if (outEvts.length) {
+ dc.b4DragOut(e, outEvts);
+ dc.onDragOut(e, outEvts);
+ }
+
+ if (enterEvts.length) {
+ dc.onDragEnter(e, enterEvts);
+ }
+
+ if (overEvts.length) {
+ dc.b4DragOver(e, overEvts);
+ dc.onDragOver(e, overEvts);
+ }
+
+ if (dropEvts.length) {
+ dc.b4DragDrop(e, dropEvts);
+ dc.onDragDrop(e, dropEvts);
+ }
+
+ } else {
+ // fire dragout events
+ var len = 0;
+ for (i=0, len=outEvts.length; i<len; ++i) {
+ dc.b4DragOut(e, outEvts[i].id);
+ dc.onDragOut(e, outEvts[i].id);
+ }
+
+ // fire enter events
+ for (i=0,len=enterEvts.length; i<len; ++i) {
+ // dc.b4DragEnter(e, oDD.id);
+ dc.onDragEnter(e, enterEvts[i].id);
+ }
+
+ // fire over events
+ for (i=0,len=overEvts.length; i<len; ++i) {
+ dc.b4DragOver(e, overEvts[i].id);
+ dc.onDragOver(e, overEvts[i].id);
+ }
+
+ // fire drop events
+ for (i=0, len=dropEvts.length; i<len; ++i) {
+ dc.b4DragDrop(e, dropEvts[i].id);
+ dc.onDragDrop(e, dropEvts[i].id);
+ }
+
+ }
+
+ // notify about a drop that did not find a target
+ if (isDrop && !dropEvts.length) {
+ dc.onInvalidDrop(e);
+ }
+
+ },
+
+ /**
+ * Helper function for getting the best match from the list of drag
+ * and drop objects returned by the drag and drop events when we are
+ * in INTERSECT mode. It returns either the first object that the
+ * cursor is over, or the object that has the greatest overlap with
+ * the dragged element.
+ * @method getBestMatch
+ * @param {Ext.dd.DragDrop[]} dds The array of drag and drop objects
+ * targeted
+ * @return {Ext.dd.DragDrop} The best single match
+ */
+ getBestMatch: function(dds) {
+ var winner = null;
+ // Return null if the input is not what we expect
+ //if (!dds || !dds.length || dds.length == 0) {
+ // winner = null;
+ // If there is only one item, it wins
+ //} else if (dds.length == 1) {
+
+ var len = dds.length;
+
+ if (len == 1) {
+ winner = dds[0];
+ } else {
+ // Loop through the targeted items
+ for (var i=0; i<len; ++i) {
+ var dd = dds[i];
+ // If the cursor is over the object, it wins. If the
+ // cursor is over multiple matches, the first one we come
+ // to wins.
+ if (dd.cursorIsOver) {
+ winner = dd;
+ break;
+ // Otherwise the object with the most overlap wins
+ } else {
+ if (!winner ||
+ winner.overlap.getArea() < dd.overlap.getArea()) {
+ winner = dd;
+ }
+ }
+ }
+ }
+
+ return winner;
+ },
+
+ /**
+ * Refreshes the cache of the top-left and bottom-right points of the
+ * drag and drop objects in the specified group(s). This is in the
+ * format that is stored in the drag and drop instance, so typical
+ * usage is:
+ * <code>
+ * Ext.dd.DragDropManager.refreshCache(ddinstance.groups);
+ * </code>
+ * Alternatively:
+ * <code>
+ * Ext.dd.DragDropManager.refreshCache({group1:true, group2:true});
+ * </code>
+ * @TODO this really should be an indexed array. Alternatively this
+ * method could accept both.
+ * @method refreshCache
+ * @param {Object} groups an associative array of groups to refresh
+ */
+ refreshCache: function(groups) {
+ for (var sGroup in groups) {
+ if ("string" != typeof sGroup) {
+ continue;
+ }
+ for (var i in this.ids[sGroup]) {
+ var oDD = this.ids[sGroup][i];
+
+ if (this.isTypeOfDD(oDD)) {
+ // if (this.isTypeOfDD(oDD) && oDD.isTarget) {
+ var loc = this.getLocation(oDD);
+ if (loc) {
+ this.locationCache[oDD.id] = loc;
+ } else {
+ delete this.locationCache[oDD.id];
+ // this will unregister the drag and drop object if
+ // the element is not in a usable state
+ // oDD.unreg();
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * This checks to make sure an element exists and is in the DOM. The
+ * main purpose is to handle cases where innerHTML is used to remove
+ * drag and drop objects from the DOM. IE provides an 'unspecified
+ * error' when trying to access the offsetParent of such an element
+ * @method verifyEl
+ * @param {HTMLElement} el the element to check
+ * @return {Boolean} true if the element looks usable
+ */
+ verifyEl: function(el) {
+ if (el) {
+ var parent;
+ if(Ext.isIE){
+ try{
+ parent = el.offsetParent;
+ }catch(e){}
+ }else{
+ parent = el.offsetParent;
+ }
+ if (parent) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * Returns a Region object containing the drag and drop element's position
+ * and size, including the padding configured for it
+ * @method getLocation
+ * @param {Ext.dd.DragDrop} oDD the drag and drop object to get the location for.
+ * @return {Ext.util.Region} a Region object representing the total area
+ * the element occupies, including any padding
+ * the instance is configured for.
+ */
+ getLocation: function(oDD) {
+ if (! this.isTypeOfDD(oDD)) {
+ return null;
+ }
+
+ //delegate getLocation method to the
+ //drag and drop target.
+ if (oDD.getRegion) {
+ return oDD.getRegion();
+ }
+
+ var el = oDD.getEl(), pos, x1, x2, y1, y2, t, r, b, l;
+
+ try {
+ pos= Ext.Element.getXY(el);
+ } catch (e) { }
+
+ if (!pos) {
+ return null;
+ }
+
+ x1 = pos[0];
+ x2 = x1 + el.offsetWidth;
+ y1 = pos[1];
+ y2 = y1 + el.offsetHeight;
+
+ t = y1 - oDD.padding[0];
+ r = x2 + oDD.padding[1];
+ b = y2 + oDD.padding[2];
+ l = x1 - oDD.padding[3];
+
+ return Ext.create('Ext.util.Region', t, r, b, l);
+ },
+
+ /**
+ * Checks the cursor location to see if it over the target
+ * @method isOverTarget
+ * @param {Ext.util.Point} pt The point to evaluate
+ * @param {Ext.dd.DragDrop} oTarget the DragDrop object we are inspecting
+ * @return {Boolean} true if the mouse is over the target
+ * @private
+ */
+ isOverTarget: function(pt, oTarget, intersect) {
+ // use cache if available
+ var loc = this.locationCache[oTarget.id];
+ if (!loc || !this.useCache) {
+ loc = this.getLocation(oTarget);
+ this.locationCache[oTarget.id] = loc;
+
+ }
+
+ if (!loc) {
+ return false;
+ }
+
+ oTarget.cursorIsOver = loc.contains( pt );
+
+ // DragDrop is using this as a sanity check for the initial mousedown
+ // in this case we are done. In POINT mode, if the drag obj has no
+ // contraints, we are also done. Otherwise we need to evaluate the
+ // location of the target as related to the actual location of the
+ // dragged element.
+ var dc = this.dragCurrent;
+ if (!dc || !dc.getTargetCoord ||
+ (!intersect && !dc.constrainX && !dc.constrainY)) {
+ return oTarget.cursorIsOver;
+ }
+
+ oTarget.overlap = null;
+
+ // Get the current location of the drag element, this is the
+ // location of the mouse event less the delta that represents
+ // where the original mousedown happened on the element. We
+ // need to consider constraints and ticks as well.
+ var pos = dc.getTargetCoord(pt.x, pt.y);
+
+ var el = dc.getDragEl();
+ var curRegion = Ext.create('Ext.util.Region', pos.y,
+ pos.x + el.offsetWidth,
+ pos.y + el.offsetHeight,
+ pos.x );
+
+ var overlap = curRegion.intersect(loc);
+
+ if (overlap) {
+ oTarget.overlap = overlap;
+ return (intersect) ? true : oTarget.cursorIsOver;
+ } else {
+ return false;
+ }
+ },
+
+ /**
+ * unload event handler
+ * @method _onUnload
+ * @private
+ */
+ _onUnload: function(e, me) {
+ Ext.dd.DragDropManager.unregAll();
+ },
+
+ /**
+ * Cleans up the drag and drop events and objects.
+ * @method unregAll
+ * @private
+ */
+ unregAll: function() {
+
+ if (this.dragCurrent) {
+ this.stopDrag();
+ this.dragCurrent = null;
+ }
+
+ this._execOnAll("unreg", []);
+
+ for (var i in this.elementCache) {
+ delete this.elementCache[i];
+ }
+
+ this.elementCache = {};
+ this.ids = {};
+ },
+
+ /**
+ * A cache of DOM elements
+ * @property elementCache
+ * @private
+ */
+ elementCache: {},
+
+ /**
+ * Get the wrapper for the DOM element specified
+ * @method getElWrapper
+ * @param {String} id the id of the element to get
+ * @return {Ext.dd.DragDropManager.ElementWrapper} the wrapped element
+ * @private
+ * @deprecated This wrapper isn't that useful
+ */
+ getElWrapper: function(id) {
+ var oWrapper = this.elementCache[id];
+ if (!oWrapper || !oWrapper.el) {
+ oWrapper = this.elementCache[id] =
+ new this.ElementWrapper(Ext.getDom(id));
+ }
+ return oWrapper;
+ },
+
+ /**
+ * Returns the actual DOM element
+ * @method getElement
+ * @param {String} id the id of the elment to get
+ * @return {Object} The element
+ * @deprecated use Ext.lib.Ext.getDom instead
+ */
+ getElement: function(id) {
+ return Ext.getDom(id);
+ },
+
+ /**
+ * Returns the style property for the DOM element (i.e.,
+ * document.getElById(id).style)
+ * @method getCss
+ * @param {String} id the id of the elment to get
+ * @return {Object} The style property of the element
+ */
+ getCss: function(id) {
+ var el = Ext.getDom(id);
+ return (el) ? el.style : null;
+ },
+
+ /**
+ * @class Ext.dd.DragDropManager.ElementWrapper
+ * Inner class for cached elements
+ * @private
+ * @deprecated
+ */
+ ElementWrapper: function(el) {
+ /**
+ * The element
+ * @property el
+ */
+ this.el = el || null;
+ /**
+ * The element id
+ * @property id
+ */
+ this.id = this.el && el.id;
+ /**
+ * A reference to the style property
+ * @property css
+ */
+ this.css = this.el && el.style;
+ },
+
+ // The DragDropManager class continues
+ /** @class Ext.dd.DragDropManager */
+
+ /**
+ * Returns the X position of an html element
+ * @param {HTMLElement} el the element for which to get the position
+ * @return {Number} the X coordinate
+ */
+ getPosX: function(el) {
+ return Ext.Element.getX(el);
+ },
+
+ /**
+ * Returns the Y position of an html element
+ * @param {HTMLElement} el the element for which to get the position
+ * @return {Number} the Y coordinate
+ */
+ getPosY: function(el) {
+ return Ext.Element.getY(el);
+ },
+
+ /**
+ * Swap two nodes. In IE, we use the native method, for others we
+ * emulate the IE behavior
+ * @param {HTMLElement} n1 the first node to swap
+ * @param {HTMLElement} n2 the other node to swap
+ */
+ swapNode: function(n1, n2) {
+ if (n1.swapNode) {
+ n1.swapNode(n2);
+ } else {
+ var p = n2.parentNode;
+ var s = n2.nextSibling;
+
+ if (s == n1) {
+ p.insertBefore(n1, n2);
+ } else if (n2 == n1.nextSibling) {
+ p.insertBefore(n2, n1);
+ } else {
+ n1.parentNode.replaceChild(n2, n1);
+ p.insertBefore(n1, s);
+ }
+ }
+ },
+
+ /**
+ * Returns the current scroll position
+ * @private
+ */
+ getScroll: function () {
+ var doc = window.document,
+ docEl = doc.documentElement,
+ body = doc.body,
+ top = 0,
+ left = 0;
+
+ if (Ext.isGecko4) {
+ top = window.scrollYOffset;
+ left = window.scrollXOffset;
+ } else {
+ if (docEl && (docEl.scrollTop || docEl.scrollLeft)) {
+ top = docEl.scrollTop;
+ left = docEl.scrollLeft;
+ } else if (body) {
+ top = body.scrollTop;
+ left = body.scrollLeft;
+ }
+ }
+ return {
+ top: top,
+ left: left
+ };
+ },
+
+ /**
+ * Returns the specified element style property
+ * @param {HTMLElement} el the element
+ * @param {String} styleProp the style property
+ * @return {String} The value of the style property
+ */
+ getStyle: function(el, styleProp) {
+ return Ext.fly(el).getStyle(styleProp);
+ },
+
+ /**
+ * Gets the scrollTop
+ * @return {Number} the document's scrollTop
+ */
+ getScrollTop: function () {
+ return this.getScroll().top;
+ },
+
+ /**
+ * Gets the scrollLeft
+ * @return {Number} the document's scrollTop
+ */
+ getScrollLeft: function () {
+ return this.getScroll().left;
+ },
+
+ /**
+ * Sets the x/y position of an element to the location of the
+ * target element.
+ * @param {HTMLElement} moveEl The element to move
+ * @param {HTMLElement} targetEl The position reference element
+ */
+ moveToEl: function (moveEl, targetEl) {
+ var aCoord = Ext.Element.getXY(targetEl);
+ Ext.Element.setXY(moveEl, aCoord);
+ },
+
+ /**
+ * Numeric array sort function
+ * @param {Number} a
+ * @param {Number} b
+ * @returns {Number} positive, negative or 0
+ */
+ numericSort: function(a, b) {
+ return (a - b);
+ },
+
+ /**
+ * Internal counter
+ * @property {Number} _timeoutCount
+ * @private
+ */
+ _timeoutCount: 0,
+
+ /**
+ * Trying to make the load order less important. Without this we get
+ * an error if this file is loaded before the Event Utility.
+ * @private
+ */
+ _addListeners: function() {
+ if ( document ) {
+ this._onLoad();
+ } else {
+ if (this._timeoutCount > 2000) {
+ } else {
+ setTimeout(this._addListeners, 10);
+ if (document && document.body) {
+ this._timeoutCount += 1;
+ }
+ }
+ }
+ },
+
+ /**
+ * Recursively searches the immediate parent and all child nodes for
+ * the handle element in order to determine wheter or not it was
+ * clicked.
+ * @param {HTMLElement} node the html element to inspect
+ */
+ handleWasClicked: function(node, id) {
+ if (this.isHandle(id, node.id)) {
+ return true;
+ } else {
+ // check to see if this is a text node child of the one we want
+ var p = node.parentNode;
+
+ while (p) {
+ if (this.isHandle(id, p.id)) {
+ return true;
+ } else {
+ p = p.parentNode;
+ }
+ }
+ }
+
+ return false;
+ }
+}, function() {
+ this._addListeners();
+});
+
+/**
+ * @class Ext.layout.container.Box
+ * @extends Ext.layout.container.Container
+ * <p>Base Class for HBoxLayout and VBoxLayout Classes. Generally it should not need to be used directly.</p>
+ */
+
+Ext.define('Ext.layout.container.Box', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.box'],
+ extend: 'Ext.layout.container.Container',
+ alternateClassName: 'Ext.layout.BoxLayout',
+
+ requires: [
+ 'Ext.layout.container.boxOverflow.None',
+ 'Ext.layout.container.boxOverflow.Menu',
+ 'Ext.layout.container.boxOverflow.Scroller',
+ 'Ext.util.Format',
+ 'Ext.dd.DragDropManager'
+ ],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Boolean/Number/Object} animate
+ * <p>If truthy, child Component are <i>animated</i> into position whenever the Container
+ * is layed out. If this option is numeric, it is used as the animation duration in milliseconds.</p>
+ * <p>May be set as a property at any time.</p>
+ */
+
+ /**
+ * @cfg {Object} defaultMargins
+ * <p>If the individual contained items do not have a <tt>margins</tt>
+ * property specified or margin specified via CSS, the default margins from this property will be
+ * applied to each item.</p>
+ * <br><p>This property may be specified as an object containing margins
+ * to apply in the format:</p><pre><code>
+{
+ top: (top margin),
+ right: (right margin),
+ bottom: (bottom margin),
+ left: (left margin)
+}</code></pre>
+ * <p>This property may also be specified as a string containing
+ * space-separated, numeric margin values. The order of the sides associated
+ * with each value matches the way CSS processes margin values:</p>
+ * <div class="mdetail-params"><ul>
+ * <li>If there is only one value, it applies to all sides.</li>
+ * <li>If there are two values, the top and bottom borders are set to the
+ * first value and the right and left are set to the second.</li>
+ * <li>If there are three values, the top is set to the first value, the left
+ * and right are set to the second, and the bottom is set to the third.</li>
+ * <li>If there are four values, they apply to the top, right, bottom, and
+ * left, respectively.</li>
+ * </ul></div>
+ */
+ defaultMargins: {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ },
+
+ /**
+ * @cfg {String} padding
+ * <p>Sets the padding to be applied to all child items managed by this layout.</p>
+ * <p>This property must be specified as a string containing
+ * space-separated, numeric padding values. The order of the sides associated
+ * with each value matches the way CSS processes padding values:</p>
+ * <div class="mdetail-params"><ul>
+ * <li>If there is only one value, it applies to all sides.</li>
+ * <li>If there are two values, the top and bottom borders are set to the
+ * first value and the right and left are set to the second.</li>
+ * <li>If there are three values, the top is set to the first value, the left
+ * and right are set to the second, and the bottom is set to the third.</li>
+ * <li>If there are four values, they apply to the top, right, bottom, and
+ * left, respectively.</li>
+ * </ul></div>
+ */
+ padding: '0',
+ // documented in subclasses
+ pack: 'start',
+
+ /**
+ * @cfg {String} pack
+ * Controls how the child items of the container are packed together. Acceptable configuration values
+ * for this property are:
+ * <div class="mdetail-params"><ul>
+ * <li><b><tt>start</tt></b> : <b>Default</b><div class="sub-desc">child items are packed together at
+ * <b>left</b> side of container</div></li>
+ * <li><b><tt>center</tt></b> : <div class="sub-desc">child items are packed together at
+ * <b>mid-width</b> of container</div></li>
+ * <li><b><tt>end</tt></b> : <div class="sub-desc">child items are packed together at <b>right</b>
+ * side of container</div></li>
+ * </ul></div>
+ */
+ /**
+ * @cfg {Number} flex
+ * This configuration option is to be applied to <b>child <tt>items</tt></b> of the container managed
+ * by this layout. Each child item with a <tt>flex</tt> property will be flexed <b>horizontally</b>
+ * according to each item's <b>relative</b> <tt>flex</tt> value compared to the sum of all items with
+ * a <tt>flex</tt> value specified. Any child items that have either a <tt>flex = 0</tt> or
+ * <tt>flex = undefined</tt> will not be 'flexed' (the initial size will not be changed).
+ */
+
+ type: 'box',
+ scrollOffset: 0,
+ itemCls: Ext.baseCSSPrefix + 'box-item',
+ targetCls: Ext.baseCSSPrefix + 'box-layout-ct',
+ innerCls: Ext.baseCSSPrefix + 'box-inner',
+
+ bindToOwnerCtContainer: true,
+
+ // availableSpaceOffset is used to adjust the availableWidth, typically used
+ // to reserve space for a scrollbar
+ availableSpaceOffset: 0,
+
+ // whether or not to reserve the availableSpaceOffset in layout calculations
+ reserveOffset: true,
+
+ /**
+ * @cfg {Boolean} shrinkToFit
+ * True (the default) to allow fixed size components to shrink (limited to their
+ * minimum size) to avoid overflow. False to preserve fixed sizes even if they cause
+ * overflow.
+ */
+ shrinkToFit: true,
+
+ /**
+ * @cfg {Boolean} clearInnerCtOnLayout
+ */
+ clearInnerCtOnLayout: false,
+
+ flexSortFn: function (a, b) {
+ var maxParallelPrefix = 'max' + this.parallelPrefixCap,
+ infiniteValue = Infinity;
+ a = a.component[maxParallelPrefix] || infiniteValue;
+ b = b.component[maxParallelPrefix] || infiniteValue;
+ // IE 6/7 Don't like Infinity - Infinity...
+ if (!isFinite(a) && !isFinite(b)) {
+ return false;
+ }
+ return a - b;
+ },
+
+ // Sort into *descending* order.
+ minSizeSortFn: function(a, b) {
+ return b.available - a.available;
+ },
+
+ constructor: function(config) {
+ var me = this;
+
+ me.callParent(arguments);
+
+ // The sort function needs access to properties in this, so must be bound.
+ me.flexSortFn = Ext.Function.bind(me.flexSortFn, me);
+
+ me.initOverflowHandler();
+ },
+
+ /**
+ * @private
+ * Returns the current size and positioning of the passed child item.
+ * @param {Ext.Component} child The child Component to calculate the box for
+ * @return {Object} Object containing box measurements for the child. Properties are left,top,width,height.
+ */
+ getChildBox: function(child) {
+ child = child.el || this.owner.getComponent(child).el;
+ var size = child.getBox(false, true);
+ return {
+ left: size.left,
+ top: size.top,
+ width: size.width,
+ height: size.height
+ };
+ },
+
+ /**
+ * @private
+ * Calculates the size and positioning of the passed child item.
+ * @param {Ext.Component} child The child Component to calculate the box for
+ * @return {Object} Object containing box measurements for the child. Properties are left,top,width,height.
+ */
+ calculateChildBox: function(child) {
+ var me = this,
+ boxes = me.calculateChildBoxes(me.getVisibleItems(), me.getLayoutTargetSize()).boxes,
+ ln = boxes.length,
+ i = 0;
+
+ child = me.owner.getComponent(child);
+ for (; i < ln; i++) {
+ if (boxes[i].component === child) {
+ return boxes[i];
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Calculates the size and positioning of each item in the box. This iterates over all of the rendered,
+ * visible items and returns a height, width, top and left for each, as well as a reference to each. Also
+ * returns meta data such as maxSize which are useful when resizing layout wrappers such as this.innerCt.
+ * @param {Array} visibleItems The array of all rendered, visible items to be calculated for
+ * @param {Object} targetSize Object containing target size and height
+ * @return {Object} Object containing box measurements for each child, plus meta data
+ */
+ calculateChildBoxes: function(visibleItems, targetSize) {
+ var me = this,
+ math = Math,
+ mmax = math.max,
+ infiniteValue = Infinity,
+ undefinedValue,
+
+ parallelPrefix = me.parallelPrefix,
+ parallelPrefixCap = me.parallelPrefixCap,
+ perpendicularPrefix = me.perpendicularPrefix,
+ perpendicularPrefixCap = me.perpendicularPrefixCap,
+ parallelMinString = 'min' + parallelPrefixCap,
+ perpendicularMinString = 'min' + perpendicularPrefixCap,
+ perpendicularMaxString = 'max' + perpendicularPrefixCap,
+
+ parallelSize = targetSize[parallelPrefix] - me.scrollOffset,
+ perpendicularSize = targetSize[perpendicularPrefix],
+ padding = me.padding,
+ parallelOffset = padding[me.parallelBefore],
+ paddingParallel = parallelOffset + padding[me.parallelAfter],
+ perpendicularOffset = padding[me.perpendicularLeftTop],
+ paddingPerpendicular = perpendicularOffset + padding[me.perpendicularRightBottom],
+ availPerpendicularSize = mmax(0, perpendicularSize - paddingPerpendicular),
+
+ innerCtBorderWidth = me.innerCt.getBorderWidth(me.perpendicularLT + me.perpendicularRB),
+
+ isStart = me.pack == 'start',
+ isCenter = me.pack == 'center',
+ isEnd = me.pack == 'end',
+
+ constrain = Ext.Number.constrain,
+ visibleCount = visibleItems.length,
+ nonFlexSize = 0,
+ totalFlex = 0,
+ desiredSize = 0,
+ minimumSize = 0,
+ maxSize = 0,
+ boxes = [],
+ minSizes = [],
+ calculatedWidth,
+
+ i, child, childParallel, childPerpendicular, childMargins, childSize, minParallel, tmpObj, shortfall,
+ tooNarrow, availableSpace, minSize, item, length, itemIndex, box, oldSize, newSize, reduction, diff,
+ flexedBoxes, remainingSpace, remainingFlex, flexedSize, parallelMargins, calcs, offset,
+ perpendicularMargins, stretchSize;
+
+ //gather the total flex of all flexed items and the width taken up by fixed width items
+ for (i = 0; i < visibleCount; i++) {
+ child = visibleItems[i];
+ childPerpendicular = child[perpendicularPrefix];
+ if (!child.flex || !(me.align == 'stretch' || me.align == 'stretchmax')) {
+ if (child.componentLayout.initialized !== true) {
+ me.layoutItem(child);
+ }
+ }
+
+ childMargins = child.margins;
+ parallelMargins = childMargins[me.parallelBefore] + childMargins[me.parallelAfter];
+
+ // Create the box description object for this child item.
+ tmpObj = {
+ component: child,
+ margins: childMargins
+ };
+
+ // flex and not 'auto' width
+ if (child.flex) {
+ totalFlex += child.flex;
+ childParallel = undefinedValue;
+ }
+ // Not flexed or 'auto' width or undefined width
+ else {
+ if (!(child[parallelPrefix] && childPerpendicular)) {
+ childSize = child.getSize();
+ }
+ childParallel = child[parallelPrefix] || childSize[parallelPrefix];
+ childPerpendicular = childPerpendicular || childSize[perpendicularPrefix];
+ }
+
+ nonFlexSize += parallelMargins + (childParallel || 0);
+ desiredSize += parallelMargins + (child.flex ? child[parallelMinString] || 0 : childParallel);
+ minimumSize += parallelMargins + (child[parallelMinString] || childParallel || 0);
+
+ // Max height for align - force layout of non-laid out subcontainers without a numeric height
+ if (typeof childPerpendicular != 'number') {
+ // Clear any static sizing and revert to flow so we can get a proper measurement
+ // child['set' + perpendicularPrefixCap](null);
+ childPerpendicular = child['get' + perpendicularPrefixCap]();
+ }
+
+ // Track the maximum perpendicular size for use by the stretch and stretchmax align config values.
+ // Ensure that the tracked maximum perpendicular size takes into account child min[Width|Height] settings!
+ maxSize = mmax(maxSize, mmax(childPerpendicular, child[perpendicularMinString]||0) + childMargins[me.perpendicularLeftTop] + childMargins[me.perpendicularRightBottom]);
+
+ tmpObj[parallelPrefix] = childParallel || undefinedValue;
+ tmpObj.dirtySize = child.componentLayout.lastComponentSize ? (tmpObj[parallelPrefix] !== child.componentLayout.lastComponentSize[parallelPrefix]) : false;
+ tmpObj[perpendicularPrefix] = childPerpendicular || undefinedValue;
+ boxes.push(tmpObj);
+ }
+
+ // Only calculate parallel overflow indicators if we are not auto sizing
+ if (!me.autoSize) {
+ shortfall = desiredSize - parallelSize;
+ tooNarrow = minimumSize > parallelSize;
+ }
+
+ //the space available to the flexed items
+ availableSpace = mmax(0, parallelSize - nonFlexSize - paddingParallel - (me.reserveOffset ? me.availableSpaceOffset : 0));
+
+ if (tooNarrow) {
+ for (i = 0; i < visibleCount; i++) {
+ box = boxes[i];
+ minSize = visibleItems[i][parallelMinString] || visibleItems[i][parallelPrefix] || box[parallelPrefix];
+ box.dirtySize = box.dirtySize || box[parallelPrefix] != minSize;
+ box[parallelPrefix] = minSize;
+ }
+ }
+ else {
+ //all flexed items should be sized to their minimum size, other items should be shrunk down until
+ //the shortfall has been accounted for
+ if (shortfall > 0) {
+ /*
+ * When we have a shortfall but are not tooNarrow, we need to shrink the width of each non-flexed item.
+ * Flexed items are immediately reduced to their minWidth and anything already at minWidth is ignored.
+ * The remaining items are collected into the minWidths array, which is later used to distribute the shortfall.
+ */
+ for (i = 0; i < visibleCount; i++) {
+ item = visibleItems[i];
+ minSize = item[parallelMinString] || 0;
+
+ //shrink each non-flex tab by an equal amount to make them all fit. Flexed items are all
+ //shrunk to their minSize because they're flexible and should be the first to lose size
+ if (item.flex) {
+ box = boxes[i];
+ box.dirtySize = box.dirtySize || box[parallelPrefix] != minSize;
+ box[parallelPrefix] = minSize;
+ } else if (me.shrinkToFit) {
+ minSizes.push({
+ minSize: minSize,
+ available: boxes[i][parallelPrefix] - minSize,
+ index: i
+ });
+ }
+ }
+
+ //sort by descending amount of width remaining before minWidth is reached
+ Ext.Array.sort(minSizes, me.minSizeSortFn);
+
+ /*
+ * Distribute the shortfall (difference between total desired size of all items and actual size available)
+ * between the non-flexed items. We try to distribute the shortfall evenly, but apply it to items with the
+ * smallest difference between their size and minSize first, so that if reducing the size by the average
+ * amount would make that item less than its minSize, we carry the remainder over to the next item.
+ */
+ for (i = 0, length = minSizes.length; i < length; i++) {
+ itemIndex = minSizes[i].index;
+
+ if (itemIndex == undefinedValue) {
+ continue;
+ }
+ item = visibleItems[itemIndex];
+ minSize = minSizes[i].minSize;
+
+ box = boxes[itemIndex];
+ oldSize = box[parallelPrefix];
+ newSize = mmax(minSize, oldSize - math.ceil(shortfall / (length - i)));
+ reduction = oldSize - newSize;
+
+ box.dirtySize = box.dirtySize || box[parallelPrefix] != newSize;
+ box[parallelPrefix] = newSize;
+ shortfall -= reduction;
+ }
+ tooNarrow = (shortfall > 0);
+ }
+ else {
+ remainingSpace = availableSpace;
+ remainingFlex = totalFlex;
+ flexedBoxes = [];
+
+ // Create an array containing *just the flexed boxes* for allocation of remainingSpace
+ for (i = 0; i < visibleCount; i++) {
+ child = visibleItems[i];
+ if (isStart && child.flex) {
+ flexedBoxes.push(boxes[Ext.Array.indexOf(visibleItems, child)]);
+ }
+ }
+ // The flexed boxes need to be sorted in ascending order of maxSize to work properly
+ // so that unallocated space caused by maxWidth being less than flexed width
+ // can be reallocated to subsequent flexed boxes.
+ Ext.Array.sort(flexedBoxes, me.flexSortFn);
+
+ // Calculate the size of each flexed item, and attempt to set it.
+ for (i = 0; i < flexedBoxes.length; i++) {
+ calcs = flexedBoxes[i];
+ child = calcs.component;
+ childMargins = calcs.margins;
+
+ flexedSize = math.ceil((child.flex / remainingFlex) * remainingSpace);
+
+ // Implement maxSize and minSize check
+ flexedSize = Math.max(child['min' + parallelPrefixCap] || 0, math.min(child['max' + parallelPrefixCap] || infiniteValue, flexedSize));
+
+ // Remaining space has already had all parallel margins subtracted from it, so just subtract consumed size
+ remainingSpace -= flexedSize;
+ remainingFlex -= child.flex;
+
+ calcs.dirtySize = calcs.dirtySize || calcs[parallelPrefix] != flexedSize;
+ calcs[parallelPrefix] = flexedSize;
+ }
+ }
+ }
+
+ if (isCenter) {
+ parallelOffset += availableSpace / 2;
+ }
+ else if (isEnd) {
+ parallelOffset += availableSpace;
+ }
+
+ // Fix for left and right docked Components in a dock component layout. This is for docked Headers and docked Toolbars.
+ // Older Microsoft browsers do not size a position:absolute element's width to match its content.
+ // So in this case, in the updateInnerCtSize method we may need to adjust the size of the owning Container's element explicitly based upon
+ // the discovered max width. So here we put a calculatedWidth property in the metadata to facilitate this.
+ if (me.owner.dock && (Ext.isIE6 || Ext.isIE7 || Ext.isIEQuirks) && !me.owner.width && me.direction == 'vertical') {
+
+ calculatedWidth = maxSize + me.owner.el.getPadding('lr') + me.owner.el.getBorderWidth('lr');
+ if (me.owner.frameSize) {
+ calculatedWidth += me.owner.frameSize.left + me.owner.frameSize.right;
+ }
+ // If the owning element is not sized, calculate the available width to center or stretch in based upon maxSize
+ availPerpendicularSize = Math.min(availPerpendicularSize, targetSize.width = maxSize + padding.left + padding.right);
+ }
+
+ //finally, calculate the left and top position of each item
+ for (i = 0; i < visibleCount; i++) {
+ child = visibleItems[i];
+ calcs = boxes[i];
+
+ childMargins = calcs.margins;
+
+ perpendicularMargins = childMargins[me.perpendicularLeftTop] + childMargins[me.perpendicularRightBottom];
+
+ // Advance past the "before" margin
+ parallelOffset += childMargins[me.parallelBefore];
+
+ calcs[me.parallelBefore] = parallelOffset;
+ calcs[me.perpendicularLeftTop] = perpendicularOffset + childMargins[me.perpendicularLeftTop];
+
+ if (me.align == 'stretch') {
+ stretchSize = constrain(availPerpendicularSize - perpendicularMargins, child[perpendicularMinString] || 0, child[perpendicularMaxString] || infiniteValue);
+ calcs.dirtySize = calcs.dirtySize || calcs[perpendicularPrefix] != stretchSize;
+ calcs[perpendicularPrefix] = stretchSize;
+ }
+ else if (me.align == 'stretchmax') {
+ stretchSize = constrain(maxSize - perpendicularMargins, child[perpendicularMinString] || 0, child[perpendicularMaxString] || infiniteValue);
+ calcs.dirtySize = calcs.dirtySize || calcs[perpendicularPrefix] != stretchSize;
+ calcs[perpendicularPrefix] = stretchSize;
+ }
+ else if (me.align == me.alignCenteringString) {
+ // When calculating a centered position within the content box of the innerCt, the width of the borders must be subtracted from
+ // the size to yield the space available to center within.
+ // The updateInnerCtSize method explicitly adds the border widths to the set size of the innerCt.
+ diff = mmax(availPerpendicularSize, maxSize) - innerCtBorderWidth - calcs[perpendicularPrefix];
+ if (diff > 0) {
+ calcs[me.perpendicularLeftTop] = perpendicularOffset + Math.round(diff / 2);
+ }
+ }
+
+ // Advance past the box size and the "after" margin
+ parallelOffset += (calcs[parallelPrefix] || 0) + childMargins[me.parallelAfter];
+ }
+
+ return {
+ boxes: boxes,
+ meta : {
+ calculatedWidth: calculatedWidth,
+ maxSize: maxSize,
+ nonFlexSize: nonFlexSize,
+ desiredSize: desiredSize,
+ minimumSize: minimumSize,
+ shortfall: shortfall,
+ tooNarrow: tooNarrow
+ }
+ };
+ },
+
+ onRemove: function(comp){
+ this.callParent(arguments);
+ if (this.overflowHandler) {
+ this.overflowHandler.onRemove(comp);
+ }
+ },
+
+ /**
+ * @private
+ */
+ initOverflowHandler: function() {
+ var handler = this.overflowHandler;
+
+ if (typeof handler == 'string') {
+ handler = {
+ type: handler
+ };
+ }
+
+ var handlerType = 'None';
+ if (handler && handler.type !== undefined) {
+ handlerType = handler.type;
+ }
+
+ var constructor = Ext.layout.container.boxOverflow[handlerType];
+ if (constructor[this.type]) {
+ constructor = constructor[this.type];
+ }
+
+ this.overflowHandler = Ext.create('Ext.layout.container.boxOverflow.' + handlerType, this, handler);
+ },
+
+ /**
+ * @private
+ * Runs the child box calculations and caches them in childBoxCache. Subclasses can used these cached values
+ * when laying out
+ */
+ onLayout: function() {
+ this.callParent();
+ // Clear the innerCt size so it doesn't influence the child items.
+ if (this.clearInnerCtOnLayout === true && this.adjustmentPass !== true) {
+ this.innerCt.setSize(null, null);
+ }
+
+ var me = this,
+ targetSize = me.getLayoutTargetSize(),
+ items = me.getVisibleItems(),
+ calcs = me.calculateChildBoxes(items, targetSize),
+ boxes = calcs.boxes,
+ meta = calcs.meta,
+ handler, method, results;
+
+ if (me.autoSize && calcs.meta.desiredSize) {
+ targetSize[me.parallelPrefix] = calcs.meta.desiredSize;
+ }
+
+ //invoke the overflow handler, if one is configured
+ if (meta.shortfall > 0) {
+ handler = me.overflowHandler;
+ method = meta.tooNarrow ? 'handleOverflow': 'clearOverflow';
+
+ results = handler[method](calcs, targetSize);
+
+ if (results) {
+ if (results.targetSize) {
+ targetSize = results.targetSize;
+ }
+
+ if (results.recalculate) {
+ items = me.getVisibleItems();
+ calcs = me.calculateChildBoxes(items, targetSize);
+ boxes = calcs.boxes;
+ }
+ }
+ } else {
+ me.overflowHandler.clearOverflow();
+ }
+
+ /**
+ * @private
+ * @property layoutTargetLastSize
+ * @type Object
+ * Private cache of the last measured size of the layout target. This should never be used except by
+ * BoxLayout subclasses during their onLayout run.
+ */
+ me.layoutTargetLastSize = targetSize;
+
+ /**
+ * @private
+ * @property childBoxCache
+ * @type Array
+ * Array of the last calculated height, width, top and left positions of each visible rendered component
+ * within the Box layout.
+ */
+ me.childBoxCache = calcs;
+
+ me.updateInnerCtSize(targetSize, calcs);
+ me.updateChildBoxes(boxes);
+ me.handleTargetOverflow(targetSize);
+ },
+
+ animCallback: Ext.emptyFn,
+
+ /**
+ * Resizes and repositions each child component
+ * @param {Object[]} boxes The box measurements
+ */
+ updateChildBoxes: function(boxes) {
+ var me = this,
+ i = 0,
+ length = boxes.length,
+ animQueue = [],
+ dd = Ext.dd.DDM.getDDById(me.innerCt.id), // Any DD active on this layout's element (The BoxReorderer plugin does this.)
+ oldBox, newBox, changed, comp, boxAnim, animCallback;
+
+ for (; i < length; i++) {
+ newBox = boxes[i];
+ comp = newBox.component;
+
+ // If a Component is being drag/dropped, skip positioning it.
+ // Accomodate the BoxReorderer plugin: Its current dragEl must not be positioned by the layout
+ if (dd && (dd.getDragEl() === comp.el.dom)) {
+ continue;
+ }
+
+ changed = false;
+
+ oldBox = me.getChildBox(comp);
+
+ // If we are animating, we build up an array of Anim config objects, one for each
+ // child Component which has any changed box properties. Those with unchanged
+ // properties are not animated.
+ if (me.animate) {
+ // Animate may be a config object containing callback.
+ animCallback = me.animate.callback || me.animate;
+ boxAnim = {
+ layoutAnimation: true, // Component Target handler must use set*Calculated*Size
+ target: comp,
+ from: {},
+ to: {},
+ listeners: {}
+ };
+ // Only set from and to properties when there's a change.
+ // Perform as few Component setter methods as possible.
+ // Temporarily set the property values that we are not animating
+ // so that doComponentLayout does not auto-size them.
+ if (!isNaN(newBox.width) && (newBox.width != oldBox.width)) {
+ changed = true;
+ // boxAnim.from.width = oldBox.width;
+ boxAnim.to.width = newBox.width;
+ }
+ if (!isNaN(newBox.height) && (newBox.height != oldBox.height)) {
+ changed = true;
+ // boxAnim.from.height = oldBox.height;
+ boxAnim.to.height = newBox.height;
+ }
+ if (!isNaN(newBox.left) && (newBox.left != oldBox.left)) {
+ changed = true;
+ // boxAnim.from.left = oldBox.left;
+ boxAnim.to.left = newBox.left;
+ }
+ if (!isNaN(newBox.top) && (newBox.top != oldBox.top)) {
+ changed = true;
+ // boxAnim.from.top = oldBox.top;
+ boxAnim.to.top = newBox.top;
+ }
+ if (changed) {
+ animQueue.push(boxAnim);
+ }
+ } else {
+ if (newBox.dirtySize) {
+ if (newBox.width !== oldBox.width || newBox.height !== oldBox.height) {
+ me.setItemSize(comp, newBox.width, newBox.height);
+ }
+ }
+ // Don't set positions to NaN
+ if (isNaN(newBox.left) || isNaN(newBox.top)) {
+ continue;
+ }
+ comp.setPosition(newBox.left, newBox.top);
+ }
+ }
+
+ // Kick off any queued animations
+ length = animQueue.length;
+ if (length) {
+
+ // A function which cleans up when a Component's animation is done.
+ // The last one to finish calls the callback.
+ var afterAnimate = function(anim) {
+ // When we've animated all changed boxes into position, clear our busy flag and call the callback.
+ length -= 1;
+ if (!length) {
+ me.animCallback(anim);
+ me.layoutBusy = false;
+ if (Ext.isFunction(animCallback)) {
+ animCallback();
+ }
+ }
+ };
+
+ var beforeAnimate = function() {
+ me.layoutBusy = true;
+ };
+
+ // Start each box animation off
+ for (i = 0, length = animQueue.length; i < length; i++) {
+ boxAnim = animQueue[i];
+
+ // Clean up the Component after. Clean up the *layout* after the last animation finishes
+ boxAnim.listeners.afteranimate = afterAnimate;
+
+ // The layout is busy during animation, and may not be called, so set the flag when the first animation begins
+ if (!i) {
+ boxAnim.listeners.beforeanimate = beforeAnimate;
+ }
+ if (me.animate.duration) {
+ boxAnim.duration = me.animate.duration;
+ }
+ comp = boxAnim.target;
+ delete boxAnim.target;
+ // Stop any currently running animation
+ comp.stopAnimation();
+ comp.animate(boxAnim);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Called by onRender just before the child components are sized and positioned. This resizes the innerCt
+ * to make sure all child items fit within it. We call this before sizing the children because if our child
+ * items are larger than the previous innerCt size the browser will insert scrollbars and then remove them
+ * again immediately afterwards, giving a performance hit.
+ * Subclasses should provide an implementation.
+ * @param {Object} currentSize The current height and width of the innerCt
+ * @param {Object} calculations The new box calculations of all items to be laid out
+ */
+ updateInnerCtSize: function(tSize, calcs) {
+ var me = this,
+ mmax = Math.max,
+ align = me.align,
+ padding = me.padding,
+ width = tSize.width,
+ height = tSize.height,
+ meta = calcs.meta,
+ innerCtWidth,
+ innerCtHeight;
+
+ if (me.direction == 'horizontal') {
+ innerCtWidth = width;
+ innerCtHeight = meta.maxSize + padding.top + padding.bottom + me.innerCt.getBorderWidth('tb');
+
+ if (align == 'stretch') {
+ innerCtHeight = height;
+ }
+ else if (align == 'middle') {
+ innerCtHeight = mmax(height, innerCtHeight);
+ }
+ } else {
+ innerCtHeight = height;
+ innerCtWidth = meta.maxSize + padding.left + padding.right + me.innerCt.getBorderWidth('lr');
+
+ if (align == 'stretch') {
+ innerCtWidth = width;
+ }
+ else if (align == 'center') {
+ innerCtWidth = mmax(width, innerCtWidth);
+ }
+ }
+ me.getRenderTarget().setSize(innerCtWidth || undefined, innerCtHeight || undefined);
+
+ // If a calculated width has been found (and this only happens for auto-width vertical docked Components in old Microsoft browsers)
+ // then, if the Component has not assumed the size of its content, set it to do so.
+ if (meta.calculatedWidth && me.owner.el.getWidth() > meta.calculatedWidth) {
+ me.owner.el.setWidth(meta.calculatedWidth);
+ }
+
+ if (me.innerCt.dom.scrollTop) {
+ me.innerCt.dom.scrollTop = 0;
+ }
+ },
+
+ /**
+ * @private
+ * This should be called after onLayout of any BoxLayout subclass. If the target's overflow is not set to 'hidden',
+ * we need to lay out a second time because the scrollbars may have modified the height and width of the layout
+ * target. Having a Box layout inside such a target is therefore not recommended.
+ * @param {Object} previousTargetSize The size and height of the layout target before we just laid out
+ * @param {Ext.container.Container} container The container
+ * @param {Ext.Element} target The target element
+ * @return True if the layout overflowed, and was reflowed in a secondary onLayout call.
+ */
+ handleTargetOverflow: function(previousTargetSize) {
+ var target = this.getTarget(),
+ overflow = target.getStyle('overflow'),
+ newTargetSize;
+
+ if (overflow && overflow != 'hidden' && !this.adjustmentPass) {
+ newTargetSize = this.getLayoutTargetSize();
+ if (newTargetSize.width != previousTargetSize.width || newTargetSize.height != previousTargetSize.height) {
+ this.adjustmentPass = true;
+ this.onLayout();
+ return true;
+ }
+ }
+
+ delete this.adjustmentPass;
+ },
+
+ // private
+ isValidParent : function(item, target, position) {
+ // Note: Box layouts do not care about order within the innerCt element because it's an absolutely positioning layout
+ // We only care whether the item is a direct child of the innerCt element.
+ var itemEl = item.el ? item.el.dom : Ext.getDom(item);
+ return (itemEl && this.innerCt && itemEl.parentNode === this.innerCt.dom) || false;
+ },
+
+ // Overridden method from AbstractContainer.
+ // Used in the base AbstractLayout.beforeLayout method to render all items into.
+ getRenderTarget: function() {
+ if (!this.innerCt) {
+ // the innerCt prevents wrapping and shuffling while the container is resizing
+ this.innerCt = this.getTarget().createChild({
+ cls: this.innerCls,
+ role: 'presentation'
+ });
+ this.padding = Ext.util.Format.parseBox(this.padding);
+ }
+ return this.innerCt;
+ },
+
+ // private
+ renderItem: function(item, target) {
+ this.callParent(arguments);
+ var me = this,
+ itemEl = item.getEl(),
+ style = itemEl.dom.style,
+ margins = item.margins || item.margin;
+
+ // Parse the item's margin/margins specification
+ if (margins) {
+ if (Ext.isString(margins) || Ext.isNumber(margins)) {
+ margins = Ext.util.Format.parseBox(margins);
+ } else {
+ Ext.applyIf(margins, {top: 0, right: 0, bottom: 0, left: 0});
+ }
+ } else {
+ margins = Ext.apply({}, me.defaultMargins);
+ }
+
+ // Add any before/after CSS margins to the configured margins, and zero the CSS margins
+ margins.top += itemEl.getMargin('t');
+ margins.right += itemEl.getMargin('r');
+ margins.bottom += itemEl.getMargin('b');
+ margins.left += itemEl.getMargin('l');
+ margins.height = margins.top + margins.bottom;
+ margins.width = margins.left + margins.right;
+ style.marginTop = style.marginRight = style.marginBottom = style.marginLeft = '0';
+
+ // Item must reference calculated margins.
+ item.margins = margins;
+ },
+
+ /**
+ * @private
+ */
+ destroy: function() {
+ Ext.destroy(this.innerCt, this.overflowHandler);
+ this.callParent(arguments);
+ }
+});
+/**
+ * A layout that arranges items horizontally across a Container. This layout optionally divides available horizontal
+ * space between child items containing a numeric `flex` configuration.
+ *
+ * This layout may also be used to set the heights of child items by configuring it with the {@link #align} option.
+ *
+ * @example
+ * Ext.create('Ext.Panel', {
+ * width: 500,
+ * height: 300,
+ * title: "HBoxLayout Panel",
+ * layout: {
+ * type: 'hbox',
+ * align: 'stretch'
+ * },
+ * renderTo: document.body,
+ * items: [{
+ * xtype: 'panel',
+ * title: 'Inner Panel One',
+ * flex: 2
+ * },{
+ * xtype: 'panel',
+ * title: 'Inner Panel Two',
+ * flex: 1
+ * },{
+ * xtype: 'panel',
+ * title: 'Inner Panel Three',
+ * flex: 1
+ * }]
+ * });
+ */
+Ext.define('Ext.layout.container.HBox', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.hbox'],
+ extend: 'Ext.layout.container.Box',
+ alternateClassName: 'Ext.layout.HBoxLayout',
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String} align
+ * Controls how the child items of the container are aligned. Acceptable configuration values for this property are:
+ *
+ * - **top** : **Default** child items are aligned vertically at the **top** of the container
+ * - **middle** : child items are aligned vertically in the **middle** of the container
+ * - **stretch** : child items are stretched vertically to fill the height of the container
+ * - **stretchmax** : child items are stretched vertically to the height of the largest item.
+ */
+ align: 'top', // top, middle, stretch, strechmax
+
+ //@private
+ alignCenteringString: 'middle',
+
+ type : 'hbox',
+
+ direction: 'horizontal',
+
+ // When creating an argument list to setSize, use this order
+ parallelSizeIndex: 0,
+ perpendicularSizeIndex: 1,
+
+ parallelPrefix: 'width',
+ parallelPrefixCap: 'Width',
+ parallelLT: 'l',
+ parallelRB: 'r',
+ parallelBefore: 'left',
+ parallelBeforeCap: 'Left',
+ parallelAfter: 'right',
+ parallelPosition: 'x',
+
+ perpendicularPrefix: 'height',
+ perpendicularPrefixCap: 'Height',
+ perpendicularLT: 't',
+ perpendicularRB: 'b',
+ perpendicularLeftTop: 'top',
+ perpendicularRightBottom: 'bottom',
+ perpendicularPosition: 'y',
+ configureItem: function(item) {
+ if (item.flex) {
+ item.layoutManagedWidth = 1;
+ } else {
+ item.layoutManagedWidth = 2;
+ }
+
+ if (this.align === 'stretch' || this.align === 'stretchmax') {
+ item.layoutManagedHeight = 1;
+ } else {
+ item.layoutManagedHeight = 2;
+ }
+ this.callParent(arguments);
+ }
+});
+/**
+ * A layout that arranges items vertically down a Container. This layout optionally divides available vertical space
+ * between child items containing a numeric `flex` configuration.
+ *
+ * This layout may also be used to set the widths of child items by configuring it with the {@link #align} option.
+ *
+ * @example
+ * Ext.create('Ext.Panel', {
+ * width: 500,
+ * height: 400,
+ * title: "VBoxLayout Panel",
+ * layout: {
+ * type: 'vbox',
+ * align: 'center'
+ * },
+ * renderTo: document.body,
+ * items: [{
+ * xtype: 'panel',
+ * title: 'Inner Panel One',
+ * width: 250,
+ * flex: 2
+ * },
+ * {
+ * xtype: 'panel',
+ * title: 'Inner Panel Two',
+ * width: 250,
+ * flex: 4
+ * },
+ * {
+ * xtype: 'panel',
+ * title: 'Inner Panel Three',
+ * width: '50%',
+ * flex: 4
+ * }]
+ * });
+ */
+Ext.define('Ext.layout.container.VBox', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.vbox'],
+ extend: 'Ext.layout.container.Box',
+ alternateClassName: 'Ext.layout.VBoxLayout',
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String} align
+ * Controls how the child items of the container are aligned. Acceptable configuration values for this property are:
+ *
+ * - **left** : **Default** child items are aligned horizontally at the **left** side of the container
+ * - **center** : child items are aligned horizontally at the **mid-width** of the container
+ * - **stretch** : child items are stretched horizontally to fill the width of the container
+ * - **stretchmax** : child items are stretched horizontally to the size of the largest item.
+ */
+ align : 'left', // left, center, stretch, strechmax
+
+ //@private
+ alignCenteringString: 'center',
+
+ type: 'vbox',
+
+ direction: 'vertical',
+
+ // When creating an argument list to setSize, use this order
+ parallelSizeIndex: 1,
+ perpendicularSizeIndex: 0,
+
+ parallelPrefix: 'height',
+ parallelPrefixCap: 'Height',
+ parallelLT: 't',
+ parallelRB: 'b',
+ parallelBefore: 'top',
+ parallelBeforeCap: 'Top',
+ parallelAfter: 'bottom',
+ parallelPosition: 'y',
+
+ perpendicularPrefix: 'width',
+ perpendicularPrefixCap: 'Width',
+ perpendicularLT: 'l',
+ perpendicularRB: 'r',
+ perpendicularLeftTop: 'left',
+ perpendicularRightBottom: 'right',
+ perpendicularPosition: 'x',
+ configureItem: function(item) {
+ if (item.flex) {
+ item.layoutManagedHeight = 1;
+ } else {
+ item.layoutManagedHeight = 2;
+ }
+
+ if (this.align === 'stretch' || this.align === 'stretchmax') {
+ item.layoutManagedWidth = 1;
+ } else {
+ item.layoutManagedWidth = 2;
+ }
+ this.callParent(arguments);
+ }
+});
+/**
+ * @class Ext.FocusManager
+
+The FocusManager is responsible for globally:
+
+1. Managing component focus
+2. Providing basic keyboard navigation
+3. (optional) Provide a visual cue for focused components, in the form of a focus ring/frame.
+
+To activate the FocusManager, simply call `Ext.FocusManager.enable();`. In turn, you may
+deactivate the FocusManager by subsequently calling `Ext.FocusManager.disable();. The
+FocusManager is disabled by default.
+
+To enable the optional focus frame, pass `true` or `{focusFrame: true}` to {@link #enable}.
+
+Another feature of the FocusManager is to provide basic keyboard focus navigation scoped to any {@link Ext.container.Container}
+that would like to have navigation between its child {@link Ext.Component}'s. The {@link Ext.container.Container} can simply
+call {@link #subscribe Ext.FocusManager.subscribe} to take advantage of this feature, and can at any time call
+{@link #unsubscribe Ext.FocusManager.unsubscribe} to turn the navigation off.
+
+ * @singleton
+ * @author Jarred Nicholls <jarred@sencha.com>
+ * @docauthor Jarred Nicholls <jarred@sencha.com>
+ */
+Ext.define('Ext.FocusManager', {
+ singleton: true,
+ alternateClassName: 'Ext.FocusMgr',
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ requires: [
+ 'Ext.ComponentManager',
+ 'Ext.ComponentQuery',
+ 'Ext.util.HashMap',
+ 'Ext.util.KeyNav'
+ ],
+
+ /**
+ * @property {Boolean} enabled
+ * Whether or not the FocusManager is currently enabled
+ */
+ enabled: false,
+
+ /**
+ * @property {Ext.Component} focusedCmp
+ * The currently focused component. Defaults to `undefined`.
+ */
+
+ focusElementCls: Ext.baseCSSPrefix + 'focus-element',
+
+ focusFrameCls: Ext.baseCSSPrefix + 'focus-frame',
+
+ /**
+ * @property {String[]} whitelist
+ * A list of xtypes that should ignore certain navigation input keys and
+ * allow for the default browser event/behavior. These input keys include:
+ *
+ * 1. Backspace
+ * 2. Delete
+ * 3. Left
+ * 4. Right
+ * 5. Up
+ * 6. Down
+ *
+ * The FocusManager will not attempt to navigate when a component is an xtype (or descendents thereof)
+ * that belongs to this whitelist. E.g., an {@link Ext.form.field.Text} should allow
+ * the user to move the input cursor left and right, and to delete characters, etc.
+ */
+ whitelist: [
+ 'textfield'
+ ],
+
+ tabIndexWhitelist: [
+ 'a',
+ 'button',
+ 'embed',
+ 'frame',
+ 'iframe',
+ 'img',
+ 'input',
+ 'object',
+ 'select',
+ 'textarea'
+ ],
+
+ constructor: function() {
+ var me = this,
+ CQ = Ext.ComponentQuery;
+
+ me.addEvents(
+ /**
+ * @event beforecomponentfocus
+ * Fires before a component becomes focused. Return `false` to prevent
+ * the component from gaining focus.
+ * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
+ * @param {Ext.Component} cmp The component that is being focused
+ * @param {Ext.Component} previousCmp The component that was previously focused,
+ * or `undefined` if there was no previously focused component.
+ */
+ 'beforecomponentfocus',
+
+ /**
+ * @event componentfocus
+ * Fires after a component becomes focused.
+ * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
+ * @param {Ext.Component} cmp The component that has been focused
+ * @param {Ext.Component} previousCmp The component that was previously focused,
+ * or `undefined` if there was no previously focused component.
+ */
+ 'componentfocus',
+
+ /**
+ * @event disable
+ * Fires when the FocusManager is disabled
+ * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
+ */
+ 'disable',
+
+ /**
+ * @event enable
+ * Fires when the FocusManager is enabled
+ * @param {Ext.FocusManager} fm A reference to the FocusManager singleton
+ */
+ 'enable'
+ );
+
+ // Setup KeyNav that's bound to document to catch all
+ // unhandled/bubbled key events for navigation
+ me.keyNav = Ext.create('Ext.util.KeyNav', Ext.getDoc(), {
+ disabled: true,
+ scope: me,
+
+ backspace: me.focusLast,
+ enter: me.navigateIn,
+ esc: me.navigateOut,
+ tab: me.navigateSiblings
+
+ //space: me.navigateIn,
+ //del: me.focusLast,
+ //left: me.navigateSiblings,
+ //right: me.navigateSiblings,
+ //down: me.navigateSiblings,
+ //up: me.navigateSiblings
+ });
+
+ me.focusData = {};
+ me.subscribers = Ext.create('Ext.util.HashMap');
+ me.focusChain = {};
+
+ // Setup some ComponentQuery pseudos
+ Ext.apply(CQ.pseudos, {
+ focusable: function(cmps) {
+ var len = cmps.length,
+ results = [],
+ i = 0,
+ c,
+
+ isFocusable = function(x) {
+ return x && x.focusable !== false && CQ.is(x, '[rendered]:not([destroying]):not([isDestroyed]):not([disabled]){isVisible(true)}{el && c.el.dom && c.el.isVisible()}');
+ };
+
+ for (; i < len; i++) {
+ c = cmps[i];
+ if (isFocusable(c)) {
+ results.push(c);
+ }
+ }
+
+ return results;
+ },
+
+ nextFocus: function(cmps, idx, step) {
+ step = step || 1;
+ idx = parseInt(idx, 10);
+
+ var len = cmps.length,
+ i = idx + step,
+ c;
+
+ for (; i != idx; i += step) {
+ if (i >= len) {
+ i = 0;
+ } else if (i < 0) {
+ i = len - 1;
+ }
+
+ c = cmps[i];
+ if (CQ.is(c, ':focusable')) {
+ return [c];
+ } else if (c.placeholder && CQ.is(c.placeholder, ':focusable')) {
+ return [c.placeholder];
+ }
+ }
+
+ return [];
+ },
+
+ prevFocus: function(cmps, idx) {
+ return this.nextFocus(cmps, idx, -1);
+ },
+
+ root: function(cmps) {
+ var len = cmps.length,
+ results = [],
+ i = 0,
+ c;
+
+ for (; i < len; i++) {
+ c = cmps[i];
+ if (!c.ownerCt) {
+ results.push(c);
+ }
+ }
+
+ return results;
+ }
+ });
+ },
+
+ /**
+ * Adds the specified xtype to the {@link #whitelist}.
+ * @param {String/String[]} xtype Adds the xtype(s) to the {@link #whitelist}.
+ */
+ addXTypeToWhitelist: function(xtype) {
+ var me = this;
+
+ if (Ext.isArray(xtype)) {
+ Ext.Array.forEach(xtype, me.addXTypeToWhitelist, me);
+ return;
+ }
+
+ if (!Ext.Array.contains(me.whitelist, xtype)) {
+ me.whitelist.push(xtype);
+ }
+ },
+
+ clearComponent: function(cmp) {
+ clearTimeout(this.cmpFocusDelay);
+ if (!cmp.isDestroyed) {
+ cmp.blur();
+ }
+ },
+
+ /**
+ * Disables the FocusManager by turning of all automatic focus management and keyboard navigation
+ */
+ disable: function() {
+ var me = this;
+
+ if (!me.enabled) {
+ return;
+ }
+
+ delete me.options;
+ me.enabled = false;
+
+ Ext.ComponentManager.all.un('add', me.onComponentCreated, me);
+
+ me.removeDOM();
+
+ // Stop handling key navigation
+ me.keyNav.disable();
+
+ // disable focus for all components
+ me.setFocusAll(false);
+
+ me.fireEvent('disable', me);
+ },
+
+ /**
+ * Enables the FocusManager by turning on all automatic focus management and keyboard navigation
+ * @param {Boolean/Object} options Either `true`/`false` to turn on the focus frame, or an object of the following options:
+ - focusFrame : Boolean
+ `true` to show the focus frame around a component when it is focused. Defaults to `false`.
+ * @markdown
+ */
+ enable: function(options) {
+ var me = this;
+
+ if (options === true) {
+ options = { focusFrame: true };
+ }
+ me.options = options = options || {};
+
+ if (me.enabled) {
+ return;
+ }
+
+ // Handle components that are newly added after we are enabled
+ Ext.ComponentManager.all.on('add', me.onComponentCreated, me);
+
+ me.initDOM(options);
+
+ // Start handling key navigation
+ me.keyNav.enable();
+
+ // enable focus for all components
+ me.setFocusAll(true, options);
+
+ // Finally, let's focus our global focus el so we start fresh
+ me.focusEl.focus();
+ delete me.focusedCmp;
+
+ me.enabled = true;
+ me.fireEvent('enable', me);
+ },
+
+ focusLast: function(e) {
+ var me = this;
+
+ if (me.isWhitelisted(me.focusedCmp)) {
+ return true;
+ }
+
+ // Go back to last focused item
+ if (me.previousFocusedCmp) {
+ me.previousFocusedCmp.focus();
+ }
+ },
+
+ getRootComponents: function() {
+ var me = this,
+ CQ = Ext.ComponentQuery,
+ inline = CQ.query(':focusable:root:not([floating])'),
+ floating = CQ.query(':focusable:root[floating]');
+
+ // Floating items should go to the top of our root stack, and be ordered
+ // by their z-index (highest first)
+ floating.sort(function(a, b) {
+ return a.el.getZIndex() > b.el.getZIndex();
+ });
+
+ return floating.concat(inline);
+ },
+
+ initDOM: function(options) {
+ var me = this,
+ sp = ' ',
+ cls = me.focusFrameCls;
+
+ if (!Ext.isReady) {
+ Ext.onReady(me.initDOM, me);
+ return;
+ }
+
+ // Create global focus element
+ if (!me.focusEl) {
+ me.focusEl = Ext.getBody().createChild({
+ tabIndex: '-1',
+ cls: me.focusElementCls,
+ html: sp
+ });
+ }
+
+ // Create global focus frame
+ if (!me.focusFrame && options.focusFrame) {
+ me.focusFrame = Ext.getBody().createChild({
+ cls: cls,
+ children: [
+ { cls: cls + '-top' },
+ { cls: cls + '-bottom' },
+ { cls: cls + '-left' },
+ { cls: cls + '-right' }
+ ],
+ style: 'top: -100px; left: -100px;'
+ });
+ me.focusFrame.setVisibilityMode(Ext.Element.DISPLAY);
+ me.focusFrameWidth = 2;
+ me.focusFrame.hide().setLeftTop(0, 0);
+ }
+ },
+
+ isWhitelisted: function(cmp) {
+ return cmp && Ext.Array.some(this.whitelist, function(x) {
+ return cmp.isXType(x);
+ });
+ },
+
+ navigateIn: function(e) {
+ var me = this,
+ focusedCmp = me.focusedCmp,
+ rootCmps,
+ firstChild;
+
+ if (!focusedCmp) {
+ // No focus yet, so focus the first root cmp on the page
+ rootCmps = me.getRootComponents();
+ if (rootCmps.length) {
+ rootCmps[0].focus();
+ }
+ } else {
+ // Drill into child ref items of the focused cmp, if applicable.
+ // This works for any Component with a getRefItems implementation.
+ firstChild = Ext.ComponentQuery.query('>:focusable', focusedCmp)[0];
+ if (firstChild) {
+ firstChild.focus();
+ } else {
+ // Let's try to fire a click event, as if it came from the mouse
+ if (Ext.isFunction(focusedCmp.onClick)) {
+ e.button = 0;
+ focusedCmp.onClick(e);
+ focusedCmp.focus();
+ }
+ }
+ }
+ },
+
+ navigateOut: function(e) {
+ var me = this,
+ parent;
+
+ if (!me.focusedCmp || !(parent = me.focusedCmp.up(':focusable'))) {
+ me.focusEl.focus();
+ } else {
+ parent.focus();
+ }
+
+ // In some browsers (Chrome) FocusManager can handle this before other
+ // handlers. Ext Windows have their own Esc key handling, so we need to
+ // return true here to allow the event to bubble.
+ return true;
+ },
+
+ navigateSiblings: function(e, source, parent) {
+ var me = this,
+ src = source || me,
+ key = e.getKey(),
+ EO = Ext.EventObject,
+ goBack = e.shiftKey || key == EO.LEFT || key == EO.UP,
+ checkWhitelist = key == EO.LEFT || key == EO.RIGHT || key == EO.UP || key == EO.DOWN,
+ nextSelector = goBack ? 'prev' : 'next',
+ idx, next, focusedCmp;
+
+ focusedCmp = (src.focusedCmp && src.focusedCmp.comp) || src.focusedCmp;
+ if (!focusedCmp && !parent) {
+ return;
+ }
+
+ if (checkWhitelist && me.isWhitelisted(focusedCmp)) {
+ return true;
+ }
+
+ parent = parent || focusedCmp.up();
+ if (parent) {
+ idx = focusedCmp ? Ext.Array.indexOf(parent.getRefItems(), focusedCmp) : -1;
+ next = Ext.ComponentQuery.query('>:' + nextSelector + 'Focus(' + idx + ')', parent)[0];
+ if (next && focusedCmp !== next) {
+ next.focus();
+ return next;
+ }
+ }
+ },
+
+ onComponentBlur: function(cmp, e) {
+ var me = this;
+
+ if (me.focusedCmp === cmp) {
+ me.previousFocusedCmp = cmp;
+ delete me.focusedCmp;
+ }
+
+ if (me.focusFrame) {
+ me.focusFrame.hide();
+ }
+ },
+
+ onComponentCreated: function(hash, id, cmp) {
+ this.setFocus(cmp, true, this.options);
+ },
+
+ onComponentDestroy: function(cmp) {
+ this.setFocus(cmp, false);
+ },
+
+ onComponentFocus: function(cmp, e) {
+ var me = this,
+ chain = me.focusChain;
+
+ if (!Ext.ComponentQuery.is(cmp, ':focusable')) {
+ me.clearComponent(cmp);
+
+ // Check our focus chain, so we don't run into a never ending recursion
+ // If we've attempted (unsuccessfully) to focus this component before,
+ // then we're caught in a loop of child->parent->...->child and we
+ // need to cut the loop off rather than feed into it.
+ if (chain[cmp.id]) {
+ return;
+ }
+
+ // Try to focus the parent instead
+ var parent = cmp.up();
+ if (parent) {
+ // Add component to our focus chain to detect infinite focus loop
+ // before we fire off an attempt to focus our parent.
+ // See the comments above.
+ chain[cmp.id] = true;
+ parent.focus();
+ }
+
+ return;
+ }
+
+ // Clear our focus chain when we have a focusable component
+ me.focusChain = {};
+
+ // Defer focusing for 90ms so components can do a layout/positioning
+ // and give us an ability to buffer focuses
+ clearTimeout(me.cmpFocusDelay);
+ if (arguments.length !== 2) {
+ me.cmpFocusDelay = Ext.defer(me.onComponentFocus, 90, me, [cmp, e]);
+ return;
+ }
+
+ if (me.fireEvent('beforecomponentfocus', me, cmp, me.previousFocusedCmp) === false) {
+ me.clearComponent(cmp);
+ return;
+ }
+
+ me.focusedCmp = cmp;
+
+ // If we have a focus frame, show it around the focused component
+ if (me.shouldShowFocusFrame(cmp)) {
+ var cls = '.' + me.focusFrameCls + '-',
+ ff = me.focusFrame,
+ fw = me.focusFrameWidth,
+ box = cmp.el.getPageBox(),
+
+ // Size the focus frame's t/b/l/r according to the box
+ // This leaves a hole in the middle of the frame so user
+ // interaction w/ the mouse can continue
+ bt = box.top,
+ bl = box.left,
+ bw = box.width,
+ bh = box.height,
+ ft = ff.child(cls + 'top'),
+ fb = ff.child(cls + 'bottom'),
+ fl = ff.child(cls + 'left'),
+ fr = ff.child(cls + 'right');
+
+ ft.setWidth(bw).setLeftTop(bl, bt);
+ fb.setWidth(bw).setLeftTop(bl, bt + bh - fw);
+ fl.setHeight(bh - fw - fw).setLeftTop(bl, bt + fw);
+ fr.setHeight(bh - fw - fw).setLeftTop(bl + bw - fw, bt + fw);
+
+ ff.show();
+ }
+
+ me.fireEvent('componentfocus', me, cmp, me.previousFocusedCmp);
+ },
+
+ onComponentHide: function(cmp) {
+ var me = this,
+ CQ = Ext.ComponentQuery,
+ cmpHadFocus = false,
+ focusedCmp,
+ parent;
+
+ if (me.focusedCmp) {
+ focusedCmp = CQ.query('[id=' + me.focusedCmp.id + ']', cmp)[0];
+ cmpHadFocus = me.focusedCmp.id === cmp.id || focusedCmp;
+
+ if (focusedCmp) {
+ me.clearComponent(focusedCmp);
+ }
+ }
+
+ me.clearComponent(cmp);
+
+ if (cmpHadFocus) {
+ parent = CQ.query('^:focusable', cmp)[0];
+ if (parent) {
+ parent.focus();
+ }
+ }
+ },
+
+ removeDOM: function() {
+ var me = this;
+
+ // If we are still enabled globally, or there are still subscribers
+ // then we will halt here, since our DOM stuff is still being used
+ if (me.enabled || me.subscribers.length) {
+ return;
+ }
+
+ Ext.destroy(
+ me.focusEl,
+ me.focusFrame
+ );
+ delete me.focusEl;
+ delete me.focusFrame;
+ delete me.focusFrameWidth;
+ },
+
+ /**
+ * Removes the specified xtype from the {@link #whitelist}.
+ * @param {String/String[]} xtype Removes the xtype(s) from the {@link #whitelist}.
+ */
+ removeXTypeFromWhitelist: function(xtype) {
+ var me = this;
+
+ if (Ext.isArray(xtype)) {
+ Ext.Array.forEach(xtype, me.removeXTypeFromWhitelist, me);
+ return;
+ }
+
+ Ext.Array.remove(me.whitelist, xtype);
+ },
+
+ setFocus: function(cmp, focusable, options) {
+ var me = this,
+ el, dom, data,
+
+ needsTabIndex = function(n) {
+ return !Ext.Array.contains(me.tabIndexWhitelist, n.tagName.toLowerCase())
+ && n.tabIndex <= 0;
+ };
+
+ options = options || {};
+
+ // Come back and do this after the component is rendered
+ if (!cmp.rendered) {
+ cmp.on('afterrender', Ext.pass(me.setFocus, arguments, me), me, { single: true });
+ return;
+ }
+
+ el = cmp.getFocusEl();
+ dom = el.dom;
+
+ // Decorate the component's focus el for focus-ability
+ if ((focusable && !me.focusData[cmp.id]) || (!focusable && me.focusData[cmp.id])) {
+ if (focusable) {
+ data = {
+ focusFrame: options.focusFrame
+ };
+
+ // Only set -1 tabIndex if we need it
+ // inputs, buttons, and anchor tags do not need it,
+ // and neither does any DOM that has it set already
+ // programmatically or in markup.
+ if (needsTabIndex(dom)) {
+ data.tabIndex = dom.tabIndex;
+ dom.tabIndex = -1;
+ }
+
+ el.on({
+ focus: data.focusFn = Ext.bind(me.onComponentFocus, me, [cmp], 0),
+ blur: data.blurFn = Ext.bind(me.onComponentBlur, me, [cmp], 0),
+ scope: me
+ });
+ cmp.on({
+ hide: me.onComponentHide,
+ close: me.onComponentHide,
+ beforedestroy: me.onComponentDestroy,
+ scope: me
+ });
+
+ me.focusData[cmp.id] = data;
+ } else {
+ data = me.focusData[cmp.id];
+ if ('tabIndex' in data) {
+ dom.tabIndex = data.tabIndex;
+ }
+ el.un('focus', data.focusFn, me);
+ el.un('blur', data.blurFn, me);
+ cmp.un('hide', me.onComponentHide, me);
+ cmp.un('close', me.onComponentHide, me);
+ cmp.un('beforedestroy', me.onComponentDestroy, me);
+
+ delete me.focusData[cmp.id];
+ }
+ }
+ },
+
+ setFocusAll: function(focusable, options) {
+ var me = this,
+ cmps = Ext.ComponentManager.all.getArray(),
+ len = cmps.length,
+ cmp,
+ i = 0;
+
+ for (; i < len; i++) {
+ me.setFocus(cmps[i], focusable, options);
+ }
+ },
+
+ setupSubscriberKeys: function(container, keys) {
+ var me = this,
+ el = container.getFocusEl(),
+ scope = keys.scope,
+ handlers = {
+ backspace: me.focusLast,
+ enter: me.navigateIn,
+ esc: me.navigateOut,
+ scope: me
+ },
+
+ navSiblings = function(e) {
+ if (me.focusedCmp === container) {
+ // Root the sibling navigation to this container, so that we
+ // can automatically dive into the container, rather than forcing
+ // the user to hit the enter key to dive in.
+ return me.navigateSiblings(e, me, container);
+ } else {
+ return me.navigateSiblings(e);
+ }
+ };
+
+ Ext.iterate(keys, function(key, cb) {
+ handlers[key] = function(e) {
+ var ret = navSiblings(e);
+
+ if (Ext.isFunction(cb) && cb.call(scope || container, e, ret) === true) {
+ return true;
+ }
+
+ return ret;
+ };
+ }, me);
+
+ return Ext.create('Ext.util.KeyNav', el, handlers);
+ },
+
+ shouldShowFocusFrame: function(cmp) {
+ var me = this,
+ opts = me.options || {};
+
+ if (!me.focusFrame || !cmp) {
+ return false;
+ }
+
+ // Global trumps
+ if (opts.focusFrame) {
+ return true;
+ }
+
+ if (me.focusData[cmp.id].focusFrame) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Subscribes an {@link Ext.container.Container} to provide basic keyboard focus navigation between its child {@link Ext.Component}'s.
+ * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} on which to enable keyboard functionality and focus management.
+ * @param {Boolean/Object} options An object of the following options
+ * @param {Array/Object} options.keys
+ * An array containing the string names of navigation keys to be supported. The allowed values are:
+ *
+ * - 'left'
+ * - 'right'
+ * - 'up'
+ * - 'down'
+ *
+ * Or, an object containing those key names as keys with `true` or a callback function as their value. A scope may also be passed. E.g.:
+ *
+ * {
+ * left: this.onLeftKey,
+ * right: this.onRightKey,
+ * scope: this
+ * }
+ *
+ * @param {Boolean} options.focusFrame
+ * `true` to show the focus frame around a component when it is focused. Defaults to `false`.
+ */
+ subscribe: function(container, options) {
+ var me = this,
+ EA = Ext.Array,
+ data = {},
+ subs = me.subscribers,
+
+ // Recursively add focus ability as long as a descendent container isn't
+ // itself subscribed to the FocusManager, or else we'd have unwanted side
+ // effects for subscribing a descendent container twice.
+ safeSetFocus = function(cmp) {
+ if (cmp.isContainer && !subs.containsKey(cmp.id)) {
+ EA.forEach(cmp.query('>'), safeSetFocus);
+ me.setFocus(cmp, true, options);
+ cmp.on('add', data.onAdd, me);
+ } else if (!cmp.isContainer) {
+ me.setFocus(cmp, true, options);
+ }
+ };
+
+ // We only accept containers
+ if (!container || !container.isContainer) {
+ return;
+ }
+
+ if (!container.rendered) {
+ container.on('afterrender', Ext.pass(me.subscribe, arguments, me), me, { single: true });
+ return;
+ }
+
+ // Init the DOM, incase this is the first time it will be used
+ me.initDOM(options);
+
+ // Create key navigation for subscriber based on keys option
+ data.keyNav = me.setupSubscriberKeys(container, options.keys);
+
+ // We need to keep track of components being added to our subscriber
+ // and any containers nested deeply within it (omg), so let's do that.
+ // Components that are removed are globally handled.
+ // Also keep track of destruction of our container for auto-unsubscribe.
+ data.onAdd = function(ct, cmp, idx) {
+ safeSetFocus(cmp);
+ };
+ container.on('beforedestroy', me.unsubscribe, me);
+
+ // Now we setup focusing abilities for the container and all its components
+ safeSetFocus(container);
+
+ // Add to our subscribers list
+ subs.add(container.id, data);
+ },
+
+ /**
+ * Unsubscribes an {@link Ext.container.Container} from keyboard focus management.
+ * @param {Ext.container.Container} container A reference to the {@link Ext.container.Container} to unsubscribe from the FocusManager.
+ */
+ unsubscribe: function(container) {
+ var me = this,
+ EA = Ext.Array,
+ subs = me.subscribers,
+ data,
+
+ // Recursively remove focus ability as long as a descendent container isn't
+ // itself subscribed to the FocusManager, or else we'd have unwanted side
+ // effects for unsubscribing an ancestor container.
+ safeSetFocus = function(cmp) {
+ if (cmp.isContainer && !subs.containsKey(cmp.id)) {
+ EA.forEach(cmp.query('>'), safeSetFocus);
+ me.setFocus(cmp, false);
+ cmp.un('add', data.onAdd, me);
+ } else if (!cmp.isContainer) {
+ me.setFocus(cmp, false);
+ }
+ };
+
+ if (!container || !subs.containsKey(container.id)) {
+ return;
+ }
+
+ data = subs.get(container.id);
+ data.keyNav.destroy();
+ container.un('beforedestroy', me.unsubscribe, me);
+ subs.removeAtKey(container.id);
+ safeSetFocus(container);
+ me.removeDOM();
+ }
+});
+/**
+ * Basic Toolbar class. Although the {@link Ext.container.Container#defaultType defaultType} for Toolbar is {@link Ext.button.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
+ * Ext.create('Ext.toolbar.Toolbar', {
+ * renderTo: document.body,
+ * width : 500,
+ * items: [
+ * {
+ * // xtype: 'button', // default for Toolbars
+ * text: 'Button'
+ * },
+ * {
+ * xtype: 'splitbutton',
+ * text : 'Split Button'
+ * },
+ * // begin using the right-justified button container
+ * '->', // same as { xtype: 'tbfill' }
+ * {
+ * 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'
+ * ]
+ * });
+ *
+ * Toolbars have {@link #enable} and {@link #disable} methods which when called, will enable/disable all items within your toolbar.
+ *
+ * @example
+ * Ext.create('Ext.toolbar.Toolbar', {
+ * renderTo: document.body,
+ * width : 400,
+ * items: [
+ * {
+ * text: 'Button'
+ * },
+ * {
+ * xtype: 'splitbutton',
+ * text : 'Split Button'
+ * },
+ * '->',
+ * {
+ * xtype : 'textfield',
+ * name : 'field1',
+ * emptyText: 'enter search term'
+ * }
+ * ]
+ * });
+ *
+ * Example
+ *
+ * @example
+ * var enableBtn = Ext.create('Ext.button.Button', {
+ * text : 'Enable All Items',
+ * disabled: true,
+ * scope : this,
+ * handler : function() {
+ * //disable the enable button and enable the disable button
+ * enableBtn.disable();
+ * disableBtn.enable();
+ *
+ * //enable the toolbar
+ * toolbar.enable();
+ * }
+ * });
+ *
+ * var disableBtn = Ext.create('Ext.button.Button', {
+ * text : 'Disable All Items',
+ * scope : this,
+ * handler : function() {
+ * //enable the enable button and disable button
+ * disableBtn.disable();
+ * enableBtn.enable();
+ *
+ * //disable the toolbar
+ * toolbar.disable();
+ * }
+ * });
+ *
+ * var toolbar = Ext.create('Ext.toolbar.Toolbar', {
+ * renderTo: document.body,
+ * width : 400,
+ * margin : '5 0 0 0',
+ * items : [enableBtn, disableBtn]
+ * });
+ *
+ * Adding items to and removing items from a toolbar is as simple as calling the {@link #add} and {@link #remove} methods. There is also a {@link #removeAll} method
+ * which remove all items within the toolbar.
+ *
+ * @example
+ * var toolbar = Ext.create('Ext.toolbar.Toolbar', {
+ * renderTo: document.body,
+ * width : 700,
+ * items: [
+ * {
+ * text: 'Example Button'
+ * }
+ * ]
+ * });
+ *
+ * var addedItems = [];
+ *
+ * Ext.create('Ext.toolbar.Toolbar', {
+ * renderTo: document.body,
+ * width : 700,
+ * margin : '5 0 0 0',
+ * items : [
+ * {
+ * text : 'Add a button',
+ * scope : this,
+ * handler: function() {
+ * var text = prompt('Please enter the text for your button:');
+ * addedItems.push(toolbar.add({
+ * text: text
+ * }));
+ * }
+ * },
+ * {
+ * text : 'Add a text item',
+ * scope : this,
+ * handler: function() {
+ * var text = prompt('Please enter the text for your item:');
+ * addedItems.push(toolbar.add(text));
+ * }
+ * },
+ * {
+ * text : 'Add a toolbar seperator',
+ * scope : this,
+ * handler: function() {
+ * addedItems.push(toolbar.add('-'));
+ * }
+ * },
+ * {
+ * text : 'Add a toolbar spacer',
+ * scope : this,
+ * handler: function() {
+ * addedItems.push(toolbar.add('->'));
+ * }
+ * },
+ * '->',
+ * {
+ * text : 'Remove last inserted item',
+ * scope : this,
+ * handler: function() {
+ * if (addedItems.length) {
+ * toolbar.remove(addedItems.pop());
+ * } else if (toolbar.items.length) {
+ * toolbar.remove(toolbar.items.last());
+ * } else {
+ * alert('No items in the toolbar');
+ * }
+ * }
+ * },
+ * {
+ * text : 'Remove all items',
+ * scope : this,
+ * handler: function() {
+ * toolbar.removeAll();
+ * }
+ * }
+ * ]
+ * });
+ *
+ * @constructor
+ * Creates a new Toolbar
+ * @param {Object/Object[]} config A config object or an array of buttons to <code>{@link #add}</code>
+ * @docauthor Robert Dougan <rob@sencha.com>
+ */
+Ext.define('Ext.toolbar.Toolbar', {
+ extend: 'Ext.container.Container',
+ requires: [
+ 'Ext.toolbar.Fill',
+ 'Ext.layout.container.HBox',
+ 'Ext.layout.container.VBox',
+ 'Ext.FocusManager'
+ ],
+ uses: [
+ 'Ext.toolbar.Separator'
+ ],
+ alias: 'widget.toolbar',
+ alternateClassName: 'Ext.Toolbar',
+
+ isToolbar: true,
+ baseCls : Ext.baseCSSPrefix + 'toolbar',
+ ariaRole : 'toolbar',
+
+ defaultType: 'button',
+
+ /**
+ * @cfg {Boolean} vertical
+ * Set to `true` to make the toolbar vertical. The layout will become a `vbox`.
+ */
+ vertical: false,
+
+ /**
+ * @cfg {String/Object} layout
+ * This class assigns a default layout (`layout: 'hbox'`).
+ * 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.Container#layout} for additional information.
+ */
+
+ /**
+ * @cfg {Boolean} enableOverflow
+ * Configure true to make the toolbar provide a button which activates a dropdown Menu to show
+ * items which overflow the Toolbar's width.
+ */
+ enableOverflow: false,
+
+ /**
+ * @cfg {String} menuTriggerCls
+ * Configure the icon class of the overflow button.
+ */
+ menuTriggerCls: Ext.baseCSSPrefix + 'toolbar-more-icon',
+
+ // private
+ trackMenus: true,
+
+ itemCls: Ext.baseCSSPrefix + 'toolbar-item',
+
+ initComponent: function() {
+ var me = this,
+ keys;
+
+ // check for simplified (old-style) overflow config:
+ if (!me.layout && me.enableOverflow) {
+ me.layout = { overflowHandler: 'Menu' };
+ }
+
+ if (me.dock === 'right' || me.dock === 'left') {
+ me.vertical = true;
+ }
+
+ me.layout = Ext.applyIf(Ext.isString(me.layout) ? {
+ type: me.layout
+ } : me.layout || {}, {
+ type: me.vertical ? 'vbox' : 'hbox',
+ align: me.vertical ? 'stretchmax' : 'middle',
+ clearInnerCtOnLayout: true
+ });
+
+ if (me.vertical) {
+ me.addClsWithUI('vertical');
+ }
+
+ // @TODO: remove this hack and implement a more general solution
+ if (me.ui === 'footer') {
+ me.ignoreBorderManagement = true;
+ }
+
+ me.callParent();
+
+ /**
+ * @event overflowchange
+ * Fires after the overflow state has changed.
+ * @param {Object} c The Container
+ * @param {Boolean} lastOverflow overflow state
+ */
+ me.addEvents('overflowchange');
+
+ // Subscribe to Ext.FocusManager for key navigation
+ keys = me.vertical ? ['up', 'down'] : ['left', 'right'];
+ Ext.FocusManager.subscribe(me, {
+ keys: keys
+ });
+ },
+
+ getRefItems: function(deep) {
+ var me = this,
+ items = me.callParent(arguments),
+ layout = me.layout,
+ handler;
+
+ if (deep && me.enableOverflow) {
+ handler = layout.overflowHandler;
+ if (handler && handler.menu) {
+ items = items.concat(handler.menu.getRefItems(deep));
+ }
+ }
+ return items;
+ },
+
+ /**
+ * 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.Container#add}.
+ *
+ * @param {Object...} args The following types of arguments are all valid:
+ * - `{@link Ext.button.Button config}`: A valid button config object
+ * - `HtmlElement`: Any standard HTML element
+ * - `Field`: Any form field
+ * - `Item`: Any subclass of {@link Ext.toolbar.Item}
+ * - `String`: Any generic string (gets wrapped in a {@link Ext.toolbar.TextItem}).
+ * Note that there are a few special strings that are treated differently as explained next.
+ * - `'-'`: Creates a separator element
+ * - `' '`: Creates a spacer element
+ * - `'->'`: Creates a fill element
+ *
+ * @method add
+ */
+
+ // private
+ lookupComponent: function(c) {
+ if (Ext.isString(c)) {
+ var shortcut = Ext.toolbar.Toolbar.shortcuts[c];
+ if (shortcut) {
+ c = {
+ xtype: shortcut
+ };
+ } else {
+ c = {
+ xtype: 'tbtext',
+ text: c
+ };
+ }
+ this.applyDefaults(c);
+ }
+ return this.callParent(arguments);
+ },
+
+ // private
+ applyDefaults: function(c) {
+ if (!Ext.isString(c)) {
+ c = this.callParent(arguments);
+ var d = this.internalDefaults;
+ if (c.events) {
+ Ext.applyIf(c.initialConfig, d);
+ Ext.apply(c, d);
+ } else {
+ Ext.applyIf(c, d);
+ }
+ }
+ return c;
+ },
+
+ // private
+ trackMenu: function(item, remove) {
+ if (this.trackMenus && item.menu) {
+ var method = remove ? 'mun' : 'mon',
+ me = this;
+
+ me[method](item, 'mouseover', me.onButtonOver, me);
+ me[method](item, 'menushow', me.onButtonMenuShow, me);
+ me[method](item, 'menuhide', me.onButtonMenuHide, me);
+ }
+ },
+
+ // private
+ constructButton: function(item) {
+ return item.events ? item : this.createComponent(item, item.split ? 'splitbutton' : this.defaultType);
+ },
+
+ // private
+ onBeforeAdd: function(component) {
+ if (component.is('field') || (component.is('button') && this.ui != 'footer')) {
+ component.ui = component.ui + '-toolbar';
+ }
+
+ // Any separators needs to know if is vertical or not
+ if (component instanceof Ext.toolbar.Separator) {
+ component.setUI((this.vertical) ? 'vertical' : 'horizontal');
+ }
+
+ this.callParent(arguments);
+ },
+
+ // private
+ onAdd: function(component) {
+ this.callParent(arguments);
+
+ this.trackMenu(component);
+ if (this.disabled) {
+ component.disable();
+ }
+ },
+
+ // private
+ onRemove: function(c) {
+ this.callParent(arguments);
+ this.trackMenu(c, true);
+ },
+
+ // private
+ onButtonOver: 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;
+ }
+}, function() {
+ this.shortcuts = {
+ '-' : 'tbseparator',
+ ' ' : 'tbspacer',
+ '->': 'tbfill'
+ };
+});
+/**
+ * @class Ext.panel.AbstractPanel
+ * @extends Ext.container.Container
+ * A base class which provides methods common to Panel classes across the Sencha product range.
+ * @private
+ */
+Ext.define('Ext.panel.AbstractPanel', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.container.Container',
+
+ requires: ['Ext.util.MixedCollection', 'Ext.Element', 'Ext.toolbar.Toolbar'],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String} [baseCls='x-panel']
+ * The base CSS class to apply to this panel's element.
+ */
+ baseCls : Ext.baseCSSPrefix + 'panel',
+
+ /**
+ * @cfg {Number/String} bodyPadding
+ * A shortcut for setting a padding style on the body element. The value can either be
+ * a number to be applied to all sides, or a normal css string describing padding.
+ */
+
+ /**
+ * @cfg {Boolean} bodyBorder
+ * A shortcut to add or remove the border on the body of a panel. This only applies to a panel
+ * which has the {@link #frame} configuration set to `true`.
+ */
+
+ /**
+ * @cfg {String/Object/Function} bodyStyle
+ * Custom CSS styles to be applied to the panel's body element, which can be supplied as a valid CSS style string,
+ * an object containing style property name/value pairs or a function that returns such a string or object.
+ * For example, these two formats are interpreted to be equivalent:<pre><code>
+bodyStyle: 'background:#ffc; padding:10px;'
+
+bodyStyle: {
+ background: '#ffc',
+ padding: '10px'
+}
+ * </code></pre>
+ */
+
+ /**
+ * @cfg {String/String[]} bodyCls
+ * A CSS class, space-delimited string of classes, or array of classes to be applied to the panel's body element.
+ * The following examples are all valid:<pre><code>
+bodyCls: 'foo'
+bodyCls: 'foo bar'
+bodyCls: ['foo', 'bar']
+ * </code></pre>
+ */
+
+ isPanel: true,
+
+ componentLayout: 'dock',
+
+ /**
+ * @cfg {Object} defaultDockWeights
+ * This object holds the default weights applied to dockedItems that have no weight. These start with a
+ * weight of 1, to allow negative weights to insert before top items and are odd numbers
+ * so that even weights can be used to get between different dock orders.
+ *
+ * To make default docking order match border layout, do this:
+ * <pre><code>
+Ext.panel.AbstractPanel.prototype.defaultDockWeights = { top: 1, bottom: 3, left: 5, right: 7 };</code></pre>
+ * Changing these defaults as above or individually on this object will effect all Panels.
+ * To change the defaults on a single panel, you should replace the entire object:
+ * <pre><code>
+initComponent: function () {
+ // NOTE: Don't change members of defaultDockWeights since the object is shared.
+ this.defaultDockWeights = { top: 1, bottom: 3, left: 5, right: 7 };
+
+ this.callParent();
+}</code></pre>
+ *
+ * To change only one of the default values, you do this:
+ * <pre><code>
+initComponent: function () {
+ // NOTE: Don't change members of defaultDockWeights since the object is shared.
+ this.defaultDockWeights = Ext.applyIf({ top: 10 }, this.defaultDockWeights);
+
+ this.callParent();
+}</code></pre>
+ */
+ defaultDockWeights: { top: 1, left: 3, right: 5, bottom: 7 },
+
+ renderTpl: [
+ '<div id="{id}-body" class="{baseCls}-body<tpl if="bodyCls"> {bodyCls}</tpl>',
+ ' {baseCls}-body-{ui}<tpl if="uiCls">',
+ '<tpl for="uiCls"> {parent.baseCls}-body-{parent.ui}-{.}</tpl>',
+ '</tpl>"<tpl if="bodyStyle"> style="{bodyStyle}"</tpl>>',
+ '</div>'
+ ],
+
+ // TODO: Move code examples into product-specific files. The code snippet below is Touch only.
+ /**
+ * @cfg {Object/Object[]} dockedItems
+ * A component or series of components to be added as docked items to this panel.
+ * The docked items can be docked to either the top, right, left or bottom of a panel.
+ * This is typically used for things like toolbars or tab bars:
+ * <pre><code>
+var panel = new Ext.panel.Panel({
+ fullscreen: true,
+ dockedItems: [{
+ xtype: 'toolbar',
+ dock: 'top',
+ items: [{
+ text: 'Docked to the top'
+ }]
+ }]
+});</code></pre>
+ */
+
+ border: true,
+
+ initComponent : function() {
+ var me = this;
+
+ me.addEvents(
+ /**
+ * @event bodyresize
+ * Fires after the Panel has been resized.
+ * @param {Ext.panel.Panel} p the Panel which has been resized.
+ * @param {Number} width The Panel body's new width.
+ * @param {Number} height The Panel body's new height.
+ */
+ 'bodyresize'
+ // // inherited
+ // 'activate',
+ // // inherited
+ // 'deactivate'
+ );
+
+ me.addChildEls('body');
+
+ //!frame
+ //!border
+
+ if (me.frame && me.border && me.bodyBorder === undefined) {
+ me.bodyBorder = false;
+ }
+ if (me.frame && me.border && (me.bodyBorder === false || me.bodyBorder === 0)) {
+ me.manageBodyBorders = true;
+ }
+
+ me.callParent();
+ },
+
+ // @private
+ initItems : function() {
+ var me = this,
+ items = me.dockedItems;
+
+ me.callParent();
+ me.dockedItems = Ext.create('Ext.util.MixedCollection', false, me.getComponentId);
+ if (items) {
+ me.addDocked(items);
+ }
+ },
+
+ /**
+ * Finds a docked component by id, itemId or position. Also see {@link #getDockedItems}
+ * @param {String/Number} comp The id, itemId or position of the docked component (see {@link #getComponent} for details)
+ * @return {Ext.Component} The docked component (if found)
+ */
+ getDockedComponent: function(comp) {
+ if (Ext.isObject(comp)) {
+ comp = comp.getItemId();
+ }
+ return this.dockedItems.get(comp);
+ },
+
+ /**
+ * Attempts a default component lookup (see {@link Ext.container.Container#getComponent}). If the component is not found in the normal
+ * items, the dockedItems are searched and the matched component (if any) returned (see {@link #getDockedComponent}). Note that docked
+ * items will only be matched by component id or itemId -- if you pass a numeric index only non-docked child components will be searched.
+ * @param {String/Number} comp The component id, itemId or position to find
+ * @return {Ext.Component} The component (if found)
+ */
+ getComponent: function(comp) {
+ var component = this.callParent(arguments);
+ if (component === undefined && !Ext.isNumber(comp)) {
+ // If the arg is a numeric index skip docked items
+ component = this.getDockedComponent(comp);
+ }
+ return component;
+ },
+
+ /**
+ * Parses the {@link bodyStyle} config if available to create a style string that will be applied to the body element.
+ * This also includes {@link bodyPadding} and {@link bodyBorder} if available.
+ * @return {String} A CSS style string with body styles, padding and border.
+ * @private
+ */
+ initBodyStyles: function() {
+ var me = this,
+ bodyStyle = me.bodyStyle,
+ styles = [],
+ Element = Ext.Element,
+ prop;
+
+ if (Ext.isFunction(bodyStyle)) {
+ bodyStyle = bodyStyle();
+ }
+ if (Ext.isString(bodyStyle)) {
+ styles = bodyStyle.split(';');
+ } else {
+ for (prop in bodyStyle) {
+ if (bodyStyle.hasOwnProperty(prop)) {
+ styles.push(prop + ':' + bodyStyle[prop]);
+ }
+ }
+ }
+
+ if (me.bodyPadding !== undefined) {
+ styles.push('padding: ' + Element.unitizeBox((me.bodyPadding === true) ? 5 : me.bodyPadding));
+ }
+ if (me.frame && me.bodyBorder) {
+ if (!Ext.isNumber(me.bodyBorder)) {
+ me.bodyBorder = 1;
+ }
+ styles.push('border-width: ' + Element.unitizeBox(me.bodyBorder));
+ }
+ delete me.bodyStyle;
+ return styles.length ? styles.join(';') : undefined;
+ },
+
+ /**
+ * Parse the {@link bodyCls} config if available to create a comma-delimited string of
+ * CSS classes to be applied to the body element.
+ * @return {String} The CSS class(es)
+ * @private
+ */
+ initBodyCls: function() {
+ var me = this,
+ cls = '',
+ bodyCls = me.bodyCls;
+
+ if (bodyCls) {
+ Ext.each(bodyCls, function(v) {
+ cls += " " + v;
+ });
+ delete me.bodyCls;
+ }
+ return cls.length > 0 ? cls : undefined;
+ },
+
+ /**
+ * Initialized the renderData to be used when rendering the renderTpl.
+ * @return {Object} Object with keys and values that are going to be applied to the renderTpl
+ * @private
+ */
+ initRenderData: function() {
+ return Ext.applyIf(this.callParent(), {
+ bodyStyle: this.initBodyStyles(),
+ bodyCls: this.initBodyCls()
+ });
+ },
+
+ /**
+ * Adds docked item(s) to the panel.
+ * @param {Object/Object[]} component The Component or array of components to add. The components
+ * must include a 'dock' parameter on each component to indicate where it should be docked ('top', 'right',
+ * 'bottom', 'left').
+ * @param {Number} pos (optional) The index at which the Component will be added
+ */
+ addDocked : function(items, pos) {
+ var me = this,
+ i = 0,
+ item, length;
+
+ items = me.prepareItems(items);
+ length = items.length;
+
+ for (; i < length; i++) {
+ item = items[i];
+ item.dock = item.dock || 'top';
+
+ // Allow older browsers to target docked items to style without borders
+ if (me.border === false) {
+ // item.cls = item.cls || '' + ' ' + me.baseCls + '-noborder-docked-' + item.dock;
+ }
+
+ if (pos !== undefined) {
+ me.dockedItems.insert(pos + i, item);
+ }
+ else {
+ me.dockedItems.add(item);
+ }
+ item.onAdded(me, i);
+ me.onDockedAdd(item);
+ }
+
+ // Set flag which means that beforeLayout will not veto the layout due to the size not changing
+ me.componentLayout.childrenChanged = true;
+ if (me.rendered && !me.suspendLayout) {
+ me.doComponentLayout();
+ }
+ return items;
+ },
+
+ // Placeholder empty functions
+ onDockedAdd : Ext.emptyFn,
+ onDockedRemove : Ext.emptyFn,
+
+ /**
+ * Inserts docked item(s) to the panel at the indicated position.
+ * @param {Number} pos The index at which the Component will be inserted
+ * @param {Object/Object[]} component. The Component or array of components to add. The components
+ * must include a 'dock' paramater on each component to indicate where it should be docked ('top', 'right',
+ * 'bottom', 'left').
+ */
+ insertDocked : function(pos, items) {
+ this.addDocked(items, pos);
+ },
+
+ /**
+ * Removes the docked item from the panel.
+ * @param {Ext.Component} item. The Component to remove.
+ * @param {Boolean} autoDestroy (optional) Destroy the component after removal.
+ */
+ removeDocked : function(item, autoDestroy) {
+ var me = this,
+ layout,
+ hasLayout;
+
+ if (!me.dockedItems.contains(item)) {
+ return item;
+ }
+
+ layout = me.componentLayout;
+ hasLayout = layout && me.rendered;
+
+ if (hasLayout) {
+ layout.onRemove(item);
+ }
+
+ me.dockedItems.remove(item);
+ item.onRemoved();
+ me.onDockedRemove(item);
+
+ if (autoDestroy === true || (autoDestroy !== false && me.autoDestroy)) {
+ item.destroy();
+ } else if (hasLayout) {
+ // not destroying, make any layout related removals
+ layout.afterRemove(item);
+ }
+
+
+ // Set flag which means that beforeLayout will not veto the layout due to the size not changing
+ me.componentLayout.childrenChanged = true;
+ if (!me.destroying && !me.suspendLayout) {
+ me.doComponentLayout();
+ }
+
+ return item;
+ },
+
+ /**
+ * Retrieve an array of all currently docked Components.
+ * @param {String} cqSelector A {@link Ext.ComponentQuery ComponentQuery} selector string to filter the returned items.
+ * @return {Ext.Component[]} An array of components.
+ */
+ getDockedItems : function(cqSelector) {
+ var me = this,
+ defaultWeight = me.defaultDockWeights,
+ dockedItems;
+
+ if (me.dockedItems && me.dockedItems.items.length) {
+ // Allow filtering of returned docked items by CQ selector.
+ if (cqSelector) {
+ dockedItems = Ext.ComponentQuery.query(cqSelector, me.dockedItems.items);
+ } else {
+ dockedItems = me.dockedItems.items.slice();
+ }
+
+ Ext.Array.sort(dockedItems, function(a, b) {
+ // Docked items are ordered by their visual representation by default (t,l,r,b)
+ var aw = a.weight || defaultWeight[a.dock],
+ bw = b.weight || defaultWeight[b.dock];
+ if (Ext.isNumber(aw) && Ext.isNumber(bw)) {
+ return aw - bw;
+ }
+ return 0;
+ });
+
+ return dockedItems;
+ }
+ return [];
+ },
+
+ // inherit docs
+ addUIClsToElement: function(cls, force) {
+ var me = this,
+ result = me.callParent(arguments),
+ classes = [Ext.baseCSSPrefix + cls, me.baseCls + '-body-' + cls, me.baseCls + '-body-' + me.ui + '-' + cls],
+ array, i;
+
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.addCls(me.bodyCls);
+ } else {
+ me.body.addCls(classes);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ for (i = 0; i < classes.length; i++) {
+ if (!Ext.Array.contains(array, classes[i])) {
+ array.push(classes[i]);
+ }
+ }
+
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = classes.join(' ');
+ }
+ }
+
+ return result;
+ },
+
+ // inherit docs
+ removeUIClsFromElement: function(cls, force) {
+ var me = this,
+ result = me.callParent(arguments),
+ classes = [Ext.baseCSSPrefix + cls, me.baseCls + '-body-' + cls, me.baseCls + '-body-' + me.ui + '-' + cls],
+ array, i;
+
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.removeCls(me.bodyCls);
+ } else {
+ me.body.removeCls(classes);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ for (i = 0; i < classes.length; i++) {
+ Ext.Array.remove(array, classes[i]);
+ }
+
+ me.bodyCls = array.join(' ');
+ }
+ }
+
+ return result;
+ },
+
+ // inherit docs
+ addUIToElement: function(force) {
+ var me = this,
+ cls = me.baseCls + '-body-' + me.ui,
+ array;
+
+ me.callParent(arguments);
+
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.addCls(me.bodyCls);
+ } else {
+ me.body.addCls(cls);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ if (!Ext.Array.contains(array, cls)) {
+ array.push(cls);
+ }
+
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = cls;
+ }
+ }
+ },
+
+ // inherit docs
+ removeUIFromElement: function() {
+ var me = this,
+ cls = me.baseCls + '-body-' + me.ui,
+ array;
+
+ me.callParent(arguments);
+
+ if (me.rendered) {
+ if (me.bodyCls) {
+ me.body.removeCls(me.bodyCls);
+ } else {
+ me.body.removeCls(cls);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+ Ext.Array.remove(array, cls);
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = cls;
+ }
+ }
+ },
+
+ // @private
+ getTargetEl : function() {
+ return this.body;
+ },
+
+ getRefItems: function(deep) {
+ var items = this.callParent(arguments),
+ // deep fetches all docked items, and their descendants using '*' selector and then '* *'
+ dockedItems = this.getDockedItems(deep ? '*,* *' : undefined),
+ ln = dockedItems.length,
+ i = 0,
+ item;
+
+ // Find the index where we go from top/left docked items to right/bottom docked items
+ for (; i < ln; i++) {
+ item = dockedItems[i];
+ if (item.dock === 'right' || item.dock === 'bottom') {
+ break;
+ }
+ }
+
+ // Return docked items in the top/left position before our container items, and
+ // return right/bottom positioned items after our container items.
+ // See AbstractDock.renderItems() for more information.
+ return Ext.Array.splice(dockedItems, 0, i).concat(items).concat(dockedItems);
+ },
+
+ beforeDestroy: function(){
+ var docked = this.dockedItems,
+ c;
+
+ if (docked) {
+ while ((c = docked.first())) {
+ this.removeDocked(c, true);
+ }
+ }
+ this.callParent();
+ },
+
+ setBorder: function(border) {
+ var me = this;
+ me.border = (border !== undefined) ? border : true;
+ if (me.rendered) {
+ me.doComponentLayout();
+ }
+ }
+});
+/**
+ * @class Ext.panel.Header
+ * @extends Ext.container.Container
+ * Simple header class which is used for on {@link Ext.panel.Panel} and {@link Ext.window.Window}
+ */
+Ext.define('Ext.panel.Header', {
+ extend: 'Ext.container.Container',
+ uses: ['Ext.panel.Tool', 'Ext.draw.Component', 'Ext.util.CSS'],
+ alias: 'widget.header',
+
+ isHeader : true,
+ defaultType : 'tool',
+ indicateDrag : false,
+ weight : -1,
+
+ renderTpl: [
+ '<div id="{id}-body" class="{baseCls}-body<tpl if="bodyCls"> {bodyCls}</tpl>',
+ '<tpl if="uiCls">',
+ '<tpl for="uiCls"> {parent.baseCls}-body-{parent.ui}-{.}</tpl>',
+ '</tpl>"',
+ '<tpl if="bodyStyle"> style="{bodyStyle}"</tpl>></div>'],
+
+ /**
+ * @cfg {String} title
+ * The title text to display
+ */
+
+ /**
+ * @cfg {String} iconCls
+ * CSS class for icon in header. Used for displaying an icon to the left of a title.
+ */
+
+ initComponent: function() {
+ var me = this,
+ ruleStyle,
+ rule,
+ style,
+ titleTextEl,
+ ui;
+
+ me.indicateDragCls = me.baseCls + '-draggable';
+ me.title = me.title || ' ';
+ me.tools = me.tools || [];
+ me.items = me.items || [];
+ me.orientation = me.orientation || 'horizontal';
+ me.dock = (me.dock) ? me.dock : (me.orientation == 'horizontal') ? 'top' : 'left';
+
+ //add the dock as a ui
+ //this is so we support top/right/left/bottom headers
+ me.addClsWithUI(me.orientation);
+ me.addClsWithUI(me.dock);
+
+ me.addChildEls('body');
+
+ // Add Icon
+ if (!Ext.isEmpty(me.iconCls)) {
+ me.initIconCmp();
+ me.items.push(me.iconCmp);
+ }
+
+ // Add Title
+ if (me.orientation == 'vertical') {
+ // Hack for IE6/7's inability to display an inline-block
+ if (Ext.isIE6 || Ext.isIE7) {
+ me.width = this.width || 24;
+ } else if (Ext.isIEQuirks) {
+ me.width = this.width || 25;
+ }
+
+ me.layout = {
+ type : 'vbox',
+ align: 'center',
+ clearInnerCtOnLayout: true,
+ bindToOwnerCtContainer: false
+ };
+ me.textConfig = {
+ cls: me.baseCls + '-text',
+ type: 'text',
+ text: me.title,
+ rotate: {
+ degrees: 90
+ }
+ };
+ ui = me.ui;
+ if (Ext.isArray(ui)) {
+ ui = ui[0];
+ }
+ ruleStyle = '.' + me.baseCls + '-text-' + ui;
+ if (Ext.scopeResetCSS) {
+ ruleStyle = '.' + Ext.baseCSSPrefix + 'reset ' + ruleStyle;
+ }
+ rule = Ext.util.CSS.getRule(ruleStyle);
+ if (rule) {
+ style = rule.style;
+ }
+ if (style) {
+ Ext.apply(me.textConfig, {
+ 'font-family': style.fontFamily,
+ 'font-weight': style.fontWeight,
+ 'font-size': style.fontSize,
+ fill: style.color
+ });
+ }
+ me.titleCmp = Ext.create('Ext.draw.Component', {
+ ariaRole : 'heading',
+ focusable: false,
+ viewBox: false,
+ flex : 1,
+ autoSize: true,
+ margins: '5 0 0 0',
+ items: [ me.textConfig ],
+ // this is a bit of a cheat: we are not selecting an element of titleCmp
+ // but rather of titleCmp.items[0] (so we cannot use childEls)
+ renderSelectors: {
+ textEl: '.' + me.baseCls + '-text'
+ }
+ });
+ } else {
+ me.layout = {
+ type : 'hbox',
+ align: 'middle',
+ clearInnerCtOnLayout: true,
+ bindToOwnerCtContainer: false
+ };
+ me.titleCmp = Ext.create('Ext.Component', {
+ xtype : 'component',
+ ariaRole : 'heading',
+ focusable: false,
+ flex : 1,
+ cls: me.baseCls + '-text-container',
+ renderTpl : [
+ '<span id="{id}-textEl" class="{cls}-text {cls}-text-{ui}">{title}</span>'
+ ],
+ renderData: {
+ title: me.title,
+ cls : me.baseCls,
+ ui : me.ui
+ },
+ childEls: ['textEl']
+ });
+ }
+ me.items.push(me.titleCmp);
+
+ // Add Tools
+ me.items = me.items.concat(me.tools);
+ this.callParent();
+ },
+
+ initIconCmp: function() {
+ this.iconCmp = Ext.create('Ext.Component', {
+ focusable: false,
+ renderTpl : [
+ '<img id="{id}-iconEl" alt="" src="{blank}" class="{cls}-icon {iconCls}"/>'
+ ],
+ renderData: {
+ blank : Ext.BLANK_IMAGE_URL,
+ cls : this.baseCls,
+ iconCls: this.iconCls,
+ orientation: this.orientation
+ },
+ childEls: ['iconEl'],
+ iconCls: this.iconCls
+ });
+ },
+
+ afterRender: function() {
+ var me = this;
+
+ me.el.unselectable();
+ if (me.indicateDrag) {
+ me.el.addCls(me.indicateDragCls);
+ }
+ me.mon(me.el, {
+ click: me.onClick,
+ scope: me
+ });
+ me.callParent();
+ },
+
+ afterLayout: function() {
+ var me = this;
+ me.callParent(arguments);
+
+ // IE7 needs a forced repaint to make the top framing div expand to full width
+ if (Ext.isIE7) {
+ me.el.repaint();
+ }
+ },
+
+ // inherit docs
+ addUIClsToElement: function(cls, force) {
+ var me = this,
+ result = me.callParent(arguments),
+ classes = [me.baseCls + '-body-' + cls, me.baseCls + '-body-' + me.ui + '-' + cls],
+ array, i;
+
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.addCls(me.bodyCls);
+ } else {
+ me.body.addCls(classes);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ for (i = 0; i < classes.length; i++) {
+ if (!Ext.Array.contains(array, classes[i])) {
+ array.push(classes[i]);
+ }
+ }
+
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = classes.join(' ');
+ }
+ }
+
+ return result;
+ },
+
+ // inherit docs
+ removeUIClsFromElement: function(cls, force) {
+ var me = this,
+ result = me.callParent(arguments),
+ classes = [me.baseCls + '-body-' + cls, me.baseCls + '-body-' + me.ui + '-' + cls],
+ array, i;
+
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.removeCls(me.bodyCls);
+ } else {
+ me.body.removeCls(classes);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ for (i = 0; i < classes.length; i++) {
+ Ext.Array.remove(array, classes[i]);
+ }
+
+ me.bodyCls = array.join(' ');
+ }
+ }
+
+ return result;
+ },
+
+ // inherit docs
+ addUIToElement: function(force) {
+ var me = this,
+ array, cls;
+
+ me.callParent(arguments);
+
+ cls = me.baseCls + '-body-' + me.ui;
+ if (!force && me.rendered) {
+ if (me.bodyCls) {
+ me.body.addCls(me.bodyCls);
+ } else {
+ me.body.addCls(cls);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+
+ if (!Ext.Array.contains(array, cls)) {
+ array.push(cls);
+ }
+
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = cls;
+ }
+ }
+
+ if (!force && me.titleCmp && me.titleCmp.rendered && me.titleCmp.textEl) {
+ me.titleCmp.textEl.addCls(me.baseCls + '-text-' + me.ui);
+ }
+ },
+
+ // inherit docs
+ removeUIFromElement: function() {
+ var me = this,
+ array, cls;
+
+ me.callParent(arguments);
+
+ cls = me.baseCls + '-body-' + me.ui;
+ if (me.rendered) {
+ if (me.bodyCls) {
+ me.body.removeCls(me.bodyCls);
+ } else {
+ me.body.removeCls(cls);
+ }
+ } else {
+ if (me.bodyCls) {
+ array = me.bodyCls.split(' ');
+ Ext.Array.remove(array, cls);
+ me.bodyCls = array.join(' ');
+ } else {
+ me.bodyCls = cls;
+ }
+ }
+
+ if (me.titleCmp && me.titleCmp.rendered && me.titleCmp.textEl) {
+ me.titleCmp.textEl.removeCls(me.baseCls + '-text-' + me.ui);
+ }
+ },
+
+ onClick: function(e) {
+ if (!e.getTarget(Ext.baseCSSPrefix + 'tool')) {
+ this.fireEvent('click', e);
+ }
+ },
+
+ getTargetEl: function() {
+ return this.body || this.frameBody || this.el;
+ },
+
+ /**
+ * Sets the title of the header.
+ * @param {String} title The title to be set
+ */
+ setTitle: function(title) {
+ var me = this;
+ if (me.rendered) {
+ if (me.titleCmp.rendered) {
+ if (me.titleCmp.surface) {
+ me.title = title || '';
+ var sprite = me.titleCmp.surface.items.items[0],
+ surface = me.titleCmp.surface;
+
+ surface.remove(sprite);
+ me.textConfig.type = 'text';
+ me.textConfig.text = title;
+ sprite = surface.add(me.textConfig);
+ sprite.setAttributes({
+ rotate: {
+ degrees: 90
+ }
+ }, true);
+ me.titleCmp.autoSizeSurface();
+ } else {
+ me.title = title || ' ';
+ me.titleCmp.textEl.update(me.title);
+ }
+ } else {
+ me.titleCmp.on({
+ render: function() {
+ me.setTitle(title);
+ },
+ single: true
+ });
+ }
+ } else {
+ me.on({
+ render: function() {
+ me.layout.layout();
+ me.setTitle(title);
+ },
+ single: true
+ });
+ }
+ },
+
+ /**
+ * Sets the CSS class that provides the icon image for this header. This method will replace any existing
+ * icon class if one has already been set.
+ * @param {String} cls The new CSS class name
+ */
+ setIconCls: function(cls) {
+ var me = this,
+ isEmpty = !cls || !cls.length,
+ iconCmp = me.iconCmp,
+ el;
+
+ me.iconCls = cls;
+ if (!me.iconCmp && !isEmpty) {
+ me.initIconCmp();
+ me.insert(0, me.iconCmp);
+ } else if (iconCmp) {
+ if (isEmpty) {
+ me.iconCmp.destroy();
+ } else {
+ el = iconCmp.iconEl;
+ el.removeCls(iconCmp.iconCls);
+ el.addCls(cls);
+ iconCmp.iconCls = cls;
+ }
+ }
+ },
+
+ /**
+ * Add a tool to the header
+ * @param {Object} tool
+ */
+ addTool: function(tool) {
+ this.tools.push(this.add(tool));
+ },
+
+ /**
+ * @private
+ * Set up the tools.<tool type> link in the owning Panel.
+ * Bind the tool to its owning Panel.
+ * @param component
+ * @param index
+ */
+ onAdd: function(component, index) {
+ this.callParent([arguments]);
+ if (component instanceof Ext.panel.Tool) {
+ component.bindTo(this.ownerCt);
+ this.tools[component.type] = component;
+ }
+ }
+});
+
+/**
+ * @class Ext.fx.target.Element
+ * @extends Ext.fx.target.Target
+ *
+ * This class represents a animation target for an {@link Ext.Element}. In general this class will not be
+ * created directly, the {@link Ext.Element} will be passed to the animation and
+ * and the appropriate target will be created.
+ */
+Ext.define('Ext.fx.target.Element', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.fx.target.Target',
+
+ /* End Definitions */
+
+ type: 'element',
+
+ getElVal: function(el, attr, val) {
+ if (val == undefined) {
+ if (attr === 'x') {
+ val = el.getX();
+ }
+ else if (attr === 'y') {
+ val = el.getY();
+ }
+ else if (attr === 'scrollTop') {
+ val = el.getScroll().top;
+ }
+ else if (attr === 'scrollLeft') {
+ val = el.getScroll().left;
+ }
+ else if (attr === 'height') {
+ val = el.getHeight();
+ }
+ else if (attr === 'width') {
+ val = el.getWidth();
+ }
+ else {
+ val = el.getStyle(attr);
+ }
+ }
+ return val;
+ },
+
+ getAttr: function(attr, val) {
+ var el = this.target;
+ return [[ el, this.getElVal(el, attr, val)]];
+ },
+
+ setAttr: function(targetData) {
+ var target = this.target,
+ ln = targetData.length,
+ attrs, attr, o, i, j, ln2, element, value;
+ for (i = 0; i < ln; i++) {
+ attrs = targetData[i].attrs;
+ for (attr in attrs) {
+ if (attrs.hasOwnProperty(attr)) {
+ ln2 = attrs[attr].length;
+ for (j = 0; j < ln2; j++) {
+ o = attrs[attr][j];
+ element = o[0];
+ value = o[1];
+ if (attr === 'x') {
+ element.setX(value);
+ }
+ else if (attr === 'y') {
+ element.setY(value);
+ }
+ else if (attr === 'scrollTop') {
+ element.scrollTo('top', value);
+ }
+ else if (attr === 'scrollLeft') {
+ element.scrollTo('left',value);
+ }
+ else {
+ element.setStyle(attr, value);
+ }
+ }
+ }
+ }
+ }
+ }
+});
+
+/**
+ * @class Ext.fx.target.CompositeElement
+ * @extends Ext.fx.target.Element
+ *
+ * This class represents a animation target for a {@link Ext.CompositeElement}. It allows
+ * each {@link Ext.Element} in the group to be animated as a whole. In general this class will not be
+ * created directly, the {@link Ext.CompositeElement} will be passed to the animation and
+ * and the appropriate target will be created.
+ */
+Ext.define('Ext.fx.target.CompositeElement', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.fx.target.Element',
+
+ /* End Definitions */
+
+ isComposite: true,
+
+ constructor: function(target) {
+ target.id = target.id || Ext.id(null, 'ext-composite-');
+ this.callParent([target]);
+ },
+
+ getAttr: function(attr, val) {
+ var out = [],
+ target = this.target;
+ target.each(function(el) {
+ out.push([el, this.getElVal(el, attr, val)]);
+ }, this);
+ return out;
+ }
+});
+
+/**
+ * @class Ext.fx.Manager
+ * Animation Manager which keeps track of all current animations and manages them on a frame by frame basis.
+ * @private
+ * @singleton
+ */
+
+Ext.define('Ext.fx.Manager', {
+
+ /* Begin Definitions */
+
+ singleton: true,
+
+ requires: ['Ext.util.MixedCollection',
+ 'Ext.fx.target.Element',
+ 'Ext.fx.target.CompositeElement',
+ 'Ext.fx.target.Sprite',
+ 'Ext.fx.target.CompositeSprite',
+ 'Ext.fx.target.Component'],
+
+ mixins: {
+ queue: 'Ext.fx.Queue'
+ },
+
+ /* End Definitions */
+
+ constructor: function() {
+ this.items = Ext.create('Ext.util.MixedCollection');
+ this.mixins.queue.constructor.call(this);
+
+ // this.requestAnimFrame = (function() {
+ // var raf = window.requestAnimationFrame ||
+ // window.webkitRequestAnimationFrame ||
+ // window.mozRequestAnimationFrame ||
+ // window.oRequestAnimationFrame ||
+ // window.msRequestAnimationFrame;
+ // if (raf) {
+ // return function(callback, element) {
+ // raf(callback);
+ // };
+ // }
+ // else {
+ // return function(callback, element) {
+ // window.setTimeout(callback, Ext.fx.Manager.interval);
+ // };
+ // }
+ // })();
+ },
+
+ /**
+ * @cfg {Number} interval Default interval in miliseconds to calculate each frame. Defaults to 16ms (~60fps)
+ */
+ interval: 16,
+
+ /**
+ * @cfg {Boolean} forceJS Turn off to not use CSS3 transitions when they are available
+ */
+ forceJS: true,
+
+ // @private Target factory
+ createTarget: function(target) {
+ var me = this,
+ useCSS3 = !me.forceJS && Ext.supports.Transitions,
+ targetObj;
+
+ me.useCSS3 = useCSS3;
+
+ // dom id
+ if (Ext.isString(target)) {
+ target = Ext.get(target);
+ }
+ // dom element
+ if (target && target.tagName) {
+ target = Ext.get(target);
+ targetObj = Ext.create('Ext.fx.target.' + 'Element' + (useCSS3 ? 'CSS' : ''), target);
+ me.targets.add(targetObj);
+ return targetObj;
+ }
+ if (Ext.isObject(target)) {
+ // Element
+ if (target.dom) {
+ targetObj = Ext.create('Ext.fx.target.' + 'Element' + (useCSS3 ? 'CSS' : ''), target);
+ }
+ // Element Composite
+ else if (target.isComposite) {
+ targetObj = Ext.create('Ext.fx.target.' + 'CompositeElement' + (useCSS3 ? 'CSS' : ''), target);
+ }
+ // Draw Sprite
+ else if (target.isSprite) {
+ targetObj = Ext.create('Ext.fx.target.Sprite', target);
+ }
+ // Draw Sprite Composite
+ else if (target.isCompositeSprite) {
+ targetObj = Ext.create('Ext.fx.target.CompositeSprite', target);
+ }
+ // Component
+ else if (target.isComponent) {
+ targetObj = Ext.create('Ext.fx.target.Component', target);
+ }
+ else if (target.isAnimTarget) {
+ return target;
+ }
+ else {
+ return null;
+ }
+ me.targets.add(targetObj);
+ return targetObj;
+ }
+ else {
+ return null;
+ }
+ },
+
+ /**
+ * Add an Anim to the manager. This is done automatically when an Anim instance is created.
+ * @param {Ext.fx.Anim} anim
+ */
+ addAnim: function(anim) {
+ var items = this.items,
+ task = this.task;
+ // var me = this,
+ // items = me.items,
+ // cb = function() {
+ // if (items.length) {
+ // me.task = true;
+ // me.runner();
+ // me.requestAnimFrame(cb);
+ // }
+ // else {
+ // me.task = false;
+ // }
+ // };
+
+ items.add(anim);
+
+ // Start the timer if not already running
+ if (!task && items.length) {
+ task = this.task = {
+ run: this.runner,
+ interval: this.interval,
+ scope: this
+ };
+ Ext.TaskManager.start(task);
+ }
+
+ // //Start the timer if not already running
+ // if (!me.task && items.length) {
+ // me.requestAnimFrame(cb);
+ // }
+ },
+
+ /**
+ * Remove an Anim from the manager. This is done automatically when an Anim ends.
+ * @param {Ext.fx.Anim} anim
+ */
+ removeAnim: function(anim) {
+ // this.items.remove(anim);
+ var items = this.items,
+ task = this.task;
+ items.remove(anim);
+ // Stop the timer if there are no more managed Anims
+ if (task && !items.length) {
+ Ext.TaskManager.stop(task);
+ delete this.task;
+ }
+ },
+
+ /**
+ * @private
+ * Filter function to determine which animations need to be started
+ */
+ startingFilter: function(o) {
+ return o.paused === false && o.running === false && o.iterations > 0;
+ },
+
+ /**
+ * @private
+ * Filter function to determine which animations are still running
+ */
+ runningFilter: function(o) {
+ return o.paused === false && o.running === true && o.isAnimator !== true;
+ },
+
+ /**
+ * @private
+ * Runner function being called each frame
+ */
+ runner: function() {
+ var me = this,
+ items = me.items;
+
+ me.targetData = {};
+ me.targetArr = {};
+
+ // Single timestamp for all animations this interval
+ me.timestamp = new Date();
+
+ // Start any items not current running
+ items.filterBy(me.startingFilter).each(me.startAnim, me);
+
+ // Build the new attributes to be applied for all targets in this frame
+ items.filterBy(me.runningFilter).each(me.runAnim, me);
+
+ // Apply all the pending changes to their targets
+ me.applyPendingAttrs();
+ },
+
+ /**
+ * @private
+ * Start the individual animation (initialization)
+ */
+ startAnim: function(anim) {
+ anim.start(this.timestamp);
+ },
+
+ /**
+ * @private
+ * Run the individual animation for this frame
+ */
+ runAnim: function(anim) {
+ if (!anim) {
+ return;
+ }
+ var me = this,
+ targetId = anim.target.getId(),
+ useCSS3 = me.useCSS3 && anim.target.type == 'element',
+ elapsedTime = me.timestamp - anim.startTime,
+ target, o;
+
+ this.collectTargetData(anim, elapsedTime, useCSS3);
+
+ // For CSS3 animation, we need to immediately set the first frame's attributes without any transition
+ // to get a good initial state, then add the transition properties and set the final attributes.
+ if (useCSS3) {
+ // Flush the collected attributes, without transition
+ anim.target.setAttr(me.targetData[targetId], true);
+
+ // Add the end frame data
+ me.targetData[targetId] = [];
+ me.collectTargetData(anim, anim.duration, useCSS3);
+
+ // Pause the animation so runAnim doesn't keep getting called
+ anim.paused = true;
+
+ target = anim.target.target;
+ // We only want to attach an event on the last element in a composite
+ if (anim.target.isComposite) {
+ target = anim.target.target.last();
+ }
+
+ // Listen for the transitionend event
+ o = {};
+ o[Ext.supports.CSS3TransitionEnd] = anim.lastFrame;
+ o.scope = anim;
+ o.single = true;
+ target.on(o);
+ }
+ // For JS animation, trigger the lastFrame handler if this is the final frame
+ else if (elapsedTime >= anim.duration) {
+ me.applyPendingAttrs(true);
+ delete me.targetData[targetId];
+ delete me.targetArr[targetId];
+ anim.lastFrame();
+ }
+ },
+
+ /**
+ * Collect target attributes for the given Anim object at the given timestamp
+ * @param {Ext.fx.Anim} anim The Anim instance
+ * @param {Number} timestamp Time after the anim's start time
+ */
+ collectTargetData: function(anim, elapsedTime, useCSS3) {
+ var targetId = anim.target.getId(),
+ targetData = this.targetData[targetId],
+ data;
+
+ if (!targetData) {
+ targetData = this.targetData[targetId] = [];
+ this.targetArr[targetId] = anim.target;
+ }
+
+ data = {
+ duration: anim.duration,
+ easing: (useCSS3 && anim.reverse) ? anim.easingFn.reverse().toCSS3() : anim.easing,
+ attrs: {}
+ };
+ Ext.apply(data.attrs, anim.runAnim(elapsedTime));
+ targetData.push(data);
+ },
+
+ /**
+ * @private
+ * Apply all pending attribute changes to their targets
+ */
+ applyPendingAttrs: function(isLastFrame) {
+ var targetData = this.targetData,
+ targetArr = this.targetArr,
+ targetId;
+ for (targetId in targetData) {
+ if (targetData.hasOwnProperty(targetId)) {
+ targetArr[targetId].setAttr(targetData[targetId], false, isLastFrame);
+ }
+ }
+ }
+});
+
+/**
+ * @class Ext.fx.Animator
+ *
+ * This class is used to run keyframe based animations, which follows the CSS3 based animation structure.
+ * Keyframe animations differ from typical from/to animations in that they offer the ability to specify values
+ * at various points throughout the animation.
+ *
+ * ## Using Keyframes
+ *
+ * The {@link #keyframes} option is the most important part of specifying an animation when using this
+ * class. A key frame is a point in a particular animation. We represent this as a percentage of the
+ * total animation duration. At each key frame, we can specify the target values at that time. Note that
+ * you *must* specify the values at 0% and 100%, the start and ending values. There is also a {@link #keyframe}
+ * event that fires after each key frame is reached.
+ *
+ * ## Example
+ *
+ * In the example below, we modify the values of the element at each fifth throughout the animation.
+ *
+ * @example
+ * Ext.create('Ext.fx.Animator', {
+ * target: Ext.getBody().createChild({
+ * style: {
+ * width: '100px',
+ * height: '100px',
+ * 'background-color': 'red'
+ * }
+ * }),
+ * duration: 10000, // 10 seconds
+ * keyframes: {
+ * 0: {
+ * opacity: 1,
+ * backgroundColor: 'FF0000'
+ * },
+ * 20: {
+ * x: 30,
+ * opacity: 0.5
+ * },
+ * 40: {
+ * x: 130,
+ * backgroundColor: '0000FF'
+ * },
+ * 60: {
+ * y: 80,
+ * opacity: 0.3
+ * },
+ * 80: {
+ * width: 200,
+ * y: 200
+ * },
+ * 100: {
+ * opacity: 1,
+ * backgroundColor: '00FF00'
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.fx.Animator', {
+
+ /* Begin Definitions */
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ requires: ['Ext.fx.Manager'],
+
+ /* End Definitions */
+
+ isAnimator: true,
+
+ /**
+ * @cfg {Number} duration
+ * Time in milliseconds for the animation to last. Defaults to 250.
+ */
+ duration: 250,
+
+ /**
+ * @cfg {Number} delay
+ * Time to delay before starting the animation. Defaults to 0.
+ */
+ delay: 0,
+
+ /* private used to track a delayed starting time */
+ delayStart: 0,
+
+ /**
+ * @cfg {Boolean} dynamic
+ * Currently only for Component Animation: Only set a component's outer element size bypassing layouts. Set to true to do full layouts for every frame of the animation. Defaults to false.
+ */
+ dynamic: false,
+
+ /**
+ * @cfg {String} easing
+ *
+ * This describes how the intermediate values used during a transition will be calculated. It allows for a transition to change
+ * speed over its duration.
+ *
+ * - backIn
+ * - backOut
+ * - bounceIn
+ * - bounceOut
+ * - ease
+ * - easeIn
+ * - easeOut
+ * - easeInOut
+ * - elasticIn
+ * - elasticOut
+ * - cubic-bezier(x1, y1, x2, y2)
+ *
+ * Note that cubic-bezier will create a custom easing curve following the CSS3 [transition-timing-function][0]
+ * specification. The four values specify points P1 and P2 of the curve as (x1, y1, x2, y2). All values must
+ * be in the range [0, 1] or the definition is invalid.
+ *
+ * [0]: http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
+ */
+ easing: 'ease',
+
+ /**
+ * Flag to determine if the animation has started
+ * @property running
+ * @type Boolean
+ */
+ running: false,
+
+ /**
+ * Flag to determine if the animation is paused. Only set this to true if you need to
+ * keep the Anim instance around to be unpaused later; otherwise call {@link #end}.
+ * @property paused
+ * @type Boolean
+ */
+ paused: false,
+
+ /**
+ * @private
+ */
+ damper: 1,
+
+ /**
+ * @cfg {Number} iterations
+ * Number of times to execute the animation. Defaults to 1.
+ */
+ iterations: 1,
+
+ /**
+ * Current iteration the animation is running.
+ * @property currentIteration
+ * @type Number
+ */
+ currentIteration: 0,
+
+ /**
+ * Current keyframe step of the animation.
+ * @property keyframeStep
+ * @type Number
+ */
+ keyframeStep: 0,
+
+ /**
+ * @private
+ */
+ animKeyFramesRE: /^(from|to|\d+%?)$/,
+
+ /**
+ * @cfg {Ext.fx.target.Target} target
+ * The Ext.fx.target to apply the animation to. If not specified during initialization, this can be passed to the applyAnimator
+ * method to apply the same animation to many targets.
+ */
+
+ /**
+ * @cfg {Object} keyframes
+ * Animation keyframes follow the CSS3 Animation configuration pattern. 'from' is always considered '0%' and 'to'
+ * is considered '100%'.<b>Every keyframe declaration must have a keyframe rule for 0% and 100%, possibly defined using
+ * "from" or "to"</b>. A keyframe declaration without these keyframe selectors is invalid and will not be available for
+ * animation. The keyframe declaration for a keyframe rule consists of properties and values. Properties that are unable to
+ * be animated are ignored in these rules, with the exception of 'easing' which can be changed at each keyframe. For example:
+ <pre><code>
+keyframes : {
+ '0%': {
+ left: 100
+ },
+ '40%': {
+ left: 150
+ },
+ '60%': {
+ left: 75
+ },
+ '100%': {
+ left: 100
+ }
+}
+ </code></pre>
+ */
+ constructor: function(config) {
+ var me = this;
+ config = Ext.apply(me, config || {});
+ me.config = config;
+ me.id = Ext.id(null, 'ext-animator-');
+ me.addEvents(
+ /**
+ * @event beforeanimate
+ * Fires before the animation starts. A handler can return false to cancel the animation.
+ * @param {Ext.fx.Animator} this
+ */
+ 'beforeanimate',
+ /**
+ * @event keyframe
+ * Fires at each keyframe.
+ * @param {Ext.fx.Animator} this
+ * @param {Number} keyframe step number
+ */
+ 'keyframe',
+ /**
+ * @event afteranimate
+ * Fires when the animation is complete.
+ * @param {Ext.fx.Animator} this
+ * @param {Date} startTime
+ */
+ 'afteranimate'
+ );
+ me.mixins.observable.constructor.call(me, config);
+ me.timeline = [];
+ me.createTimeline(me.keyframes);
+ if (me.target) {
+ me.applyAnimator(me.target);
+ Ext.fx.Manager.addAnim(me);
+ }
+ },
+
+ /**
+ * @private
+ */
+ sorter: function (a, b) {
+ return a.pct - b.pct;
+ },
+
+ /**
+ * @private
+ * Takes the given keyframe configuration object and converts it into an ordered array with the passed attributes per keyframe
+ * or applying the 'to' configuration to all keyframes. Also calculates the proper animation duration per keyframe.
+ */
+ createTimeline: function(keyframes) {
+ var me = this,
+ attrs = [],
+ to = me.to || {},
+ duration = me.duration,
+ prevMs, ms, i, ln, pct, anim, nextAnim, attr;
+
+ for (pct in keyframes) {
+ if (keyframes.hasOwnProperty(pct) && me.animKeyFramesRE.test(pct)) {
+ attr = {attrs: Ext.apply(keyframes[pct], to)};
+ // CSS3 spec allow for from/to to be specified.
+ if (pct == "from") {
+ pct = 0;
+ }
+ else if (pct == "to") {
+ pct = 100;
+ }
+ // convert % values into integers
+ attr.pct = parseInt(pct, 10);
+ attrs.push(attr);
+ }
+ }
+ // Sort by pct property
+ Ext.Array.sort(attrs, me.sorter);
+ // Only an end
+ //if (attrs[0].pct) {
+ // attrs.unshift({pct: 0, attrs: element.attrs});
+ //}
+
+ ln = attrs.length;
+ for (i = 0; i < ln; i++) {
+ prevMs = (attrs[i - 1]) ? duration * (attrs[i - 1].pct / 100) : 0;
+ ms = duration * (attrs[i].pct / 100);
+ me.timeline.push({
+ duration: ms - prevMs,
+ attrs: attrs[i].attrs
+ });
+ }
+ },
+
+ /**
+ * Applies animation to the Ext.fx.target
+ * @private
+ * @param target
+ * @type String/Object
+ */
+ applyAnimator: function(target) {
+ var me = this,
+ anims = [],
+ timeline = me.timeline,
+ reverse = me.reverse,
+ ln = timeline.length,
+ anim, easing, damper, initial, attrs, lastAttrs, i;
+
+ if (me.fireEvent('beforeanimate', me) !== false) {
+ for (i = 0; i < ln; i++) {
+ anim = timeline[i];
+ attrs = anim.attrs;
+ easing = attrs.easing || me.easing;
+ damper = attrs.damper || me.damper;
+ delete attrs.easing;
+ delete attrs.damper;
+ anim = Ext.create('Ext.fx.Anim', {
+ target: target,
+ easing: easing,
+ damper: damper,
+ duration: anim.duration,
+ paused: true,
+ to: attrs
+ });
+ anims.push(anim);
+ }
+ me.animations = anims;
+ me.target = anim.target;
+ for (i = 0; i < ln - 1; i++) {
+ anim = anims[i];
+ anim.nextAnim = anims[i + 1];
+ anim.on('afteranimate', function() {
+ this.nextAnim.paused = false;
+ });
+ anim.on('afteranimate', function() {
+ this.fireEvent('keyframe', this, ++this.keyframeStep);
+ }, me);
+ }
+ anims[ln - 1].on('afteranimate', function() {
+ this.lastFrame();
+ }, me);
+ }
+ },
+
+ /**
+ * @private
+ * Fires beforeanimate and sets the running flag.
+ */
+ start: function(startTime) {
+ var me = this,
+ delay = me.delay,
+ delayStart = me.delayStart,
+ delayDelta;
+ if (delay) {
+ if (!delayStart) {
+ me.delayStart = startTime;
+ return;
+ }
+ else {
+ delayDelta = startTime - delayStart;
+ if (delayDelta < delay) {
+ return;
+ }
+ else {
+ // Compensate for frame delay;
+ startTime = new Date(delayStart.getTime() + delay);
+ }
+ }
+ }
+ if (me.fireEvent('beforeanimate', me) !== false) {
+ me.startTime = startTime;
+ me.running = true;
+ me.animations[me.keyframeStep].paused = false;
+ }
+ },
+
+ /**
+ * @private
+ * Perform lastFrame cleanup and handle iterations
+ * @returns a hash of the new attributes.
+ */
+ lastFrame: function() {
+ var me = this,
+ iter = me.iterations,
+ iterCount = me.currentIteration;
+
+ iterCount++;
+ if (iterCount < iter) {
+ me.startTime = new Date();
+ me.currentIteration = iterCount;
+ me.keyframeStep = 0;
+ me.applyAnimator(me.target);
+ me.animations[me.keyframeStep].paused = false;
+ }
+ else {
+ me.currentIteration = 0;
+ me.end();
+ }
+ },
+
+ /**
+ * Fire afteranimate event and end the animation. Usually called automatically when the
+ * animation reaches its final frame, but can also be called manually to pre-emptively
+ * stop and destroy the running animation.
+ */
+ end: function() {
+ var me = this;
+ me.fireEvent('afteranimate', me, me.startTime, new Date() - me.startTime);
+ }
+});
+/**
+ * @class Ext.fx.Easing
+ *
+ * This class contains a series of function definitions used to modify values during an animation.
+ * They describe how the intermediate values used during a transition will be calculated. It allows for a transition to change
+ * speed over its duration. The following options are available:
+ *
+ * - linear The default easing type
+ * - backIn
+ * - backOut
+ * - bounceIn
+ * - bounceOut
+ * - ease
+ * - easeIn
+ * - easeOut
+ * - easeInOut
+ * - elasticIn
+ * - elasticOut
+ * - cubic-bezier(x1, y1, x2, y2)
+ *
+ * Note that cubic-bezier will create a custom easing curve following the CSS3 [transition-timing-function][0]
+ * specification. The four values specify points P1 and P2 of the curve as (x1, y1, x2, y2). All values must
+ * be in the range [0, 1] or the definition is invalid.
+ *
+ * [0]: http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
+ *
+ * @singleton
+ */
+Ext.ns('Ext.fx');
+
+Ext.require('Ext.fx.CubicBezier', function() {
+ var math = Math,
+ pi = math.PI,
+ pow = math.pow,
+ sin = math.sin,
+ sqrt = math.sqrt,
+ abs = math.abs,
+ backInSeed = 1.70158;
+ Ext.fx.Easing = {
+ // ease: Ext.fx.CubicBezier.cubicBezier(0.25, 0.1, 0.25, 1),
+ // linear: Ext.fx.CubicBezier.cubicBezier(0, 0, 1, 1),
+ // 'ease-in': Ext.fx.CubicBezier.cubicBezier(0.42, 0, 1, 1),
+ // 'ease-out': Ext.fx.CubicBezier.cubicBezier(0, 0.58, 1, 1),
+ // 'ease-in-out': Ext.fx.CubicBezier.cubicBezier(0.42, 0, 0.58, 1),
+ // 'easeIn': Ext.fx.CubicBezier.cubicBezier(0.42, 0, 1, 1),
+ // 'easeOut': Ext.fx.CubicBezier.cubicBezier(0, 0.58, 1, 1),
+ // 'easeInOut': Ext.fx.CubicBezier.cubicBezier(0.42, 0, 0.58, 1)
+ };
+
+ Ext.apply(Ext.fx.Easing, {
+ linear: function(n) {
+ return n;
+ },
+ ease: function(n) {
+ var q = 0.07813 - n / 2,
+ alpha = -0.25,
+ Q = sqrt(0.0066 + q * q),
+ x = Q - q,
+ X = pow(abs(x), 1/3) * (x < 0 ? -1 : 1),
+ y = -Q - q,
+ Y = pow(abs(y), 1/3) * (y < 0 ? -1 : 1),
+ t = X + Y + 0.25;
+ return pow(1 - t, 2) * 3 * t * 0.1 + (1 - t) * 3 * t * t + t * t * t;
+ },
+ easeIn: function (n) {
+ return pow(n, 1.7);
+ },
+ easeOut: function (n) {
+ return pow(n, 0.48);
+ },
+ easeInOut: function(n) {
+ var q = 0.48 - n / 1.04,
+ Q = sqrt(0.1734 + q * q),
+ x = Q - q,
+ X = pow(abs(x), 1/3) * (x < 0 ? -1 : 1),
+ y = -Q - q,
+ Y = pow(abs(y), 1/3) * (y < 0 ? -1 : 1),
+ t = X + Y + 0.5;
+ return (1 - t) * 3 * t * t + t * t * t;
+ },
+ backIn: function (n) {
+ return n * n * ((backInSeed + 1) * n - backInSeed);
+ },
+ backOut: function (n) {
+ n = n - 1;
+ return n * n * ((backInSeed + 1) * n + backInSeed) + 1;
+ },
+ elasticIn: function (n) {
+ if (n === 0 || n === 1) {
+ return n;
+ }
+ var p = 0.3,
+ s = p / 4;
+ return pow(2, -10 * n) * sin((n - s) * (2 * pi) / p) + 1;
+ },
+ elasticOut: function (n) {
+ return 1 - Ext.fx.Easing.elasticIn(1 - n);
+ },
+ bounceIn: function (n) {
+ return 1 - Ext.fx.Easing.bounceOut(1 - n);
+ },
+ bounceOut: function (n) {
+ var s = 7.5625,
+ p = 2.75,
+ l;
+ if (n < (1 / p)) {
+ l = s * n * n;
+ } else {
+ if (n < (2 / p)) {
+ n -= (1.5 / p);
+ l = s * n * n + 0.75;
+ } else {
+ if (n < (2.5 / p)) {
+ n -= (2.25 / p);
+ l = s * n * n + 0.9375;
+ } else {
+ n -= (2.625 / p);
+ l = s * n * n + 0.984375;
+ }
+ }
+ }
+ return l;
+ }
+ });
+ Ext.apply(Ext.fx.Easing, {
+ 'back-in': Ext.fx.Easing.backIn,
+ 'back-out': Ext.fx.Easing.backOut,
+ 'ease-in': Ext.fx.Easing.easeIn,
+ 'ease-out': Ext.fx.Easing.easeOut,
+ 'elastic-in': Ext.fx.Easing.elasticIn,
+ 'elastic-out': Ext.fx.Easing.elasticIn,
+ 'bounce-in': Ext.fx.Easing.bounceIn,
+ 'bounce-out': Ext.fx.Easing.bounceOut,
+ 'ease-in-out': Ext.fx.Easing.easeInOut
+ });
+});
+/**
+ * @class Ext.draw.Draw
+ * Base Drawing class. Provides base drawing functions.
+ * @private
+ */
+Ext.define('Ext.draw.Draw', {
+ /* Begin Definitions */
+
+ singleton: true,
+
+ requires: ['Ext.draw.Color'],
+
+ /* End Definitions */
+
+ pathToStringRE: /,?([achlmqrstvxz]),?/gi,
+ pathCommandRE: /([achlmqstvz])[\s,]*((-?\d*\.?\d*(?:e[-+]?\d+)?\s*,?\s*)+)/ig,
+ pathValuesRE: /(-?\d*\.?\d*(?:e[-+]?\d+)?)\s*,?\s*/ig,
+ stopsRE: /^(\d+%?)$/,
+ radian: Math.PI / 180,
+
+ availableAnimAttrs: {
+ along: "along",
+ blur: null,
+ "clip-rect": "csv",
+ cx: null,
+ cy: null,
+ fill: "color",
+ "fill-opacity": null,
+ "font-size": null,
+ height: null,
+ opacity: null,
+ path: "path",
+ r: null,
+ rotation: "csv",
+ rx: null,
+ ry: null,
+ scale: "csv",
+ stroke: "color",
+ "stroke-opacity": null,
+ "stroke-width": null,
+ translation: "csv",
+ width: null,
+ x: null,
+ y: null
+ },
+
+ is: function(o, type) {
+ type = String(type).toLowerCase();
+ return (type == "object" && o === Object(o)) ||
+ (type == "undefined" && typeof o == type) ||
+ (type == "null" && o === null) ||
+ (type == "array" && Array.isArray && Array.isArray(o)) ||
+ (Object.prototype.toString.call(o).toLowerCase().slice(8, -1)) == type;
+ },
+
+ ellipsePath: function(sprite) {
+ var attr = sprite.attr;
+ return Ext.String.format("M{0},{1}A{2},{3},0,1,1,{0},{4}A{2},{3},0,1,1,{0},{1}z", attr.x, attr.y - attr.ry, attr.rx, attr.ry, attr.y + attr.ry);
+ },
+
+ rectPath: function(sprite) {
+ var attr = sprite.attr;
+ if (attr.radius) {
+ return Ext.String.format("M{0},{1}l{2},0a{3},{3},0,0,1,{3},{3}l0,{5}a{3},{3},0,0,1,{4},{3}l{6},0a{3},{3},0,0,1,{4},{4}l0,{7}a{3},{3},0,0,1,{3},{4}z", attr.x + attr.radius, attr.y, attr.width - attr.radius * 2, attr.radius, -attr.radius, attr.height - attr.radius * 2, attr.radius * 2 - attr.width, attr.radius * 2 - attr.height);
+ }
+ else {
+ return Ext.String.format("M{0},{1}l{2},0,0,{3},{4},0z", attr.x, attr.y, attr.width, attr.height, -attr.width);
+ }
+ },
+
+ // To be deprecated, converts itself (an arrayPath) to a proper SVG path string
+ path2string: function () {
+ return this.join(",").replace(Ext.draw.Draw.pathToStringRE, "$1");
+ },
+
+ // Convert the passed arrayPath to a proper SVG path string (d attribute)
+ pathToString: function(arrayPath) {
+ return arrayPath.join(",").replace(Ext.draw.Draw.pathToStringRE, "$1");
+ },
+
+ parsePathString: function (pathString) {
+ if (!pathString) {
+ return null;
+ }
+ var paramCounts = {a: 7, c: 6, h: 1, l: 2, m: 2, q: 4, s: 4, t: 2, v: 1, z: 0},
+ data = [],
+ me = this;
+ if (me.is(pathString, "array") && me.is(pathString[0], "array")) { // rough assumption
+ data = me.pathClone(pathString);
+ }
+ if (!data.length) {
+ String(pathString).replace(me.pathCommandRE, function (a, b, c) {
+ var params = [],
+ name = b.toLowerCase();
+ c.replace(me.pathValuesRE, function (a, b) {
+ b && params.push(+b);
+ });
+ if (name == "m" && params.length > 2) {
+ data.push([b].concat(Ext.Array.splice(params, 0, 2)));
+ name = "l";
+ b = (b == "m") ? "l" : "L";
+ }
+ while (params.length >= paramCounts[name]) {
+ data.push([b].concat(Ext.Array.splice(params, 0, paramCounts[name])));
+ if (!paramCounts[name]) {
+ break;
+ }
+ }
+ });
+ }
+ data.toString = me.path2string;
+ return data;
+ },
+
+ mapPath: function (path, matrix) {
+ if (!matrix) {
+ return path;
+ }
+ var x, y, i, ii, j, jj, pathi;
+ path = this.path2curve(path);
+ for (i = 0, ii = path.length; i < ii; i++) {
+ pathi = path[i];
+ for (j = 1, jj = pathi.length; j < jj-1; j += 2) {
+ x = matrix.x(pathi[j], pathi[j + 1]);
+ y = matrix.y(pathi[j], pathi[j + 1]);
+ pathi[j] = x;
+ pathi[j + 1] = y;
+ }
+ }
+ return path;
+ },
+
+ pathClone: function(pathArray) {
+ var res = [],
+ j, jj, i, ii;
+ if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) { // rough assumption
+ pathArray = this.parsePathString(pathArray);
+ }
+ for (i = 0, ii = pathArray.length; i < ii; i++) {
+ res[i] = [];
+ for (j = 0, jj = pathArray[i].length; j < jj; j++) {
+ res[i][j] = pathArray[i][j];
+ }
+ }
+ res.toString = this.path2string;
+ return res;
+ },
+
+ pathToAbsolute: function (pathArray) {
+ if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) { // rough assumption
+ pathArray = this.parsePathString(pathArray);
+ }
+ var res = [],
+ x = 0,
+ y = 0,
+ mx = 0,
+ my = 0,
+ i = 0,
+ ln = pathArray.length,
+ r, pathSegment, j, ln2;
+ // MoveTo initial x/y position
+ if (ln && pathArray[0][0] == "M") {
+ x = +pathArray[0][1];
+ y = +pathArray[0][2];
+ mx = x;
+ my = y;
+ i++;
+ res[0] = ["M", x, y];
+ }
+ for (; i < ln; i++) {
+ r = res[i] = [];
+ pathSegment = pathArray[i];
+ if (pathSegment[0] != pathSegment[0].toUpperCase()) {
+ r[0] = pathSegment[0].toUpperCase();
+ switch (r[0]) {
+ // Elliptical Arc
+ case "A":
+ r[1] = pathSegment[1];
+ r[2] = pathSegment[2];
+ r[3] = pathSegment[3];
+ r[4] = pathSegment[4];
+ r[5] = pathSegment[5];
+ r[6] = +(pathSegment[6] + x);
+ r[7] = +(pathSegment[7] + y);
+ break;
+ // Vertical LineTo
+ case "V":
+ r[1] = +pathSegment[1] + y;
+ break;
+ // Horizontal LineTo
+ case "H":
+ r[1] = +pathSegment[1] + x;
+ break;
+ case "M":
+ // MoveTo
+ mx = +pathSegment[1] + x;
+ my = +pathSegment[2] + y;
+ default:
+ j = 1;
+ ln2 = pathSegment.length;
+ for (; j < ln2; j++) {
+ r[j] = +pathSegment[j] + ((j % 2) ? x : y);
+ }
+ }
+ }
+ else {
+ j = 0;
+ ln2 = pathSegment.length;
+ for (; j < ln2; j++) {
+ res[i][j] = pathSegment[j];
+ }
+ }
+ switch (r[0]) {
+ // ClosePath
+ case "Z":
+ x = mx;
+ y = my;
+ break;
+ // Horizontal LineTo
+ case "H":
+ x = r[1];
+ break;
+ // Vertical LineTo
+ case "V":
+ y = r[1];
+ break;
+ // MoveTo
+ case "M":
+ pathSegment = res[i];
+ ln2 = pathSegment.length;
+ mx = pathSegment[ln2 - 2];
+ my = pathSegment[ln2 - 1];
+ default:
+ pathSegment = res[i];
+ ln2 = pathSegment.length;
+ x = pathSegment[ln2 - 2];
+ y = pathSegment[ln2 - 1];
+ }
+ }
+ res.toString = this.path2string;
+ return res;
+ },
+
+ // TO BE DEPRECATED
+ pathToRelative: function (pathArray) {
+ if (!this.is(pathArray, "array") || !this.is(pathArray && pathArray[0], "array")) {
+ pathArray = this.parsePathString(pathArray);
+ }
+ var res = [],
+ x = 0,
+ y = 0,
+ mx = 0,
+ my = 0,
+ start = 0;
+ if (pathArray[0][0] == "M") {
+ x = pathArray[0][1];
+ y = pathArray[0][2];
+ mx = x;
+ my = y;
+ start++;
+ res.push(["M", x, y]);
+ }
+ for (var i = start, ii = pathArray.length; i < ii; i++) {
+ var r = res[i] = [],
+ pa = pathArray[i];
+ if (pa[0] != pa[0].toLowerCase()) {
+ r[0] = pa[0].toLowerCase();
+ switch (r[0]) {
+ case "a":
+ r[1] = pa[1];
+ r[2] = pa[2];
+ r[3] = pa[3];
+ r[4] = pa[4];
+ r[5] = pa[5];
+ r[6] = +(pa[6] - x).toFixed(3);
+ r[7] = +(pa[7] - y).toFixed(3);
+ break;
+ case "v":
+ r[1] = +(pa[1] - y).toFixed(3);
+ break;
+ case "m":
+ mx = pa[1];
+ my = pa[2];
+ default:
+ for (var j = 1, jj = pa.length; j < jj; j++) {
+ r[j] = +(pa[j] - ((j % 2) ? x : y)).toFixed(3);
+ }
+ }
+ } else {
+ r = res[i] = [];
+ if (pa[0] == "m") {
+ mx = pa[1] + x;
+ my = pa[2] + y;
+ }
+ for (var k = 0, kk = pa.length; k < kk; k++) {
+ res[i][k] = pa[k];
+ }
+ }
+ var len = res[i].length;
+ switch (res[i][0]) {
+ case "z":
+ x = mx;
+ y = my;
+ break;
+ case "h":
+ x += +res[i][len - 1];
+ break;
+ case "v":
+ y += +res[i][len - 1];
+ break;
+ default:
+ x += +res[i][len - 2];
+ y += +res[i][len - 1];
+ }
+ }
+ res.toString = this.path2string;
+ return res;
+ },
+
+ // Returns a path converted to a set of curveto commands
+ path2curve: function (path) {
+ var me = this,
+ points = me.pathToAbsolute(path),
+ ln = points.length,
+ attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
+ i, seg, segLn, point;
+
+ for (i = 0; i < ln; i++) {
+ points[i] = me.command2curve(points[i], attrs);
+ if (points[i].length > 7) {
+ points[i].shift();
+ point = points[i];
+ while (point.length) {
+ Ext.Array.splice(points, i++, 0, ["C"].concat(Ext.Array.splice(point, 0, 6)));
+ }
+ Ext.Array.erase(points, i, 1);
+ ln = points.length;
+ }
+ seg = points[i];
+ segLn = seg.length;
+ attrs.x = seg[segLn - 2];
+ attrs.y = seg[segLn - 1];
+ attrs.bx = parseFloat(seg[segLn - 4]) || attrs.x;
+ attrs.by = parseFloat(seg[segLn - 3]) || attrs.y;
+ }
+ return points;
+ },
+
+ interpolatePaths: function (path, path2) {
+ var me = this,
+ p = me.pathToAbsolute(path),
+ p2 = me.pathToAbsolute(path2),
+ attrs = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
+ attrs2 = {x: 0, y: 0, bx: 0, by: 0, X: 0, Y: 0, qx: null, qy: null},
+ fixArc = function (pp, i) {
+ if (pp[i].length > 7) {
+ pp[i].shift();
+ var pi = pp[i];
+ while (pi.length) {
+ Ext.Array.splice(pp, i++, 0, ["C"].concat(Ext.Array.splice(pi, 0, 6)));
+ }
+ Ext.Array.erase(pp, i, 1);
+ ii = Math.max(p.length, p2.length || 0);
+ }
+ },
+ fixM = function (path1, path2, a1, a2, i) {
+ if (path1 && path2 && path1[i][0] == "M" && path2[i][0] != "M") {
+ Ext.Array.splice(path2, i, 0, ["M", a2.x, a2.y]);
+ a1.bx = 0;
+ a1.by = 0;
+ a1.x = path1[i][1];
+ a1.y = path1[i][2];
+ ii = Math.max(p.length, p2.length || 0);
+ }
+ };
+ for (var i = 0, ii = Math.max(p.length, p2.length || 0); i < ii; i++) {
+ p[i] = me.command2curve(p[i], attrs);
+ fixArc(p, i);
+ (p2[i] = me.command2curve(p2[i], attrs2));
+ fixArc(p2, i);
+ fixM(p, p2, attrs, attrs2, i);
+ fixM(p2, p, attrs2, attrs, i);
+ var seg = p[i],
+ seg2 = p2[i],
+ seglen = seg.length,
+ seg2len = seg2.length;
+ attrs.x = seg[seglen - 2];
+ attrs.y = seg[seglen - 1];
+ attrs.bx = parseFloat(seg[seglen - 4]) || attrs.x;
+ attrs.by = parseFloat(seg[seglen - 3]) || attrs.y;
+ attrs2.bx = (parseFloat(seg2[seg2len - 4]) || attrs2.x);
+ attrs2.by = (parseFloat(seg2[seg2len - 3]) || attrs2.y);
+ attrs2.x = seg2[seg2len - 2];
+ attrs2.y = seg2[seg2len - 1];
+ }
+ return [p, p2];
+ },
+
+ //Returns any path command as a curveto command based on the attrs passed
+ command2curve: function (pathCommand, d) {
+ var me = this;
+ if (!pathCommand) {
+ return ["C", d.x, d.y, d.x, d.y, d.x, d.y];
+ }
+ if (pathCommand[0] != "T" && pathCommand[0] != "Q") {
+ d.qx = d.qy = null;
+ }
+ switch (pathCommand[0]) {
+ case "M":
+ d.X = pathCommand[1];
+ d.Y = pathCommand[2];
+ break;
+ case "A":
+ pathCommand = ["C"].concat(me.arc2curve.apply(me, [d.x, d.y].concat(pathCommand.slice(1))));
+ break;
+ case "S":
+ pathCommand = ["C", d.x + (d.x - (d.bx || d.x)), d.y + (d.y - (d.by || d.y))].concat(pathCommand.slice(1));
+ break;
+ case "T":
+ d.qx = d.x + (d.x - (d.qx || d.x));
+ d.qy = d.y + (d.y - (d.qy || d.y));
+ pathCommand = ["C"].concat(me.quadratic2curve(d.x, d.y, d.qx, d.qy, pathCommand[1], pathCommand[2]));
+ break;
+ case "Q":
+ d.qx = pathCommand[1];
+ d.qy = pathCommand[2];
+ pathCommand = ["C"].concat(me.quadratic2curve(d.x, d.y, pathCommand[1], pathCommand[2], pathCommand[3], pathCommand[4]));
+ break;
+ case "L":
+ pathCommand = ["C"].concat(d.x, d.y, pathCommand[1], pathCommand[2], pathCommand[1], pathCommand[2]);
+ break;
+ case "H":
+ pathCommand = ["C"].concat(d.x, d.y, pathCommand[1], d.y, pathCommand[1], d.y);
+ break;
+ case "V":
+ pathCommand = ["C"].concat(d.x, d.y, d.x, pathCommand[1], d.x, pathCommand[1]);
+ break;
+ case "Z":
+ pathCommand = ["C"].concat(d.x, d.y, d.X, d.Y, d.X, d.Y);
+ break;
+ }
+ return pathCommand;
+ },
+
+ quadratic2curve: function (x1, y1, ax, ay, x2, y2) {
+ var _13 = 1 / 3,
+ _23 = 2 / 3;
+ return [
+ _13 * x1 + _23 * ax,
+ _13 * y1 + _23 * ay,
+ _13 * x2 + _23 * ax,
+ _13 * y2 + _23 * ay,
+ x2,
+ y2
+ ];
+ },
+
+ rotate: function (x, y, rad) {
+ var cos = Math.cos(rad),
+ sin = Math.sin(rad),
+ X = x * cos - y * sin,
+ Y = x * sin + y * cos;
+ return {x: X, y: Y};
+ },
+
+ arc2curve: function (x1, y1, rx, ry, angle, large_arc_flag, sweep_flag, x2, y2, recursive) {
+ // for more information of where this Math came from visit:
+ // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
+ var me = this,
+ PI = Math.PI,
+ radian = me.radian,
+ _120 = PI * 120 / 180,
+ rad = radian * (+angle || 0),
+ res = [],
+ math = Math,
+ mcos = math.cos,
+ msin = math.sin,
+ msqrt = math.sqrt,
+ mabs = math.abs,
+ masin = math.asin,
+ xy, cos, sin, x, y, h, rx2, ry2, k, cx, cy, f1, f2, df, c1, s1, c2, s2,
+ t, hx, hy, m1, m2, m3, m4, newres, i, ln, f2old, x2old, y2old;
+ if (!recursive) {
+ xy = me.rotate(x1, y1, -rad);
+ x1 = xy.x;
+ y1 = xy.y;
+ xy = me.rotate(x2, y2, -rad);
+ x2 = xy.x;
+ y2 = xy.y;
+ cos = mcos(radian * angle);
+ sin = msin(radian * angle);
+ x = (x1 - x2) / 2;
+ y = (y1 - y2) / 2;
+ h = (x * x) / (rx * rx) + (y * y) / (ry * ry);
+ if (h > 1) {
+ h = msqrt(h);
+ rx = h * rx;
+ ry = h * ry;
+ }
+ rx2 = rx * rx;
+ ry2 = ry * ry;
+ k = (large_arc_flag == sweep_flag ? -1 : 1) *
+ msqrt(mabs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)));
+ cx = k * rx * y / ry + (x1 + x2) / 2;
+ cy = k * -ry * x / rx + (y1 + y2) / 2;
+ f1 = masin(((y1 - cy) / ry).toFixed(7));
+ f2 = masin(((y2 - cy) / ry).toFixed(7));
+
+ f1 = x1 < cx ? PI - f1 : f1;
+ f2 = x2 < cx ? PI - f2 : f2;
+ if (f1 < 0) {
+ f1 = PI * 2 + f1;
+ }
+ if (f2 < 0) {
+ f2 = PI * 2 + f2;
+ }
+ if (sweep_flag && f1 > f2) {
+ f1 = f1 - PI * 2;
+ }
+ if (!sweep_flag && f2 > f1) {
+ f2 = f2 - PI * 2;
+ }
+ }
+ else {
+ f1 = recursive[0];
+ f2 = recursive[1];
+ cx = recursive[2];
+ cy = recursive[3];
+ }
+ df = f2 - f1;
+ if (mabs(df) > _120) {
+ f2old = f2;
+ x2old = x2;
+ y2old = y2;
+ f2 = f1 + _120 * (sweep_flag && f2 > f1 ? 1 : -1);
+ x2 = cx + rx * mcos(f2);
+ y2 = cy + ry * msin(f2);
+ res = me.arc2curve(x2, y2, rx, ry, angle, 0, sweep_flag, x2old, y2old, [f2, f2old, cx, cy]);
+ }
+ df = f2 - f1;
+ c1 = mcos(f1);
+ s1 = msin(f1);
+ c2 = mcos(f2);
+ s2 = msin(f2);
+ t = math.tan(df / 4);
+ hx = 4 / 3 * rx * t;
+ hy = 4 / 3 * ry * t;
+ m1 = [x1, y1];
+ m2 = [x1 + hx * s1, y1 - hy * c1];
+ m3 = [x2 + hx * s2, y2 - hy * c2];
+ m4 = [x2, y2];
+ m2[0] = 2 * m1[0] - m2[0];
+ m2[1] = 2 * m1[1] - m2[1];
+ if (recursive) {
+ return [m2, m3, m4].concat(res);
+ }
+ else {
+ res = [m2, m3, m4].concat(res).join().split(",");
+ newres = [];
+ ln = res.length;
+ for (i = 0; i < ln; i++) {
+ newres[i] = i % 2 ? me.rotate(res[i - 1], res[i], rad).y : me.rotate(res[i], res[i + 1], rad).x;
+ }
+ return newres;
+ }
+ },
+
+ // TO BE DEPRECATED
+ rotateAndTranslatePath: function (sprite) {
+ var alpha = sprite.rotation.degrees,
+ cx = sprite.rotation.x,
+ cy = sprite.rotation.y,
+ dx = sprite.translation.x,
+ dy = sprite.translation.y,
+ path,
+ i,
+ p,
+ xy,
+ j,
+ res = [];
+ if (!alpha && !dx && !dy) {
+ return this.pathToAbsolute(sprite.attr.path);
+ }
+ dx = dx || 0;
+ dy = dy || 0;
+ path = this.pathToAbsolute(sprite.attr.path);
+ for (i = path.length; i--;) {
+ p = res[i] = path[i].slice();
+ if (p[0] == "A") {
+ xy = this.rotatePoint(p[6], p[7], alpha, cx, cy);
+ p[6] = xy.x + dx;
+ p[7] = xy.y + dy;
+ } else {
+ j = 1;
+ while (p[j + 1] != null) {
+ xy = this.rotatePoint(p[j], p[j + 1], alpha, cx, cy);
+ p[j] = xy.x + dx;
+ p[j + 1] = xy.y + dy;
+ j += 2;
+ }
+ }
+ }
+ return res;
+ },
+
+ // TO BE DEPRECATED
+ rotatePoint: function (x, y, alpha, cx, cy) {
+ if (!alpha) {
+ return {
+ x: x,
+ y: y
+ };
+ }
+ cx = cx || 0;
+ cy = cy || 0;
+ x = x - cx;
+ y = y - cy;
+ alpha = alpha * this.radian;
+ var cos = Math.cos(alpha),
+ sin = Math.sin(alpha);
+ return {
+ x: x * cos - y * sin + cx,
+ y: x * sin + y * cos + cy
+ };
+ },
+
+ pathDimensions: function (path) {
+ if (!path || !(path + "")) {
+ return {x: 0, y: 0, width: 0, height: 0};
+ }
+ path = this.path2curve(path);
+ var x = 0,
+ y = 0,
+ X = [],
+ Y = [],
+ i = 0,
+ ln = path.length,
+ p, xmin, ymin, dim;
+ for (; i < ln; i++) {
+ p = path[i];
+ if (p[0] == "M") {
+ x = p[1];
+ y = p[2];
+ X.push(x);
+ Y.push(y);
+ }
+ else {
+ dim = this.curveDim(x, y, p[1], p[2], p[3], p[4], p[5], p[6]);
+ X = X.concat(dim.min.x, dim.max.x);
+ Y = Y.concat(dim.min.y, dim.max.y);
+ x = p[5];
+ y = p[6];
+ }
+ }
+ xmin = Math.min.apply(0, X);
+ ymin = Math.min.apply(0, Y);
+ return {
+ x: xmin,
+ y: ymin,
+ path: path,
+ width: Math.max.apply(0, X) - xmin,
+ height: Math.max.apply(0, Y) - ymin
+ };
+ },
+
+ intersectInside: function(path, cp1, cp2) {
+ return (cp2[0] - cp1[0]) * (path[1] - cp1[1]) > (cp2[1] - cp1[1]) * (path[0] - cp1[0]);
+ },
+
+ intersectIntersection: function(s, e, cp1, cp2) {
+ var p = [],
+ dcx = cp1[0] - cp2[0],
+ dcy = cp1[1] - cp2[1],
+ dpx = s[0] - e[0],
+ dpy = s[1] - e[1],
+ n1 = cp1[0] * cp2[1] - cp1[1] * cp2[0],
+ n2 = s[0] * e[1] - s[1] * e[0],
+ n3 = 1 / (dcx * dpy - dcy * dpx);
+
+ p[0] = (n1 * dpx - n2 * dcx) * n3;
+ p[1] = (n1 * dpy - n2 * dcy) * n3;
+ return p;
+ },
+
+ intersect: function(subjectPolygon, clipPolygon) {
+ var me = this,
+ i = 0,
+ ln = clipPolygon.length,
+ cp1 = clipPolygon[ln - 1],
+ outputList = subjectPolygon,
+ cp2, s, e, point, ln2, inputList, j;
+ for (; i < ln; ++i) {
+ cp2 = clipPolygon[i];
+ inputList = outputList;
+ outputList = [];
+ s = inputList[inputList.length - 1];
+ j = 0;
+ ln2 = inputList.length;
+ for (; j < ln2; j++) {
+ e = inputList[j];
+ if (me.intersectInside(e, cp1, cp2)) {
+ if (!me.intersectInside(s, cp1, cp2)) {
+ outputList.push(me.intersectIntersection(s, e, cp1, cp2));
+ }
+ outputList.push(e);
+ }
+ else if (me.intersectInside(s, cp1, cp2)) {
+ outputList.push(me.intersectIntersection(s, e, cp1, cp2));
+ }
+ s = e;
+ }
+ cp1 = cp2;
+ }
+ return outputList;
+ },
+
+ curveDim: function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
+ var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x),
+ b = 2 * (c1x - p1x) - 2 * (c2x - c1x),
+ c = p1x - c1x,
+ t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a,
+ t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a,
+ y = [p1y, p2y],
+ x = [p1x, p2x],
+ dot;
+ if (Math.abs(t1) > 1e12) {
+ t1 = 0.5;
+ }
+ if (Math.abs(t2) > 1e12) {
+ t2 = 0.5;
+ }
+ if (t1 > 0 && t1 < 1) {
+ dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ if (t2 > 0 && t2 < 1) {
+ dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y);
+ b = 2 * (c1y - p1y) - 2 * (c2y - c1y);
+ c = p1y - c1y;
+ t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a;
+ t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a;
+ if (Math.abs(t1) > 1e12) {
+ t1 = 0.5;
+ }
+ if (Math.abs(t2) > 1e12) {
+ t2 = 0.5;
+ }
+ if (t1 > 0 && t1 < 1) {
+ dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ if (t2 > 0 && t2 < 1) {
+ dot = this.findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
+ x.push(dot.x);
+ y.push(dot.y);
+ }
+ return {
+ min: {x: Math.min.apply(0, x), y: Math.min.apply(0, y)},
+ max: {x: Math.max.apply(0, x), y: Math.max.apply(0, y)}
+ };
+ },
+
+ /**
+ * @private
+ *
+ * Calculates bezier curve control anchor points for a particular point in a path, with a
+ * smoothing curve applied. The smoothness of the curve is controlled by the 'value' parameter.
+ * Note that this algorithm assumes that the line being smoothed is normalized going from left
+ * to right; it makes special adjustments assuming this orientation.
+ *
+ * @param {Number} prevX X coordinate of the previous point in the path
+ * @param {Number} prevY Y coordinate of the previous point in the path
+ * @param {Number} curX X coordinate of the current point in the path
+ * @param {Number} curY Y coordinate of the current point in the path
+ * @param {Number} nextX X coordinate of the next point in the path
+ * @param {Number} nextY Y coordinate of the next point in the path
+ * @param {Number} value A value to control the smoothness of the curve; this is used to
+ * divide the distance between points, so a value of 2 corresponds to
+ * half the distance between points (a very smooth line) while higher values
+ * result in less smooth curves. Defaults to 4.
+ * @return {Object} Object containing x1, y1, x2, y2 bezier control anchor points; x1 and y1
+ * are the control point for the curve toward the previous path point, and
+ * x2 and y2 are the control point for the curve toward the next path point.
+ */
+ getAnchors: function (prevX, prevY, curX, curY, nextX, nextY, value) {
+ value = value || 4;
+ var M = Math,
+ PI = M.PI,
+ halfPI = PI / 2,
+ abs = M.abs,
+ sin = M.sin,
+ cos = M.cos,
+ atan = M.atan,
+ control1Length, control2Length, control1Angle, control2Angle,
+ control1X, control1Y, control2X, control2Y, alpha;
+
+ // Find the length of each control anchor line, by dividing the horizontal distance
+ // between points by the value parameter.
+ control1Length = (curX - prevX) / value;
+ control2Length = (nextX - curX) / value;
+
+ // Determine the angle of each control anchor line. If the middle point is a vertical
+ // turnaround then we force it to a flat horizontal angle to prevent the curve from
+ // dipping above or below the middle point. Otherwise we use an angle that points
+ // toward the previous/next target point.
+ if ((curY >= prevY && curY >= nextY) || (curY <= prevY && curY <= nextY)) {
+ control1Angle = control2Angle = halfPI;
+ } else {
+ control1Angle = atan((curX - prevX) / abs(curY - prevY));
+ if (prevY < curY) {
+ control1Angle = PI - control1Angle;
+ }
+ control2Angle = atan((nextX - curX) / abs(curY - nextY));
+ if (nextY < curY) {
+ control2Angle = PI - control2Angle;
+ }
+ }
+
+ // Adjust the calculated angles so they point away from each other on the same line
+ alpha = halfPI - ((control1Angle + control2Angle) % (PI * 2)) / 2;
+ if (alpha > halfPI) {
+ alpha -= PI;
+ }
+ control1Angle += alpha;
+ control2Angle += alpha;
+
+ // Find the control anchor points from the angles and length
+ control1X = curX - control1Length * sin(control1Angle);
+ control1Y = curY + control1Length * cos(control1Angle);
+ control2X = curX + control2Length * sin(control2Angle);
+ control2Y = curY + control2Length * cos(control2Angle);
+
+ // One last adjustment, make sure that no control anchor point extends vertically past
+ // its target prev/next point, as that results in curves dipping above or below and
+ // bending back strangely. If we find this happening we keep the control angle but
+ // reduce the length of the control line so it stays within bounds.
+ if ((curY > prevY && control1Y < prevY) || (curY < prevY && control1Y > prevY)) {
+ control1X += abs(prevY - control1Y) * (control1X - curX) / (control1Y - curY);
+ control1Y = prevY;
+ }
+ if ((curY > nextY && control2Y < nextY) || (curY < nextY && control2Y > nextY)) {
+ control2X -= abs(nextY - control2Y) * (control2X - curX) / (control2Y - curY);
+ control2Y = nextY;
+ }
+
+ return {
+ x1: control1X,
+ y1: control1Y,
+ x2: control2X,
+ y2: control2Y
+ };
+ },
+
+ /* Smoothing function for a path. Converts a path into cubic beziers. Value defines the divider of the distance between points.
+ * Defaults to a value of 4.
+ */
+ smooth: function (originalPath, value) {
+ var path = this.path2curve(originalPath),
+ newp = [path[0]],
+ x = path[0][1],
+ y = path[0][2],
+ j,
+ points,
+ i = 1,
+ ii = path.length,
+ beg = 1,
+ mx = x,
+ my = y,
+ cx = 0,
+ cy = 0;
+ for (; i < ii; i++) {
+ var pathi = path[i],
+ pathil = pathi.length,
+ pathim = path[i - 1],
+ pathiml = pathim.length,
+ pathip = path[i + 1],
+ pathipl = pathip && pathip.length;
+ if (pathi[0] == "M") {
+ mx = pathi[1];
+ my = pathi[2];
+ j = i + 1;
+ while (path[j][0] != "C") {
+ j++;
+ }
+ cx = path[j][5];
+ cy = path[j][6];
+ newp.push(["M", mx, my]);
+ beg = newp.length;
+ x = mx;
+ y = my;
+ continue;
+ }
+ if (pathi[pathil - 2] == mx && pathi[pathil - 1] == my && (!pathip || pathip[0] == "M")) {
+ var begl = newp[beg].length;
+ points = this.getAnchors(pathim[pathiml - 2], pathim[pathiml - 1], mx, my, newp[beg][begl - 2], newp[beg][begl - 1], value);
+ newp[beg][1] = points.x2;
+ newp[beg][2] = points.y2;
+ }
+ else if (!pathip || pathip[0] == "M") {
+ points = {
+ x1: pathi[pathil - 2],
+ y1: pathi[pathil - 1]
+ };
+ } else {
+ points = this.getAnchors(pathim[pathiml - 2], pathim[pathiml - 1], pathi[pathil - 2], pathi[pathil - 1], pathip[pathipl - 2], pathip[pathipl - 1], value);
+ }
+ newp.push(["C", x, y, points.x1, points.y1, pathi[pathil - 2], pathi[pathil - 1]]);
+ x = points.x2;
+ y = points.y2;
+ }
+ return newp;
+ },
+
+ findDotAtSegment: function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
+ var t1 = 1 - t;
+ return {
+ x: Math.pow(t1, 3) * p1x + Math.pow(t1, 2) * 3 * t * c1x + t1 * 3 * t * t * c2x + Math.pow(t, 3) * p2x,
+ y: Math.pow(t1, 3) * p1y + Math.pow(t1, 2) * 3 * t * c1y + t1 * 3 * t * t * c2y + Math.pow(t, 3) * p2y
+ };
+ },
+
+ /**
+ * A utility method to deduce an appropriate tick configuration for the data set of given
+ * feature.
+ *
+ * @param {Number/Date} from The minimum value in the data
+ * @param {Number/Date} to The maximum value in the data
+ * @param {Number} stepsMax The maximum number of ticks
+ * @return {Object} The calculated step and ends info; When `from` and `to` are Dates, refer to the
+ * return value of {@link #snapEndsByDate}. For numerical `from` and `to` the return value contains:
+ * @return {Number} return.from The result start value, which may be lower than the original start value
+ * @return {Number} return.to The result end value, which may be higher than the original end value
+ * @return {Number} return.power The calculate power.
+ * @return {Number} return.step The value size of each step
+ * @return {Number} return.steps The number of steps.
+ */
+ snapEnds: function (from, to, stepsMax) {
+ if (Ext.isDate(from)) {
+ return this.snapEndsByDate(from, to, stepsMax);
+ }
+ var step = (to - from) / stepsMax,
+ level = Math.floor(Math.log(step) / Math.LN10) + 1,
+ m = Math.pow(10, level),
+ cur,
+ modulo = Math.round((step % m) * Math.pow(10, 2 - level)),
+ interval = [[0, 15], [20, 4], [30, 2], [40, 4], [50, 9], [60, 4], [70, 2], [80, 4], [100, 15]],
+ stepCount = 0,
+ value,
+ weight,
+ i,
+ topValue,
+ topWeight = 1e9,
+ ln = interval.length;
+ cur = from = Math.floor(from / m) * m;
+ for (i = 0; i < ln; i++) {
+ value = interval[i][0];
+ weight = (value - modulo) < 0 ? 1e6 : (value - modulo) / interval[i][1];
+ if (weight < topWeight) {
+ topValue = value;
+ topWeight = weight;
+ }
+ }
+ step = Math.floor(step * Math.pow(10, -level)) * Math.pow(10, level) + topValue * Math.pow(10, level - 2);
+ while (cur < to) {
+ cur += step;
+ stepCount++;
+ }
+ to = +cur.toFixed(10);
+ return {
+ from: from,
+ to: to,
+ power: level,
+ step: step,
+ steps: stepCount
+ };
+ },
+
+ /**
+ * A utility method to deduce an appropriate tick configuration for the data set of given
+ * feature when data is Dates. Refer to {@link #snapEnds} for numeric data.
+ *
+ * @param {Date} from The minimum value in the data
+ * @param {Date} to The maximum value in the data
+ * @param {Number} stepsMax The maximum number of ticks
+ * @param {Boolean} lockEnds If true, the 'from' and 'to' parameters will be used as fixed end values
+ * and will not be adjusted
+ * @return {Object} The calculated step and ends info; properties are:
+ * @return {Date} return.from The result start value, which may be lower than the original start value
+ * @return {Date} return.to The result end value, which may be higher than the original end value
+ * @return {Number} return.step The value size of each step
+ * @return {Number} return.steps The number of steps.
+ * NOTE: the steps may not divide the from/to range perfectly evenly;
+ * there may be a smaller distance between the last step and the end value than between prior
+ * steps, particularly when the `endsLocked` param is true. Therefore it is best to not use
+ * the `steps` result when finding the axis tick points, instead use the `step`, `to`, and
+ * `from` to find the correct point for each tick.
+ */
+ snapEndsByDate: function (from, to, stepsMax, lockEnds) {
+ var selectedStep = false, scales = [
+ [Ext.Date.MILLI, [1, 2, 3, 5, 10, 20, 30, 50, 100, 200, 300, 500]],
+ [Ext.Date.SECOND, [1, 2, 3, 5, 10, 15, 30]],
+ [Ext.Date.MINUTE, [1, 2, 3, 5, 10, 20, 30]],
+ [Ext.Date.HOUR, [1, 2, 3, 4, 6, 12]],
+ [Ext.Date.DAY, [1, 2, 3, 7, 14]],
+ [Ext.Date.MONTH, [1, 2, 3, 4, 6]]
+ ], j, yearDiff;
+
+ // Find the most desirable scale
+ Ext.each(scales, function(scale, i) {
+ for (j = 0; j < scale[1].length; j++) {
+ if (to < Ext.Date.add(from, scale[0], scale[1][j] * stepsMax)) {
+ selectedStep = [scale[0], scale[1][j]];
+ return false;
+ }
+ }
+ });
+ if (!selectedStep) {
+ yearDiff = this.snapEnds(from.getFullYear(), to.getFullYear() + 1, stepsMax, lockEnds);
+ selectedStep = [Date.YEAR, Math.round(yearDiff.step)];
+ }
+ return this.snapEndsByDateAndStep(from, to, selectedStep, lockEnds);
+ },
+
+
+ /**
+ * A utility method to deduce an appropriate tick configuration for the data set of given
+ * feature and specific step size.
+ * @param {Date} from The minimum value in the data
+ * @param {Date} to The maximum value in the data
+ * @param {Array} step An array with two components: The first is the unit of the step (day, month, year, etc).
+ * The second one is the number of units for the step (1, 2, etc.).
+ * @param {Boolean} lockEnds If true, the 'from' and 'to' parameters will be used as fixed end values
+ * and will not be adjusted
+ * @return {Object} See the return value of {@link #snapEndsByDate}.
+ */
+ snapEndsByDateAndStep: function(from, to, step, lockEnds) {
+ var fromStat = [from.getFullYear(), from.getMonth(), from.getDate(),
+ from.getHours(), from.getMinutes(), from.getSeconds(), from.getMilliseconds()],
+ steps = 0, testFrom, testTo;
+ if (lockEnds) {
+ testFrom = from;
+ } else {
+ switch (step[0]) {
+ case Ext.Date.MILLI:
+ testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
+ fromStat[4], fromStat[5], Math.floor(fromStat[6] / step[1]) * step[1]);
+ break;
+ case Ext.Date.SECOND:
+ testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
+ fromStat[4], Math.floor(fromStat[5] / step[1]) * step[1], 0);
+ break;
+ case Ext.Date.MINUTE:
+ testFrom = new Date(fromStat[0], fromStat[1], fromStat[2], fromStat[3],
+ Math.floor(fromStat[4] / step[1]) * step[1], 0, 0);
+ break;
+ case Ext.Date.HOUR:
+ testFrom = new Date(fromStat[0], fromStat[1], fromStat[2],
+ Math.floor(fromStat[3] / step[1]) * step[1], 0, 0, 0);
+ break;
+ case Ext.Date.DAY:
+ testFrom = new Date(fromStat[0], fromStat[1],
+ Math.floor(fromStat[2] - 1 / step[1]) * step[1] + 1, 0, 0, 0, 0);
+ break;
+ case Ext.Date.MONTH:
+ testFrom = new Date(fromStat[0], Math.floor(fromStat[1] / step[1]) * step[1], 1, 0, 0, 0, 0);
+ break;
+ default: // Ext.Date.YEAR
+ testFrom = new Date(Math.floor(fromStat[0] / step[1]) * step[1], 0, 1, 0, 0, 0, 0);
+ break;
+ }
+ }
+
+ testTo = testFrom;
+ // TODO(zhangbei) : We can do it better somehow...
+ while (testTo < to) {
+ testTo = Ext.Date.add(testTo, step[0], step[1]);
+ steps++;
+ }
+
+ if (lockEnds) {
+ testTo = to;
+ }
+ return {
+ from : +testFrom,
+ to : +testTo,
+ step : (testTo - testFrom) / steps,
+ steps : steps
+ };
+ },
+
+ sorter: function (a, b) {
+ return a.offset - b.offset;
+ },
+
+ rad: function(degrees) {
+ return degrees % 360 * Math.PI / 180;
+ },
+
+ degrees: function(radian) {
+ return radian * 180 / Math.PI % 360;
+ },
+
+ withinBox: function(x, y, bbox) {
+ bbox = bbox || {};
+ return (x >= bbox.x && x <= (bbox.x + bbox.width) && y >= bbox.y && y <= (bbox.y + bbox.height));
+ },
+
+ parseGradient: function(gradient) {
+ var me = this,
+ type = gradient.type || 'linear',
+ angle = gradient.angle || 0,
+ radian = me.radian,
+ stops = gradient.stops,
+ stopsArr = [],
+ stop,
+ vector,
+ max,
+ stopObj;
+
+ if (type == 'linear') {
+ vector = [0, 0, Math.cos(angle * radian), Math.sin(angle * radian)];
+ max = 1 / (Math.max(Math.abs(vector[2]), Math.abs(vector[3])) || 1);
+ vector[2] *= max;
+ vector[3] *= max;
+ if (vector[2] < 0) {
+ vector[0] = -vector[2];
+ vector[2] = 0;
+ }
+ if (vector[3] < 0) {
+ vector[1] = -vector[3];
+ vector[3] = 0;
+ }
+ }
+
+ for (stop in stops) {
+ if (stops.hasOwnProperty(stop) && me.stopsRE.test(stop)) {
+ stopObj = {
+ offset: parseInt(stop, 10),
+ color: Ext.draw.Color.toHex(stops[stop].color) || '#ffffff',
+ opacity: stops[stop].opacity || 1
+ };
+ stopsArr.push(stopObj);
+ }
+ }
+ // Sort by pct property
+ Ext.Array.sort(stopsArr, me.sorter);
+ if (type == 'linear') {
+ return {
+ id: gradient.id,
+ type: type,
+ vector: vector,
+ stops: stopsArr
+ };
+ }
+ else {
+ return {
+ id: gradient.id,
+ type: type,
+ centerX: gradient.centerX,
+ centerY: gradient.centerY,
+ focalX: gradient.focalX,
+ focalY: gradient.focalY,
+ radius: gradient.radius,
+ vector: vector,
+ stops: stopsArr
+ };
+ }
+ }
+});
+
+
+/**
+ * @class Ext.fx.PropertyHandler
+ * @ignore
+ */
+Ext.define('Ext.fx.PropertyHandler', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.draw.Draw'],
+
+ statics: {
+ defaultHandler: {
+ pixelDefaultsRE: /width|height|top$|bottom$|left$|right$/i,
+ unitRE: /^(-?\d*\.?\d*){1}(em|ex|px|in|cm|mm|pt|pc|%)*$/,
+ scrollRE: /^scroll/i,
+
+ computeDelta: function(from, end, damper, initial, attr) {
+ damper = (typeof damper == 'number') ? damper : 1;
+ var unitRE = this.unitRE,
+ match = unitRE.exec(from),
+ start, units;
+ if (match) {
+ from = match[1];
+ units = match[2];
+ if (!this.scrollRE.test(attr) && !units && this.pixelDefaultsRE.test(attr)) {
+ units = 'px';
+ }
+ }
+ from = +from || 0;
+
+ match = unitRE.exec(end);
+ if (match) {
+ end = match[1];
+ units = match[2] || units;
+ }
+ end = +end || 0;
+ start = (initial != null) ? initial : from;
+ return {
+ from: from,
+ delta: (end - start) * damper,
+ units: units
+ };
+ },
+
+ get: function(from, end, damper, initialFrom, attr) {
+ var ln = from.length,
+ out = [],
+ i, initial, res, j, len;
+ for (i = 0; i < ln; i++) {
+ if (initialFrom) {
+ initial = initialFrom[i][1].from;
+ }
+ if (Ext.isArray(from[i][1]) && Ext.isArray(end)) {
+ res = [];
+ j = 0;
+ len = from[i][1].length;
+ for (; j < len; j++) {
+ res.push(this.computeDelta(from[i][1][j], end[j], damper, initial, attr));
+ }
+ out.push([from[i][0], res]);
+ }
+ else {
+ out.push([from[i][0], this.computeDelta(from[i][1], end, damper, initial, attr)]);
+ }
+ }
+ return out;
+ },
+
+ set: function(values, easing) {
+ var ln = values.length,
+ out = [],
+ i, val, res, len, j;
+ for (i = 0; i < ln; i++) {
+ val = values[i][1];
+ if (Ext.isArray(val)) {
+ res = [];
+ j = 0;
+ len = val.length;
+ for (; j < len; j++) {
+ res.push(val[j].from + (val[j].delta * easing) + (val[j].units || 0));
+ }
+ out.push([values[i][0], res]);
+ } else {
+ out.push([values[i][0], val.from + (val.delta * easing) + (val.units || 0)]);
+ }
+ }
+ return out;
+ }
+ },
+ color: {
+ rgbRE: /^rgb\(([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\)$/i,
+ hexRE: /^#?([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i,
+ hex3RE: /^#?([0-9A-F]{1})([0-9A-F]{1})([0-9A-F]{1})$/i,
+
+ parseColor : function(color, damper) {
+ damper = (typeof damper == 'number') ? damper : 1;
+ var base,
+ out = false,
+ match;
+
+ Ext.each([this.hexRE, this.rgbRE, this.hex3RE], function(re, idx) {
+ base = (idx % 2 == 0) ? 16 : 10;
+ match = re.exec(color);
+ if (match && match.length == 4) {
+ if (idx == 2) {
+ match[1] += match[1];
+ match[2] += match[2];
+ match[3] += match[3];
+ }
+ out = {
+ red: parseInt(match[1], base),
+ green: parseInt(match[2], base),
+ blue: parseInt(match[3], base)
+ };
+ return false;
+ }
+ });
+ return out || color;
+ },
+
+ computeDelta: function(from, end, damper, initial) {
+ from = this.parseColor(from);
+ end = this.parseColor(end, damper);
+ var start = initial ? initial : from,
+ tfrom = typeof start,
+ tend = typeof end;
+ //Extra check for when the color string is not recognized.
+ if (tfrom == 'string' || tfrom == 'undefined'
+ || tend == 'string' || tend == 'undefined') {
+ return end || start;
+ }
+ return {
+ from: from,
+ delta: {
+ red: Math.round((end.red - start.red) * damper),
+ green: Math.round((end.green - start.green) * damper),
+ blue: Math.round((end.blue - start.blue) * damper)
+ }
+ };
+ },
+
+ get: function(start, end, damper, initialFrom) {
+ var ln = start.length,
+ out = [],
+ i, initial;
+ for (i = 0; i < ln; i++) {
+ if (initialFrom) {
+ initial = initialFrom[i][1].from;
+ }
+ out.push([start[i][0], this.computeDelta(start[i][1], end, damper, initial)]);
+ }
+ return out;
+ },
+
+ set: function(values, easing) {
+ var ln = values.length,
+ out = [],
+ i, val, parsedString, from, delta;
+ for (i = 0; i < ln; i++) {
+ val = values[i][1];
+ if (val) {
+ from = val.from;
+ delta = val.delta;
+ //multiple checks to reformat the color if it can't recognized by computeDelta.
+ val = (typeof val == 'object' && 'red' in val)?
+ 'rgb(' + val.red + ', ' + val.green + ', ' + val.blue + ')' : val;
+ val = (typeof val == 'object' && val.length)? val[0] : val;
+ if (typeof val == 'undefined') {
+ return [];
+ }
+ parsedString = typeof val == 'string'? val :
+ 'rgb(' + [
+ (from.red + Math.round(delta.red * easing)) % 256,
+ (from.green + Math.round(delta.green * easing)) % 256,
+ (from.blue + Math.round(delta.blue * easing)) % 256
+ ].join(',') + ')';
+ out.push([
+ values[i][0],
+ parsedString
+ ]);
+ }
+ }
+ return out;
+ }
+ },
+ object: {
+ interpolate: function(prop, damper) {
+ damper = (typeof damper == 'number') ? damper : 1;
+ var out = {},
+ p;
+ for(p in prop) {
+ out[p] = parseInt(prop[p], 10) * damper;
+ }
+ return out;
+ },
+
+ computeDelta: function(from, end, damper, initial) {
+ from = this.interpolate(from);
+ end = this.interpolate(end, damper);
+ var start = initial ? initial : from,
+ delta = {},
+ p;
+
+ for(p in end) {
+ delta[p] = end[p] - start[p];
+ }
+ return {
+ from: from,
+ delta: delta
+ };
+ },
+
+ get: function(start, end, damper, initialFrom) {
+ var ln = start.length,
+ out = [],
+ i, initial;
+ for (i = 0; i < ln; i++) {
+ if (initialFrom) {
+ initial = initialFrom[i][1].from;
+ }
+ out.push([start[i][0], this.computeDelta(start[i][1], end, damper, initial)]);
+ }
+ return out;
+ },
+
+ set: function(values, easing) {
+ var ln = values.length,
+ out = [],
+ outObject = {},
+ i, from, delta, val, p;
+ for (i = 0; i < ln; i++) {
+ val = values[i][1];
+ from = val.from;
+ delta = val.delta;
+ for (p in from) {
+ outObject[p] = Math.round(from[p] + delta[p] * easing);
+ }
+ out.push([
+ values[i][0],
+ outObject
+ ]);
+ }
+ return out;
+ }
+ },
+
+ path: {
+ computeDelta: function(from, end, damper, initial) {
+ damper = (typeof damper == 'number') ? damper : 1;
+ var start;
+ from = +from || 0;
+ end = +end || 0;
+ start = (initial != null) ? initial : from;
+ return {
+ from: from,
+ delta: (end - start) * damper
+ };
+ },
+
+ forcePath: function(path) {
+ if (!Ext.isArray(path) && !Ext.isArray(path[0])) {
+ path = Ext.draw.Draw.parsePathString(path);
+ }
+ return path;
+ },
+
+ get: function(start, end, damper, initialFrom) {
+ var endPath = this.forcePath(end),
+ out = [],
+ startLn = start.length,
+ startPathLn, pointsLn, i, deltaPath, initial, j, k, path, startPath;
+ for (i = 0; i < startLn; i++) {
+ startPath = this.forcePath(start[i][1]);
+
+ deltaPath = Ext.draw.Draw.interpolatePaths(startPath, endPath);
+ startPath = deltaPath[0];
+ endPath = deltaPath[1];
+
+ startPathLn = startPath.length;
+ path = [];
+ for (j = 0; j < startPathLn; j++) {
+ deltaPath = [startPath[j][0]];
+ pointsLn = startPath[j].length;
+ for (k = 1; k < pointsLn; k++) {
+ initial = initialFrom && initialFrom[0][1][j][k].from;
+ deltaPath.push(this.computeDelta(startPath[j][k], endPath[j][k], damper, initial));
+ }
+ path.push(deltaPath);
+ }
+ out.push([start[i][0], path]);
+ }
+ return out;
+ },
+
+ set: function(values, easing) {
+ var ln = values.length,
+ out = [],
+ i, j, k, newPath, calcPath, deltaPath, deltaPathLn, pointsLn;
+ for (i = 0; i < ln; i++) {
+ deltaPath = values[i][1];
+ newPath = [];
+ deltaPathLn = deltaPath.length;
+ for (j = 0; j < deltaPathLn; j++) {
+ calcPath = [deltaPath[j][0]];
+ pointsLn = deltaPath[j].length;
+ for (k = 1; k < pointsLn; k++) {
+ calcPath.push(deltaPath[j][k].from + deltaPath[j][k].delta * easing);
+ }
+ newPath.push(calcPath.join(','));
+ }
+ out.push([values[i][0], newPath.join(',')]);
+ }
+ return out;
+ }
+ }
+ /* End Definitions */
+ }
+}, function() {
+ Ext.each([
+ 'outlineColor',
+ 'backgroundColor',
+ 'borderColor',
+ 'borderTopColor',
+ 'borderRightColor',
+ 'borderBottomColor',
+ 'borderLeftColor',
+ 'fill',
+ 'stroke'
+ ], function(prop) {
+ this[prop] = this.color;
+ }, this);
+});
+/**
+ * @class Ext.fx.Anim
+ *
+ * This class manages animation for a specific {@link #target}. The animation allows
+ * animation of various properties on the target, such as size, position, color and others.
+ *
+ * ## Starting Conditions
+ * The starting conditions for the animation are provided by the {@link #from} configuration.
+ * Any/all of the properties in the {@link #from} configuration can be specified. If a particular
+ * property is not defined, the starting value for that property will be read directly from the target.
+ *
+ * ## End Conditions
+ * The ending conditions for the animation are provided by the {@link #to} configuration. These mark
+ * the final values once the animations has finished. The values in the {@link #from} can mirror
+ * those in the {@link #to} configuration to provide a starting point.
+ *
+ * ## Other Options
+ * - {@link #duration}: Specifies the time period of the animation.
+ * - {@link #easing}: Specifies the easing of the animation.
+ * - {@link #iterations}: Allows the animation to repeat a number of times.
+ * - {@link #alternate}: Used in conjunction with {@link #iterations}, reverses the direction every second iteration.
+ *
+ * ## Example Code
+ *
+ * @example
+ * var myComponent = Ext.create('Ext.Component', {
+ * renderTo: document.body,
+ * width: 200,
+ * height: 200,
+ * style: 'border: 1px solid red;'
+ * });
+ *
+ * Ext.create('Ext.fx.Anim', {
+ * target: myComponent,
+ * duration: 1000,
+ * from: {
+ * width: 400 //starting width 400
+ * },
+ * to: {
+ * width: 300, //end width 300
+ * height: 300 // end width 300
+ * }
+ * });
+ */
+Ext.define('Ext.fx.Anim', {
+
+ /* Begin Definitions */
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ requires: ['Ext.fx.Manager', 'Ext.fx.Animator', 'Ext.fx.Easing', 'Ext.fx.CubicBezier', 'Ext.fx.PropertyHandler'],
+
+ /* End Definitions */
+
+ isAnimation: true,
+
+ /**
+ * @cfg {Function} callback
+ * A function to be run after the animation has completed.
+ */
+
+ /**
+ * @cfg {Function} scope
+ * The scope that the {@link #callback} function will be called with
+ */
+
+ /**
+ * @cfg {Number} duration
+ * Time in milliseconds for a single animation to last. Defaults to 250. If the {@link #iterations} property is
+ * specified, then each animate will take the same duration for each iteration.
+ */
+ duration: 250,
+
+ /**
+ * @cfg {Number} delay
+ * Time to delay before starting the animation. Defaults to 0.
+ */
+ delay: 0,
+
+ /* private used to track a delayed starting time */
+ delayStart: 0,
+
+ /**
+ * @cfg {Boolean} dynamic
+ * Currently only for Component Animation: Only set a component's outer element size bypassing layouts. Set to true to do full layouts for every frame of the animation. Defaults to false.
+ */
+ dynamic: false,
+
+ /**
+ * @cfg {String} easing
+This describes how the intermediate values used during a transition will be calculated. It allows for a transition to change
+speed over its duration.
+
+ -backIn
+ -backOut
+ -bounceIn
+ -bounceOut
+ -ease
+ -easeIn
+ -easeOut
+ -easeInOut
+ -elasticIn
+ -elasticOut
+ -cubic-bezier(x1, y1, x2, y2)
+
+Note that cubic-bezier will create a custom easing curve following the CSS3 [transition-timing-function][0]
+specification. The four values specify points P1 and P2 of the curve as (x1, y1, x2, y2). All values must
+be in the range [0, 1] or the definition is invalid.
+
+[0]: http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
+
+ * @markdown
+ */
+ easing: 'ease',
+
+ /**
+ * @cfg {Object} keyframes
+ * Animation keyframes follow the CSS3 Animation configuration pattern. 'from' is always considered '0%' and 'to'
+ * is considered '100%'.<b>Every keyframe declaration must have a keyframe rule for 0% and 100%, possibly defined using
+ * "from" or "to"</b>. A keyframe declaration without these keyframe selectors is invalid and will not be available for
+ * animation. The keyframe declaration for a keyframe rule consists of properties and values. Properties that are unable to
+ * be animated are ignored in these rules, with the exception of 'easing' which can be changed at each keyframe. For example:
+ <pre><code>
+keyframes : {
+ '0%': {
+ left: 100
+ },
+ '40%': {
+ left: 150
+ },
+ '60%': {
+ left: 75
+ },
+ '100%': {
+ left: 100
+ }
+}
+ </code></pre>
+ */
+
+ /**
+ * @private
+ */
+ damper: 1,
+
+ /**
+ * @private
+ */
+ bezierRE: /^(?:cubic-)?bezier\(([^,]+),([^,]+),([^,]+),([^\)]+)\)/,
+
+ /**
+ * Run the animation from the end to the beginning
+ * Defaults to false.
+ * @cfg {Boolean} reverse
+ */
+ reverse: false,
+
+ /**
+ * Flag to determine if the animation has started
+ * @property running
+ * @type Boolean
+ */
+ running: false,
+
+ /**
+ * Flag to determine if the animation is paused. Only set this to true if you need to
+ * keep the Anim instance around to be unpaused later; otherwise call {@link #end}.
+ * @property paused
+ * @type Boolean
+ */
+ paused: false,
+
+ /**
+ * Number of times to execute the animation. Defaults to 1.
+ * @cfg {Number} iterations
+ */
+ iterations: 1,
+
+ /**
+ * Used in conjunction with iterations to reverse the animation each time an iteration completes.
+ * @cfg {Boolean} alternate
+ * Defaults to false.
+ */
+ alternate: false,
+
+ /**
+ * Current iteration the animation is running.
+ * @property currentIteration
+ * @type Number
+ */
+ currentIteration: 0,
+
+ /**
+ * Starting time of the animation.
+ * @property startTime
+ * @type Date
+ */
+ startTime: 0,
+
+ /**
+ * Contains a cache of the interpolators to be used.
+ * @private
+ * @property propHandlers
+ * @type Object
+ */
+
+ /**
+ * @cfg {String/Object} target
+ * The {@link Ext.fx.target.Target} to apply the animation to. This should only be specified when creating an Ext.fx.Anim directly.
+ * The target does not need to be a {@link Ext.fx.target.Target} instance, it can be the underlying object. For example, you can
+ * pass a Component, Element or Sprite as the target and the Anim will create the appropriate {@link Ext.fx.target.Target} object
+ * automatically.
+ */
+
+ /**
+ * @cfg {Object} from
+ * An object containing property/value pairs for the beginning of the animation. If not specified, the current state of the
+ * Ext.fx.target will be used. For example:
+<pre><code>
+from : {
+ opacity: 0, // Transparent
+ color: '#ffffff', // White
+ left: 0
+}
+</code></pre>
+ */
+
+ /**
+ * @cfg {Object} to
+ * An object containing property/value pairs for the end of the animation. For example:
+ <pre><code>
+ to : {
+ opacity: 1, // Opaque
+ color: '#00ff00', // Green
+ left: 500
+ }
+ </code></pre>
+ */
+
+ // @private
+ constructor: function(config) {
+ var me = this,
+ curve;
+
+ config = config || {};
+ // If keyframes are passed, they really want an Animator instead.
+ if (config.keyframes) {
+ return Ext.create('Ext.fx.Animator', config);
+ }
+ config = Ext.apply(me, config);
+ if (me.from === undefined) {
+ me.from = {};
+ }
+ me.propHandlers = {};
+ me.config = config;
+ me.target = Ext.fx.Manager.createTarget(me.target);
+ me.easingFn = Ext.fx.Easing[me.easing];
+ me.target.dynamic = me.dynamic;
+
+ // If not a pre-defined curve, try a cubic-bezier
+ if (!me.easingFn) {
+ me.easingFn = String(me.easing).match(me.bezierRE);
+ if (me.easingFn && me.easingFn.length == 5) {
+ curve = me.easingFn;
+ me.easingFn = Ext.fx.CubicBezier.cubicBezier(+curve[1], +curve[2], +curve[3], +curve[4]);
+ }
+ }
+ me.id = Ext.id(null, 'ext-anim-');
+ Ext.fx.Manager.addAnim(me);
+ me.addEvents(
+ /**
+ * @event beforeanimate
+ * Fires before the animation starts. A handler can return false to cancel the animation.
+ * @param {Ext.fx.Anim} this
+ */
+ 'beforeanimate',
+ /**
+ * @event afteranimate
+ * Fires when the animation is complete.
+ * @param {Ext.fx.Anim} this
+ * @param {Date} startTime
+ */
+ 'afteranimate',
+ /**
+ * @event lastframe
+ * Fires when the animation's last frame has been set.
+ * @param {Ext.fx.Anim} this
+ * @param {Date} startTime
+ */
+ 'lastframe'
+ );
+ me.mixins.observable.constructor.call(me, config);
+ if (config.callback) {
+ me.on('afteranimate', config.callback, config.scope);
+ }
+ return me;
+ },
+
+ /**
+ * @private
+ * Helper to the target
+ */
+ setAttr: function(attr, value) {
+ return Ext.fx.Manager.items.get(this.id).setAttr(this.target, attr, value);
+ },
+
+ /**
+ * @private
+ * Set up the initial currentAttrs hash.
+ */
+ initAttrs: function() {
+ var me = this,
+ from = me.from,
+ to = me.to,
+ initialFrom = me.initialFrom || {},
+ out = {},
+ start, end, propHandler, attr;
+
+ for (attr in to) {
+ if (to.hasOwnProperty(attr)) {
+ start = me.target.getAttr(attr, from[attr]);
+ end = to[attr];
+ // Use default (numeric) property handler
+ if (!Ext.fx.PropertyHandler[attr]) {
+ if (Ext.isObject(end)) {
+ propHandler = me.propHandlers[attr] = Ext.fx.PropertyHandler.object;
+ } else {
+ propHandler = me.propHandlers[attr] = Ext.fx.PropertyHandler.defaultHandler;
+ }
+ }
+ // Use custom handler
+ else {
+ propHandler = me.propHandlers[attr] = Ext.fx.PropertyHandler[attr];
+ }
+ out[attr] = propHandler.get(start, end, me.damper, initialFrom[attr], attr);
+ }
+ }
+ me.currentAttrs = out;
+ },
+
+ /**
+ * @private
+ * Fires beforeanimate and sets the running flag.
+ */
+ start: function(startTime) {
+ var me = this,
+ delay = me.delay,
+ delayStart = me.delayStart,
+ delayDelta;
+ if (delay) {
+ if (!delayStart) {
+ me.delayStart = startTime;
+ return;
+ }
+ else {
+ delayDelta = startTime - delayStart;
+ if (delayDelta < delay) {
+ return;
+ }
+ else {
+ // Compensate for frame delay;
+ startTime = new Date(delayStart.getTime() + delay);
+ }
+ }
+ }
+ if (me.fireEvent('beforeanimate', me) !== false) {
+ me.startTime = startTime;
+ if (!me.paused && !me.currentAttrs) {
+ me.initAttrs();
+ }
+ me.running = true;
+ }
+ },
+
+ /**
+ * @private
+ * Calculate attribute value at the passed timestamp.
+ * @returns a hash of the new attributes.
+ */
+ runAnim: function(elapsedTime) {
+ var me = this,
+ attrs = me.currentAttrs,
+ duration = me.duration,
+ easingFn = me.easingFn,
+ propHandlers = me.propHandlers,
+ ret = {},
+ easing, values, attr, lastFrame;
+
+ if (elapsedTime >= duration) {
+ elapsedTime = duration;
+ lastFrame = true;
+ }
+ if (me.reverse) {
+ elapsedTime = duration - elapsedTime;
+ }
+
+ for (attr in attrs) {
+ if (attrs.hasOwnProperty(attr)) {
+ values = attrs[attr];
+ easing = lastFrame ? 1 : easingFn(elapsedTime / duration);
+ ret[attr] = propHandlers[attr].set(values, easing);
+ }
+ }
+ return ret;
+ },
+
+ /**
+ * @private
+ * Perform lastFrame cleanup and handle iterations
+ * @returns a hash of the new attributes.
+ */
+ lastFrame: function() {
+ var me = this,
+ iter = me.iterations,
+ iterCount = me.currentIteration;
+
+ iterCount++;
+ if (iterCount < iter) {
+ if (me.alternate) {
+ me.reverse = !me.reverse;
+ }
+ me.startTime = new Date();
+ me.currentIteration = iterCount;
+ // Turn off paused for CSS3 Transitions
+ me.paused = false;
+ }
+ else {
+ me.currentIteration = 0;
+ me.end();
+ me.fireEvent('lastframe', me, me.startTime);
+ }
+ },
+
+ /**
+ * Fire afteranimate event and end the animation. Usually called automatically when the
+ * animation reaches its final frame, but can also be called manually to pre-emptively
+ * stop and destroy the running animation.
+ */
+ end: function() {
+ var me = this;
+ me.startTime = 0;
+ me.paused = false;
+ me.running = false;
+ Ext.fx.Manager.removeAnim(me);
+ me.fireEvent('afteranimate', me, me.startTime);
+ }
+});
+// Set flag to indicate that Fx is available. Class might not be available immediately.
+Ext.enableFx = true;
+
+/*
+ * This is a derivative of the similarly named class in the YUI Library.
+ * The original license:
+ * Copyright (c) 2006, Yahoo! Inc. All rights reserved.
+ * Code licensed under the BSD License:
+ * http://developer.yahoo.net/yui/license.txt
+ */
+
+
+/**
+ * Defines the interface and base operation of items that that can be
+ * dragged or can be drop targets. It was designed to be extended, overriding
+ * the event handlers for startDrag, onDrag, onDragOver and onDragOut.
+ * Up to three html elements can be associated with a DragDrop instance:
+ *
+ * - linked element: the element that is passed into the constructor.
+ * This is the element which defines the boundaries for interaction with
+ * other DragDrop objects.
+ *
+ * - handle element(s): The drag operation only occurs if the element that
+ * was clicked matches a handle element. By default this is the linked
+ * element, but there are times that you will want only a portion of the
+ * linked element to initiate the drag operation, and the setHandleElId()
+ * method provides a way to define this.
+ *
+ * - drag element: this represents the element that would be moved along
+ * with the cursor during a drag operation. By default, this is the linked
+ * element itself as in {@link Ext.dd.DD}. setDragElId() lets you define
+ * a separate element that would be moved, as in {@link Ext.dd.DDProxy}.
+ *
+ * This class should not be instantiated until the onload event to ensure that
+ * the associated elements are available.
+ * The following would define a DragDrop obj that would interact with any
+ * other DragDrop obj in the "group1" group:
+ *
+ * dd = new Ext.dd.DragDrop("div1", "group1");
+ *
+ * Since none of the event handlers have been implemented, nothing would
+ * actually happen if you were to run the code above. Normally you would
+ * override this class or one of the default implementations, but you can
+ * also override the methods you want on an instance of the class...
+ *
+ * dd.onDragDrop = function(e, id) {
+ * alert("dd was dropped on " + id);
+ * }
+ *
+ */
+Ext.define('Ext.dd.DragDrop', {
+ requires: ['Ext.dd.DragDropManager'],
+
+ /**
+ * Creates new DragDrop.
+ * @param {String} id of the element that is linked to this instance
+ * @param {String} sGroup the group of related DragDrop objects
+ * @param {Object} config an object containing configurable attributes.
+ * Valid properties for DragDrop:
+ *
+ * - padding
+ * - isTarget
+ * - maintainOffset
+ * - primaryButtonOnly
+ */
+ constructor: function(id, sGroup, config) {
+ if(id) {
+ this.init(id, sGroup, config);
+ }
+ },
+
+ /**
+ * Set to false to enable a DragDrop object to fire drag events while dragging
+ * over its own Element. Defaults to true - DragDrop objects do not by default
+ * fire drag events to themselves.
+ * @property ignoreSelf
+ * @type Boolean
+ */
+
+ /**
+ * The id of the element associated with this object. This is what we
+ * refer to as the "linked element" because the size and position of
+ * this element is used to determine when the drag and drop objects have
+ * interacted.
+ * @property id
+ * @type String
+ */
+ id: null,
+
+ /**
+ * Configuration attributes passed into the constructor
+ * @property config
+ * @type Object
+ */
+ config: null,
+
+ /**
+ * The id of the element that will be dragged. By default this is same
+ * as the linked element, but could be changed to another element. Ex:
+ * Ext.dd.DDProxy
+ * @property dragElId
+ * @type String
+ * @private
+ */
+ dragElId: null,
+
+ /**
+ * The ID of the element that initiates the drag operation. By default
+ * this is the linked element, but could be changed to be a child of this
+ * element. This lets us do things like only starting the drag when the
+ * header element within the linked html element is clicked.
+ * @property handleElId
+ * @type String
+ * @private
+ */
+ handleElId: null,
+
+ /**
+ * An object who's property names identify HTML tags to be considered invalid as drag handles.
+ * A non-null property value identifies the tag as invalid. Defaults to the
+ * following value which prevents drag operations from being initiated by <a> elements:<pre><code>
+{
+ A: "A"
+}</code></pre>
+ * @property invalidHandleTypes
+ * @type Object
+ */
+ invalidHandleTypes: null,
+
+ /**
+ * An object who's property names identify the IDs of elements to be considered invalid as drag handles.
+ * A non-null property value identifies the ID as invalid. For example, to prevent
+ * dragging from being initiated on element ID "foo", use:<pre><code>
+{
+ foo: true
+}</code></pre>
+ * @property invalidHandleIds
+ * @type Object
+ */
+ invalidHandleIds: null,
+
+ /**
+ * An Array of CSS class names for elements to be considered in valid as drag handles.
+ * @property {String[]} invalidHandleClasses
+ */
+ invalidHandleClasses: null,
+
+ /**
+ * The linked element's absolute X position at the time the drag was
+ * started
+ * @property startPageX
+ * @type Number
+ * @private
+ */
+ startPageX: 0,
+
+ /**
+ * The linked element's absolute X position at the time the drag was
+ * started
+ * @property startPageY
+ * @type Number
+ * @private
+ */
+ startPageY: 0,
+
+ /**
+ * The group defines a logical collection of DragDrop objects that are
+ * related. Instances only get events when interacting with other
+ * DragDrop object in the same group. This lets us define multiple
+ * groups using a single DragDrop subclass if we want.
+ * @property groups
+ * @type Object An object in the format {'group1':true, 'group2':true}
+ */
+ groups: null,
+
+ /**
+ * Individual drag/drop instances can be locked. This will prevent
+ * onmousedown start drag.
+ * @property locked
+ * @type Boolean
+ * @private
+ */
+ locked: false,
+
+ /**
+ * Locks this instance
+ */
+ lock: function() {
+ this.locked = true;
+ },
+
+ /**
+ * When set to true, other DD objects in cooperating DDGroups do not receive
+ * notification events when this DD object is dragged over them. Defaults to false.
+ * @property moveOnly
+ * @type Boolean
+ */
+ moveOnly: false,
+
+ /**
+ * Unlocks this instace
+ */
+ unlock: function() {
+ this.locked = false;
+ },
+
+ /**
+ * By default, all instances can be a drop target. This can be disabled by
+ * setting isTarget to false.
+ * @property isTarget
+ * @type Boolean
+ */
+ isTarget: true,
+
+ /**
+ * The padding configured for this drag and drop object for calculating
+ * the drop zone intersection with this object.
+ * An array containing the 4 padding values: [top, right, bottom, left]
+ * @property {Number[]} padding
+ */
+ padding: null,
+
+ /**
+ * Cached reference to the linked element
+ * @property _domRef
+ * @private
+ */
+ _domRef: null,
+
+ /**
+ * Internal typeof flag
+ * @property __ygDragDrop
+ * @private
+ */
+ __ygDragDrop: true,
+
+ /**
+ * Set to true when horizontal contraints are applied
+ * @property constrainX
+ * @type Boolean
+ * @private
+ */
+ constrainX: false,
+
+ /**
+ * Set to true when vertical contraints are applied
+ * @property constrainY
+ * @type Boolean
+ * @private
+ */
+ constrainY: false,
+
+ /**
+ * The left constraint
+ * @property minX
+ * @type Number
+ * @private
+ */
+ minX: 0,
+
+ /**
+ * The right constraint
+ * @property maxX
+ * @type Number
+ * @private
+ */
+ maxX: 0,
+
+ /**
+ * The up constraint
+ * @property minY
+ * @type Number
+ * @private
+ */
+ minY: 0,
+
+ /**
+ * The down constraint
+ * @property maxY
+ * @type Number
+ * @private
+ */
+ maxY: 0,
+
+ /**
+ * Maintain offsets when we resetconstraints. Set to true when you want
+ * the position of the element relative to its parent to stay the same
+ * when the page changes
+ *
+ * @property maintainOffset
+ * @type Boolean
+ */
+ maintainOffset: false,
+
+ /**
+ * Array of pixel locations the element will snap to if we specified a
+ * horizontal graduation/interval. This array is generated automatically
+ * when you define a tick interval.
+ * @property {Number[]} xTicks
+ */
+ xTicks: null,
+
+ /**
+ * Array of pixel locations the element will snap to if we specified a
+ * vertical graduation/interval. This array is generated automatically
+ * when you define a tick interval.
+ * @property {Number[]} yTicks
+ */
+ yTicks: null,
+
+ /**
+ * By default the drag and drop instance will only respond to the primary
+ * button click (left button for a right-handed mouse). Set to true to
+ * allow drag and drop to start with any mouse click that is propogated
+ * by the browser
+ * @property primaryButtonOnly
+ * @type Boolean
+ */
+ primaryButtonOnly: true,
+
+ /**
+ * The available property is false until the linked dom element is accessible.
+ * @property available
+ * @type Boolean
+ */
+ available: false,
+
+ /**
+ * By default, drags can only be initiated if the mousedown occurs in the
+ * region the linked element is. This is done in part to work around a
+ * bug in some browsers that mis-report the mousedown if the previous
+ * mouseup happened outside of the window. This property is set to true
+ * if outer handles are defined. Defaults to false.
+ *
+ * @property hasOuterHandles
+ * @type Boolean
+ */
+ hasOuterHandles: false,
+
+ /**
+ * Code that executes immediately before the startDrag event
+ * @private
+ */
+ b4StartDrag: function(x, y) { },
+
+ /**
+ * Abstract method called after a drag/drop object is clicked
+ * and the drag or mousedown time thresholds have beeen met.
+ * @param {Number} X click location
+ * @param {Number} Y click location
+ */
+ startDrag: function(x, y) { /* override this */ },
+
+ /**
+ * Code that executes immediately before the onDrag event
+ * @private
+ */
+ b4Drag: function(e) { },
+
+ /**
+ * Abstract method called during the onMouseMove event while dragging an
+ * object.
+ * @param {Event} e the mousemove event
+ */
+ onDrag: function(e) { /* override this */ },
+
+ /**
+ * Abstract method called when this element fist begins hovering over
+ * another DragDrop obj
+ * @param {Event} e the mousemove event
+ * @param {String/Ext.dd.DragDrop[]} id In POINT mode, the element
+ * id this is hovering over. In INTERSECT mode, an array of one or more
+ * dragdrop items being hovered over.
+ */
+ onDragEnter: function(e, id) { /* override this */ },
+
+ /**
+ * Code that executes immediately before the onDragOver event
+ * @private
+ */
+ b4DragOver: function(e) { },
+
+ /**
+ * Abstract method called when this element is hovering over another
+ * DragDrop obj
+ * @param {Event} e the mousemove event
+ * @param {String/Ext.dd.DragDrop[]} id In POINT mode, the element
+ * id this is hovering over. In INTERSECT mode, an array of dd items
+ * being hovered over.
+ */
+ onDragOver: function(e, id) { /* override this */ },
+
+ /**
+ * Code that executes immediately before the onDragOut event
+ * @private
+ */
+ b4DragOut: function(e) { },
+
+ /**
+ * Abstract method called when we are no longer hovering over an element
+ * @param {Event} e the mousemove event
+ * @param {String/Ext.dd.DragDrop[]} id In POINT mode, the element
+ * id this was hovering over. In INTERSECT mode, an array of dd items
+ * that the mouse is no longer over.
+ */
+ onDragOut: function(e, id) { /* override this */ },
+
+ /**
+ * Code that executes immediately before the onDragDrop event
+ * @private
+ */
+ b4DragDrop: function(e) { },
+
+ /**
+ * Abstract method called when this item is dropped on another DragDrop
+ * obj
+ * @param {Event} e the mouseup event
+ * @param {String/Ext.dd.DragDrop[]} id In POINT mode, the element
+ * id this was dropped on. In INTERSECT mode, an array of dd items this
+ * was dropped on.
+ */
+ onDragDrop: function(e, id) { /* override this */ },
+
+ /**
+ * Abstract method called when this item is dropped on an area with no
+ * drop target
+ * @param {Event} e the mouseup event
+ */
+ onInvalidDrop: function(e) { /* override this */ },
+
+ /**
+ * Code that executes immediately before the endDrag event
+ * @private
+ */
+ b4EndDrag: function(e) { },
+
+ /**
+ * Called when we are done dragging the object
+ * @param {Event} e the mouseup event
+ */
+ endDrag: function(e) { /* override this */ },
+
+ /**
+ * Code executed immediately before the onMouseDown event
+ * @param {Event} e the mousedown event
+ * @private
+ */
+ b4MouseDown: function(e) { },
+
+ /**
+ * Called when a drag/drop obj gets a mousedown
+ * @param {Event} e the mousedown event
+ */
+ onMouseDown: function(e) { /* override this */ },
+
+ /**
+ * Called when a drag/drop obj gets a mouseup
+ * @param {Event} e the mouseup event
+ */
+ onMouseUp: function(e) { /* override this */ },
+
+ /**
+ * Override the onAvailable method to do what is needed after the initial
+ * position was determined.
+ */
+ onAvailable: function () {
+ },
+
+ /**
+ * @property {Object} defaultPadding
+ * Provides default constraint padding to "constrainTo" elements.
+ */
+ defaultPadding: {
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0
+ },
+
+ /**
+ * Initializes the drag drop object's constraints to restrict movement to a certain element.
+ *
+ * Usage:
+ *
+ * var dd = new Ext.dd.DDProxy("dragDiv1", "proxytest",
+ * { dragElId: "existingProxyDiv" });
+ * dd.startDrag = function(){
+ * this.constrainTo("parent-id");
+ * };
+ *
+ * Or you can initalize it using the {@link Ext.Element} object:
+ *
+ * Ext.get("dragDiv1").initDDProxy("proxytest", {dragElId: "existingProxyDiv"}, {
+ * startDrag : function(){
+ * this.constrainTo("parent-id");
+ * }
+ * });
+ *
+ * @param {String/HTMLElement/Ext.Element} constrainTo The element or element ID to constrain to.
+ * @param {Object/Number} pad (optional) Pad provides a way to specify "padding" of the constraints,
+ * and can be either a number for symmetrical padding (4 would be equal to `{left:4, right:4, top:4, bottom:4}`) or
+ * an object containing the sides to pad. For example: `{right:10, bottom:10}`
+ * @param {Boolean} inContent (optional) Constrain the draggable in the content box of the element (inside padding and borders)
+ */
+ constrainTo : function(constrainTo, pad, inContent){
+ if(Ext.isNumber(pad)){
+ pad = {left: pad, right:pad, top:pad, bottom:pad};
+ }
+ pad = pad || this.defaultPadding;
+ var b = Ext.get(this.getEl()).getBox(),
+ ce = Ext.get(constrainTo),
+ s = ce.getScroll(),
+ c,
+ cd = ce.dom;
+ if(cd == document.body){
+ c = { x: s.left, y: s.top, width: Ext.Element.getViewWidth(), height: Ext.Element.getViewHeight()};
+ }else{
+ var xy = ce.getXY();
+ c = {x : xy[0], y: xy[1], width: cd.clientWidth, height: cd.clientHeight};
+ }
+
+
+ var topSpace = b.y - c.y,
+ leftSpace = b.x - c.x;
+
+ this.resetConstraints();
+ this.setXConstraint(leftSpace - (pad.left||0), // left
+ c.width - leftSpace - b.width - (pad.right||0), //right
+ this.xTickSize
+ );
+ this.setYConstraint(topSpace - (pad.top||0), //top
+ c.height - topSpace - b.height - (pad.bottom||0), //bottom
+ this.yTickSize
+ );
+ },
+
+ /**
+ * Returns a reference to the linked element
+ * @return {HTMLElement} the html element
+ */
+ getEl: function() {
+ if (!this._domRef) {
+ this._domRef = Ext.getDom(this.id);
+ }
+
+ return this._domRef;
+ },
+
+ /**
+ * Returns a reference to the actual element to drag. By default this is
+ * the same as the html element, but it can be assigned to another
+ * element. An example of this can be found in Ext.dd.DDProxy
+ * @return {HTMLElement} the html element
+ */
+ getDragEl: function() {
+ return Ext.getDom(this.dragElId);
+ },
+
+ /**
+ * Sets up the DragDrop object. Must be called in the constructor of any
+ * Ext.dd.DragDrop subclass
+ * @param {String} id the id of the linked element
+ * @param {String} sGroup the group of related items
+ * @param {Object} config configuration attributes
+ */
+ init: function(id, sGroup, config) {
+ this.initTarget(id, sGroup, config);
+ Ext.EventManager.on(this.id, "mousedown", this.handleMouseDown, this);
+ // Ext.EventManager.on(this.id, "selectstart", Event.preventDefault);
+ },
+
+ /**
+ * Initializes Targeting functionality only... the object does not
+ * get a mousedown handler.
+ * @param {String} id the id of the linked element
+ * @param {String} sGroup the group of related items
+ * @param {Object} config configuration attributes
+ */
+ initTarget: function(id, sGroup, config) {
+ // configuration attributes
+ this.config = config || {};
+
+ // create a local reference to the drag and drop manager
+ this.DDMInstance = Ext.dd.DragDropManager;
+ // initialize the groups array
+ this.groups = {};
+
+ // assume that we have an element reference instead of an id if the
+ // parameter is not a string
+ if (typeof id !== "string") {
+ id = Ext.id(id);
+ }
+
+ // set the id
+ this.id = id;
+
+ // add to an interaction group
+ this.addToGroup((sGroup) ? sGroup : "default");
+
+ // We don't want to register this as the handle with the manager
+ // so we just set the id rather than calling the setter.
+ this.handleElId = id;
+
+ // the linked element is the element that gets dragged by default
+ this.setDragElId(id);
+
+ // by default, clicked anchors will not start drag operations.
+ this.invalidHandleTypes = { A: "A" };
+ this.invalidHandleIds = {};
+ this.invalidHandleClasses = [];
+
+ this.applyConfig();
+
+ this.handleOnAvailable();
+ },
+
+ /**
+ * Applies the configuration parameters that were passed into the constructor.
+ * This is supposed to happen at each level through the inheritance chain. So
+ * a DDProxy implentation will execute apply config on DDProxy, DD, and
+ * DragDrop in order to get all of the parameters that are available in
+ * each object.
+ */
+ applyConfig: function() {
+
+ // configurable properties:
+ // padding, isTarget, maintainOffset, primaryButtonOnly
+ this.padding = this.config.padding || [0, 0, 0, 0];
+ this.isTarget = (this.config.isTarget !== false);
+ this.maintainOffset = (this.config.maintainOffset);
+ this.primaryButtonOnly = (this.config.primaryButtonOnly !== false);
+
+ },
+
+ /**
+ * Executed when the linked element is available
+ * @private
+ */
+ handleOnAvailable: function() {
+ this.available = true;
+ this.resetConstraints();
+ this.onAvailable();
+ },
+
+ /**
+ * Configures the padding for the target zone in px. Effectively expands
+ * (or reduces) the virtual object size for targeting calculations.
+ * Supports css-style shorthand; if only one parameter is passed, all sides
+ * will have that padding, and if only two are passed, the top and bottom
+ * will have the first param, the left and right the second.
+ * @param {Number} iTop Top pad
+ * @param {Number} iRight Right pad
+ * @param {Number} iBot Bot pad
+ * @param {Number} iLeft Left pad
+ */
+ setPadding: function(iTop, iRight, iBot, iLeft) {
+ // this.padding = [iLeft, iRight, iTop, iBot];
+ if (!iRight && 0 !== iRight) {
+ this.padding = [iTop, iTop, iTop, iTop];
+ } else if (!iBot && 0 !== iBot) {
+ this.padding = [iTop, iRight, iTop, iRight];
+ } else {
+ this.padding = [iTop, iRight, iBot, iLeft];
+ }
+ },
+
+ /**
+ * Stores the initial placement of the linked element.
+ * @param {Number} diffX the X offset, default 0
+ * @param {Number} diffY the Y offset, default 0
+ */
+ setInitPosition: function(diffX, diffY) {
+ var el = this.getEl();
+
+ if (!this.DDMInstance.verifyEl(el)) {
+ return;
+ }
+
+ var dx = diffX || 0;
+ var dy = diffY || 0;
+
+ var p = Ext.Element.getXY( el );
+
+ this.initPageX = p[0] - dx;
+ this.initPageY = p[1] - dy;
+
+ this.lastPageX = p[0];
+ this.lastPageY = p[1];
+
+ this.setStartPosition(p);
+ },
+
+ /**
+ * Sets the start position of the element. This is set when the obj
+ * is initialized, the reset when a drag is started.
+ * @param pos current position (from previous lookup)
+ * @private
+ */
+ setStartPosition: function(pos) {
+ var p = pos || Ext.Element.getXY( this.getEl() );
+ this.deltaSetXY = null;
+
+ this.startPageX = p[0];
+ this.startPageY = p[1];
+ },
+
+ /**
+ * Adds this instance to a group of related drag/drop objects. All
+ * instances belong to at least one group, and can belong to as many
+ * groups as needed.
+ * @param {String} sGroup the name of the group
+ */
+ addToGroup: function(sGroup) {
+ this.groups[sGroup] = true;
+ this.DDMInstance.regDragDrop(this, sGroup);
+ },
+
+ /**
+ * Removes this instance from the supplied interaction group
+ * @param {String} sGroup The group to drop
+ */
+ removeFromGroup: function(sGroup) {
+ if (this.groups[sGroup]) {
+ delete this.groups[sGroup];
+ }
+
+ this.DDMInstance.removeDDFromGroup(this, sGroup);
+ },
+
+ /**
+ * Allows you to specify that an element other than the linked element
+ * will be moved with the cursor during a drag
+ * @param {String} id the id of the element that will be used to initiate the drag
+ */
+ setDragElId: function(id) {
+ this.dragElId = id;
+ },
+
+ /**
+ * Allows you to specify a child of the linked element that should be
+ * used to initiate the drag operation. An example of this would be if
+ * you have a content div with text and links. Clicking anywhere in the
+ * content area would normally start the drag operation. Use this method
+ * to specify that an element inside of the content div is the element
+ * that starts the drag operation.
+ * @param {String} id the id of the element that will be used to
+ * initiate the drag.
+ */
+ setHandleElId: function(id) {
+ if (typeof id !== "string") {
+ id = Ext.id(id);
+ }
+ this.handleElId = id;
+ this.DDMInstance.regHandle(this.id, id);
+ },
+
+ /**
+ * Allows you to set an element outside of the linked element as a drag
+ * handle
+ * @param {String} id the id of the element that will be used to initiate the drag
+ */
+ setOuterHandleElId: function(id) {
+ if (typeof id !== "string") {
+ id = Ext.id(id);
+ }
+ Ext.EventManager.on(id, "mousedown", this.handleMouseDown, this);
+ this.setHandleElId(id);
+
+ this.hasOuterHandles = true;
+ },
+
+ /**
+ * Removes all drag and drop hooks for this element
+ */
+ unreg: function() {
+ Ext.EventManager.un(this.id, "mousedown", this.handleMouseDown, this);
+ this._domRef = null;
+ this.DDMInstance._remove(this);
+ },
+
+ destroy : function(){
+ this.unreg();
+ },
+
+ /**
+ * Returns true if this instance is locked, or the drag drop mgr is locked
+ * (meaning that all drag/drop is disabled on the page.)
+ * @return {Boolean} true if this obj or all drag/drop is locked, else
+ * false
+ */
+ isLocked: function() {
+ return (this.DDMInstance.isLocked() || this.locked);
+ },
+
+ /**
+ * Called when this object is clicked
+ * @param {Event} e
+ * @param {Ext.dd.DragDrop} oDD the clicked dd object (this dd obj)
+ * @private
+ */
+ handleMouseDown: function(e, oDD){
+ if (this.primaryButtonOnly && e.button != 0) {
+ return;
+ }
+
+ if (this.isLocked()) {
+ return;
+ }
+
+ this.DDMInstance.refreshCache(this.groups);
+
+ var pt = e.getPoint();
+ if (!this.hasOuterHandles && !this.DDMInstance.isOverTarget(pt, this) ) {
+ } else {
+ if (this.clickValidator(e)) {
+ // set the initial element position
+ this.setStartPosition();
+ this.b4MouseDown(e);
+ this.onMouseDown(e);
+
+ this.DDMInstance.handleMouseDown(e, this);
+
+ this.DDMInstance.stopEvent(e);
+ } else {
+
+
+ }
+ }
+ },
+
+ clickValidator: function(e) {
+ var target = e.getTarget();
+ return ( this.isValidHandleChild(target) &&
+ (this.id == this.handleElId ||
+ this.DDMInstance.handleWasClicked(target, this.id)) );
+ },
+
+ /**
+ * Allows you to specify a tag name that should not start a drag operation
+ * when clicked. This is designed to facilitate embedding links within a
+ * drag handle that do something other than start the drag.
+ * @method addInvalidHandleType
+ * @param {String} tagName the type of element to exclude
+ */
+ addInvalidHandleType: function(tagName) {
+ var type = tagName.toUpperCase();
+ this.invalidHandleTypes[type] = type;
+ },
+
+ /**
+ * Lets you to specify an element id for a child of a drag handle
+ * that should not initiate a drag
+ * @method addInvalidHandleId
+ * @param {String} id the element id of the element you wish to ignore
+ */
+ addInvalidHandleId: function(id) {
+ if (typeof id !== "string") {
+ id = Ext.id(id);
+ }
+ this.invalidHandleIds[id] = id;
+ },
+
+ /**
+ * Lets you specify a css class of elements that will not initiate a drag
+ * @param {String} cssClass the class of the elements you wish to ignore
+ */
+ addInvalidHandleClass: function(cssClass) {
+ this.invalidHandleClasses.push(cssClass);
+ },
+
+ /**
+ * Unsets an excluded tag name set by addInvalidHandleType
+ * @param {String} tagName the type of element to unexclude
+ */
+ removeInvalidHandleType: function(tagName) {
+ var type = tagName.toUpperCase();
+ // this.invalidHandleTypes[type] = null;
+ delete this.invalidHandleTypes[type];
+ },
+
+ /**
+ * Unsets an invalid handle id
+ * @param {String} id the id of the element to re-enable
+ */
+ removeInvalidHandleId: function(id) {
+ if (typeof id !== "string") {
+ id = Ext.id(id);
+ }
+ delete this.invalidHandleIds[id];
+ },
+
+ /**
+ * Unsets an invalid css class
+ * @param {String} cssClass the class of the element(s) you wish to
+ * re-enable
+ */
+ removeInvalidHandleClass: function(cssClass) {
+ for (var i=0, len=this.invalidHandleClasses.length; i<len; ++i) {
+ if (this.invalidHandleClasses[i] == cssClass) {
+ delete this.invalidHandleClasses[i];
+ }
+ }
+ },
+
+ /**
+ * Checks the tag exclusion list to see if this click should be ignored
+ * @param {HTMLElement} node the HTMLElement to evaluate
+ * @return {Boolean} true if this is a valid tag type, false if not
+ */
+ isValidHandleChild: function(node) {
+
+ var valid = true;
+ // var n = (node.nodeName == "#text") ? node.parentNode : node;
+ var nodeName;
+ try {
+ nodeName = node.nodeName.toUpperCase();
+ } catch(e) {
+ nodeName = node.nodeName;
+ }
+ valid = valid && !this.invalidHandleTypes[nodeName];
+ valid = valid && !this.invalidHandleIds[node.id];
+
+ for (var i=0, len=this.invalidHandleClasses.length; valid && i<len; ++i) {
+ valid = !Ext.fly(node).hasCls(this.invalidHandleClasses[i]);
+ }
+
+
+ return valid;
+
+ },
+
+ /**
+ * Creates the array of horizontal tick marks if an interval was specified
+ * in setXConstraint().
+ * @private
+ */
+ setXTicks: function(iStartX, iTickSize) {
+ this.xTicks = [];
+ this.xTickSize = iTickSize;
+
+ var tickMap = {};
+
+ for (var i = this.initPageX; i >= this.minX; i = i - iTickSize) {
+ if (!tickMap[i]) {
+ this.xTicks[this.xTicks.length] = i;
+ tickMap[i] = true;
+ }
+ }
+
+ for (i = this.initPageX; i <= this.maxX; i = i + iTickSize) {
+ if (!tickMap[i]) {
+ this.xTicks[this.xTicks.length] = i;
+ tickMap[i] = true;
+ }
+ }
+
+ Ext.Array.sort(this.xTicks, this.DDMInstance.numericSort);
+ },
+
+ /**
+ * Creates the array of vertical tick marks if an interval was specified in
+ * setYConstraint().
+ * @private
+ */
+ setYTicks: function(iStartY, iTickSize) {
+ this.yTicks = [];
+ this.yTickSize = iTickSize;
+
+ var tickMap = {};
+
+ for (var i = this.initPageY; i >= this.minY; i = i - iTickSize) {
+ if (!tickMap[i]) {
+ this.yTicks[this.yTicks.length] = i;
+ tickMap[i] = true;
+ }
+ }
+
+ for (i = this.initPageY; i <= this.maxY; i = i + iTickSize) {
+ if (!tickMap[i]) {
+ this.yTicks[this.yTicks.length] = i;
+ tickMap[i] = true;
+ }
+ }
+
+ Ext.Array.sort(this.yTicks, this.DDMInstance.numericSort);
+ },
+
+ /**
+ * By default, the element can be dragged any place on the screen. Use
+ * this method to limit the horizontal travel of the element. Pass in
+ * 0,0 for the parameters if you want to lock the drag to the y axis.
+ * @param {Number} iLeft the number of pixels the element can move to the left
+ * @param {Number} iRight the number of pixels the element can move to the
+ * right
+ * @param {Number} iTickSize (optional) parameter for specifying that the
+ * element should move iTickSize pixels at a time.
+ */
+ setXConstraint: function(iLeft, iRight, iTickSize) {
+ this.leftConstraint = iLeft;
+ this.rightConstraint = iRight;
+
+ this.minX = this.initPageX - iLeft;
+ this.maxX = this.initPageX + iRight;
+ if (iTickSize) { this.setXTicks(this.initPageX, iTickSize); }
+
+ this.constrainX = true;
+ },
+
+ /**
+ * Clears any constraints applied to this instance. Also clears ticks
+ * since they can't exist independent of a constraint at this time.
+ */
+ clearConstraints: function() {
+ this.constrainX = false;
+ this.constrainY = false;
+ this.clearTicks();
+ },
+
+ /**
+ * Clears any tick interval defined for this instance
+ */
+ clearTicks: function() {
+ this.xTicks = null;
+ this.yTicks = null;
+ this.xTickSize = 0;
+ this.yTickSize = 0;
+ },
+
+ /**
+ * By default, the element can be dragged any place on the screen. Set
+ * this to limit the vertical travel of the element. Pass in 0,0 for the
+ * parameters if you want to lock the drag to the x axis.
+ * @param {Number} iUp the number of pixels the element can move up
+ * @param {Number} iDown the number of pixels the element can move down
+ * @param {Number} iTickSize (optional) parameter for specifying that the
+ * element should move iTickSize pixels at a time.
+ */
+ setYConstraint: function(iUp, iDown, iTickSize) {
+ this.topConstraint = iUp;
+ this.bottomConstraint = iDown;
+
+ this.minY = this.initPageY - iUp;
+ this.maxY = this.initPageY + iDown;
+ if (iTickSize) { this.setYTicks(this.initPageY, iTickSize); }
+
+ this.constrainY = true;
+
+ },
+
+ /**
+ * Must be called if you manually reposition a dd element.
+ * @param {Boolean} maintainOffset
+ */
+ resetConstraints: function() {
+ // Maintain offsets if necessary
+ if (this.initPageX || this.initPageX === 0) {
+ // figure out how much this thing has moved
+ var dx = (this.maintainOffset) ? this.lastPageX - this.initPageX : 0;
+ var dy = (this.maintainOffset) ? this.lastPageY - this.initPageY : 0;
+
+ this.setInitPosition(dx, dy);
+
+ // This is the first time we have detected the element's position
+ } else {
+ this.setInitPosition();
+ }
+
+ if (this.constrainX) {
+ this.setXConstraint( this.leftConstraint,
+ this.rightConstraint,
+ this.xTickSize );
+ }
+
+ if (this.constrainY) {
+ this.setYConstraint( this.topConstraint,
+ this.bottomConstraint,
+ this.yTickSize );
+ }
+ },
+
+ /**
+ * Normally the drag element is moved pixel by pixel, but we can specify
+ * that it move a number of pixels at a time. This method resolves the
+ * location when we have it set up like this.
+ * @param {Number} val where we want to place the object
+ * @param {Number[]} tickArray sorted array of valid points
+ * @return {Number} the closest tick
+ * @private
+ */
+ getTick: function(val, tickArray) {
+ if (!tickArray) {
+ // If tick interval is not defined, it is effectively 1 pixel,
+ // so we return the value passed to us.
+ return val;
+ } else if (tickArray[0] >= val) {
+ // The value is lower than the first tick, so we return the first
+ // tick.
+ return tickArray[0];
+ } else {
+ for (var i=0, len=tickArray.length; i<len; ++i) {
+ var next = i + 1;
+ if (tickArray[next] && tickArray[next] >= val) {
+ var diff1 = val - tickArray[i];
+ var diff2 = tickArray[next] - val;
+ return (diff2 > diff1) ? tickArray[i] : tickArray[next];
+ }
+ }
+
+ // The value is larger than the last tick, so we return the last
+ // tick.
+ return tickArray[tickArray.length - 1];
+ }
+ },
+
+ /**
+ * toString method
+ * @return {String} string representation of the dd obj
+ */
+ toString: function() {
+ return ("DragDrop " + this.id);
+ }
+
+});
+
+/*
+ * This is a derivative of the similarly named class in the YUI Library.
+ * The original license:
+ * Copyright (c) 2006, Yahoo! Inc. All rights reserved.
+ * Code licensed under the BSD License:
+ * http://developer.yahoo.net/yui/license.txt
+ */
+
+
+/**
+ * @class Ext.dd.DD
+ * A DragDrop implementation where the linked element follows the
+ * mouse cursor during a drag.
+ * @extends Ext.dd.DragDrop
+ */
+Ext.define('Ext.dd.DD', {
+ extend: 'Ext.dd.DragDrop',
+ requires: ['Ext.dd.DragDropManager'],
+
+ /**
+ * Creates new DD instance.
+ * @param {String} id the id of the linked element
+ * @param {String} sGroup the group of related DragDrop items
+ * @param {Object} config an object containing configurable attributes.
+ * Valid properties for DD: scroll
+ */
+ constructor: function(id, sGroup, config) {
+ if (id) {
+ this.init(id, sGroup, config);
+ }
+ },
+
+ /**
+ * When set to true, the utility automatically tries to scroll the browser
+ * window when a drag and drop element is dragged near the viewport boundary.
+ * Defaults to true.
+ * @property scroll
+ * @type Boolean
+ */
+ scroll: true,
+
+ /**
+ * Sets the pointer offset to the distance between the linked element's top
+ * left corner and the location the element was clicked
+ * @method autoOffset
+ * @param {Number} iPageX the X coordinate of the click
+ * @param {Number} iPageY the Y coordinate of the click
+ */
+ autoOffset: function(iPageX, iPageY) {
+ var x = iPageX - this.startPageX;
+ var y = iPageY - this.startPageY;
+ this.setDelta(x, y);
+ },
+
+ /**
+ * Sets the pointer offset. You can call this directly to force the
+ * offset to be in a particular location (e.g., pass in 0,0 to set it
+ * to the center of the object)
+ * @method setDelta
+ * @param {Number} iDeltaX the distance from the left
+ * @param {Number} iDeltaY the distance from the top
+ */
+ setDelta: function(iDeltaX, iDeltaY) {
+ this.deltaX = iDeltaX;
+ this.deltaY = iDeltaY;
+ },
+
+ /**
+ * Sets the drag element to the location of the mousedown or click event,
+ * maintaining the cursor location relative to the location on the element
+ * that was clicked. Override this if you want to place the element in a
+ * location other than where the cursor is.
+ * @method setDragElPos
+ * @param {Number} iPageX the X coordinate of the mousedown or drag event
+ * @param {Number} iPageY the Y coordinate of the mousedown or drag event
+ */
+ setDragElPos: function(iPageX, iPageY) {
+ // the first time we do this, we are going to check to make sure
+ // the element has css positioning
+
+ var el = this.getDragEl();
+ this.alignElWithMouse(el, iPageX, iPageY);
+ },
+
+ /**
+ * Sets the element to the location of the mousedown or click event,
+ * maintaining the cursor location relative to the location on the element
+ * that was clicked. Override this if you want to place the element in a
+ * location other than where the cursor is.
+ * @method alignElWithMouse
+ * @param {HTMLElement} el the element to move
+ * @param {Number} iPageX the X coordinate of the mousedown or drag event
+ * @param {Number} iPageY the Y coordinate of the mousedown or drag event
+ */
+ alignElWithMouse: function(el, iPageX, iPageY) {
+ var oCoord = this.getTargetCoord(iPageX, iPageY),
+ fly = el.dom ? el : Ext.fly(el, '_dd'),
+ elSize = fly.getSize(),
+ EL = Ext.Element,
+ vpSize;
+
+ if (!this.deltaSetXY) {
+ vpSize = this.cachedViewportSize = { width: EL.getDocumentWidth(), height: EL.getDocumentHeight() };
+ var aCoord = [
+ Math.max(0, Math.min(oCoord.x, vpSize.width - elSize.width)),
+ Math.max(0, Math.min(oCoord.y, vpSize.height - elSize.height))
+ ];
+ fly.setXY(aCoord);
+ var newLeft = fly.getLeft(true);
+ var newTop = fly.getTop(true);
+ this.deltaSetXY = [newLeft - oCoord.x, newTop - oCoord.y];
+ } else {
+ vpSize = this.cachedViewportSize;
+ fly.setLeftTop(
+ Math.max(0, Math.min(oCoord.x + this.deltaSetXY[0], vpSize.width - elSize.width)),
+ Math.max(0, Math.min(oCoord.y + this.deltaSetXY[1], vpSize.height - elSize.height))
+ );
+ }
+
+ this.cachePosition(oCoord.x, oCoord.y);
+ this.autoScroll(oCoord.x, oCoord.y, el.offsetHeight, el.offsetWidth);
+ return oCoord;
+ },
+
+ /**
+ * Saves the most recent position so that we can reset the constraints and
+ * tick marks on-demand. We need to know this so that we can calculate the
+ * number of pixels the element is offset from its original position.
+ * @method cachePosition
+ * @param {Number} iPageX (optional) the current x position (this just makes it so we
+ * don't have to look it up again)
+ * @param {Number} iPageY (optional) the current y position (this just makes it so we
+ * don't have to look it up again)
+ */
+ cachePosition: function(iPageX, iPageY) {
+ if (iPageX) {
+ this.lastPageX = iPageX;
+ this.lastPageY = iPageY;
+ } else {
+ var aCoord = Ext.Element.getXY(this.getEl());
+ this.lastPageX = aCoord[0];
+ this.lastPageY = aCoord[1];
+ }
+ },
+
+ /**
+ * Auto-scroll the window if the dragged object has been moved beyond the
+ * visible window boundary.
+ * @method autoScroll
+ * @param {Number} x the drag element's x position
+ * @param {Number} y the drag element's y position
+ * @param {Number} h the height of the drag element
+ * @param {Number} w the width of the drag element
+ * @private
+ */
+ autoScroll: function(x, y, h, w) {
+
+ if (this.scroll) {
+ // The client height
+ var clientH = Ext.Element.getViewHeight();
+
+ // The client width
+ var clientW = Ext.Element.getViewWidth();
+
+ // The amt scrolled down
+ var st = this.DDMInstance.getScrollTop();
+
+ // The amt scrolled right
+ var sl = this.DDMInstance.getScrollLeft();
+
+ // Location of the bottom of the element
+ var bot = h + y;
+
+ // Location of the right of the element
+ var right = w + x;
+
+ // The distance from the cursor to the bottom of the visible area,
+ // adjusted so that we don't scroll if the cursor is beyond the
+ // element drag constraints
+ var toBot = (clientH + st - y - this.deltaY);
+
+ // The distance from the cursor to the right of the visible area
+ var toRight = (clientW + sl - x - this.deltaX);
+
+
+ // How close to the edge the cursor must be before we scroll
+ // var thresh = (document.all) ? 100 : 40;
+ var thresh = 40;
+
+ // How many pixels to scroll per autoscroll op. This helps to reduce
+ // clunky scrolling. IE is more sensitive about this ... it needs this
+ // value to be higher.
+ var scrAmt = (document.all) ? 80 : 30;
+
+ // Scroll down if we are near the bottom of the visible page and the
+ // obj extends below the crease
+ if ( bot > clientH && toBot < thresh ) {
+ window.scrollTo(sl, st + scrAmt);
+ }
+
+ // Scroll up if the window is scrolled down and the top of the object
+ // goes above the top border
+ if ( y < st && st > 0 && y - st < thresh ) {
+ window.scrollTo(sl, st - scrAmt);
+ }
+
+ // Scroll right if the obj is beyond the right border and the cursor is
+ // near the border.
+ if ( right > clientW && toRight < thresh ) {
+ window.scrollTo(sl + scrAmt, st);
+ }
+
+ // Scroll left if the window has been scrolled to the right and the obj
+ // extends past the left border
+ if ( x < sl && sl > 0 && x - sl < thresh ) {
+ window.scrollTo(sl - scrAmt, st);
+ }
+ }
+ },
+
+ /**
+ * Finds the location the element should be placed if we want to move
+ * it to where the mouse location less the click offset would place us.
+ * @method getTargetCoord
+ * @param {Number} iPageX the X coordinate of the click
+ * @param {Number} iPageY the Y coordinate of the click
+ * @return an object that contains the coordinates (Object.x and Object.y)
+ * @private
+ */
+ getTargetCoord: function(iPageX, iPageY) {
+ var x = iPageX - this.deltaX;
+ var y = iPageY - this.deltaY;
+
+ if (this.constrainX) {
+ if (x < this.minX) {
+ x = this.minX;
+ }
+ if (x > this.maxX) {
+ x = this.maxX;
+ }
+ }
+
+ if (this.constrainY) {
+ if (y < this.minY) {
+ y = this.minY;
+ }
+ if (y > this.maxY) {
+ y = this.maxY;
+ }
+ }
+
+ x = this.getTick(x, this.xTicks);
+ y = this.getTick(y, this.yTicks);
+
+
+ return {x: x, y: y};
+ },
+
+ /**
+ * Sets up config options specific to this class. Overrides
+ * Ext.dd.DragDrop, but all versions of this method through the
+ * inheritance chain are called
+ */
+ applyConfig: function() {
+ this.callParent();
+ this.scroll = (this.config.scroll !== false);
+ },
+
+ /**
+ * Event that fires prior to the onMouseDown event. Overrides
+ * Ext.dd.DragDrop.
+ */
+ b4MouseDown: function(e) {
+ // this.resetConstraints();
+ this.autoOffset(e.getPageX(), e.getPageY());
+ },
+
+ /**
+ * Event that fires prior to the onDrag event. Overrides
+ * Ext.dd.DragDrop.
+ */
+ b4Drag: function(e) {
+ this.setDragElPos(e.getPageX(), e.getPageY());
+ },
+
+ toString: function() {
+ return ("DD " + this.id);
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // Debugging ygDragDrop events that can be overridden
+ //////////////////////////////////////////////////////////////////////////
+ /*
+ startDrag: function(x, y) {
+ },
+
+ onDrag: function(e) {
+ },
+
+ onDragEnter: function(e, id) {
+ },
+
+ onDragOver: function(e, id) {
+ },
+
+ onDragOut: function(e, id) {
+ },
+
+ onDragDrop: function(e, id) {
+ },
+
+ endDrag: function(e) {
+ }
+
+ */
+
+});
+
+/*
+ * This is a derivative of the similarly named class in the YUI Library.
+ * The original license:
+ * Copyright (c) 2006, Yahoo! Inc. All rights reserved.
+ * Code licensed under the BSD License:
+ * http://developer.yahoo.net/yui/license.txt
+ */
+
+/**
+ * @class Ext.dd.DDProxy
+ * @extends Ext.dd.DD
+ * A DragDrop implementation that inserts an empty, bordered div into
+ * the document that follows the cursor during drag operations. At the time of
+ * the click, the frame div is resized to the dimensions of the linked html
+ * element, and moved to the exact location of the linked element.
+ *
+ * References to the "frame" element refer to the single proxy element that
+ * was created to be dragged in place of all DDProxy elements on the
+ * page.
+ */
+Ext.define('Ext.dd.DDProxy', {
+ extend: 'Ext.dd.DD',
+
+ statics: {
+ /**
+ * The default drag frame div id
+ * @static
+ */
+ dragElId: "ygddfdiv"
+ },
+
+ /**
+ * Creates new DDProxy.
+ * @param {String} id the id of the linked html element
+ * @param {String} sGroup the group of related DragDrop objects
+ * @param {Object} config an object containing configurable attributes.
+ * Valid properties for DDProxy in addition to those in DragDrop:
+ *
+ * - resizeFrame
+ * - centerFrame
+ * - dragElId
+ */
+ constructor: function(id, sGroup, config) {
+ if (id) {
+ this.init(id, sGroup, config);
+ this.initFrame();
+ }
+ },
+
+ /**
+ * By default we resize the drag frame to be the same size as the element
+ * we want to drag (this is to get the frame effect). We can turn it off
+ * if we want a different behavior.
+ * @property resizeFrame
+ * @type Boolean
+ */
+ resizeFrame: true,
+
+ /**
+ * By default the frame is positioned exactly where the drag element is, so
+ * we use the cursor offset provided by Ext.dd.DD. Another option that works only if
+ * you do not have constraints on the obj is to have the drag frame centered
+ * around the cursor. Set centerFrame to true for this effect.
+ * @property centerFrame
+ * @type Boolean
+ */
+ centerFrame: false,
+
+ /**
+ * Creates the proxy element if it does not yet exist
+ * @method createFrame
+ */
+ createFrame: function() {
+ var self = this;
+ var body = document.body;
+
+ if (!body || !body.firstChild) {
+ setTimeout( function() { self.createFrame(); }, 50 );
+ return;
+ }
+
+ var div = this.getDragEl();
+
+ if (!div) {
+ div = document.createElement("div");
+ div.id = this.dragElId;
+ var s = div.style;
+
+ s.position = "absolute";
+ s.visibility = "hidden";
+ s.cursor = "move";
+ s.border = "2px solid #aaa";
+ s.zIndex = 999;
+
+ // appendChild can blow up IE if invoked prior to the window load event
+ // while rendering a table. It is possible there are other scenarios
+ // that would cause this to happen as well.
+ body.insertBefore(div, body.firstChild);
+ }
+ },
+
+ /**
+ * Initialization for the drag frame element. Must be called in the
+ * constructor of all subclasses
+ * @method initFrame
+ */
+ initFrame: function() {
+ this.createFrame();
+ },
+
+ applyConfig: function() {
+ this.callParent();
+
+ this.resizeFrame = (this.config.resizeFrame !== false);
+ this.centerFrame = (this.config.centerFrame);
+ this.setDragElId(this.config.dragElId || Ext.dd.DDProxy.dragElId);
+ },
+
+ /**
+ * Resizes the drag frame to the dimensions of the clicked object, positions
+ * it over the object, and finally displays it
+ * @method showFrame
+ * @param {Number} iPageX X click position
+ * @param {Number} iPageY Y click position
+ * @private
+ */
+ showFrame: function(iPageX, iPageY) {
+ var el = this.getEl();
+ var dragEl = this.getDragEl();
+ var s = dragEl.style;
+
+ this._resizeProxy();
+
+ if (this.centerFrame) {
+ this.setDelta( Math.round(parseInt(s.width, 10)/2),
+ Math.round(parseInt(s.height, 10)/2) );
+ }
+
+ this.setDragElPos(iPageX, iPageY);
+
+ Ext.fly(dragEl).show();
+ },
+
+ /**
+ * The proxy is automatically resized to the dimensions of the linked
+ * element when a drag is initiated, unless resizeFrame is set to false
+ * @method _resizeProxy
+ * @private
+ */
+ _resizeProxy: function() {
+ if (this.resizeFrame) {
+ var el = this.getEl();
+ Ext.fly(this.getDragEl()).setSize(el.offsetWidth, el.offsetHeight);
+ }
+ },
+
+ // overrides Ext.dd.DragDrop
+ b4MouseDown: function(e) {
+ var x = e.getPageX();
+ var y = e.getPageY();
+ this.autoOffset(x, y);
+ this.setDragElPos(x, y);
+ },
+
+ // overrides Ext.dd.DragDrop
+ b4StartDrag: function(x, y) {
+ // show the drag frame
+ this.showFrame(x, y);
+ },
+
+ // overrides Ext.dd.DragDrop
+ b4EndDrag: function(e) {
+ Ext.fly(this.getDragEl()).hide();
+ },
+
+ // overrides Ext.dd.DragDrop
+ // By default we try to move the element to the last location of the frame.
+ // This is so that the default behavior mirrors that of Ext.dd.DD.
+ endDrag: function(e) {
+
+ var lel = this.getEl();
+ var del = this.getDragEl();
+
+ // Show the drag frame briefly so we can get its position
+ del.style.visibility = "";
+
+ this.beforeMove();
+ // Hide the linked element before the move to get around a Safari
+ // rendering bug.
+ lel.style.visibility = "hidden";
+ Ext.dd.DDM.moveToEl(lel, del);
+ del.style.visibility = "hidden";
+ lel.style.visibility = "";
+
+ this.afterDrag();
+ },
+
+ beforeMove : function(){
+
+ },
+
+ afterDrag : function(){
+
+ },
+
+ toString: function() {
+ return ("DDProxy " + this.id);
+ }
+
+});
+
+/**
+ * @class Ext.dd.DragSource
+ * @extends Ext.dd.DDProxy
+ * A simple class that provides the basic implementation needed to make any element draggable.
+ */
+Ext.define('Ext.dd.DragSource', {
+ extend: 'Ext.dd.DDProxy',
+ requires: [
+ 'Ext.dd.StatusProxy',
+ 'Ext.dd.DragDropManager'
+ ],
+
+ /**
+ * @cfg {String} ddGroup
+ * A named drag drop group to which this object belongs. If a group is specified, then this object will only
+ * interact with other drag drop objects in the same group.
+ */
+
+ /**
+ * @cfg {String} [dropAllowed="x-dd-drop-ok"]
+ * The CSS class returned to the drag source when drop is allowed.
+ */
+ dropAllowed : Ext.baseCSSPrefix + 'dd-drop-ok',
+ /**
+ * @cfg {String} [dropNotAllowed="x-dd-drop-nodrop"]
+ * The CSS class returned to the drag source when drop is not allowed.
+ */
+ dropNotAllowed : Ext.baseCSSPrefix + 'dd-drop-nodrop',
+
+ /**
+ * @cfg {Boolean} animRepair
+ * If true, animates the proxy element back to the position of the handle element used to trigger the drag.
+ */
+ animRepair: true,
+
+ /**
+ * @cfg {String} repairHighlightColor
+ * The color to use when visually highlighting the drag source in the afterRepair
+ * method after a failed drop (defaults to light blue). The color must be a 6 digit hex value, without
+ * a preceding '#'.
+ */
+ repairHighlightColor: 'c3daf9',
+
+ /**
+ * Creates new drag-source.
+ * @constructor
+ * @param {String/HTMLElement/Ext.Element} el The container element or ID of it.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(el, config) {
+ this.el = Ext.get(el);
+ if(!this.dragData){
+ this.dragData = {};
+ }
+
+ Ext.apply(this, config);
+
+ if(!this.proxy){
+ this.proxy = Ext.create('Ext.dd.StatusProxy', {
+ animRepair: this.animRepair
+ });
+ }
+ this.callParent([this.el.dom, this.ddGroup || this.group,
+ {dragElId : this.proxy.id, resizeFrame: false, isTarget: false, scroll: this.scroll === true}]);
+
+ this.dragging = false;
+ },
+
+ /**
+ * Returns the data object associated with this drag source
+ * @return {Object} data An object containing arbitrary data
+ */
+ getDragData : function(e){
+ return this.dragData;
+ },
+
+ // private
+ onDragEnter : function(e, id){
+ var target = Ext.dd.DragDropManager.getDDById(id);
+ this.cachedTarget = target;
+ if (this.beforeDragEnter(target, e, id) !== false) {
+ if (target.isNotifyTarget) {
+ var status = target.notifyEnter(this, e, this.dragData);
+ this.proxy.setStatus(status);
+ } else {
+ this.proxy.setStatus(this.dropAllowed);
+ }
+
+ if (this.afterDragEnter) {
+ /**
+ * An empty function by default, but provided so that you can perform a custom action
+ * when the dragged item enters the drop target by providing an implementation.
+ * @param {Ext.dd.DragDrop} target The drop target
+ * @param {Event} e The event object
+ * @param {String} id The id of the dragged element
+ * @method afterDragEnter
+ */
+ this.afterDragEnter(target, e, id);
+ }
+ }
+ },
+
+ /**
+ * An empty function by default, but provided so that you can perform a custom action
+ * before the dragged item enters the drop target and optionally cancel the onDragEnter.
+ * @param {Ext.dd.DragDrop} target The drop target
+ * @param {Event} e The event object
+ * @param {String} id The id of the dragged element
+ * @return {Boolean} isValid True if the drag event is valid, else false to cancel
+ */
+ beforeDragEnter: function(target, e, id) {
+ return true;
+ },
+
+ // private
+ alignElWithMouse: function() {
+ this.callParent(arguments);
+ this.proxy.sync();
+ },
+
+ // private
+ onDragOver: function(e, id) {
+ var target = this.cachedTarget || Ext.dd.DragDropManager.getDDById(id);
+ if (this.beforeDragOver(target, e, id) !== false) {
+ if(target.isNotifyTarget){
+ var status = target.notifyOver(this, e, this.dragData);
+ this.proxy.setStatus(status);
+ }
+
+ if (this.afterDragOver) {
+ /**
+ * An empty function by default, but provided so that you can perform a custom action
+ * while the dragged item is over the drop target by providing an implementation.
+ * @param {Ext.dd.DragDrop} target The drop target
+ * @param {Event} e The event object
+ * @param {String} id The id of the dragged element
+ * @method afterDragOver
+ */
+ this.afterDragOver(target, e, id);
+ }
+ }
+ },
+
+ /**
+ * An empty function by default, but provided so that you can perform a custom action
+ * while the dragged item is over the drop target and optionally cancel the onDragOver.
+ * @param {Ext.dd.DragDrop} target The drop target
+ * @param {Event} e The event object
+ * @param {String} id The id of the dragged element
+ * @return {Boolean} isValid True if the drag event is valid, else false to cancel
+ */
+ beforeDragOver: function(target, e, id) {
+ return true;
+ },
+
+ // private
+ onDragOut: function(e, id) {
+ var target = this.cachedTarget || Ext.dd.DragDropManager.getDDById(id);
+ if (this.beforeDragOut(target, e, id) !== false) {
+ if (target.isNotifyTarget) {
+ target.notifyOut(this, e, this.dragData);
+ }
+ this.proxy.reset();
+ if (this.afterDragOut) {
+ /**
+ * An empty function by default, but provided so that you can perform a custom action
+ * after the dragged item is dragged out of the target without dropping.
+ * @param {Ext.dd.DragDrop} target The drop target
+ * @param {Event} e The event object
+ * @param {String} id The id of the dragged element
+ * @method afterDragOut
+ */
+ this.afterDragOut(target, e, id);
+ }
+ }
+ this.cachedTarget = null;
+ },
+
+ /**
+ * An empty function by default, but provided so that you can perform a custom action before the dragged
+ * item is dragged out of the target without dropping, and optionally cancel the onDragOut.
+ * @param {Ext.dd.DragDrop} target The drop target
+ * @param {Event} e The event object
+ * @param {String} id The id of the dragged element
+ * @return {Boolean} isValid True if the drag event is valid, else false to cancel
+ */
+ beforeDragOut: function(target, e, id){
+ return true;
+ },
+
+ // private
+ onDragDrop: function(e, id){
+ var target = this.cachedTarget || Ext.dd.DragDropManager.getDDById(id);
+ if (this.beforeDragDrop(target, e, id) !== false) {
+ if (target.isNotifyTarget) {
+ if (target.notifyDrop(this, e, this.dragData) !== false) { // valid drop?
+ this.onValidDrop(target, e, id);
+ } else {
+ this.onInvalidDrop(target, e, id);
+ }
+ } else {
+ this.onValidDrop(target, e, id);
+ }
+
+ if (this.afterDragDrop) {
+ /**
+ * An empty function by default, but provided so that you can perform a custom action
+ * after a valid drag drop has occurred by providing an implementation.
+ * @param {Ext.dd.DragDrop} target The drop target
+ * @param {Event} e The event object
+ * @param {String} id The id of the dropped element
+ * @method afterDragDrop
+ */
+ this.afterDragDrop(target, e, id);
+ }
+ }
+ delete this.cachedTarget;
+ },
+
+ /**
+ * An empty function by default, but provided so that you can perform a custom action before the dragged
+ * item is dropped onto the target and optionally cancel the onDragDrop.
+ * @param {Ext.dd.DragDrop} target The drop target
+ * @param {Event} e The event object
+ * @param {String} id The id of the dragged element
+ * @return {Boolean} isValid True if the drag drop event is valid, else false to cancel
+ */
+ beforeDragDrop: function(target, e, id){
+ return true;
+ },
+
+ // private
+ onValidDrop: function(target, e, id){
+ this.hideProxy();
+ if(this.afterValidDrop){
+ /**
+ * An empty function by default, but provided so that you can perform a custom action
+ * after a valid drop has occurred by providing an implementation.
+ * @param {Object} target The target DD
+ * @param {Event} e The event object
+ * @param {String} id The id of the dropped element
+ * @method afterValidDrop
+ */
+ this.afterValidDrop(target, e, id);
+ }
+ },
+
+ // private
+ getRepairXY: function(e, data){
+ return this.el.getXY();
+ },
+
+ // private
+ onInvalidDrop: function(target, e, id) {
+ this.beforeInvalidDrop(target, e, id);
+ if (this.cachedTarget) {
+ if(this.cachedTarget.isNotifyTarget){
+ this.cachedTarget.notifyOut(this, e, this.dragData);
+ }
+ this.cacheTarget = null;
+ }
+ this.proxy.repair(this.getRepairXY(e, this.dragData), this.afterRepair, this);
+
+ if (this.afterInvalidDrop) {
+ /**
+ * An empty function by default, but provided so that you can perform a custom action
+ * after an invalid drop has occurred by providing an implementation.
+ * @param {Event} e The event object
+ * @param {String} id The id of the dropped element
+ * @method afterInvalidDrop
+ */
+ this.afterInvalidDrop(e, id);
+ }
+ },
+
+ // private
+ afterRepair: function() {
+ var me = this;
+ if (Ext.enableFx) {
+ me.el.highlight(me.repairHighlightColor);
+ }
+ me.dragging = false;
+ },
+
+ /**
+ * An empty function by default, but provided so that you can perform a custom action after an invalid
+ * drop has occurred.
+ * @param {Ext.dd.DragDrop} target The drop target
+ * @param {Event} e The event object
+ * @param {String} id The id of the dragged element
+ * @return {Boolean} isValid True if the invalid drop should proceed, else false to cancel
+ */
+ beforeInvalidDrop: function(target, e, id) {
+ return true;
+ },
+
+ // private
+ handleMouseDown: function(e) {
+ if (this.dragging) {
+ return;
+ }
+ var data = this.getDragData(e);
+ if (data && this.onBeforeDrag(data, e) !== false) {
+ this.dragData = data;
+ this.proxy.stop();
+ this.callParent(arguments);
+ }
+ },
+
+ /**
+ * An empty function by default, but provided so that you can perform a custom action before the initial
+ * drag event begins and optionally cancel it.
+ * @param {Object} data An object containing arbitrary data to be shared with drop targets
+ * @param {Event} e The event object
+ * @return {Boolean} isValid True if the drag event is valid, else false to cancel
+ */
+ onBeforeDrag: function(data, e){
+ return true;
+ },
+
+ /**
+ * An empty function by default, but provided so that you can perform a custom action once the initial
+ * drag event has begun. The drag cannot be canceled from this function.
+ * @param {Number} x The x position of the click on the dragged object
+ * @param {Number} y The y position of the click on the dragged object
+ * @method
+ */
+ onStartDrag: Ext.emptyFn,
+
+ // private override
+ startDrag: function(x, y) {
+ this.proxy.reset();
+ this.dragging = true;
+ this.proxy.update("");
+ this.onInitDrag(x, y);
+ this.proxy.show();
+ },
+
+ // private
+ onInitDrag: function(x, y) {
+ var clone = this.el.dom.cloneNode(true);
+ clone.id = Ext.id(); // prevent duplicate ids
+ this.proxy.update(clone);
+ this.onStartDrag(x, y);
+ return true;
+ },
+
+ /**
+ * Returns the drag source's underlying {@link Ext.dd.StatusProxy}
+ * @return {Ext.dd.StatusProxy} proxy The StatusProxy
+ */
+ getProxy: function() {
+ return this.proxy;
+ },
+
+ /**
+ * Hides the drag source's {@link Ext.dd.StatusProxy}
+ */
+ hideProxy: function() {
+ this.proxy.hide();
+ this.proxy.reset(true);
+ this.dragging = false;
+ },
+
+ // private
+ triggerCacheRefresh: function() {
+ Ext.dd.DDM.refreshCache(this.groups);
+ },
+
+ // private - override to prevent hiding
+ b4EndDrag: function(e) {
+ },
+
+ // private - override to prevent moving
+ endDrag : function(e){
+ this.onEndDrag(this.dragData, e);
+ },
+
+ // private
+ onEndDrag : function(data, e){
+ },
+
+ // private - pin to cursor
+ autoOffset : function(x, y) {
+ this.setDelta(-12, -20);
+ },
+
+ destroy: function(){
+ this.callParent();
+ Ext.destroy(this.proxy);
+ }
+});
+
+// private - DD implementation for Panels
+Ext.define('Ext.panel.DD', {
+ extend: 'Ext.dd.DragSource',
+ requires: ['Ext.panel.Proxy'],
+
+ constructor : function(panel, cfg){
+ this.panel = panel;
+ this.dragData = {panel: panel};
+ this.proxy = Ext.create('Ext.panel.Proxy', panel, cfg);
+
+ this.callParent([panel.el, cfg]);
+
+ Ext.defer(function() {
+ var header = panel.header,
+ el = panel.body;
+
+ if(header){
+ this.setHandleElId(header.id);
+ el = header.el;
+ }
+ el.setStyle('cursor', 'move');
+ this.scroll = false;
+ }, 200, this);
+ },
+
+ showFrame: Ext.emptyFn,
+ startDrag: Ext.emptyFn,
+ b4StartDrag: function(x, y) {
+ this.proxy.show();
+ },
+ b4MouseDown: function(e) {
+ var x = e.getPageX(),
+ y = e.getPageY();
+ this.autoOffset(x, y);
+ },
+ onInitDrag : function(x, y){
+ this.onStartDrag(x, y);
+ return true;
+ },
+ createFrame : Ext.emptyFn,
+ getDragEl : function(e){
+ return this.proxy.ghost.el.dom;
+ },
+ endDrag : function(e){
+ this.proxy.hide();
+ this.panel.saveState();
+ },
+
+ autoOffset : function(x, y) {
+ x -= this.startPageX;
+ y -= this.startPageY;
+ this.setDelta(x, y);
+ }
+});
+
+/**
+ * @class Ext.layout.component.Dock
+ * @extends Ext.layout.component.AbstractDock
+ * @private
+ */
+Ext.define('Ext.layout.component.Dock', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.dock'],
+
+ extend: 'Ext.layout.component.AbstractDock'
+
+ /* End Definitions */
+
+});
+/**
+ * Panel is a container that has specific functionality and structural components that make it the perfect building
+ * block for application-oriented user interfaces.
+ *
+ * Panels are, by virtue of their inheritance from {@link Ext.container.Container}, capable of being configured with a
+ * {@link Ext.container.Container#layout layout}, and containing child Components.
+ *
+ * When either specifying child {@link #items} of a Panel, or dynamically {@link Ext.container.Container#add adding}
+ * Components to a Panel, remember to consider how you wish the Panel to arrange those child elements, and whether those
+ * child elements need to be sized using one of Ext's built-in `{@link Ext.container.Container#layout layout}`
+ * schemes. By default, Panels use the {@link Ext.layout.container.Auto Auto} scheme. This simply renders child
+ * components, appending them one after the other inside the Container, and **does not apply any sizing** at all.
+ *
+ * {@img Ext.panel.Panel/panel.png Panel components}
+ *
+ * A Panel may also contain {@link #bbar bottom} and {@link #tbar top} toolbars, along with separate {@link
+ * Ext.panel.Header header}, {@link #fbar footer} and body sections.
+ *
+ * Panel also provides built-in {@link #collapsible collapsible, expandable} and {@link #closable} behavior. Panels can
+ * be easily dropped into any {@link Ext.container.Container Container} or layout, and the layout and rendering pipeline
+ * is {@link Ext.container.Container#add completely managed by the framework}.
+ *
+ * **Note:** By default, the `{@link #closable close}` header tool _destroys_ the Panel resulting in removal of the
+ * Panel and the destruction of any descendant Components. This makes the Panel object, and all its descendants
+ * **unusable**. To enable the close tool to simply _hide_ a Panel for later re-use, configure the Panel with
+ * `{@link #closeAction closeAction}: 'hide'`.
+ *
+ * Usually, Panels are used as constituents within an application, in which case, they would be used as child items of
+ * Containers, and would themselves use Ext.Components as child {@link #items}. However to illustrate simply rendering a
+ * Panel into the document, here's how to do it:
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Hello',
+ * width: 200,
+ * html: '<p>World!</p>',
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * A more realistic scenario is a Panel created to house input fields which will not be rendered, but used as a
+ * constituent part of a Container:
+ *
+ * @example
+ * var filterPanel = Ext.create('Ext.panel.Panel', {
+ * bodyPadding: 5, // Don't want content to crunch against the borders
+ * width: 300,
+ * title: 'Filters',
+ * items: [{
+ * xtype: 'datefield',
+ * fieldLabel: 'Start date'
+ * }, {
+ * xtype: 'datefield',
+ * fieldLabel: 'End date'
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * Note that the Panel above is not configured to render into the document, nor is it configured with a size or
+ * position. In a real world scenario, the Container into which the Panel is added will use a {@link #layout} to render,
+ * size and position its child Components.
+ *
+ * Panels will often use specific {@link #layout}s to provide an application with shape and structure by containing and
+ * arranging child Components:
+ *
+ * @example
+ * var resultsPanel = Ext.create('Ext.panel.Panel', {
+ * title: 'Results',
+ * width: 600,
+ * height: 400,
+ * renderTo: Ext.getBody(),
+ * layout: {
+ * type: 'vbox', // Arrange child items vertically
+ * align: 'stretch', // Each takes up full width
+ * padding: 5
+ * },
+ * items: [{ // Results grid specified as a config object with an xtype of 'grid'
+ * xtype: 'grid',
+ * columns: [{header: 'Column One'}], // One header just for show. There's no data,
+ * store: Ext.create('Ext.data.ArrayStore', {}), // A dummy empty data store
+ * flex: 1 // Use 1/3 of Container's height (hint to Box layout)
+ * }, {
+ * xtype: 'splitter' // A splitter between the two child items
+ * }, { // Details Panel specified as a config object (no xtype defaults to 'panel').
+ * title: 'Details',
+ * bodyPadding: 5,
+ * items: [{
+ * fieldLabel: 'Data item',
+ * xtype: 'textfield'
+ * }], // An array of form fields
+ * flex: 2 // Use 2/3 of Container's height (hint to Box layout)
+ * }]
+ * });
+ *
+ * The example illustrates one possible method of displaying search results. The Panel contains a grid with the
+ * resulting data arranged in rows. Each selected row may be displayed in detail in the Panel below. The {@link
+ * Ext.layout.container.VBox vbox} layout is used to arrange the two vertically. It is configured to stretch child items
+ * horizontally to full width. Child items may either be configured with a numeric height, or with a `flex` value to
+ * distribute available space proportionately.
+ *
+ * This Panel itself may be a child item of, for exaple, a {@link Ext.tab.Panel} which will size its child items to fit
+ * within its content area.
+ *
+ * Using these techniques, as long as the **layout** is chosen and configured correctly, an application may have any
+ * level of nested containment, all dynamically sized according to configuration, the user's preference and available
+ * browser size.
+ */
+Ext.define('Ext.panel.Panel', {
+ extend: 'Ext.panel.AbstractPanel',
+ requires: [
+ 'Ext.panel.Header',
+ 'Ext.fx.Anim',
+ 'Ext.util.KeyMap',
+ 'Ext.panel.DD',
+ 'Ext.XTemplate',
+ 'Ext.layout.component.Dock',
+ 'Ext.util.Memento'
+ ],
+ alias: 'widget.panel',
+ alternateClassName: 'Ext.Panel',
+
+ /**
+ * @cfg {String} collapsedCls
+ * A CSS class to add to the panel's element after it has been collapsed.
+ */
+ collapsedCls: 'collapsed',
+
+ /**
+ * @cfg {Boolean} animCollapse
+ * `true` to animate the transition when the panel is collapsed, `false` to skip the animation (defaults to `true`
+ * if the {@link Ext.fx.Anim} class is available, otherwise `false`). May also be specified as the animation
+ * duration in milliseconds.
+ */
+ animCollapse: Ext.enableFx,
+
+ /**
+ * @cfg {Number} minButtonWidth
+ * Minimum width of all footer toolbar buttons in pixels. If set, this will be used as the default
+ * value for the {@link Ext.button.Button#minWidth} config of each Button added to the **footer toolbar** via the
+ * {@link #fbar} or {@link #buttons} configurations. It will be ignored for buttons that have a minWidth configured
+ * some other way, e.g. in their own config object or via the {@link Ext.container.Container#defaults defaults} of
+ * their parent container.
+ */
+ minButtonWidth: 75,
+
+ /**
+ * @cfg {Boolean} collapsed
+ * `true` to render the panel collapsed, `false` to render it expanded.
+ */
+ collapsed: false,
+
+ /**
+ * @cfg {Boolean} collapseFirst
+ * `true` to make sure the collapse/expand toggle button always renders first (to the left of) any other tools in
+ * the panel's title bar, `false` to render it last.
+ */
+ collapseFirst: true,
+
+ /**
+ * @cfg {Boolean} hideCollapseTool
+ * `true` to hide the expand/collapse toggle button when `{@link #collapsible} == true`, `false` to display it.
+ */
+ hideCollapseTool: false,
+
+ /**
+ * @cfg {Boolean} titleCollapse
+ * `true` to allow expanding and collapsing the panel (when `{@link #collapsible} = true`) by clicking anywhere in
+ * the header bar, `false`) to allow it only by clicking to tool butto).
+ */
+ titleCollapse: false,
+
+ /**
+ * @cfg {String} collapseMode
+ * **Important: this config is only effective for {@link #collapsible} Panels which are direct child items of a
+ * {@link Ext.layout.container.Border border layout}.**
+ *
+ * When _not_ a direct child item of a {@link Ext.layout.container.Border border layout}, then the Panel's header
+ * remains visible, and the body is collapsed to zero dimensions. If the Panel has no header, then a new header
+ * (orientated correctly depending on the {@link #collapseDirection}) will be inserted to show a the title and a re-
+ * expand tool.
+ *
+ * When a child item of a {@link Ext.layout.container.Border border layout}, this config has two options:
+ *
+ * - **`undefined/omitted`**
+ *
+ * When collapsed, a placeholder {@link Ext.panel.Header Header} is injected into the layout to represent the Panel
+ * and to provide a UI with a Tool to allow the user to re-expand the Panel.
+ *
+ * - **`header`** :
+ *
+ * The Panel collapses to leave its header visible as when not inside a {@link Ext.layout.container.Border border
+ * layout}.
+ */
+
+ /**
+ * @cfg {Ext.Component/Object} placeholder
+ * **Important: This config is only effective for {@link #collapsible} Panels which are direct child items of a
+ * {@link Ext.layout.container.Border border layout} when not using the `'header'` {@link #collapseMode}.**
+ *
+ * **Optional.** A Component (or config object for a Component) to show in place of this Panel when this Panel is
+ * collapsed by a {@link Ext.layout.container.Border border layout}. Defaults to a generated {@link Ext.panel.Header
+ * Header} containing a {@link Ext.panel.Tool Tool} to re-expand the Panel.
+ */
+
+ /**
+ * @cfg {Boolean} floatable
+ * **Important: This config is only effective for {@link #collapsible} Panels which are direct child items of a
+ * {@link Ext.layout.container.Border border layout}.**
+ *
+ * true to allow clicking a collapsed Panel's {@link #placeholder} to display the Panel floated above the layout,
+ * false to force the user to fully expand a collapsed region by clicking the expand button to see it again.
+ */
+ floatable: true,
+
+ /**
+ * @cfg {Boolean} overlapHeader
+ * True to overlap the header in a panel over the framing of the panel itself. This is needed when frame:true (and
+ * is done automatically for you). Otherwise it is undefined. If you manually add rounded corners to a panel header
+ * which does not have frame:true, this will need to be set to true.
+ */
+
+ /**
+ * @cfg {Boolean} collapsible
+ * True to make the panel collapsible and have an expand/collapse toggle Tool added into the header tool button
+ * area. False to keep the panel sized either statically, or by an owning layout manager, with no toggle Tool.
+ *
+ * See {@link #collapseMode} and {@link #collapseDirection}
+ */
+ collapsible: false,
+
+ /**
+ * @cfg {Boolean} collapseDirection
+ * The direction to collapse the Panel when the toggle button is clicked.
+ *
+ * Defaults to the {@link #headerPosition}
+ *
+ * **Important: This config is _ignored_ for {@link #collapsible} Panels which are direct child items of a {@link
+ * Ext.layout.container.Border border layout}.**
+ *
+ * Specify as `'top'`, `'bottom'`, `'left'` or `'right'`.
+ */
+
+ /**
+ * @cfg {Boolean} closable
+ * True to display the 'close' tool button and allow the user to close the window, false to hide the button and
+ * disallow closing the window.
+ *
+ * By default, when close is requested by clicking the close button in the header, the {@link #close} method will be
+ * called. This will _{@link Ext.Component#destroy destroy}_ the Panel and its content meaning that it may not be
+ * reused.
+ *
+ * To make closing a Panel _hide_ the Panel so that it may be reused, set {@link #closeAction} to 'hide'.
+ */
+ closable: false,
+
+ /**
+ * @cfg {String} closeAction
+ * The action to take when the close header tool is clicked:
+ *
+ * - **`'{@link #destroy}'`** :
+ *
+ * {@link #destroy remove} the window from the DOM and {@link Ext.Component#destroy destroy} it and all descendant
+ * Components. The window will **not** be available to be redisplayed via the {@link #show} method.
+ *
+ * - **`'{@link #hide}'`** :
+ *
+ * {@link #hide} the window by setting visibility to hidden and applying negative offsets. The window will be
+ * available to be redisplayed via the {@link #show} method.
+ *
+ * **Note:** This behavior has changed! setting *does* affect the {@link #close} method which will invoke the
+ * approriate closeAction.
+ */
+ closeAction: 'destroy',
+
+ /**
+ * @cfg {Object/Object[]} dockedItems
+ * A component or series of components to be added as docked items to this panel. The docked items can be docked to
+ * either the top, right, left or bottom of a panel. This is typically used for things like toolbars or tab bars:
+ *
+ * var panel = new Ext.panel.Panel({
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'top',
+ * items: [{
+ * text: 'Docked to the top'
+ * }]
+ * }]
+ * });
+ */
+
+ /**
+ * @cfg {Boolean} preventHeader
+ * Prevent a Header from being created and shown.
+ */
+ preventHeader: false,
+
+ /**
+ * @cfg {String} headerPosition
+ * Specify as `'top'`, `'bottom'`, `'left'` or `'right'`.
+ */
+ headerPosition: 'top',
+
+ /**
+ * @cfg {Boolean} frame
+ * True to apply a frame to the panel.
+ */
+ frame: false,
+
+ /**
+ * @cfg {Boolean} frameHeader
+ * True to apply a frame to the panel panels header (if 'frame' is true).
+ */
+ frameHeader: true,
+
+ /**
+ * @cfg {Object[]/Ext.panel.Tool[]} tools
+ * An array of {@link Ext.panel.Tool} configs/instances to be added to the header tool area. The tools are stored as
+ * child components of the header container. They can be accessed using {@link #down} and {#query}, as well as the
+ * other component methods. The toggle tool is automatically created if {@link #collapsible} is set to true.
+ *
+ * Note that, apart from the toggle tool which is provided when a panel is collapsible, these tools only provide the
+ * visual button. Any required functionality must be provided by adding handlers that implement the necessary
+ * behavior.
+ *
+ * Example usage:
+ *
+ * tools:[{
+ * type:'refresh',
+ * tooltip: 'Refresh form Data',
+ * // hidden:true,
+ * handler: function(event, toolEl, panel){
+ * // refresh logic
+ * }
+ * },
+ * {
+ * type:'help',
+ * tooltip: 'Get Help',
+ * handler: function(event, toolEl, panel){
+ * // show help here
+ * }
+ * }]
+ */
+
+ /**
+ * @cfg {String} [title='']
+ * The title text to be used to display in the {@link Ext.panel.Header panel header}. When a
+ * `title` is specified the {@link Ext.panel.Header} will automatically be created and displayed unless
+ * {@link #preventHeader} is set to `true`.
+ */
+
+ /**
+ * @cfg {String} iconCls
+ * CSS class for icon in header. Used for displaying an icon to the left of a title.
+ */
+
+ initComponent: function() {
+ var me = this,
+ cls;
+
+ me.addEvents(
+
+ /**
+ * @event beforeclose
+ * Fires before the user closes the panel. Return false from any listener to stop the close event being
+ * fired
+ * @param {Ext.panel.Panel} panel The Panel object
+ */
+ 'beforeclose',
+
+ /**
+ * @event beforeexpand
+ * Fires before this panel is expanded. Return false to prevent the expand.
+ * @param {Ext.panel.Panel} p The Panel being expanded.
+ * @param {Boolean} animate True if the expand is animated, else false.
+ */
+ "beforeexpand",
+
+ /**
+ * @event beforecollapse
+ * Fires before this panel is collapsed. Return false to prevent the collapse.
+ * @param {Ext.panel.Panel} p The Panel being collapsed.
+ * @param {String} direction . The direction of the collapse. One of
+ *
+ * - Ext.Component.DIRECTION_TOP
+ * - Ext.Component.DIRECTION_RIGHT
+ * - Ext.Component.DIRECTION_BOTTOM
+ * - Ext.Component.DIRECTION_LEFT
+ *
+ * @param {Boolean} animate True if the collapse is animated, else false.
+ */
+ "beforecollapse",
+
+ /**
+ * @event expand
+ * Fires after this Panel has expanded.
+ * @param {Ext.panel.Panel} p The Panel that has been expanded.
+ */
+ "expand",
+
+ /**
+ * @event collapse
+ * Fires after this Panel hass collapsed.
+ * @param {Ext.panel.Panel} p The Panel that has been collapsed.
+ */
+ "collapse",
+
+ /**
+ * @event titlechange
+ * Fires after the Panel title has been set or changed.
+ * @param {Ext.panel.Panel} p the Panel which has been resized.
+ * @param {String} newTitle The new title.
+ * @param {String} oldTitle The previous panel title.
+ */
+ 'titlechange',
+
+ /**
+ * @event iconchange
+ * Fires after the Panel iconCls has been set or changed.
+ * @param {Ext.panel.Panel} p the Panel which has been resized.
+ * @param {String} newIconCls The new iconCls.
+ * @param {String} oldIconCls The previous panel iconCls.
+ */
+ 'iconchange'
+ );
+
+ // Save state on these two events.
+ this.addStateEvents('expand', 'collapse');
+
+ if (me.unstyled) {
+ me.setUI('plain');
+ }
+
+ if (me.frame) {
+ me.setUI(me.ui + '-framed');
+ }
+
+ // Backwards compatibility
+ me.bridgeToolbars();
+
+ me.callParent();
+ me.collapseDirection = me.collapseDirection || me.headerPosition || Ext.Component.DIRECTION_TOP;
+ },
+
+ setBorder: function(border) {
+ // var me = this,
+ // method = (border === false || border === 0) ? 'addClsWithUI' : 'removeClsWithUI';
+ //
+ // me.callParent(arguments);
+ //
+ // if (me.collapsed) {
+ // me[method](me.collapsedCls + '-noborder');
+ // }
+ //
+ // if (me.header) {
+ // me.header.setBorder(border);
+ // if (me.collapsed) {
+ // me.header[method](me.collapsedCls + '-noborder');
+ // }
+ // }
+
+ this.callParent(arguments);
+ },
+
+ beforeDestroy: function() {
+ Ext.destroy(
+ this.ghostPanel,
+ this.dd
+ );
+ this.callParent();
+ },
+
+ initAria: function() {
+ this.callParent();
+ this.initHeaderAria();
+ },
+
+ initHeaderAria: function() {
+ var me = this,
+ el = me.el,
+ header = me.header;
+ if (el && header) {
+ el.dom.setAttribute('aria-labelledby', header.titleCmp.id);
+ }
+ },
+
+ getHeader: function() {
+ return this.header;
+ },
+
+ /**
+ * Set a title for the panel's header. See {@link Ext.panel.Header#title}.
+ * @param {String} newTitle
+ */
+ setTitle: function(newTitle) {
+ var me = this,
+ oldTitle = this.title;
+
+ me.title = newTitle;
+ if (me.header) {
+ me.header.setTitle(newTitle);
+ } else {
+ me.updateHeader();
+ }
+
+ if (me.reExpander) {
+ me.reExpander.setTitle(newTitle);
+ }
+ me.fireEvent('titlechange', me, newTitle, oldTitle);
+ },
+
+ /**
+ * Set the iconCls for the panel's header. See {@link Ext.panel.Header#iconCls}. It will fire the
+ * {@link #iconchange} event after completion.
+ * @param {String} newIconCls The new CSS class name
+ */
+ setIconCls: function(newIconCls) {
+ var me = this,
+ oldIconCls = me.iconCls;
+
+ me.iconCls = newIconCls;
+ var header = me.header;
+ if (header) {
+ header.setIconCls(newIconCls);
+ }
+ me.fireEvent('iconchange', me, newIconCls, oldIconCls);
+ },
+
+ bridgeToolbars: function() {
+ var me = this,
+ docked = [],
+ fbar,
+ fbarDefaults,
+ minButtonWidth = me.minButtonWidth;
+
+ function initToolbar (toolbar, pos, useButtonAlign) {
+ if (Ext.isArray(toolbar)) {
+ toolbar = {
+ xtype: 'toolbar',
+ items: toolbar
+ };
+ }
+ else if (!toolbar.xtype) {
+ toolbar.xtype = 'toolbar';
+ }
+ toolbar.dock = pos;
+ if (pos == 'left' || pos == 'right') {
+ toolbar.vertical = true;
+ }
+
+ // Legacy support for buttonAlign (only used by buttons/fbar)
+ if (useButtonAlign) {
+ toolbar.layout = Ext.applyIf(toolbar.layout || {}, {
+ // default to 'end' (right-aligned) if me.buttonAlign is undefined or invalid
+ pack: { left:'start', center:'center' }[me.buttonAlign] || 'end'
+ });
+ }
+ return toolbar;
+ }
+
+ // Short-hand toolbars (tbar, bbar and fbar plus new lbar and rbar):
+
+ /**
+ * @cfg {String} buttonAlign
+ * The alignment of any buttons added to this panel. Valid values are 'right', 'left' and 'center' (defaults to
+ * 'right' for buttons/fbar, 'left' for other toolbar types).
+ *
+ * **NOTE:** The prefered way to specify toolbars is to use the dockedItems config. Instead of buttonAlign you
+ * would add the layout: { pack: 'start' | 'center' | 'end' } option to the dockedItem config.
+ */
+
+ /**
+ * @cfg {Object/Object[]} tbar
+ * Convenience config. Short for 'Top Bar'.
+ *
+ * tbar: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ *
+ * is equivalent to
+ *
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'top',
+ * items: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ * }]
+ */
+ if (me.tbar) {
+ docked.push(initToolbar(me.tbar, 'top'));
+ me.tbar = null;
+ }
+
+ /**
+ * @cfg {Object/Object[]} bbar
+ * Convenience config. Short for 'Bottom Bar'.
+ *
+ * bbar: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ *
+ * is equivalent to
+ *
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'bottom',
+ * items: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ * }]
+ */
+ if (me.bbar) {
+ docked.push(initToolbar(me.bbar, 'bottom'));
+ me.bbar = null;
+ }
+
+ /**
+ * @cfg {Object/Object[]} buttons
+ * Convenience config used for adding buttons docked to the bottom of the panel. This is a
+ * synonym for the {@link #fbar} config.
+ *
+ * buttons: [
+ * { text: 'Button 1' }
+ * ]
+ *
+ * is equivalent to
+ *
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'bottom',
+ * ui: 'footer',
+ * defaults: {minWidth: {@link #minButtonWidth}},
+ * items: [
+ * { xtype: 'component', flex: 1 },
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ * }]
+ *
+ * The {@link #minButtonWidth} is used as the default {@link Ext.button.Button#minWidth minWidth} for
+ * each of the buttons in the buttons toolbar.
+ */
+ if (me.buttons) {
+ me.fbar = me.buttons;
+ me.buttons = null;
+ }
+
+ /**
+ * @cfg {Object/Object[]} fbar
+ * Convenience config used for adding items to the bottom of the panel. Short for Footer Bar.
+ *
+ * fbar: [
+ * { type: 'button', text: 'Button 1' }
+ * ]
+ *
+ * is equivalent to
+ *
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'bottom',
+ * ui: 'footer',
+ * defaults: {minWidth: {@link #minButtonWidth}},
+ * items: [
+ * { xtype: 'component', flex: 1 },
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ * }]
+ *
+ * The {@link #minButtonWidth} is used as the default {@link Ext.button.Button#minWidth minWidth} for
+ * each of the buttons in the fbar.
+ */
+ if (me.fbar) {
+ fbar = initToolbar(me.fbar, 'bottom', true); // only we useButtonAlign
+ fbar.ui = 'footer';
+
+ // Apply the minButtonWidth config to buttons in the toolbar
+ if (minButtonWidth) {
+ fbarDefaults = fbar.defaults;
+ fbar.defaults = function(config) {
+ var defaults = fbarDefaults || {};
+ if ((!config.xtype || config.xtype === 'button' || (config.isComponent && config.isXType('button'))) &&
+ !('minWidth' in defaults)) {
+ defaults = Ext.apply({minWidth: minButtonWidth}, defaults);
+ }
+ return defaults;
+ };
+ }
+
+ docked.push(fbar);
+ me.fbar = null;
+ }
+
+ /**
+ * @cfg {Object/Object[]} lbar
+ * Convenience config. Short for 'Left Bar' (left-docked, vertical toolbar).
+ *
+ * lbar: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ *
+ * is equivalent to
+ *
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'left',
+ * items: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ * }]
+ */
+ if (me.lbar) {
+ docked.push(initToolbar(me.lbar, 'left'));
+ me.lbar = null;
+ }
+
+ /**
+ * @cfg {Object/Object[]} rbar
+ * Convenience config. Short for 'Right Bar' (right-docked, vertical toolbar).
+ *
+ * rbar: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ *
+ * is equivalent to
+ *
+ * dockedItems: [{
+ * xtype: 'toolbar',
+ * dock: 'right',
+ * items: [
+ * { xtype: 'button', text: 'Button 1' }
+ * ]
+ * }]
+ */
+ if (me.rbar) {
+ docked.push(initToolbar(me.rbar, 'right'));
+ me.rbar = null;
+ }
+
+ if (me.dockedItems) {
+ if (!Ext.isArray(me.dockedItems)) {
+ me.dockedItems = [me.dockedItems];
+ }
+ me.dockedItems = me.dockedItems.concat(docked);
+ } else {
+ me.dockedItems = docked;
+ }
+ },
+
+ /**
+ * @private
+ * Tools are a Panel-specific capabilty.
+ * Panel uses initTools. Subclasses may contribute tools by implementing addTools.
+ */
+ initTools: function() {
+ var me = this;
+
+ me.tools = me.tools ? Ext.Array.clone(me.tools) : [];
+
+ // Add a collapse tool unless configured to not show a collapse tool
+ // or to not even show a header.
+ if (me.collapsible && !(me.hideCollapseTool || me.header === false)) {
+ me.collapseDirection = me.collapseDirection || me.headerPosition || 'top';
+ me.collapseTool = me.expandTool = me.createComponent({
+ xtype: 'tool',
+ type: 'collapse-' + me.collapseDirection,
+ expandType: me.getOppositeDirection(me.collapseDirection),
+ handler: me.toggleCollapse,
+ scope: me
+ });
+
+ // Prepend collapse tool is configured to do so.
+ if (me.collapseFirst) {
+ me.tools.unshift(me.collapseTool);
+ }
+ }
+
+ // Add subclass-specific tools.
+ me.addTools();
+
+ // Make Panel closable.
+ if (me.closable) {
+ me.addClsWithUI('closable');
+ me.addTool({
+ type: 'close',
+ handler: Ext.Function.bind(me.close, this, [])
+ });
+ }
+
+ // Append collapse tool if needed.
+ if (me.collapseTool && !me.collapseFirst) {
+ me.tools.push(me.collapseTool);
+ }
+ },
+
+ /**
+ * @private
+ * @template
+ * Template method to be implemented in subclasses to add their tools after the collapsible tool.
+ */
+ addTools: Ext.emptyFn,
+
+ /**
+ * Closes the Panel. By default, this method, removes it from the DOM, {@link Ext.Component#destroy destroy}s the
+ * Panel object and all its descendant Components. The {@link #beforeclose beforeclose} event is fired before the
+ * close happens and will cancel the close action if it returns false.
+ *
+ * **Note:** This method is also affected by the {@link #closeAction} setting. For more explicit control use
+ * {@link #destroy} and {@link #hide} methods.
+ */
+ close: function() {
+ if (this.fireEvent('beforeclose', this) !== false) {
+ this.doClose();
+ }
+ },
+
+ // private
+ doClose: function() {
+ this.fireEvent('close', this);
+ this[this.closeAction]();
+ },
+
+ onRender: function(ct, position) {
+ var me = this,
+ topContainer;
+
+ // Add class-specific header tools.
+ // Panel adds collapsible and closable.
+ me.initTools();
+
+ // Dock the header/title
+ me.updateHeader();
+
+ // Call to super after adding the header, to prevent an unnecessary re-layout
+ me.callParent(arguments);
+ },
+
+ afterRender: function() {
+ var me = this;
+
+ me.callParent(arguments);
+
+ // Instate the collapsed state after render. We need to wait for
+ // this moment so that we have established at least some of our size (from our
+ // configured dimensions or from content via the component layout)
+ if (me.collapsed) {
+ me.collapsed = false;
+ me.collapse(null, false, true);
+ }
+ },
+
+ /**
+ * Create, hide, or show the header component as appropriate based on the current config.
+ * @private
+ * @param {Boolean} force True to force the header to be created
+ */
+ updateHeader: function(force) {
+ var me = this,
+ header = me.header,
+ title = me.title,
+ tools = me.tools;
+
+ if (!me.preventHeader && (force || title || (tools && tools.length))) {
+ if (!header) {
+ header = me.header = Ext.create('Ext.panel.Header', {
+ title : title,
+ orientation : (me.headerPosition == 'left' || me.headerPosition == 'right') ? 'vertical' : 'horizontal',
+ dock : me.headerPosition || 'top',
+ textCls : me.headerTextCls,
+ iconCls : me.iconCls,
+ baseCls : me.baseCls + '-header',
+ tools : tools,
+ ui : me.ui,
+ indicateDrag: me.draggable,
+ border : me.border,
+ frame : me.frame && me.frameHeader,
+ ignoreParentFrame : me.frame || me.overlapHeader,
+ ignoreBorderManagement: me.frame || me.ignoreHeaderBorderManagement,
+ listeners : me.collapsible && me.titleCollapse ? {
+ click: me.toggleCollapse,
+ scope: me
+ } : null
+ });
+ me.addDocked(header, 0);
+
+ // Reference the Header's tool array.
+ // Header injects named references.
+ me.tools = header.tools;
+ }
+ header.show();
+ me.initHeaderAria();
+ } else if (header) {
+ header.hide();
+ }
+ },
+
+ // inherit docs
+ setUI: function(ui) {
+ var me = this;
+
+ me.callParent(arguments);
+
+ if (me.header) {
+ me.header.setUI(ui);
+ }
+ },
+
+ // private
+ getContentTarget: function() {
+ return this.body;
+ },
+
+ getTargetEl: function() {
+ return this.body || this.frameBody || this.el;
+ },
+
+ // the overrides below allow for collapsed regions inside the border layout to be hidden
+
+ // inherit docs
+ isVisible: function(deep){
+ var me = this;
+ if (me.collapsed && me.placeholder) {
+ return me.placeholder.isVisible(deep);
+ }
+ return me.callParent(arguments);
+ },
+
+ // inherit docs
+ onHide: function(){
+ var me = this;
+ if (me.collapsed && me.placeholder) {
+ me.placeholder.hide();
+ } else {
+ me.callParent(arguments);
+ }
+ },
+
+ // inherit docs
+ onShow: function(){
+ var me = this;
+ if (me.collapsed && me.placeholder) {
+ // force hidden back to true, since this gets set by the layout
+ me.hidden = true;
+ me.placeholder.show();
+ } else {
+ me.callParent(arguments);
+ }
+ },
+
+ addTool: function(tool) {
+ var me = this,
+ header = me.header;
+
+ if (Ext.isArray(tool)) {
+ Ext.each(tool, me.addTool, me);
+ return;
+ }
+ me.tools.push(tool);
+ if (header) {
+ header.addTool(tool);
+ }
+ me.updateHeader();
+ },
+
+ getOppositeDirection: function(d) {
+ var c = Ext.Component;
+ switch (d) {
+ case c.DIRECTION_TOP:
+ return c.DIRECTION_BOTTOM;
+ case c.DIRECTION_RIGHT:
+ return c.DIRECTION_LEFT;
+ case c.DIRECTION_BOTTOM:
+ return c.DIRECTION_TOP;
+ case c.DIRECTION_LEFT:
+ return c.DIRECTION_RIGHT;
+ }
+ },
+
+ /**
+ * Collapses the panel body so that the body becomes hidden. Docked Components parallel to the border towards which
+ * the collapse takes place will remain visible. Fires the {@link #beforecollapse} event which will cancel the
+ * collapse action if it returns false.
+ *
+ * @param {String} direction . The direction to collapse towards. Must be one of
+ *
+ * - Ext.Component.DIRECTION_TOP
+ * - Ext.Component.DIRECTION_RIGHT
+ * - Ext.Component.DIRECTION_BOTTOM
+ * - Ext.Component.DIRECTION_LEFT
+ *
+ * @param {Boolean} [animate] True to animate the transition, else false (defaults to the value of the
+ * {@link #animCollapse} panel config)
+ * @return {Ext.panel.Panel} this
+ */
+ collapse: function(direction, animate, /* private - passed if called at render time */ internal) {
+ var me = this,
+ c = Ext.Component,
+ height = me.getHeight(),
+ width = me.getWidth(),
+ frameInfo,
+ newSize = 0,
+ dockedItems = me.dockedItems.items,
+ dockedItemCount = dockedItems.length,
+ i = 0,
+ comp,
+ pos,
+ anim = {
+ from: {
+ height: height,
+ width: width
+ },
+ to: {
+ height: height,
+ width: width
+ },
+ listeners: {
+ afteranimate: me.afterCollapse,
+ scope: me
+ },
+ duration: Ext.Number.from(animate, Ext.fx.Anim.prototype.duration)
+ },
+ reExpander,
+ reExpanderOrientation,
+ reExpanderDock,
+ getDimension,
+ collapseDimension;
+
+ if (!direction) {
+ direction = me.collapseDirection;
+ }
+
+ // If internal (Called because of initial collapsed state), then no animation, and no events.
+ if (internal) {
+ animate = false;
+ } else if (me.collapsed || me.fireEvent('beforecollapse', me, direction, animate) === false) {
+ return false;
+ }
+
+ reExpanderDock = direction;
+ me.expandDirection = me.getOppositeDirection(direction);
+
+ // Track docked items which we hide during collapsed state
+ me.hiddenDocked = [];
+
+ switch (direction) {
+ case c.DIRECTION_TOP:
+ case c.DIRECTION_BOTTOM:
+ reExpanderOrientation = 'horizontal';
+ collapseDimension = 'height';
+ getDimension = 'getHeight';
+
+ // Attempt to find a reExpander Component (docked in a horizontal orientation)
+ // Also, collect all other docked items which we must hide after collapse.
+ for (; i < dockedItemCount; i++) {
+ comp = dockedItems[i];
+ if (comp.isVisible()) {
+ if (comp.isXType('header', true) && (!comp.dock || comp.dock == 'top' || comp.dock == 'bottom')) {
+ reExpander = comp;
+ } else {
+ me.hiddenDocked.push(comp);
+ }
+ } else if (comp === me.reExpander) {
+ reExpander = comp;
+ }
+ }
+
+ if (direction == Ext.Component.DIRECTION_BOTTOM) {
+ pos = me.getPosition()[1] - Ext.fly(me.el.dom.offsetParent).getRegion().top;
+ anim.from.top = pos;
+ }
+ break;
+
+ case c.DIRECTION_LEFT:
+ case c.DIRECTION_RIGHT:
+ reExpanderOrientation = 'vertical';
+ collapseDimension = 'width';
+ getDimension = 'getWidth';
+
+ // Attempt to find a reExpander Component (docked in a vecrtical orientation)
+ // Also, collect all other docked items which we must hide after collapse.
+ for (; i < dockedItemCount; i++) {
+ comp = dockedItems[i];
+ if (comp.isVisible()) {
+ if (comp.isHeader && (comp.dock == 'left' || comp.dock == 'right')) {
+ reExpander = comp;
+ } else {
+ me.hiddenDocked.push(comp);
+ }
+ } else if (comp === me.reExpander) {
+ reExpander = comp;
+ }
+ }
+
+ if (direction == Ext.Component.DIRECTION_RIGHT) {
+ pos = me.getPosition()[0] - Ext.fly(me.el.dom.offsetParent).getRegion().left;
+ anim.from.left = pos;
+ }
+ break;
+
+ default:
+ throw('Panel collapse must be passed a valid Component collapse direction');
+ }
+
+ // Disable toggle tool during animated collapse
+ if (animate && me.collapseTool) {
+ me.collapseTool.disable();
+ }
+
+ // Add the collapsed class now, so that collapsed CSS rules are applied before measurements are taken.
+ me.addClsWithUI(me.collapsedCls);
+ // if (me.border === false) {
+ // me.addClsWithUI(me.collapsedCls + '-noborder');
+ // }
+
+ // We found a header: Measure it to find the collapse-to size.
+ if (reExpander && reExpander.rendered) {
+
+ //we must add the collapsed cls to the header and then remove to get the proper height
+ reExpander.addClsWithUI(me.collapsedCls);
+ reExpander.addClsWithUI(me.collapsedCls + '-' + reExpander.dock);
+ if (me.border && (!me.frame || (me.frame && Ext.supports.CSS3BorderRadius))) {
+ reExpander.addClsWithUI(me.collapsedCls + '-border-' + reExpander.dock);
+ }
+
+ frameInfo = reExpander.getFrameInfo();
+
+ //get the size
+ newSize = reExpander[getDimension]() + (frameInfo ? frameInfo[direction] : 0);
+
+ //and remove
+ reExpander.removeClsWithUI(me.collapsedCls);
+ reExpander.removeClsWithUI(me.collapsedCls + '-' + reExpander.dock);
+ if (me.border && (!me.frame || (me.frame && Ext.supports.CSS3BorderRadius))) {
+ reExpander.removeClsWithUI(me.collapsedCls + '-border-' + reExpander.dock);
+ }
+ }
+ // No header: Render and insert a temporary one, and then measure it.
+ else {
+ reExpander = {
+ hideMode: 'offsets',
+ temporary: true,
+ title: me.title,
+ orientation: reExpanderOrientation,
+ dock: reExpanderDock,
+ textCls: me.headerTextCls,
+ iconCls: me.iconCls,
+ baseCls: me.baseCls + '-header',
+ ui: me.ui,
+ frame: me.frame && me.frameHeader,
+ ignoreParentFrame: me.frame || me.overlapHeader,
+ indicateDrag: me.draggable,
+ cls: me.baseCls + '-collapsed-placeholder ' + ' ' + Ext.baseCSSPrefix + 'docked ' + me.baseCls + '-' + me.ui + '-collapsed',
+ renderTo: me.el
+ };
+ if (!me.hideCollapseTool) {
+ reExpander[(reExpander.orientation == 'horizontal') ? 'tools' : 'items'] = [{
+ xtype: 'tool',
+ type: 'expand-' + me.expandDirection,
+ handler: me.toggleCollapse,
+ scope: me
+ }];
+ }
+
+ // Capture the size of the re-expander.
+ // For vertical headers in IE6 and IE7, this will be sized by a CSS rule in _panel.scss
+ reExpander = me.reExpander = Ext.create('Ext.panel.Header', reExpander);
+ newSize = reExpander[getDimension]() + ((reExpander.frame) ? reExpander.frameSize[direction] : 0);
+ reExpander.hide();
+
+ // Insert the new docked item
+ me.insertDocked(0, reExpander);
+ }
+
+ me.reExpander = reExpander;
+ me.reExpander.addClsWithUI(me.collapsedCls);
+ me.reExpander.addClsWithUI(me.collapsedCls + '-' + reExpander.dock);
+ if (me.border && (!me.frame || (me.frame && Ext.supports.CSS3BorderRadius))) {
+ me.reExpander.addClsWithUI(me.collapsedCls + '-border-' + me.reExpander.dock);
+ }
+
+ // If collapsing right or down, we'll be also animating the left or top.
+ if (direction == Ext.Component.DIRECTION_RIGHT) {
+ anim.to.left = pos + (width - newSize);
+ } else if (direction == Ext.Component.DIRECTION_BOTTOM) {
+ anim.to.top = pos + (height - newSize);
+ }
+
+ // Animate to the new size
+ anim.to[collapseDimension] = newSize;
+
+ // When we collapse a panel, the panel is in control of one dimension (depending on
+ // collapse direction) and sets that on the component. We must restore the user's
+ // original value (including non-existance) when we expand. Using this technique, we
+ // mimic setCalculatedSize for the dimension we do not control and setSize for the
+ // one we do (only while collapsed).
+ if (!me.collapseMemento) {
+ me.collapseMemento = new Ext.util.Memento(me);
+ }
+ me.collapseMemento.capture(['width', 'height', 'minWidth', 'minHeight', 'layoutManagedHeight', 'layoutManagedWidth']);
+
+ // Remove any flex config before we attempt to collapse.
+ me.savedFlex = me.flex;
+ me.minWidth = 0;
+ me.minHeight = 0;
+ delete me.flex;
+ me.suspendLayout = true;
+
+ if (animate) {
+ me.animate(anim);
+ } else {
+ me.setSize(anim.to.width, anim.to.height);
+ if (Ext.isDefined(anim.to.left) || Ext.isDefined(anim.to.top)) {
+ me.setPosition(anim.to.left, anim.to.top);
+ }
+ me.afterCollapse(false, internal);
+ }
+ return me;
+ },
+
+ afterCollapse: function(animated, internal) {
+ var me = this,
+ i = 0,
+ l = me.hiddenDocked.length;
+
+ me.collapseMemento.restore(['minWidth', 'minHeight']);
+
+ // Now we can restore the dimension we don't control to its original state
+ // Leave the value in the memento so that it can be correctly restored
+ // if it is set by animation.
+ if (Ext.Component.VERTICAL_DIRECTION_Re.test(me.expandDirection)) {
+ me.layoutManagedHeight = 2;
+ me.collapseMemento.restore('width', false);
+ } else {
+ me.layoutManagedWidth = 2;
+ me.collapseMemento.restore('height', false);
+ }
+
+ // We must hide the body, otherwise it overlays docked items which come before
+ // it in the DOM order. Collapsing its dimension won't work - padding and borders keep a size.
+ me.saveScrollTop = me.body.dom.scrollTop;
+ me.body.setStyle('display', 'none');
+
+ for (; i < l; i++) {
+ me.hiddenDocked[i].hide();
+ }
+ if (me.reExpander) {
+ me.reExpander.updateFrame();
+ me.reExpander.show();
+ }
+ me.collapsed = true;
+ me.suspendLayout = false;
+
+ if (!internal) {
+ if (me.ownerCt) {
+ // Because Component layouts only inform upstream containers if they have changed size,
+ // explicitly lay out the container now, because the lastComponentsize will have been set by the non-animated setCalculatedSize.
+ if (animated) {
+ me.ownerCt.layout.layout();
+ }
+ } else if (me.reExpander.temporary) {
+ me.doComponentLayout();
+ }
+ }
+
+ if (me.resizer) {
+ me.resizer.disable();
+ }
+
+ // If me Panel was configured with a collapse tool in its header, flip it's type
+ if (me.collapseTool) {
+ me.collapseTool.setType('expand-' + me.expandDirection);
+ }
+ if (!internal) {
+ me.fireEvent('collapse', me);
+ }
+
+ // Re-enable the toggle tool after an animated collapse
+ if (animated && me.collapseTool) {
+ me.collapseTool.enable();
+ }
+ },
+
+ /**
+ * Expands the panel body so that it becomes visible. Fires the {@link #beforeexpand} event which will cancel the
+ * expand action if it returns false.
+ * @param {Boolean} [animate] True to animate the transition, else false (defaults to the value of the
+ * {@link #animCollapse} panel config)
+ * @return {Ext.panel.Panel} this
+ */
+ expand: function(animate) {
+ var me = this;
+ if (!me.collapsed || me.fireEvent('beforeexpand', me, animate) === false) {
+ return false;
+ }
+
+ var i = 0,
+ l = me.hiddenDocked.length,
+ direction = me.expandDirection,
+ height = me.getHeight(),
+ width = me.getWidth(),
+ pos, anim;
+
+ // Disable toggle tool during animated expand
+ if (animate && me.collapseTool) {
+ me.collapseTool.disable();
+ }
+
+ // Show any docked items that we hid on collapse
+ // And hide the injected reExpander Header
+ for (; i < l; i++) {
+ me.hiddenDocked[i].hidden = false;
+ me.hiddenDocked[i].el.show();
+ }
+ if (me.reExpander) {
+ if (me.reExpander.temporary) {
+ me.reExpander.hide();
+ } else {
+ me.reExpander.removeClsWithUI(me.collapsedCls);
+ me.reExpander.removeClsWithUI(me.collapsedCls + '-' + me.reExpander.dock);
+ if (me.border && (!me.frame || (me.frame && Ext.supports.CSS3BorderRadius))) {
+ me.reExpander.removeClsWithUI(me.collapsedCls + '-border-' + me.reExpander.dock);
+ }
+ me.reExpander.updateFrame();
+ }
+ }
+
+ // If me Panel was configured with a collapse tool in its header, flip it's type
+ if (me.collapseTool) {
+ me.collapseTool.setType('collapse-' + me.collapseDirection);
+ }
+
+ // Restore body display and scroll position
+ me.body.setStyle('display', '');
+ me.body.dom.scrollTop = me.saveScrollTop;
+
+ // Unset the flag before the potential call to calculateChildBox to calculate our newly flexed size
+ me.collapsed = false;
+
+ // Remove any collapsed styling before any animation begins
+ me.removeClsWithUI(me.collapsedCls);
+ // if (me.border === false) {
+ // me.removeClsWithUI(me.collapsedCls + '-noborder');
+ // }
+
+ anim = {
+ to: {
+ },
+ from: {
+ height: height,
+ width: width
+ },
+ listeners: {
+ afteranimate: me.afterExpand,
+ scope: me
+ }
+ };
+
+ if ((direction == Ext.Component.DIRECTION_TOP) || (direction == Ext.Component.DIRECTION_BOTTOM)) {
+
+ // Restore the collapsed dimension.
+ // Leave it in the memento, so that the final restoreAll can overwrite anything that animation does.
+ me.collapseMemento.restore('height', false);
+
+ // If autoHeight, measure the height now we have shown the body element.
+ if (me.height === undefined) {
+ me.setCalculatedSize(me.width, null);
+ anim.to.height = me.getHeight();
+
+ // Must size back down to collapsed for the animation.
+ me.setCalculatedSize(me.width, anim.from.height);
+ }
+ // If we were flexed, then we can't just restore to the saved size.
+ // We must restore to the currently correct, flexed size, so we much ask the Box layout what that is.
+ else if (me.savedFlex) {
+ me.flex = me.savedFlex;
+ anim.to.height = me.ownerCt.layout.calculateChildBox(me).height;
+ delete me.flex;
+ }
+ // Else, restore to saved height
+ else {
+ anim.to.height = me.height;
+ }
+
+ // top needs animating upwards
+ if (direction == Ext.Component.DIRECTION_TOP) {
+ pos = me.getPosition()[1] - Ext.fly(me.el.dom.offsetParent).getRegion().top;
+ anim.from.top = pos;
+ anim.to.top = pos - (anim.to.height - height);
+ }
+ } else if ((direction == Ext.Component.DIRECTION_LEFT) || (direction == Ext.Component.DIRECTION_RIGHT)) {
+
+ // Restore the collapsed dimension.
+ // Leave it in the memento, so that the final restoreAll can overwrite anything that animation does.
+ me.collapseMemento.restore('width', false);
+
+ // If autoWidth, measure the width now we have shown the body element.
+ if (me.width === undefined) {
+ me.setCalculatedSize(null, me.height);
+ anim.to.width = me.getWidth();
+
+ // Must size back down to collapsed for the animation.
+ me.setCalculatedSize(anim.from.width, me.height);
+ }
+ // If we were flexed, then we can't just restore to the saved size.
+ // We must restore to the currently correct, flexed size, so we much ask the Box layout what that is.
+ else if (me.savedFlex) {
+ me.flex = me.savedFlex;
+ anim.to.width = me.ownerCt.layout.calculateChildBox(me).width;
+ delete me.flex;
+ }
+ // Else, restore to saved width
+ else {
+ anim.to.width = me.width;
+ }
+
+ // left needs animating leftwards
+ if (direction == Ext.Component.DIRECTION_LEFT) {
+ pos = me.getPosition()[0] - Ext.fly(me.el.dom.offsetParent).getRegion().left;
+ anim.from.left = pos;
+ anim.to.left = pos - (anim.to.width - width);
+ }
+ }
+
+ if (animate) {
+ me.animate(anim);
+ } else {
+ me.setCalculatedSize(anim.to.width, anim.to.height);
+ if (anim.to.x) {
+ me.setLeft(anim.to.x);
+ }
+ if (anim.to.y) {
+ me.setTop(anim.to.y);
+ }
+ me.afterExpand(false);
+ }
+
+ return me;
+ },
+
+ afterExpand: function(animated) {
+ var me = this;
+
+ // Restored to a calculated flex. Delete the set width and height properties so that flex works from now on.
+ if (me.savedFlex) {
+ me.flex = me.savedFlex;
+ delete me.savedFlex;
+ delete me.width;
+ delete me.height;
+ }
+
+ // Restore width/height and dimension management flags to original values
+ if (me.collapseMemento) {
+ me.collapseMemento.restoreAll();
+ }
+
+ if (animated && me.ownerCt) {
+ // IE 6 has an intermittent repaint issue in this case so give
+ // it a little extra time to catch up before laying out.
+ Ext.defer(me.ownerCt.doLayout, Ext.isIE6 ? 1 : 0, me);
+ }
+
+ if (me.resizer) {
+ me.resizer.enable();
+ }
+
+ me.fireEvent('expand', me);
+
+ // Re-enable the toggle tool after an animated expand
+ if (animated && me.collapseTool) {
+ me.collapseTool.enable();
+ }
+ },
+
+ /**
+ * Shortcut for performing an {@link #expand} or {@link #collapse} based on the current state of the panel.
+ * @return {Ext.panel.Panel} this
+ */
+ toggleCollapse: function() {
+ if (this.collapsed) {
+ this.expand(this.animCollapse);
+ } else {
+ this.collapse(this.collapseDirection, this.animCollapse);
+ }
+ return this;
+ },
+
+ // private
+ getKeyMap : function(){
+ if(!this.keyMap){
+ this.keyMap = Ext.create('Ext.util.KeyMap', this.el, this.keys);
+ }
+ return this.keyMap;
+ },
+
+ // private
+ initDraggable : function(){
+ /**
+ * @property {Ext.dd.DragSource} dd
+ * If this Panel is configured {@link #draggable}, this property will contain an instance of {@link
+ * Ext.dd.DragSource} which handles dragging the Panel.
+ *
+ * The developer must provide implementations of the abstract methods of {@link Ext.dd.DragSource} in order to
+ * supply behaviour for each stage of the drag/drop process. See {@link #draggable}.
+ */
+ this.dd = Ext.create('Ext.panel.DD', this, Ext.isBoolean(this.draggable) ? null : this.draggable);
+ },
+
+ // private - helper function for ghost
+ ghostTools : function() {
+ var tools = [],
+ headerTools = this.header.query('tool[hidden=false]');
+
+ if (headerTools.length) {
+ Ext.each(headerTools, function(tool) {
+ // Some tools can be full components, and copying them into the ghost
+ // actually removes them from the owning panel. You could also potentially
+ // end up with duplicate DOM ids as well. To avoid any issues we just make
+ // a simple bare-minimum clone of each tool for ghosting purposes.
+ tools.push({
+ type: tool.type
+ });
+ });
+ } else {
+ tools = [{
+ type: 'placeholder'
+ }];
+ }
+ return tools;
+ },
+
+ // private - used for dragging
+ ghost: function(cls) {
+ var me = this,
+ ghostPanel = me.ghostPanel,
+ box = me.getBox(),
+ header;
+
+ if (!ghostPanel) {
+ ghostPanel = Ext.create('Ext.panel.Panel', {
+ renderTo: me.floating ? me.el.dom.parentNode : document.body,
+ floating: {
+ shadow: false
+ },
+ frame: Ext.supports.CSS3BorderRadius ? me.frame : false,
+ overlapHeader: me.overlapHeader,
+ headerPosition: me.headerPosition,
+ baseCls: me.baseCls,
+ cls: me.baseCls + '-ghost ' + (cls ||'')
+ });
+ me.ghostPanel = ghostPanel;
+ }
+ ghostPanel.floatParent = me.floatParent;
+ if (me.floating) {
+ ghostPanel.setZIndex(Ext.Number.from(me.el.getStyle('zIndex'), 0));
+ } else {
+ ghostPanel.toFront();
+ }
+ header = ghostPanel.header;
+ // restore options
+ if (header) {
+ header.suspendLayout = true;
+ Ext.Array.forEach(header.query('tool'), function(tool){
+ header.remove(tool);
+ });
+ header.suspendLayout = false;
+ }
+ ghostPanel.addTool(me.ghostTools());
+ ghostPanel.setTitle(me.title);
+ ghostPanel.setIconCls(me.iconCls);
+
+ ghostPanel.el.show();
+ ghostPanel.setPosition(box.x, box.y);
+ ghostPanel.setSize(box.width, box.height);
+ me.el.hide();
+ if (me.floatingItems) {
+ me.floatingItems.hide();
+ }
+ return ghostPanel;
+ },
+
+ // private
+ unghost: function(show, matchPosition) {
+ var me = this;
+ if (!me.ghostPanel) {
+ return;
+ }
+ if (show !== false) {
+ me.el.show();
+ if (matchPosition !== false) {
+ me.setPosition(me.ghostPanel.getPosition());
+ }
+ if (me.floatingItems) {
+ me.floatingItems.show();
+ }
+ Ext.defer(me.focus, 10, me);
+ }
+ me.ghostPanel.el.hide();
+ },
+
+ initResizable: function(resizable) {
+ if (this.collapsed) {
+ resizable.disabled = true;
+ }
+ this.callParent([resizable]);
+ }
+}, function(){
+ this.prototype.animCollapse = Ext.enableFx;
+});
+
+/**
+ * Component layout for Tip/ToolTip/etc. components
+ * @class Ext.layout.component.Tip
+ * @extends Ext.layout.component.Dock
+ * @private
+ */
+
+Ext.define('Ext.layout.component.Tip', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.tip'],
+
+ extend: 'Ext.layout.component.Dock',
+
+ /* End Definitions */
+
+ type: 'tip',
+
+ onLayout: function(width, height) {
+ var me = this,
+ owner = me.owner,
+ el = owner.el,
+ minWidth,
+ maxWidth,
+ naturalWidth,
+ constrainedWidth,
+ xy = el.getXY();
+
+ // Position offscreen so the natural width is not affected by the viewport's right edge
+ el.setXY([-9999,-9999]);
+
+ // Calculate initial layout
+ this.callParent(arguments);
+
+ // Handle min/maxWidth for auto-width tips
+ if (!Ext.isNumber(width)) {
+ minWidth = owner.minWidth;
+ maxWidth = owner.maxWidth;
+ // IE6/7 in strict mode have a problem doing an autoWidth
+ if (Ext.isStrict && (Ext.isIE6 || Ext.isIE7)) {
+ constrainedWidth = me.doAutoWidth();
+ } else {
+ naturalWidth = el.getWidth();
+ }
+ if (naturalWidth < minWidth) {
+ constrainedWidth = minWidth;
+ }
+ else if (naturalWidth > maxWidth) {
+ constrainedWidth = maxWidth;
+ }
+ if (constrainedWidth) {
+ this.callParent([constrainedWidth, height]);
+ }
+ }
+
+ // Restore position
+ el.setXY(xy);
+ },
+
+ doAutoWidth: function(){
+ var me = this,
+ owner = me.owner,
+ body = owner.body,
+ width = body.getTextWidth();
+
+ if (owner.header) {
+ width = Math.max(width, owner.header.getWidth());
+ }
+ if (!Ext.isDefined(me.frameWidth)) {
+ me.frameWidth = owner.el.getWidth() - body.getWidth();
+ }
+ width += me.frameWidth + body.getPadding('lr');
+ return width;
+ }
+});
+
+/**
+ * @class Ext.tip.Tip
+ * @extends Ext.panel.Panel
+ * This is the base class for {@link Ext.tip.QuickTip} and {@link Ext.tip.ToolTip} that provides the basic layout and
+ * positioning that all tip-based classes require. This class can be used directly for simple, statically-positioned
+ * tips that are displayed programmatically, or it can be extended to provide custom tip implementations.
+ * @xtype tip
+ */
+Ext.define('Ext.tip.Tip', {
+ extend: 'Ext.panel.Panel',
+ requires: [ 'Ext.layout.component.Tip' ],
+ alternateClassName: 'Ext.Tip',
+ /**
+ * @cfg {Boolean} [closable=false]
+ * True to render a close tool button into the tooltip header.
+ */
+ /**
+ * @cfg {Number} width
+ * Width in pixels of the tip (defaults to auto). Width will be ignored if it exceeds the bounds of
+ * {@link #minWidth} or {@link #maxWidth}. The maximum supported value is 500.
+ */
+ /**
+ * @cfg {Number} minWidth The minimum width of the tip in pixels.
+ */
+ minWidth : 40,
+ /**
+ * @cfg {Number} maxWidth The maximum width of the tip in pixels. The maximum supported value is 500.
+ */
+ maxWidth : 300,
+ /**
+ * @cfg {Boolean/String} shadow True or "sides" for the default effect, "frame" for 4-way shadow, and "drop"
+ * for bottom-right shadow.
+ */
+ shadow : "sides",
+
+ /**
+ * @cfg {String} defaultAlign
+ * <b>Experimental</b>. The default {@link Ext.Element#alignTo} anchor position value for this tip relative
+ * to its element of origin.
+ */
+ defaultAlign : "tl-bl?",
+ /**
+ * @cfg {Boolean} constrainPosition
+ * If true, then the tooltip will be automatically constrained to stay within the browser viewport.
+ */
+ constrainPosition : true,
+
+ // @inherited
+ frame: false,
+
+ // private panel overrides
+ autoRender: true,
+ hidden: true,
+ baseCls: Ext.baseCSSPrefix + 'tip',
+ floating: {
+ shadow: true,
+ shim: true,
+ constrain: true
+ },
+ focusOnToFront: false,
+ componentLayout: 'tip',
+
+ /**
+ * @cfg {String} closeAction
+ * <p>The action to take when the close header tool is clicked:
+ * <div class="mdetail-params"><ul>
+ * <li><b><code>'{@link #destroy}'</code></b> : <div class="sub-desc">
+ * {@link #destroy remove} the window from the DOM and {@link Ext.Component#destroy destroy}
+ * it and all descendant Components. The window will <b>not</b> be available to be
+ * redisplayed via the {@link #show} method.
+ * </div></li>
+ * <li><b><code>'{@link #hide}'</code></b> : <b>Default</b><div class="sub-desc">
+ * {@link #hide} the window by setting visibility to hidden and applying negative offsets.
+ * The window will be available to be redisplayed via the {@link #show} method.
+ * </div></li>
+ * </ul></div>
+ * <p><b>Note:</b> This behavior has changed! setting *does* affect the {@link #close} method
+ * which will invoke the approriate closeAction.
+ */
+ closeAction: 'hide',
+
+ ariaRole: 'tooltip',
+
+ initComponent: function() {
+ var me = this;
+
+ me.floating = Ext.apply({}, {shadow: me.shadow}, me.self.prototype.floating);
+ me.callParent(arguments);
+
+ // Or in the deprecated config. Floating.doConstrain only constrains if the constrain property is truthy.
+ me.constrain = me.constrain || me.constrainPosition;
+ },
+
+ /**
+ * Shows this tip at the specified XY position. Example usage:
+ * <pre><code>
+// Show the tip at x:50 and y:100
+tip.showAt([50,100]);
+</code></pre>
+ * @param {Number[]} xy An array containing the x and y coordinates
+ */
+ showAt : function(xy){
+ var me = this;
+ this.callParent(arguments);
+ // Show may have been vetoed.
+ if (me.isVisible()) {
+ me.setPagePosition(xy[0], xy[1]);
+ if (me.constrainPosition || me.constrain) {
+ me.doConstrain();
+ }
+ me.toFront(true);
+ }
+ },
+
+ /**
+ * <b>Experimental</b>. Shows this tip at a position relative to another element using a standard {@link Ext.Element#alignTo}
+ * anchor position value. Example usage:
+ * <pre><code>
+// Show the tip at the default position ('tl-br?')
+tip.showBy('my-el');
+
+// Show the tip's top-left corner anchored to the element's top-right corner
+tip.showBy('my-el', 'tl-tr');
+</code></pre>
+ * @param {String/HTMLElement/Ext.Element} el An HTMLElement, Ext.Element or string id of the target element to align to
+ * @param {String} [position] A valid {@link Ext.Element#alignTo} anchor position (defaults to 'tl-br?' or
+ * {@link #defaultAlign} if specified).
+ */
+ showBy : function(el, pos) {
+ this.showAt(this.el.getAlignToXY(el, pos || this.defaultAlign));
+ },
+
+ /**
+ * @private
+ * @override
+ * Set Tip draggable using base Component's draggability
+ */
+ initDraggable : function(){
+ var me = this;
+ me.draggable = {
+ el: me.getDragEl(),
+ delegate: me.header.el,
+ constrain: me,
+ constrainTo: me.el.getScopeParent()
+ };
+ // Important: Bypass Panel's initDraggable. Call direct to Component's implementation.
+ Ext.Component.prototype.initDraggable.call(me);
+ },
+
+ // Tip does not ghost. Drag is "live"
+ ghost: undefined,
+ unghost: undefined
+});
+
+/**
+ * ToolTip is a {@link Ext.tip.Tip} implementation that handles the common case of displaying a
+ * tooltip when hovering over a certain element or elements on the page. It allows fine-grained
+ * control over the tooltip's alignment relative to the target element or mouse, and the timing
+ * of when it is automatically shown and hidden.
+ *
+ * This implementation does **not** have a built-in method of automatically populating the tooltip's
+ * text based on the target element; you must either configure a fixed {@link #html} value for each
+ * ToolTip instance, or implement custom logic (e.g. in a {@link #beforeshow} event listener) to
+ * generate the appropriate tooltip content on the fly. See {@link Ext.tip.QuickTip} for a more
+ * convenient way of automatically populating and configuring a tooltip based on specific DOM
+ * attributes of each target element.
+ *
+ * # Basic Example
+ *
+ * var tip = Ext.create('Ext.tip.ToolTip', {
+ * target: 'clearButton',
+ * html: 'Press this button to clear the form'
+ * });
+ *
+ * {@img Ext.tip.ToolTip/Ext.tip.ToolTip1.png Basic Ext.tip.ToolTip}
+ *
+ * # Delegation
+ *
+ * In addition to attaching a ToolTip to a single element, you can also use delegation to attach
+ * one ToolTip to many elements under a common parent. This is more efficient than creating many
+ * ToolTip instances. To do this, point the {@link #target} config to a common ancestor of all the
+ * elements, and then set the {@link #delegate} config to a CSS selector that will select all the
+ * appropriate sub-elements.
+ *
+ * When using delegation, it is likely that you will want to programmatically change the content
+ * of the ToolTip based on each delegate element; you can do this by implementing a custom
+ * listener for the {@link #beforeshow} event. Example:
+ *
+ * var store = Ext.create('Ext.data.ArrayStore', {
+ * fields: ['company', 'price', 'change'],
+ * data: [
+ * ['3m Co', 71.72, 0.02],
+ * ['Alcoa Inc', 29.01, 0.42],
+ * ['Altria Group Inc', 83.81, 0.28],
+ * ['American Express Company', 52.55, 0.01],
+ * ['American International Group, Inc.', 64.13, 0.31],
+ * ['AT&T Inc.', 31.61, -0.48]
+ * ]
+ * });
+ *
+ * var grid = Ext.create('Ext.grid.Panel', {
+ * title: 'Array Grid',
+ * store: store,
+ * columns: [
+ * {text: 'Company', flex: 1, dataIndex: 'company'},
+ * {text: 'Price', width: 75, dataIndex: 'price'},
+ * {text: 'Change', width: 75, dataIndex: 'change'}
+ * ],
+ * height: 200,
+ * width: 400,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * grid.getView().on('render', function(view) {
+ * view.tip = Ext.create('Ext.tip.ToolTip', {
+ * // The overall target element.
+ * target: view.el,
+ * // Each grid row causes its own seperate show and hide.
+ * delegate: view.itemSelector,
+ * // Moving within the row should not hide the tip.
+ * trackMouse: true,
+ * // Render immediately so that tip.body can be referenced prior to the first show.
+ * renderTo: Ext.getBody(),
+ * listeners: {
+ * // Change content dynamically depending on which element triggered the show.
+ * beforeshow: function updateTipBody(tip) {
+ * tip.update('Over company "' + view.getRecord(tip.triggerElement).get('company') + '"');
+ * }
+ * }
+ * });
+ * });
+ *
+ * {@img Ext.tip.ToolTip/Ext.tip.ToolTip2.png Ext.tip.ToolTip with delegation}
+ *
+ * # Alignment
+ *
+ * The following configuration properties allow control over how the ToolTip is aligned relative to
+ * the target element and/or mouse pointer:
+ *
+ * - {@link #anchor}
+ * - {@link #anchorToTarget}
+ * - {@link #anchorOffset}
+ * - {@link #trackMouse}
+ * - {@link #mouseOffset}
+ *
+ * # Showing/Hiding
+ *
+ * The following configuration properties allow control over how and when the ToolTip is automatically
+ * shown and hidden:
+ *
+ * - {@link #autoHide}
+ * - {@link #showDelay}
+ * - {@link #hideDelay}
+ * - {@link #dismissDelay}
+ *
+ * @docauthor Jason Johnston <jason@sencha.com>
+ */
+Ext.define('Ext.tip.ToolTip', {
+ extend: 'Ext.tip.Tip',
+ alias: 'widget.tooltip',
+ alternateClassName: 'Ext.ToolTip',
+ /**
+ * @property {HTMLElement} triggerElement
+ * When a ToolTip is configured with the `{@link #delegate}`
+ * option to cause selected child elements of the `{@link #target}`
+ * Element to each trigger a seperate show event, this property is set to
+ * the DOM element which triggered the show.
+ */
+ /**
+ * @cfg {HTMLElement/Ext.Element/String} target
+ * The target element or string id to monitor for mouseover events to trigger
+ * showing this ToolTip.
+ */
+ /**
+ * @cfg {Boolean} [autoHide=true]
+ * True to automatically hide the tooltip after the
+ * mouse exits the target element or after the `{@link #dismissDelay}`
+ * has expired if set. If `{@link #closable} = true`
+ * a close tool button will be rendered into the tooltip header.
+ */
+ /**
+ * @cfg {Number} showDelay
+ * Delay in milliseconds before the tooltip displays after the mouse enters the target element.
+ */
+ showDelay: 500,
+ /**
+ * @cfg {Number} hideDelay
+ * Delay in milliseconds after the mouse exits the target element but before the tooltip actually hides.
+ * Set to 0 for the tooltip to hide immediately.
+ */
+ hideDelay: 200,
+ /**
+ * @cfg {Number} dismissDelay
+ * Delay in milliseconds before the tooltip automatically hides. To disable automatic hiding, set
+ * dismissDelay = 0.
+ */
+ dismissDelay: 5000,
+ /**
+ * @cfg {Number[]} [mouseOffset=[15,18]]
+ * An XY offset from the mouse position where the tooltip should be shown.
+ */
+ /**
+ * @cfg {Boolean} trackMouse
+ * True to have the tooltip follow the mouse as it moves over the target element.
+ */
+ trackMouse: false,
+ /**
+ * @cfg {String} anchor
+ * If specified, indicates that the tip should be anchored to a
+ * particular side of the target element or mouse pointer ("top", "right", "bottom",
+ * or "left"), with an arrow pointing back at the target or mouse pointer. If
+ * {@link #constrainPosition} is enabled, this will be used as a preferred value
+ * only and may be flipped as needed.
+ */
+ /**
+ * @cfg {Boolean} anchorToTarget
+ * True to anchor the tooltip to the target element, false to anchor it relative to the mouse coordinates.
+ * When `anchorToTarget` is true, use `{@link #defaultAlign}` to control tooltip alignment to the
+ * target element. When `anchorToTarget` is false, use `{@link #anchor}` instead to control alignment.
+ */
+ anchorToTarget: true,
+ /**
+ * @cfg {Number} anchorOffset
+ * A numeric pixel value used to offset the default position of the anchor arrow. When the anchor
+ * position is on the top or bottom of the tooltip, `anchorOffset` will be used as a horizontal offset.
+ * Likewise, when the anchor position is on the left or right side, `anchorOffset` will be used as
+ * a vertical offset.
+ */
+ anchorOffset: 0,
+ /**
+ * @cfg {String} delegate
+ *
+ * A {@link Ext.DomQuery DomQuery} selector which allows selection of individual elements within the
+ * `{@link #target}` element to trigger showing and hiding the ToolTip as the mouse moves within the
+ * target.
+ *
+ * When specified, the child element of the target which caused a show event is placed into the
+ * `{@link #triggerElement}` property before the ToolTip is shown.
+ *
+ * This may be useful when a Component has regular, repeating elements in it, each of which need a
+ * ToolTip which contains information specific to that element.
+ *
+ * See the delegate example in class documentation of {@link Ext.tip.ToolTip}.
+ */
+
+ // private
+ targetCounter: 0,
+ quickShowInterval: 250,
+
+ // private
+ initComponent: function() {
+ var me = this;
+ me.callParent(arguments);
+ me.lastActive = new Date();
+ me.setTarget(me.target);
+ me.origAnchor = me.anchor;
+ },
+
+ // private
+ onRender: function(ct, position) {
+ var me = this;
+ me.callParent(arguments);
+ me.anchorCls = Ext.baseCSSPrefix + 'tip-anchor-' + me.getAnchorPosition();
+ me.anchorEl = me.el.createChild({
+ cls: Ext.baseCSSPrefix + 'tip-anchor ' + me.anchorCls
+ });
+ },
+
+ // private
+ afterRender: function() {
+ var me = this,
+ zIndex;
+
+ me.callParent(arguments);
+ zIndex = parseInt(me.el.getZIndex(), 10) || 0;
+ me.anchorEl.setStyle('z-index', zIndex + 1).setVisibilityMode(Ext.Element.DISPLAY);
+ },
+
+ /**
+ * Binds this ToolTip to the specified element. The tooltip will be displayed when the mouse moves over the element.
+ * @param {String/HTMLElement/Ext.Element} t The Element, HtmlElement, or ID of an element to bind to
+ */
+ setTarget: function(target) {
+ var me = this,
+ t = Ext.get(target),
+ tg;
+
+ if (me.target) {
+ tg = Ext.get(me.target);
+ me.mun(tg, 'mouseover', me.onTargetOver, me);
+ me.mun(tg, 'mouseout', me.onTargetOut, me);
+ me.mun(tg, 'mousemove', me.onMouseMove, me);
+ }
+
+ me.target = t;
+ if (t) {
+
+ me.mon(t, {
+ // TODO - investigate why IE6/7 seem to fire recursive resize in e.getXY
+ // breaking QuickTip#onTargetOver (EXTJSIV-1608)
+ freezeEvent: true,
+
+ mouseover: me.onTargetOver,
+ mouseout: me.onTargetOut,
+ mousemove: me.onMouseMove,
+ scope: me
+ });
+ }
+ if (me.anchor) {
+ me.anchorTarget = me.target;
+ }
+ },
+
+ // private
+ onMouseMove: function(e) {
+ var me = this,
+ t = me.delegate ? e.getTarget(me.delegate) : me.triggerElement = true,
+ xy;
+ if (t) {
+ me.targetXY = e.getXY();
+ if (t === me.triggerElement) {
+ if (!me.hidden && me.trackMouse) {
+ xy = me.getTargetXY();
+ if (me.constrainPosition) {
+ xy = me.el.adjustForConstraints(xy, me.el.getScopeParent());
+ }
+ me.setPagePosition(xy);
+ }
+ } else {
+ me.hide();
+ me.lastActive = new Date(0);
+ me.onTargetOver(e);
+ }
+ } else if ((!me.closable && me.isVisible()) && me.autoHide !== false) {
+ me.hide();
+ }
+ },
+
+ // private
+ getTargetXY: function() {
+ var me = this,
+ mouseOffset;
+ if (me.delegate) {
+ me.anchorTarget = me.triggerElement;
+ }
+ if (me.anchor) {
+ me.targetCounter++;
+ var offsets = me.getOffsets(),
+ xy = (me.anchorToTarget && !me.trackMouse) ? me.el.getAlignToXY(me.anchorTarget, me.getAnchorAlign()) : me.targetXY,
+ dw = Ext.Element.getViewWidth() - 5,
+ dh = Ext.Element.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 = me.getSize(),
+ constrainPosition = me.constrainPosition;
+
+ me.anchorEl.removeCls(me.anchorCls);
+
+ if (me.targetCounter < 2 && constrainPosition) {
+ if (axy[0] < scrollX) {
+ if (me.anchorToTarget) {
+ me.defaultAlign = 'l-r';
+ if (me.mouseOffset) {
+ me.mouseOffset[0] *= -1;
+ }
+ }
+ me.anchor = 'left';
+ return me.getTargetXY();
+ }
+ if (axy[0] + sz.width > dw) {
+ if (me.anchorToTarget) {
+ me.defaultAlign = 'r-l';
+ if (me.mouseOffset) {
+ me.mouseOffset[0] *= -1;
+ }
+ }
+ me.anchor = 'right';
+ return me.getTargetXY();
+ }
+ if (axy[1] < scrollY) {
+ if (me.anchorToTarget) {
+ me.defaultAlign = 't-b';
+ if (me.mouseOffset) {
+ me.mouseOffset[1] *= -1;
+ }
+ }
+ me.anchor = 'top';
+ return me.getTargetXY();
+ }
+ if (axy[1] + sz.height > dh) {
+ if (me.anchorToTarget) {
+ me.defaultAlign = 'b-t';
+ if (me.mouseOffset) {
+ me.mouseOffset[1] *= -1;
+ }
+ }
+ me.anchor = 'bottom';
+ return me.getTargetXY();
+ }
+ }
+
+ me.anchorCls = Ext.baseCSSPrefix + 'tip-anchor-' + me.getAnchorPosition();
+ me.anchorEl.addCls(me.anchorCls);
+ me.targetCounter = 0;
+ return axy;
+ } else {
+ mouseOffset = me.getMouseOffset();
+ return (me.targetXY) ? [me.targetXY[0] + mouseOffset[0], me.targetXY[1] + mouseOffset[1]] : mouseOffset;
+ }
+ },
+
+ getMouseOffset: function() {
+ var me = this,
+ offset = me.anchor ? [0, 0] : [15, 18];
+ if (me.mouseOffset) {
+ offset[0] += me.mouseOffset[0];
+ offset[1] += me.mouseOffset[1];
+ }
+ return offset;
+ },
+
+ // private
+ getAnchorPosition: function() {
+ var me = this,
+ m;
+ if (me.anchor) {
+ me.tipAnchor = me.anchor.charAt(0);
+ } else {
+ m = me.defaultAlign.match(/^([a-z]+)-([a-z]+)(\?)?$/);
+ me.tipAnchor = m[1].charAt(0);
+ }
+
+ switch (me.tipAnchor) {
+ case 't':
+ return 'top';
+ case 'b':
+ return 'bottom';
+ case 'r':
+ return 'right';
+ }
+ return 'left';
+ },
+
+ // private
+ getAnchorAlign: function() {
+ switch (this.anchor) {
+ case 'top':
+ return 'tl-bl';
+ case 'left':
+ return 'tl-tr';
+ case 'right':
+ return 'tr-tl';
+ default:
+ return 'bl-tl';
+ }
+ },
+
+ // private
+ getOffsets: function() {
+ var me = this,
+ mouseOffset,
+ offsets,
+ ap = me.getAnchorPosition().charAt(0);
+ if (me.anchorToTarget && !me.trackMouse) {
+ switch (ap) {
+ case 't':
+ offsets = [0, 9];
+ break;
+ case 'b':
+ offsets = [0, -13];
+ break;
+ case 'r':
+ offsets = [ - 13, 0];
+ break;
+ default:
+ offsets = [9, 0];
+ break;
+ }
+ } else {
+ switch (ap) {
+ case 't':
+ offsets = [ - 15 - me.anchorOffset, 30];
+ break;
+ case 'b':
+ offsets = [ - 19 - me.anchorOffset, -13 - me.el.dom.offsetHeight];
+ break;
+ case 'r':
+ offsets = [ - 15 - me.el.dom.offsetWidth, -13 - me.anchorOffset];
+ break;
+ default:
+ offsets = [25, -13 - me.anchorOffset];
+ break;
+ }
+ }
+ mouseOffset = me.getMouseOffset();
+ offsets[0] += mouseOffset[0];
+ offsets[1] += mouseOffset[1];
+
+ return offsets;
+ },
+
+ // private
+ onTargetOver: function(e) {
+ var me = this,
+ t;
+
+ if (me.disabled || e.within(me.target.dom, true)) {
+ return;
+ }
+ t = e.getTarget(me.delegate);
+ if (t) {
+ me.triggerElement = t;
+ me.clearTimer('hide');
+ me.targetXY = e.getXY();
+ me.delayShow();
+ }
+ },
+
+ // private
+ delayShow: function() {
+ var me = this;
+ if (me.hidden && !me.showTimer) {
+ if (Ext.Date.getElapsed(me.lastActive) < me.quickShowInterval) {
+ me.show();
+ } else {
+ me.showTimer = Ext.defer(me.show, me.showDelay, me);
+ }
+ }
+ else if (!me.hidden && me.autoHide !== false) {
+ me.show();
+ }
+ },
+
+ // private
+ onTargetOut: function(e) {
+ var me = this;
+ if (me.disabled || e.within(me.target.dom, true)) {
+ return;
+ }
+ me.clearTimer('show');
+ if (me.autoHide !== false) {
+ me.delayHide();
+ }
+ },
+
+ // private
+ delayHide: function() {
+ var me = this;
+ if (!me.hidden && !me.hideTimer) {
+ me.hideTimer = Ext.defer(me.hide, me.hideDelay, me);
+ }
+ },
+
+ /**
+ * Hides this tooltip if visible.
+ */
+ hide: function() {
+ var me = this;
+ me.clearTimer('dismiss');
+ me.lastActive = new Date();
+ if (me.anchorEl) {
+ me.anchorEl.hide();
+ }
+ me.callParent(arguments);
+ delete me.triggerElement;
+ },
+
+ /**
+ * Shows this tooltip at the current event target XY position.
+ */
+ show: function() {
+ var me = this;
+
+ // Show this Component first, so that sizing can be calculated
+ // pre-show it off screen so that the el will have dimensions
+ this.callParent();
+ if (this.hidden === false) {
+ me.setPagePosition(-10000, -10000);
+
+ if (me.anchor) {
+ me.anchor = me.origAnchor;
+ }
+ me.showAt(me.getTargetXY());
+
+ if (me.anchor) {
+ me.syncAnchor();
+ me.anchorEl.show();
+ } else {
+ me.anchorEl.hide();
+ }
+ }
+ },
+
+ // inherit docs
+ showAt: function(xy) {
+ var me = this;
+ me.lastActive = new Date();
+ me.clearTimers();
+
+ // Only call if this is hidden. May have been called from show above.
+ if (!me.isVisible()) {
+ this.callParent(arguments);
+ }
+
+ // Show may have been vetoed.
+ if (me.isVisible()) {
+ me.setPagePosition(xy[0], xy[1]);
+ if (me.constrainPosition || me.constrain) {
+ me.doConstrain();
+ }
+ me.toFront(true);
+ }
+
+ if (me.dismissDelay && me.autoHide !== false) {
+ me.dismissTimer = Ext.defer(me.hide, me.dismissDelay, me);
+ }
+ if (me.anchor) {
+ me.syncAnchor();
+ if (!me.anchorEl.isVisible()) {
+ me.anchorEl.show();
+ }
+ } else {
+ me.anchorEl.hide();
+ }
+ },
+
+ // private
+ syncAnchor: function() {
+ var me = this,
+ anchorPos,
+ targetPos,
+ offset;
+ switch (me.tipAnchor.charAt(0)) {
+ case 't':
+ anchorPos = 'b';
+ targetPos = 'tl';
+ offset = [20 + me.anchorOffset, 1];
+ break;
+ case 'r':
+ anchorPos = 'l';
+ targetPos = 'tr';
+ offset = [ - 1, 12 + me.anchorOffset];
+ break;
+ case 'b':
+ anchorPos = 't';
+ targetPos = 'bl';
+ offset = [20 + me.anchorOffset, -1];
+ break;
+ default:
+ anchorPos = 'r';
+ targetPos = 'tl';
+ offset = [1, 12 + me.anchorOffset];
+ break;
+ }
+ me.anchorEl.alignTo(me.el, anchorPos + '-' + targetPos, offset);
+ },
+
+ // private
+ setPagePosition: function(x, y) {
+ var me = this;
+ me.callParent(arguments);
+ if (me.anchor) {
+ me.syncAnchor();
+ }
+ },
+
+ // private
+ clearTimer: function(name) {
+ name = name + 'Timer';
+ clearTimeout(this[name]);
+ delete this[name];
+ },
+
+ // private
+ clearTimers: function() {
+ var me = this;
+ me.clearTimer('show');
+ me.clearTimer('dismiss');
+ me.clearTimer('hide');
+ },
+
+ // private
+ onShow: function() {
+ var me = this;
+ me.callParent();
+ me.mon(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me);
+ },
+
+ // private
+ onHide: function() {
+ var me = this;
+ me.callParent();
+ me.mun(Ext.getDoc(), 'mousedown', me.onDocMouseDown, me);
+ },
+
+ // private
+ onDocMouseDown: function(e) {
+ var me = this;
+ if (me.autoHide !== true && !me.closable && !e.within(me.el.dom)) {
+ me.disable();
+ Ext.defer(me.doEnable, 100, me);
+ }
+ },
+
+ // private
+ doEnable: function() {
+ if (!this.isDestroyed) {
+ this.enable();
+ }
+ },
+
+ // private
+ onDisable: function() {
+ this.callParent();
+ this.clearTimers();
+ this.hide();
+ },
+
+ beforeDestroy: function() {
+ var me = this;
+ me.clearTimers();
+ Ext.destroy(me.anchorEl);
+ delete me.anchorEl;
+ delete me.target;
+ delete me.anchorTarget;
+ delete me.triggerElement;
+ me.callParent();
+ },
+
+ // private
+ onDestroy: function() {
+ Ext.getDoc().un('mousedown', this.onDocMouseDown, this);
+ this.callParent();
+ }
+});
+
+/**
+ * @class Ext.tip.QuickTip
+ * @extends Ext.tip.ToolTip
+ * A specialized tooltip class for tooltips that can be specified in markup and automatically managed by the global
+ * {@link Ext.tip.QuickTipManager} instance. See the QuickTipManager documentation for additional usage details and examples.
+ * @xtype quicktip
+ */
+Ext.define('Ext.tip.QuickTip', {
+ extend: 'Ext.tip.ToolTip',
+ alternateClassName: 'Ext.QuickTip',
+ /**
+ * @cfg {String/HTMLElement/Ext.Element} target The target HTMLElement, Ext.Element or id to associate with this Quicktip (defaults to the document).
+ */
+ /**
+ * @cfg {Boolean} interceptTitles True to automatically use the element's DOM title value if available.
+ */
+ interceptTitles : false,
+
+ // Force creation of header Component
+ title: ' ',
+
+ // private
+ tagConfig : {
+ namespace : "data-",
+ attribute : "qtip",
+ width : "qwidth",
+ target : "target",
+ title : "qtitle",
+ hide : "hide",
+ cls : "qclass",
+ align : "qalign",
+ anchor : "anchor"
+ },
+
+ // private
+ initComponent : function(){
+ var me = this;
+
+ me.target = me.target || Ext.getDoc();
+ me.targets = me.targets || {};
+ me.callParent();
+ },
+
+ /**
+ * Configures a new quick tip instance and assigns it to a target element. The following config values are
+ * supported (for example usage, see the {@link Ext.tip.QuickTipManager} class header):
+ * <div class="mdetail-params"><ul>
+ * <li>autoHide</li>
+ * <li>cls</li>
+ * <li>dismissDelay (overrides the singleton value)</li>
+ * <li>target (required)</li>
+ * <li>text (required)</li>
+ * <li>title</li>
+ * <li>width</li></ul></div>
+ * @param {Object} config The config object
+ */
+ register : function(config){
+ var configs = Ext.isArray(config) ? config : arguments,
+ i = 0,
+ len = configs.length,
+ target, j, targetLen;
+
+ for (; i < len; i++) {
+ config = configs[i];
+ target = config.target;
+ if (target) {
+ if (Ext.isArray(target)) {
+ for (j = 0, targetLen = target.length; j < targetLen; j++) {
+ this.targets[Ext.id(target[j])] = config;
+ }
+ } else{
+ this.targets[Ext.id(target)] = config;
+ }
+ }
+ }
+ },
+
+ /**
+ * Removes this quick tip from its element and destroys it.
+ * @param {String/HTMLElement/Ext.Element} el The element from which the quick tip is to be removed or ID of the element.
+ */
+ unregister : function(el){
+ delete this.targets[Ext.id(el)];
+ },
+
+ /**
+ * Hides a visible tip or cancels an impending show for a particular element.
+ * @param {String/HTMLElement/Ext.Element} el The element that is the target of the tip or ID of the element.
+ */
+ cancelShow: function(el){
+ var me = this,
+ activeTarget = me.activeTarget;
+
+ el = Ext.get(el).dom;
+ if (me.isVisible()) {
+ if (activeTarget && activeTarget.el == el) {
+ me.hide();
+ }
+ } else if (activeTarget && activeTarget.el == el) {
+ me.clearTimer('show');
+ }
+ },
+
+ /**
+ * @private
+ * Reads the tip text from the closest node to the event target which contains the attribute we
+ * are configured to look for. Returns an object containing the text from the attribute, and the target element from
+ * which the text was read.
+ */
+ getTipCfg: function(e) {
+ var t = e.getTarget(),
+ titleText = t.title,
+ cfg;
+
+ if (this.interceptTitles && titleText && Ext.isString(titleText)) {
+ t.qtip = titleText;
+ t.removeAttribute("title");
+ e.preventDefault();
+ return {
+ text: titleText
+ };
+ }
+ else {
+ cfg = this.tagConfig;
+ t = e.getTarget('[' + cfg.namespace + cfg.attribute + ']');
+ if (t) {
+ return {
+ target: t,
+ text: t.getAttribute(cfg.namespace + cfg.attribute)
+ };
+ }
+ }
+ },
+
+ // private
+ onTargetOver : function(e){
+ var me = this,
+ target = e.getTarget(),
+ elTarget,
+ cfg,
+ ns,
+ tipConfig,
+ autoHide;
+
+ if (me.disabled) {
+ return;
+ }
+
+ // TODO - this causes "e" to be recycled in IE6/7 (EXTJSIV-1608) so ToolTip#setTarget
+ // was changed to include freezeEvent. The issue seems to be a nested 'resize' event
+ // that smashed Ext.EventObject.
+ me.targetXY = e.getXY();
+
+ if(!target || target.nodeType !== 1 || target == document || target == document.body){
+ return;
+ }
+
+ if (me.activeTarget && ((target == me.activeTarget.el) || Ext.fly(me.activeTarget.el).contains(target))) {
+ me.clearTimer('hide');
+ me.show();
+ return;
+ }
+
+ if (target) {
+ Ext.Object.each(me.targets, function(key, value) {
+ var targetEl = Ext.fly(value.target);
+ if (targetEl && (targetEl.dom === target || targetEl.contains(target))) {
+ elTarget = targetEl.dom;
+ return false;
+ }
+ });
+ if (elTarget) {
+ me.activeTarget = me.targets[elTarget.id];
+ me.activeTarget.el = target;
+ me.anchor = me.activeTarget.anchor;
+ if (me.anchor) {
+ me.anchorTarget = target;
+ }
+ me.delayShow();
+ return;
+ }
+ }
+
+ elTarget = Ext.get(target);
+ cfg = me.tagConfig;
+ ns = cfg.namespace;
+ tipConfig = me.getTipCfg(e);
+
+ if (tipConfig) {
+
+ // getTipCfg may look up the parentNode axis for a tip text attribute and will return the new target node.
+ // Change our target element to match that from which the tip text attribute was read.
+ if (tipConfig.target) {
+ target = tipConfig.target;
+ elTarget = Ext.get(target);
+ }
+ autoHide = elTarget.getAttribute(ns + cfg.hide);
+
+ me.activeTarget = {
+ el: target,
+ text: tipConfig.text,
+ width: +elTarget.getAttribute(ns + cfg.width) || null,
+ autoHide: autoHide != "user" && autoHide !== 'false',
+ title: elTarget.getAttribute(ns + cfg.title),
+ cls: elTarget.getAttribute(ns + cfg.cls),
+ align: elTarget.getAttribute(ns + cfg.align)
+
+ };
+ me.anchor = elTarget.getAttribute(ns + cfg.anchor);
+ if (me.anchor) {
+ me.anchorTarget = target;
+ }
+ me.delayShow();
+ }
+ },
+
+ // private
+ onTargetOut : function(e){
+ var me = this;
+
+ // If moving within the current target, and it does not have a new tip, ignore the mouseout
+ if (me.activeTarget && e.within(me.activeTarget.el) && !me.getTipCfg(e)) {
+ return;
+ }
+
+ me.clearTimer('show');
+ if (me.autoHide !== false) {
+ me.delayHide();
+ }
+ },
+
+ // inherit docs
+ showAt : function(xy){
+ var me = this,
+ target = me.activeTarget;
+
+ if (target) {
+ if (!me.rendered) {
+ me.render(Ext.getBody());
+ me.activeTarget = target;
+ }
+ if (target.title) {
+ me.setTitle(target.title || '');
+ me.header.show();
+ } else {
+ me.header.hide();
+ }
+ me.body.update(target.text);
+ me.autoHide = target.autoHide;
+ me.dismissDelay = target.dismissDelay || me.dismissDelay;
+ if (me.lastCls) {
+ me.el.removeCls(me.lastCls);
+ delete me.lastCls;
+ }
+ if (target.cls) {
+ me.el.addCls(target.cls);
+ me.lastCls = target.cls;
+ }
+
+ me.setWidth(target.width);
+
+ if (me.anchor) {
+ me.constrainPosition = false;
+ } else if (target.align) { // TODO: this doesn't seem to work consistently
+ xy = me.el.getAlignToXY(target.el, target.align);
+ me.constrainPosition = false;
+ }else{
+ me.constrainPosition = true;
+ }
+ }
+ me.callParent([xy]);
+ },
+
+ // inherit docs
+ hide: function(){
+ delete this.activeTarget;
+ this.callParent();
+ }
+});
+
+/**
+ * @class Ext.tip.QuickTipManager
+ *
+ * Provides attractive and customizable tooltips for any element. The QuickTips
+ * singleton is used to configure and manage tooltips globally for multiple elements
+ * in a generic manner. To create individual tooltips with maximum customizability,
+ * you should consider either {@link Ext.tip.Tip} or {@link Ext.tip.ToolTip}.
+ *
+ * Quicktips can be configured via tag attributes directly in markup, or by
+ * registering quick tips programmatically via the {@link #register} method.
+ *
+ * The singleton's instance of {@link Ext.tip.QuickTip} is available via
+ * {@link #getQuickTip}, and supports all the methods, and all the all the
+ * configuration properties of Ext.tip.QuickTip. These settings will apply to all
+ * tooltips shown by the singleton.
+ *
+ * Below is the summary of the configuration properties which can be used.
+ * For detailed descriptions see the config options for the {@link Ext.tip.QuickTip QuickTip} class
+ *
+ * ## QuickTips singleton configs (all are optional)
+ *
+ * - `dismissDelay`
+ * - `hideDelay`
+ * - `maxWidth`
+ * - `minWidth`
+ * - `showDelay`
+ * - `trackMouse`
+ *
+ * ## Target element configs (optional unless otherwise noted)
+ *
+ * - `autoHide`
+ * - `cls`
+ * - `dismissDelay` (overrides singleton value)
+ * - `target` (required)
+ * - `text` (required)
+ * - `title`
+ * - `width`
+ *
+ * Here is an example showing how some of these config options could be used:
+ *
+ * @example
+ * // Init the singleton. Any tag-based quick tips will start working.
+ * Ext.tip.QuickTipManager.init();
+ *
+ * // Apply a set of config properties to the singleton
+ * Ext.apply(Ext.tip.QuickTipManager.getQuickTip(), {
+ * maxWidth: 200,
+ * minWidth: 100,
+ * showDelay: 50 // Show 50ms after entering target
+ * });
+ *
+ * // Create a small panel to add a quick tip to
+ * Ext.create('Ext.container.Container', {
+ * id: 'quickTipContainer',
+ * width: 200,
+ * height: 150,
+ * style: {
+ * backgroundColor:'#000000'
+ * },
+ * renderTo: Ext.getBody()
+ * });
+ *
+ *
+ * // Manually register a quick tip for a specific element
+ * Ext.tip.QuickTipManager.register({
+ * target: 'quickTipContainer',
+ * title: 'My Tooltip',
+ * text: 'This tooltip was added in code',
+ * width: 100,
+ * dismissDelay: 10000 // Hide after 10 seconds hover
+ * });
+ *
+ * To register a quick tip in markup, you simply add one or more of the valid QuickTip attributes prefixed with
+ * the **data-** namespace. The HTML element itself is automatically set as the quick tip target. Here is the summary
+ * of supported attributes (optional unless otherwise noted):
+ *
+ * - `hide`: Specifying "user" is equivalent to setting autoHide = false. Any other value will be the same as autoHide = true.
+ * - `qclass`: A CSS class to be applied to the quick tip (equivalent to the 'cls' target element config).
+ * - `qtip (required)`: The quick tip text (equivalent to the 'text' target element config).
+ * - `qtitle`: The quick tip title (equivalent to the 'title' target element config).
+ * - `qwidth`: The quick tip width (equivalent to the 'width' target element config).
+ *
+ * Here is an example of configuring an HTML element to display a tooltip from markup:
+ *
+ * // Add a quick tip to an HTML button
+ * <input type="button" value="OK" data-qtitle="OK Button" data-qwidth="100"
+ * data-qtip="This is a quick tip from markup!"></input>
+ *
+ * @singleton
+ */
+Ext.define('Ext.tip.QuickTipManager', function() {
+ var tip,
+ disabled = false;
+
+ return {
+ requires: ['Ext.tip.QuickTip'],
+ singleton: true,
+ alternateClassName: 'Ext.QuickTips',
+
+ /**
+ * Initialize the global QuickTips instance and prepare any quick tips.
+ * @param {Boolean} autoRender (optional) True to render the QuickTips container immediately to
+ * preload images. (Defaults to true)
+ * @param {Object} config (optional) config object for the created QuickTip. By
+ * default, the {@link Ext.tip.QuickTip QuickTip} class is instantiated, but this can
+ * be changed by supplying an xtype property or a className property in this object.
+ * All other properties on this object are configuration for the created component.
+ */
+ init : function (autoRender, config) {
+ if (!tip) {
+ if (!Ext.isReady) {
+ Ext.onReady(function(){
+ Ext.tip.QuickTipManager.init(autoRender);
+ });
+ return;
+ }
+
+ var tipConfig = Ext.apply({ disabled: disabled }, config),
+ className = tipConfig.className,
+ xtype = tipConfig.xtype;
+
+ if (className) {
+ delete tipConfig.className;
+ } else if (xtype) {
+ className = 'widget.' + xtype;
+ delete tipConfig.xtype;
+ }
+
+ if (autoRender !== false) {
+ tipConfig.renderTo = document.body;
+
+ }
+
+ tip = Ext.create(className || 'Ext.tip.QuickTip', tipConfig);
+ }
+ },
+
+ /**
+ * Destroy the QuickTips instance.
+ */
+ destroy: function() {
+ if (tip) {
+ var undef;
+ tip.destroy();
+ tip = undef;
+ }
+ },
+
+ // Protected method called by the dd classes
+ ddDisable : function(){
+ // don't disable it if we don't need to
+ if(tip && !disabled){
+ tip.disable();
+ }
+ },
+
+ // Protected method called by the dd classes
+ ddEnable : function(){
+ // only enable it if it hasn't been disabled
+ if(tip && !disabled){
+ tip.enable();
+ }
+ },
+
+ /**
+ * Enable quick tips globally.
+ */
+ enable : function(){
+ if(tip){
+ tip.enable();
+ }
+ disabled = false;
+ },
+
+ /**
+ * Disable quick tips globally.
+ */
+ disable : function(){
+ if(tip){
+ tip.disable();
+ }
+ disabled = true;
+ },
+
+ /**
+ * Returns true if quick tips are enabled, else false.
+ * @return {Boolean}
+ */
+ isEnabled : function(){
+ return tip !== undefined && !tip.disabled;
+ },
+
+ /**
+ * Gets the single {@link Ext.tip.QuickTip QuickTip} instance used to show tips from all registered elements.
+ * @return {Ext.tip.QuickTip}
+ */
+ getQuickTip : function(){
+ return tip;
+ },
+
+ /**
+ * Configures a new quick tip instance and assigns it to a target element. See
+ * {@link Ext.tip.QuickTip#register} for details.
+ * @param {Object} config The config object
+ */
+ register : function(){
+ tip.register.apply(tip, arguments);
+ },
+
+ /**
+ * Removes any registered quick tip from the target element and destroys it.
+ * @param {String/HTMLElement/Ext.Element} el The element from which the quick tip is to be removed or ID of the element.
+ */
+ unregister : function(){
+ tip.unregister.apply(tip, arguments);
+ },
+
+ /**
+ * Alias of {@link #register}.
+ * @param {Object} config The config object
+ */
+ tips : function(){
+ tip.register.apply(tip, arguments);
+ }
+ };
+}());
+/**
+ * Represents an Ext JS 4 application, which is typically a single page app using a {@link Ext.container.Viewport Viewport}.
+ * A typical Ext.app.Application might look like this:
+ *
+ * Ext.application({
+ * name: 'MyApp',
+ * launch: function() {
+ * Ext.create('Ext.container.Viewport', {
+ * items: {
+ * html: 'My App'
+ * }
+ * });
+ * }
+ * });
+ *
+ * This does several things. First it creates a global variable called 'MyApp' - all of your Application's classes (such
+ * as its Models, Views and Controllers) will reside under this single namespace, which drastically lowers the chances
+ * of colliding global variables.
+ *
+ * When the page is ready and all of your JavaScript has loaded, your Application's {@link #launch} function is called,
+ * at which time you can run the code that starts your app. Usually this consists of creating a Viewport, as we do in
+ * the example above.
+ *
+ * # Telling Application about the rest of the app
+ *
+ * Because an Ext.app.Application represents an entire app, we should tell it about the other parts of the app - namely
+ * the Models, Views and Controllers that are bundled with the application. Let's say we have a blog management app; we
+ * might have Models and Controllers for Posts and Comments, and Views for listing, adding and editing Posts and Comments.
+ * Here's how we'd tell our Application about all these things:
+ *
+ * Ext.application({
+ * name: 'Blog',
+ * models: ['Post', 'Comment'],
+ * controllers: ['Posts', 'Comments'],
+ *
+ * launch: function() {
+ * ...
+ * }
+ * });
+ *
+ * Note that we didn't actually list the Views directly in the Application itself. This is because Views are managed by
+ * Controllers, so it makes sense to keep those dependencies there. The Application will load each of the specified
+ * Controllers using the pathing conventions laid out in the [application architecture guide][mvc] -
+ * in this case expecting the controllers to reside in `app/controller/Posts.js` and
+ * `app/controller/Comments.js`. In turn, each Controller simply needs to list the Views it uses and they will be
+ * automatically loaded. Here's how our Posts controller like be defined:
+ *
+ * Ext.define('MyApp.controller.Posts', {
+ * extend: 'Ext.app.Controller',
+ * views: ['posts.List', 'posts.Edit'],
+ *
+ * //the rest of the Controller here
+ * });
+ *
+ * Because we told our Application about our Models and Controllers, and our Controllers about their Views, Ext JS will
+ * automatically load all of our app files for us. This means we don't have to manually add script tags into our html
+ * files whenever we add a new class, but more importantly it enables us to create a minimized build of our entire
+ * application using the Ext JS 4 SDK Tools.
+ *
+ * For more information about writing Ext JS 4 applications, please see the
+ * [application architecture guide][mvc].
+ *
+ * [mvc]: #!/guide/application_architecture
+ *
+ * @docauthor Ed Spencer
+ */
+Ext.define('Ext.app.Application', {
+ extend: 'Ext.app.Controller',
+
+ requires: [
+ 'Ext.ModelManager',
+ 'Ext.data.Model',
+ 'Ext.data.StoreManager',
+ 'Ext.tip.QuickTipManager',
+ 'Ext.ComponentManager',
+ 'Ext.app.EventBus'
+ ],
+
+ /**
+ * @cfg {String} name The name of your application. This will also be the namespace for your views, controllers
+ * models and stores. Don't use spaces or special characters in the name.
+ */
+
+ /**
+ * @cfg {Object} scope The scope to execute the {@link #launch} function in. Defaults to the Application
+ * instance.
+ */
+ scope: undefined,
+
+ /**
+ * @cfg {Boolean} enableQuickTips True to automatically set up Ext.tip.QuickTip support.
+ */
+ enableQuickTips: true,
+
+ /**
+ * @cfg {String} defaultUrl When the app is first loaded, this url will be redirected to.
+ */
+
+ /**
+ * @cfg {String} appFolder The path to the directory which contains all application's classes.
+ * This path will be registered via {@link Ext.Loader#setPath} for the namespace specified in the {@link #name name} config.
+ */
+ appFolder: 'app',
+
+ /**
+ * @cfg {Boolean} autoCreateViewport True to automatically load and instantiate AppName.view.Viewport
+ * before firing the launch function.
+ */
+ autoCreateViewport: false,
+
+ /**
+ * Creates new Application.
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config) {
+ config = config || {};
+ Ext.apply(this, config);
+
+ var requires = config.requires || [];
+
+ Ext.Loader.setPath(this.name, this.appFolder);
+
+ if (this.paths) {
+ Ext.Object.each(this.paths, function(key, value) {
+ Ext.Loader.setPath(key, value);
+ });
+ }
+
+ this.callParent(arguments);
+
+ this.eventbus = Ext.create('Ext.app.EventBus');
+
+ var controllers = Ext.Array.from(this.controllers),
+ ln = controllers && controllers.length,
+ i, controller;
+
+ this.controllers = Ext.create('Ext.util.MixedCollection');
+
+ if (this.autoCreateViewport) {
+ requires.push(this.getModuleClassName('Viewport', 'view'));
+ }
+
+ for (i = 0; i < ln; i++) {
+ requires.push(this.getModuleClassName(controllers[i], 'controller'));
+ }
+
+ Ext.require(requires);
+
+ Ext.onReady(function() {
+ for (i = 0; i < ln; i++) {
+ controller = this.getController(controllers[i]);
+ controller.init(this);
+ }
+
+ this.onBeforeLaunch.call(this);
+ }, this);
+ },
+
+ control: function(selectors, listeners, controller) {
+ this.eventbus.control(selectors, listeners, controller);
+ },
+
+ /**
+ * Called automatically when the page has completely loaded. This is an empty function that should be
+ * overridden by each application that needs to take action on page load
+ * @property launch
+ * @type Function
+ * @param {String} profile The detected {@link #profiles application profile}
+ * @return {Boolean} By default, the Application will dispatch to the configured startup controller and
+ * action immediately after running the launch function. Return false to prevent this behavior.
+ */
+ launch: Ext.emptyFn,
+
+ /**
+ * @private
+ */
+ onBeforeLaunch: function() {
+ if (this.enableQuickTips) {
+ Ext.tip.QuickTipManager.init();
+ }
+
+ if (this.autoCreateViewport) {
+ this.getView('Viewport').create();
+ }
+
+ this.launch.call(this.scope || this);
+ this.launched = true;
+ this.fireEvent('launch', this);
+
+ this.controllers.each(function(controller) {
+ controller.onLaunch(this);
+ }, this);
+ },
+
+ getModuleClassName: function(name, type) {
+ var namespace = Ext.Loader.getPrefix(name);
+
+ if (namespace.length > 0 && namespace !== name) {
+ return name;
+ }
+
+ return this.name + '.' + type + '.' + name;
+ },
+
+ getController: function(name) {
+ var controller = this.controllers.get(name);
+
+ if (!controller) {
+ controller = Ext.create(this.getModuleClassName(name, 'controller'), {
+ application: this,
+ id: name
+ });
+
+ this.controllers.add(controller);
+ }
+
+ return controller;
+ },
+
+ getStore: function(name) {
+ var store = Ext.StoreManager.get(name);
+
+ if (!store) {
+ store = Ext.create(this.getModuleClassName(name, 'store'), {
+ storeId: name
+ });
+ }
+
+ return store;
+ },
+
+ getModel: function(model) {
+ model = this.getModuleClassName(model, 'model');
+
+ return Ext.ModelManager.getModel(model);
+ },
+
+ getView: function(view) {
+ view = this.getModuleClassName(view, 'view');
+
+ return Ext.ClassManager.get(view);
+ }
+});
+
+/**
+ * @class Ext.chart.Callout
+ * A mixin providing callout functionality for Ext.chart.series.Series.
+ */
+Ext.define('Ext.chart.Callout', {
+
+ /* Begin Definitions */
+
+ /* End Definitions */
+
+ constructor: function(config) {
+ if (config.callouts) {
+ config.callouts.styles = Ext.applyIf(config.callouts.styles || {}, {
+ color: "#000",
+ font: "11px Helvetica, sans-serif"
+ });
+ this.callouts = Ext.apply(this.callouts || {}, config.callouts);
+ this.calloutsArray = [];
+ }
+ },
+
+ renderCallouts: function() {
+ if (!this.callouts) {
+ return;
+ }
+
+ var me = this,
+ items = me.items,
+ animate = me.chart.animate,
+ config = me.callouts,
+ styles = config.styles,
+ group = me.calloutsArray,
+ store = me.chart.store,
+ len = store.getCount(),
+ ratio = items.length / len,
+ previouslyPlacedCallouts = [],
+ i,
+ count,
+ j,
+ p;
+
+ for (i = 0, count = 0; i < len; i++) {
+ for (j = 0; j < ratio; j++) {
+ var item = items[count],
+ label = group[count],
+ storeItem = store.getAt(i),
+ display;
+
+ display = config.filter(storeItem);
+
+ if (!display && !label) {
+ count++;
+ continue;
+ }
+
+ if (!label) {
+ group[count] = label = me.onCreateCallout(storeItem, item, i, display, j, count);
+ }
+ for (p in label) {
+ if (label[p] && label[p].setAttributes) {
+ label[p].setAttributes(styles, true);
+ }
+ }
+ if (!display) {
+ for (p in label) {
+ if (label[p]) {
+ if (label[p].setAttributes) {
+ label[p].setAttributes({
+ hidden: true
+ }, true);
+ } else if(label[p].setVisible) {
+ label[p].setVisible(false);
+ }
+ }
+ }
+ }
+ config.renderer(label, storeItem);
+ me.onPlaceCallout(label, storeItem, item, i, display, animate,
+ j, count, previouslyPlacedCallouts);
+ previouslyPlacedCallouts.push(label);
+ count++;
+ }
+ }
+ this.hideCallouts(count);
+ },
+
+ onCreateCallout: function(storeItem, item, i, display) {
+ var me = this,
+ group = me.calloutsGroup,
+ config = me.callouts,
+ styles = config.styles,
+ width = styles.width,
+ height = styles.height,
+ chart = me.chart,
+ surface = chart.surface,
+ calloutObj = {
+ //label: false,
+ //box: false,
+ lines: false
+ };
+
+ calloutObj.lines = surface.add(Ext.apply({},
+ {
+ type: 'path',
+ path: 'M0,0',
+ stroke: me.getLegendColor() || '#555'
+ },
+ styles));
+
+ if (config.items) {
+ calloutObj.panel = Ext.create('widget.panel', {
+ style: "position: absolute;",
+ width: width,
+ height: height,
+ items: config.items,
+ renderTo: chart.el
+ });
+ }
+
+ return calloutObj;
+ },
+
+ hideCallouts: function(index) {
+ var calloutsArray = this.calloutsArray,
+ len = calloutsArray.length,
+ co,
+ p;
+ while (len-->index) {
+ co = calloutsArray[len];
+ for (p in co) {
+ if (co[p]) {
+ co[p].hide(true);
+ }
+ }
+ }
+ }
+});
+
+/**
+ * @class Ext.draw.CompositeSprite
+ * @extends Ext.util.MixedCollection
+ *
+ * A composite Sprite handles a group of sprites with common methods to a sprite
+ * such as `hide`, `show`, `setAttributes`. These methods are applied to the set of sprites
+ * added to the group.
+ *
+ * CompositeSprite extends {@link Ext.util.MixedCollection} so you can use the same methods
+ * in `MixedCollection` to iterate through sprites, add and remove elements, etc.
+ *
+ * In order to create a CompositeSprite, one has to provide a handle to the surface where it is
+ * rendered:
+ *
+ * var group = Ext.create('Ext.draw.CompositeSprite', {
+ * surface: drawComponent.surface
+ * });
+ *
+ * Then just by using `MixedCollection` methods it's possible to add {@link Ext.draw.Sprite}s:
+ *
+ * group.add(sprite1);
+ * group.add(sprite2);
+ * group.add(sprite3);
+ *
+ * And then apply common Sprite methods to them:
+ *
+ * group.setAttributes({
+ * fill: '#f00'
+ * }, true);
+ */
+Ext.define('Ext.draw.CompositeSprite', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.util.MixedCollection',
+ mixins: {
+ animate: 'Ext.util.Animate'
+ },
+
+ /* End Definitions */
+ isCompositeSprite: true,
+ constructor: function(config) {
+ var me = this;
+
+ config = config || {};
+ Ext.apply(me, config);
+
+ me.addEvents(
+ 'mousedown',
+ 'mouseup',
+ 'mouseover',
+ 'mouseout',
+ 'click'
+ );
+ me.id = Ext.id(null, 'ext-sprite-group-');
+ me.callParent();
+ },
+
+ // @private
+ onClick: function(e) {
+ this.fireEvent('click', e);
+ },
+
+ // @private
+ onMouseUp: function(e) {
+ this.fireEvent('mouseup', e);
+ },
+
+ // @private
+ onMouseDown: function(e) {
+ this.fireEvent('mousedown', e);
+ },
+
+ // @private
+ onMouseOver: function(e) {
+ this.fireEvent('mouseover', e);
+ },
+
+ // @private
+ onMouseOut: function(e) {
+ this.fireEvent('mouseout', e);
+ },
+
+ attachEvents: function(o) {
+ var me = this;
+
+ o.on({
+ scope: me,
+ mousedown: me.onMouseDown,
+ mouseup: me.onMouseUp,
+ mouseover: me.onMouseOver,
+ mouseout: me.onMouseOut,
+ click: me.onClick
+ });
+ },
+
+ // Inherit docs from MixedCollection
+ add: function(key, o) {
+ var result = this.callParent(arguments);
+ this.attachEvents(result);
+ return result;
+ },
+
+ insert: function(index, key, o) {
+ return this.callParent(arguments);
+ },
+
+ // Inherit docs from MixedCollection
+ remove: function(o) {
+ var me = this;
+
+ o.un({
+ scope: me,
+ mousedown: me.onMouseDown,
+ mouseup: me.onMouseUp,
+ mouseover: me.onMouseOver,
+ mouseout: me.onMouseOut,
+ click: me.onClick
+ });
+ return me.callParent(arguments);
+ },
+
+ /**
+ * Returns the group bounding box.
+ * Behaves like {@link Ext.draw.Sprite#getBBox} method.
+ * @return {Object} an object with x, y, width, and height properties.
+ */
+ getBBox: function() {
+ var i = 0,
+ sprite,
+ bb,
+ items = this.items,
+ len = this.length,
+ infinity = Infinity,
+ minX = infinity,
+ maxHeight = -infinity,
+ minY = infinity,
+ maxWidth = -infinity,
+ maxWidthBBox, maxHeightBBox;
+
+ for (; i < len; i++) {
+ sprite = items[i];
+ if (sprite.el) {
+ bb = sprite.getBBox();
+ minX = Math.min(minX, bb.x);
+ minY = Math.min(minY, bb.y);
+ maxHeight = Math.max(maxHeight, bb.height + bb.y);
+ maxWidth = Math.max(maxWidth, bb.width + bb.x);
+ }
+ }
+
+ return {
+ x: minX,
+ y: minY,
+ height: maxHeight - minY,
+ width: maxWidth - minX
+ };
+ },
+
+ /**
+ * Iterates through all sprites calling `setAttributes` on each one. For more information {@link Ext.draw.Sprite}
+ * provides a description of the attributes that can be set with this method.
+ * @param {Object} attrs Attributes to be changed on the sprite.
+ * @param {Boolean} redraw Flag to immediatly draw the change.
+ * @return {Ext.draw.CompositeSprite} this
+ */
+ setAttributes: function(attrs, redraw) {
+ var i = 0,
+ items = this.items,
+ len = this.length;
+
+ for (; i < len; i++) {
+ items[i].setAttributes(attrs, redraw);
+ }
+ return this;
+ },
+
+ /**
+ * Hides all sprites. If the first parameter of the method is true
+ * then a redraw will be forced for each sprite.
+ * @param {Boolean} redraw Flag to immediatly draw the change.
+ * @return {Ext.draw.CompositeSprite} this
+ */
+ hide: function(redraw) {
+ var i = 0,
+ items = this.items,
+ len = this.length;
+
+ for (; i < len; i++) {
+ items[i].hide(redraw);
+ }
+ return this;
+ },
+
+ /**
+ * Shows all sprites. If the first parameter of the method is true
+ * then a redraw will be forced for each sprite.
+ * @param {Boolean} redraw Flag to immediatly draw the change.
+ * @return {Ext.draw.CompositeSprite} this
+ */
+ show: function(redraw) {
+ var i = 0,
+ items = this.items,
+ len = this.length;
+
+ for (; i < len; i++) {
+ items[i].show(redraw);
+ }
+ return this;
+ },
+
+ redraw: function() {
+ var me = this,
+ i = 0,
+ items = me.items,
+ surface = me.getSurface(),
+ len = me.length;
+
+ if (surface) {
+ for (; i < len; i++) {
+ surface.renderItem(items[i]);
+ }
+ }
+ return me;
+ },
+
+ setStyle: function(obj) {
+ var i = 0,
+ items = this.items,
+ len = this.length,
+ item, el;
+
+ for (; i < len; i++) {
+ item = items[i];
+ el = item.el;
+ if (el) {
+ el.setStyle(obj);
+ }
+ }
+ },
+
+ addCls: function(obj) {
+ var i = 0,
+ items = this.items,
+ surface = this.getSurface(),
+ len = this.length;
+
+ if (surface) {
+ for (; i < len; i++) {
+ surface.addCls(items[i], obj);
+ }
+ }
+ },
+
+ removeCls: function(obj) {
+ var i = 0,
+ items = this.items,
+ surface = this.getSurface(),
+ len = this.length;
+
+ if (surface) {
+ for (; i < len; i++) {
+ surface.removeCls(items[i], obj);
+ }
+ }
+ },
+
+ /**
+ * Grab the surface from the items
+ * @private
+ * @return {Ext.draw.Surface} The surface, null if not found
+ */
+ getSurface: function(){
+ var first = this.first();
+ if (first) {
+ return first.surface;
+ }
+ return null;
+ },
+
+ /**
+ * Destroys the SpriteGroup
+ */
+ destroy: function(){
+ var me = this,
+ surface = me.getSurface(),
+ item;
+
+ if (surface) {
+ while (me.getCount() > 0) {
+ item = me.first();
+ me.remove(item);
+ surface.remove(item);
+ }
+ }
+ me.clearListeners();
+ }
+});
+
+/**
+ * @class Ext.layout.component.Auto
+ * @extends Ext.layout.component.Component
+ * @private
+ *
+ * <p>The AutoLayout is the default layout manager delegated by {@link Ext.Component} to
+ * render any child Elements when no <tt>{@link Ext.container.Container#layout layout}</tt> is configured.</p>
+ */
+
+Ext.define('Ext.layout.component.Auto', {
+
+ /* Begin Definitions */
+
+ alias: 'layout.autocomponent',
+
+ extend: 'Ext.layout.component.Component',
+
+ /* End Definitions */
+
+ type: 'autocomponent',
+
+ onLayout : function(width, height) {
+ this.setTargetSize(width, height);
+ }
+});
+/**
+ * @class Ext.chart.theme.Theme
+ *
+ * Provides chart theming.
+ *
+ * Used as mixins by Ext.chart.Chart.
+ */
+Ext.define('Ext.chart.theme.Theme', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.draw.Color'],
+
+ /* End Definitions */
+
+ theme: 'Base',
+ themeAttrs: false,
+
+ initTheme: function(theme) {
+ var me = this,
+ themes = Ext.chart.theme,
+ key, gradients;
+ if (theme) {
+ theme = theme.split(':');
+ for (key in themes) {
+ if (key == theme[0]) {
+ gradients = theme[1] == 'gradients';
+ me.themeAttrs = new themes[key]({
+ useGradients: gradients
+ });
+ if (gradients) {
+ me.gradients = me.themeAttrs.gradients;
+ }
+ if (me.themeAttrs.background) {
+ me.background = me.themeAttrs.background;
+ }
+ return;
+ }
+ }
+ }
+ }
+},
+// This callback is executed right after when the class is created. This scope refers to the newly created class itself
+function() {
+ /* Theme constructor: takes either a complex object with styles like:
+
+ {
+ axis: {
+ fill: '#000',
+ 'stroke-width': 1
+ },
+ axisLabelTop: {
+ fill: '#000',
+ font: '11px Arial'
+ },
+ axisLabelLeft: {
+ fill: '#000',
+ font: '11px Arial'
+ },
+ axisLabelRight: {
+ fill: '#000',
+ font: '11px Arial'
+ },
+ axisLabelBottom: {
+ fill: '#000',
+ font: '11px Arial'
+ },
+ axisTitleTop: {
+ fill: '#000',
+ font: '11px Arial'
+ },
+ axisTitleLeft: {
+ fill: '#000',
+ font: '11px Arial'
+ },
+ axisTitleRight: {
+ fill: '#000',
+ font: '11px Arial'
+ },
+ axisTitleBottom: {
+ fill: '#000',
+ font: '11px Arial'
+ },
+ series: {
+ 'stroke-width': 1
+ },
+ seriesLabel: {
+ font: '12px Arial',
+ fill: '#333'
+ },
+ marker: {
+ stroke: '#555',
+ fill: '#000',
+ radius: 3,
+ size: 3
+ },
+ seriesThemes: [{
+ fill: '#C6DBEF'
+ }, {
+ fill: '#9ECAE1'
+ }, {
+ fill: '#6BAED6'
+ }, {
+ fill: '#4292C6'
+ }, {
+ fill: '#2171B5'
+ }, {
+ fill: '#084594'
+ }],
+ markerThemes: [{
+ fill: '#084594',
+ type: 'circle'
+ }, {
+ fill: '#2171B5',
+ type: 'cross'
+ }, {
+ fill: '#4292C6',
+ type: 'plus'
+ }]
+ }
+
+ ...or also takes just an array of colors and creates the complex object:
+
+ {
+ colors: ['#aaa', '#bcd', '#eee']
+ }
+
+ ...or takes just a base color and makes a theme from it
+
+ {
+ baseColor: '#bce'
+ }
+
+ To create a new theme you may add it to the Themes object:
+
+ Ext.chart.theme.MyNewTheme = Ext.extend(Object, {
+ constructor: function(config) {
+ Ext.chart.theme.call(this, config, {
+ baseColor: '#mybasecolor'
+ });
+ }
+ });
+
+ //Proposal:
+ Ext.chart.theme.MyNewTheme = Ext.chart.createTheme('#basecolor');
+
+ ...and then to use it provide the name of the theme (as a lower case string) in the chart config.
+
+ {
+ theme: 'mynewtheme'
+ }
+ */
+
+(function() {
+ Ext.chart.theme = function(config, base) {
+ config = config || {};
+ var i = 0, l, colors, color,
+ seriesThemes, markerThemes,
+ seriesTheme, markerTheme,
+ key, gradients = [],
+ midColor, midL;
+
+ if (config.baseColor) {
+ midColor = Ext.draw.Color.fromString(config.baseColor);
+ midL = midColor.getHSL()[2];
+ if (midL < 0.15) {
+ midColor = midColor.getLighter(0.3);
+ } else if (midL < 0.3) {
+ midColor = midColor.getLighter(0.15);
+ } else if (midL > 0.85) {
+ midColor = midColor.getDarker(0.3);
+ } else if (midL > 0.7) {
+ midColor = midColor.getDarker(0.15);
+ }
+ config.colors = [ midColor.getDarker(0.3).toString(),
+ midColor.getDarker(0.15).toString(),
+ midColor.toString(),
+ midColor.getLighter(0.15).toString(),
+ midColor.getLighter(0.3).toString()];
+
+ delete config.baseColor;
+ }
+ if (config.colors) {
+ colors = config.colors.slice();
+ markerThemes = base.markerThemes;
+ seriesThemes = base.seriesThemes;
+ l = colors.length;
+ base.colors = colors;
+ for (; i < l; i++) {
+ color = colors[i];
+ markerTheme = markerThemes[i] || {};
+ seriesTheme = seriesThemes[i] || {};
+ markerTheme.fill = seriesTheme.fill = markerTheme.stroke = seriesTheme.stroke = color;
+ markerThemes[i] = markerTheme;
+ seriesThemes[i] = seriesTheme;
+ }
+ base.markerThemes = markerThemes.slice(0, l);
+ base.seriesThemes = seriesThemes.slice(0, l);
+ //the user is configuring something in particular (either markers, series or pie slices)
+ }
+ for (key in base) {
+ if (key in config) {
+ if (Ext.isObject(config[key]) && Ext.isObject(base[key])) {
+ Ext.apply(base[key], config[key]);
+ } else {
+ base[key] = config[key];
+ }
+ }
+ }
+ if (config.useGradients) {
+ colors = base.colors || (function () {
+ var ans = [];
+ for (i = 0, seriesThemes = base.seriesThemes, l = seriesThemes.length; i < l; i++) {
+ ans.push(seriesThemes[i].fill || seriesThemes[i].stroke);
+ }
+ return ans;
+ })();
+ for (i = 0, l = colors.length; i < l; i++) {
+ midColor = Ext.draw.Color.fromString(colors[i]);
+ if (midColor) {
+ color = midColor.getDarker(0.1).toString();
+ midColor = midColor.toString();
+ key = 'theme-' + midColor.substr(1) + '-' + color.substr(1);
+ gradients.push({
+ id: key,
+ angle: 45,
+ stops: {
+ 0: {
+ color: midColor.toString()
+ },
+ 100: {
+ color: color.toString()
+ }
+ }
+ });
+ colors[i] = 'url(#' + key + ')';
+ }
+ }
+ base.gradients = gradients;
+ base.colors = colors;
+ }
+ /*
+ base.axis = Ext.apply(base.axis || {}, config.axis || {});
+ base.axisLabel = Ext.apply(base.axisLabel || {}, config.axisLabel || {});
+ base.axisTitle = Ext.apply(base.axisTitle || {}, config.axisTitle || {});
+ */
+ Ext.apply(this, base);
+ };
+})();
+});
+
+/**
+ * @class Ext.chart.Mask
+ *
+ * Defines a mask for a chart's series.
+ * The 'chart' member must be set prior to rendering.
+ *
+ * A Mask can be used to select a certain region in a chart.
+ * When enabled, the `select` event will be triggered when a
+ * region is selected by the mask, allowing the user to perform
+ * other tasks like zooming on that region, etc.
+ *
+ * In order to use the mask one has to set the Chart `mask` option to
+ * `true`, `vertical` or `horizontal`. Then a possible configuration for the
+ * listener could be:
+ *
+ items: {
+ xtype: 'chart',
+ animate: true,
+ store: store1,
+ mask: 'horizontal',
+ listeners: {
+ select: {
+ fn: function(me, selection) {
+ me.setZoom(selection);
+ me.mask.hide();
+ }
+ }
+ },
+
+ * In this example we zoom the chart to that particular region. You can also get
+ * a handle to a mask instance from the chart object. The `chart.mask` element is a
+ * `Ext.Panel`.
+ *
+ */
+Ext.define('Ext.chart.Mask', {
+ require: ['Ext.chart.MaskLayer'],
+ /**
+ * Creates new Mask.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ var me = this;
+
+ me.addEvents('select');
+
+ if (config) {
+ Ext.apply(me, config);
+ }
+ if (me.mask) {
+ me.on('afterrender', function() {
+ //create a mask layer component
+ var comp = Ext.create('Ext.chart.MaskLayer', {
+ renderTo: me.el
+ });
+ comp.el.on({
+ 'mousemove': function(e) {
+ me.onMouseMove(e);
+ },
+ 'mouseup': function(e) {
+ me.resized(e);
+ }
+ });
+ //create a resize handler for the component
+ var resizeHandler = Ext.create('Ext.resizer.Resizer', {
+ el: comp.el,
+ handles: 'all',
+ pinned: true
+ });
+ resizeHandler.on({
+ 'resize': function(e) {
+ me.resized(e);
+ }
+ });
+ comp.initDraggable();
+ me.maskType = me.mask;
+ me.mask = comp;
+ me.maskSprite = me.surface.add({
+ type: 'path',
+ path: ['M', 0, 0],
+ zIndex: 1001,
+ opacity: 0.7,
+ hidden: true,
+ stroke: '#444'
+ });
+ }, me, { single: true });
+ }
+ },
+
+ resized: function(e) {
+ var me = this,
+ bbox = me.bbox || me.chartBBox,
+ x = bbox.x,
+ y = bbox.y,
+ width = bbox.width,
+ height = bbox.height,
+ box = me.mask.getBox(true),
+ max = Math.max,
+ min = Math.min,
+ staticX = box.x - x,
+ staticY = box.y - y;
+
+ staticX = max(staticX, x);
+ staticY = max(staticY, y);
+ staticX = min(staticX, width);
+ staticY = min(staticY, height);
+ box.x = staticX;
+ box.y = staticY;
+ me.fireEvent('select', me, box);
+ },
+
+ onMouseUp: function(e) {
+ var me = this,
+ bbox = me.bbox || me.chartBBox,
+ sel = me.maskSelection;
+ me.maskMouseDown = false;
+ me.mouseDown = false;
+ if (me.mouseMoved) {
+ me.onMouseMove(e);
+ me.mouseMoved = false;
+ me.fireEvent('select', me, {
+ x: sel.x - bbox.x,
+ y: sel.y - bbox.y,
+ width: sel.width,
+ height: sel.height
+ });
+ }
+ },
+
+ onMouseDown: function(e) {
+ var me = this;
+ me.mouseDown = true;
+ me.mouseMoved = false;
+ me.maskMouseDown = {
+ x: e.getPageX() - me.el.getX(),
+ y: e.getPageY() - me.el.getY()
+ };
+ },
+
+ onMouseMove: function(e) {
+ var me = this,
+ mask = me.maskType,
+ bbox = me.bbox || me.chartBBox,
+ x = bbox.x,
+ y = bbox.y,
+ math = Math,
+ floor = math.floor,
+ abs = math.abs,
+ min = math.min,
+ max = math.max,
+ height = floor(y + bbox.height),
+ width = floor(x + bbox.width),
+ posX = e.getPageX(),
+ posY = e.getPageY(),
+ staticX = posX - me.el.getX(),
+ staticY = posY - me.el.getY(),
+ maskMouseDown = me.maskMouseDown,
+ path;
+
+ me.mouseMoved = me.mouseDown;
+ staticX = max(staticX, x);
+ staticY = max(staticY, y);
+ staticX = min(staticX, width);
+ staticY = min(staticY, height);
+ if (maskMouseDown && me.mouseDown) {
+ if (mask == 'horizontal') {
+ staticY = y;
+ maskMouseDown.y = height;
+ posY = me.el.getY() + bbox.height + me.insetPadding;
+ }
+ else if (mask == 'vertical') {
+ staticX = x;
+ maskMouseDown.x = width;
+ }
+ width = maskMouseDown.x - staticX;
+ height = maskMouseDown.y - staticY;
+ path = ['M', staticX, staticY, 'l', width, 0, 0, height, -width, 0, 'z'];
+ me.maskSelection = {
+ x: width > 0 ? staticX : staticX + width,
+ y: height > 0 ? staticY : staticY + height,
+ width: abs(width),
+ height: abs(height)
+ };
+ me.mask.updateBox(me.maskSelection);
+ me.mask.show();
+ me.maskSprite.setAttributes({
+ hidden: true
+ }, true);
+ }
+ else {
+ if (mask == 'horizontal') {
+ path = ['M', staticX, y, 'L', staticX, height];
+ }
+ else if (mask == 'vertical') {
+ path = ['M', x, staticY, 'L', width, staticY];
+ }
+ else {
+ path = ['M', staticX, y, 'L', staticX, height, 'M', x, staticY, 'L', width, staticY];
+ }
+ me.maskSprite.setAttributes({
+ path: path,
+ fill: me.maskMouseDown ? me.maskSprite.stroke : false,
+ 'stroke-width': mask === true ? 1 : 3,
+ hidden: false
+ }, true);
+ }
+ },
+
+ onMouseLeave: function(e) {
+ var me = this;
+ me.mouseMoved = false;
+ me.mouseDown = false;
+ me.maskMouseDown = false;
+ me.mask.hide();
+ me.maskSprite.hide(true);
+ }
+});
+
+/**
+ * @class Ext.chart.Navigation
+ *
+ * Handles panning and zooming capabilities.
+ *
+ * Used as mixin by Ext.chart.Chart.
+ */
+Ext.define('Ext.chart.Navigation', {
+
+ constructor: function() {
+ this.originalStore = this.store;
+ },
+
+ /**
+ * Zooms the chart to the specified selection range.
+ * Can be used with a selection mask. For example:
+ *
+ * items: {
+ * xtype: 'chart',
+ * animate: true,
+ * store: store1,
+ * mask: 'horizontal',
+ * listeners: {
+ * select: {
+ * fn: function(me, selection) {
+ * me.setZoom(selection);
+ * me.mask.hide();
+ * }
+ * }
+ * }
+ * }
+ */
+ setZoom: function(zoomConfig) {
+ var me = this,
+ axes = me.axes,
+ bbox = me.chartBBox,
+ xScale = 1 / bbox.width,
+ yScale = 1 / bbox.height,
+ zoomer = {
+ x : zoomConfig.x * xScale,
+ y : zoomConfig.y * yScale,
+ width : zoomConfig.width * xScale,
+ height : zoomConfig.height * yScale
+ };
+ axes.each(function(axis) {
+ var ends = axis.calcEnds();
+ if (axis.position == 'bottom' || axis.position == 'top') {
+ var from = (ends.to - ends.from) * zoomer.x + ends.from,
+ to = (ends.to - ends.from) * zoomer.width + from;
+ axis.minimum = from;
+ axis.maximum = to;
+ } else {
+ var to = (ends.to - ends.from) * (1 - zoomer.y) + ends.from,
+ from = to - (ends.to - ends.from) * zoomer.height;
+ axis.minimum = from;
+ axis.maximum = to;
+ }
+ });
+ me.redraw(false);
+ },
+
+ /**
+ * Restores the zoom to the original value. This can be used to reset
+ * the previous zoom state set by `setZoom`. For example:
+ *
+ * myChart.restoreZoom();
+ */
+ restoreZoom: function() {
+ this.store = this.substore = this.originalStore;
+ this.redraw(true);
+ }
+
+});
+
+/**
+ * @class Ext.chart.Shape
+ * @ignore
+ */
+Ext.define('Ext.chart.Shape', {
+
+ /* Begin Definitions */
+
+ singleton: true,
+
+ /* End Definitions */
+
+ circle: function (surface, opts) {
+ return surface.add(Ext.apply({
+ type: 'circle',
+ x: opts.x,
+ y: opts.y,
+ stroke: null,
+ radius: opts.radius
+ }, opts));
+ },
+ line: function (surface, opts) {
+ return surface.add(Ext.apply({
+ type: 'rect',
+ x: opts.x - opts.radius,
+ y: opts.y - opts.radius,
+ height: 2 * opts.radius,
+ width: 2 * opts.radius / 5
+ }, opts));
+ },
+ square: function (surface, opts) {
+ return surface.add(Ext.applyIf({
+ type: 'rect',
+ x: opts.x - opts.radius,
+ y: opts.y - opts.radius,
+ height: 2 * opts.radius,
+ width: 2 * opts.radius,
+ radius: null
+ }, opts));
+ },
+ triangle: function (surface, opts) {
+ opts.radius *= 1.75;
+ return surface.add(Ext.apply({
+ type: 'path',
+ stroke: null,
+ path: "M".concat(opts.x, ",", opts.y, "m0-", opts.radius * 0.58, "l", opts.radius * 0.5, ",", opts.radius * 0.87, "-", opts.radius, ",0z")
+ }, opts));
+ },
+ diamond: function (surface, opts) {
+ var r = opts.radius;
+ r *= 1.5;
+ return surface.add(Ext.apply({
+ type: 'path',
+ stroke: null,
+ path: ["M", opts.x, opts.y - r, "l", r, r, -r, r, -r, -r, r, -r, "z"]
+ }, opts));
+ },
+ cross: function (surface, opts) {
+ var r = opts.radius;
+ r = r / 1.7;
+ return surface.add(Ext.apply({
+ type: 'path',
+ stroke: null,
+ path: "M".concat(opts.x - r, ",", opts.y, "l", [-r, -r, r, -r, r, r, r, -r, r, r, -r, r, r, r, -r, r, -r, -r, -r, r, -r, -r, "z"])
+ }, opts));
+ },
+ plus: function (surface, opts) {
+ var r = opts.radius / 1.3;
+ return surface.add(Ext.apply({
+ type: 'path',
+ stroke: null,
+ path: "M".concat(opts.x - r / 2, ",", opts.y - r / 2, "l", [0, -r, r, 0, 0, r, r, 0, 0, r, -r, 0, 0, r, -r, 0, 0, -r, -r, 0, 0, -r, "z"])
+ }, opts));
+ },
+ arrow: function (surface, opts) {
+ var r = opts.radius;
+ return surface.add(Ext.apply({
+ type: 'path',
+ path: "M".concat(opts.x - r * 0.7, ",", opts.y - r * 0.4, "l", [r * 0.6, 0, 0, -r * 0.4, r, r * 0.8, -r, r * 0.8, 0, -r * 0.4, -r * 0.6, 0], "z")
+ }, opts));
+ },
+ drop: function (surface, x, y, text, size, angle) {
+ size = size || 30;
+ angle = angle || 0;
+ surface.add({
+ type: 'path',
+ path: ['M', x, y, 'l', size, 0, 'A', size * 0.4, size * 0.4, 0, 1, 0, x + size * 0.7, y - size * 0.7, 'z'],
+ fill: '#000',
+ stroke: 'none',
+ rotate: {
+ degrees: 22.5 - angle,
+ x: x,
+ y: y
+ }
+ });
+ angle = (angle + 90) * Math.PI / 180;
+ surface.add({
+ type: 'text',
+ x: x + size * Math.sin(angle) - 10, // Shift here, Not sure why.
+ y: y + size * Math.cos(angle) + 5,
+ text: text,
+ 'font-size': size * 12 / 40,
+ stroke: 'none',
+ fill: '#fff'
+ });
+ }
+});
+/**
+ * A Surface is an interface to render methods inside a draw {@link Ext.draw.Component}.
+ * A Surface contains methods to render sprites, get bounding boxes of sprites, add
+ * sprites to the canvas, initialize other graphic components, etc. One of the most used
+ * methods for this class is the `add` method, to add Sprites to the surface.
+ *
+ * Most of the Surface methods are abstract and they have a concrete implementation
+ * in VML or SVG engines.
+ *
+ * A Surface instance can be accessed as a property of a draw component. For example:
+ *
+ * drawComponent.surface.add({
+ * type: 'circle',
+ * fill: '#ffc',
+ * radius: 100,
+ * x: 100,
+ * y: 100
+ * });
+ *
+ * The configuration object passed in the `add` method is the same as described in the {@link Ext.draw.Sprite}
+ * class documentation.
+ *
+ * # Listeners
+ *
+ * You can also add event listeners to the surface using the `Observable` listener syntax. Supported events are:
+ *
+ * - mousedown
+ * - mouseup
+ * - mouseover
+ * - mouseout
+ * - mousemove
+ * - mouseenter
+ * - mouseleave
+ * - click
+ *
+ * For example:
+ *
+ * drawComponent.surface.on({
+ * 'mousemove': function() {
+ * console.log('moving the mouse over the surface');
+ * }
+ * });
+ *
+ * # Example
+ *
+ * var drawComponent = Ext.create('Ext.draw.Component', {
+ * width: 800,
+ * height: 600,
+ * renderTo: document.body
+ * }), surface = drawComponent.surface;
+ *
+ * surface.add([{
+ * type: 'circle',
+ * radius: 10,
+ * fill: '#f00',
+ * x: 10,
+ * y: 10,
+ * group: 'circles'
+ * }, {
+ * type: 'circle',
+ * radius: 10,
+ * fill: '#0f0',
+ * x: 50,
+ * y: 50,
+ * group: 'circles'
+ * }, {
+ * type: 'circle',
+ * radius: 10,
+ * fill: '#00f',
+ * x: 100,
+ * y: 100,
+ * group: 'circles'
+ * }, {
+ * type: 'rect',
+ * width: 20,
+ * height: 20,
+ * fill: '#f00',
+ * x: 10,
+ * y: 10,
+ * group: 'rectangles'
+ * }, {
+ * type: 'rect',
+ * width: 20,
+ * height: 20,
+ * fill: '#0f0',
+ * x: 50,
+ * y: 50,
+ * group: 'rectangles'
+ * }, {
+ * type: 'rect',
+ * width: 20,
+ * height: 20,
+ * fill: '#00f',
+ * x: 100,
+ * y: 100,
+ * group: 'rectangles'
+ * }]);
+ *
+ * // Get references to my groups
+ * circles = surface.getGroup('circles');
+ * rectangles = surface.getGroup('rectangles');
+ *
+ * // Animate the circles down
+ * circles.animate({
+ * duration: 1000,
+ * to: {
+ * translate: {
+ * y: 200
+ * }
+ * }
+ * });
+ *
+ * // Animate the rectangles across
+ * rectangles.animate({
+ * duration: 1000,
+ * to: {
+ * translate: {
+ * x: 200
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.draw.Surface', {
+
+ /* Begin Definitions */
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ requires: ['Ext.draw.CompositeSprite'],
+ uses: ['Ext.draw.engine.Svg', 'Ext.draw.engine.Vml'],
+
+ separatorRe: /[, ]+/,
+
+ statics: {
+ /**
+ * Creates and returns a new concrete Surface instance appropriate for the current environment.
+ * @param {Object} config Initial configuration for the Surface instance
+ * @param {String[]} enginePriority (Optional) order of implementations to use; the first one that is
+ * available in the current environment will be used. Defaults to `['Svg', 'Vml']`.
+ * @return {Object} The created Surface or false.
+ * @static
+ */
+ create: function(config, enginePriority) {
+ enginePriority = enginePriority || ['Svg', 'Vml'];
+
+ var i = 0,
+ len = enginePriority.length,
+ surfaceClass;
+
+ for (; i < len; i++) {
+ if (Ext.supports[enginePriority[i]]) {
+ return Ext.create('Ext.draw.engine.' + enginePriority[i], config);
+ }
+ }
+ return false;
+ }
+ },
+
+ /* End Definitions */
+
+ // @private
+ availableAttrs: {
+ blur: 0,
+ "clip-rect": "0 0 1e9 1e9",
+ cursor: "default",
+ cx: 0,
+ cy: 0,
+ 'dominant-baseline': 'auto',
+ fill: "none",
+ "fill-opacity": 1,
+ font: '10px "Arial"',
+ "font-family": '"Arial"',
+ "font-size": "10",
+ "font-style": "normal",
+ "font-weight": 400,
+ gradient: "",
+ height: 0,
+ hidden: false,
+ href: "http://sencha.com/",
+ opacity: 1,
+ path: "M0,0",
+ radius: 0,
+ rx: 0,
+ ry: 0,
+ scale: "1 1",
+ src: "",
+ stroke: "#000",
+ "stroke-dasharray": "",
+ "stroke-linecap": "butt",
+ "stroke-linejoin": "butt",
+ "stroke-miterlimit": 0,
+ "stroke-opacity": 1,
+ "stroke-width": 1,
+ target: "_blank",
+ text: "",
+ "text-anchor": "middle",
+ title: "Ext Draw",
+ width: 0,
+ x: 0,
+ y: 0,
+ zIndex: 0
+ },
+
+ /**
+ * @cfg {Number} height
+ * The height of this component in pixels (defaults to auto).
+ */
+ /**
+ * @cfg {Number} width
+ * The width of this component in pixels (defaults to auto).
+ */
+
+ container: undefined,
+ height: 352,
+ width: 512,
+ x: 0,
+ y: 0,
+
+ /**
+ * @private Flag indicating that the surface implementation requires sprites to be maintained
+ * in order of their zIndex. Impls that don't require this can set it to false.
+ */
+ orderSpritesByZIndex: true,
+
+
+ /**
+ * Creates new Surface.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ var me = this;
+ config = config || {};
+ Ext.apply(me, config);
+
+ me.domRef = Ext.getDoc().dom;
+
+ me.customAttributes = {};
+
+ me.addEvents(
+ 'mousedown',
+ 'mouseup',
+ 'mouseover',
+ 'mouseout',
+ 'mousemove',
+ 'mouseenter',
+ 'mouseleave',
+ 'click'
+ );
+
+ me.mixins.observable.constructor.call(me);
+
+ me.getId();
+ me.initGradients();
+ me.initItems();
+ if (me.renderTo) {
+ me.render(me.renderTo);
+ delete me.renderTo;
+ }
+ me.initBackground(config.background);
+ },
+
+ // @private called to initialize components in the surface
+ // this is dependent on the underlying implementation.
+ initSurface: Ext.emptyFn,
+
+ // @private called to setup the surface to render an item
+ //this is dependent on the underlying implementation.
+ renderItem: Ext.emptyFn,
+
+ // @private
+ renderItems: Ext.emptyFn,
+
+ // @private
+ setViewBox: function (x, y, width, height) {
+ if (isFinite(x) && isFinite(y) && isFinite(width) && isFinite(height)) {
+ this.viewBox = {x: x, y: y, width: width, height: height};
+ this.applyViewBox();
+ }
+ },
+
+ /**
+ * Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out.
+ *
+ * For example:
+ *
+ * drawComponent.surface.addCls(sprite, 'x-visible');
+ *
+ * @param {Object} sprite The sprite to add the class to.
+ * @param {String/String[]} className The CSS class to add, or an array of classes
+ * @method
+ */
+ addCls: Ext.emptyFn,
+
+ /**
+ * Removes one or more CSS classes from the element.
+ *
+ * For example:
+ *
+ * drawComponent.surface.removeCls(sprite, 'x-visible');
+ *
+ * @param {Object} sprite The sprite to remove the class from.
+ * @param {String/String[]} className The CSS class to remove, or an array of classes
+ * @method
+ */
+ removeCls: Ext.emptyFn,
+
+ /**
+ * Sets CSS style attributes to an element.
+ *
+ * For example:
+ *
+ * drawComponent.surface.setStyle(sprite, {
+ * 'cursor': 'pointer'
+ * });
+ *
+ * @param {Object} sprite The sprite to add, or an array of classes to
+ * @param {Object} styles An Object with CSS styles.
+ * @method
+ */
+ setStyle: Ext.emptyFn,
+
+ // @private
+ initGradients: function() {
+ var gradients = this.gradients;
+ if (gradients) {
+ Ext.each(gradients, this.addGradient, this);
+ }
+ },
+
+ // @private
+ initItems: function() {
+ var items = this.items;
+ this.items = Ext.create('Ext.draw.CompositeSprite');
+ this.groups = Ext.create('Ext.draw.CompositeSprite');
+ if (items) {
+ this.add(items);
+ }
+ },
+
+ // @private
+ initBackground: function(config) {
+ var me = this,
+ width = me.width,
+ height = me.height,
+ gradientId, gradient, backgroundSprite;
+ if (config) {
+ if (config.gradient) {
+ gradient = config.gradient;
+ gradientId = gradient.id;
+ me.addGradient(gradient);
+ me.background = me.add({
+ type: 'rect',
+ x: 0,
+ y: 0,
+ width: width,
+ height: height,
+ fill: 'url(#' + gradientId + ')'
+ });
+ } else if (config.fill) {
+ me.background = me.add({
+ type: 'rect',
+ x: 0,
+ y: 0,
+ width: width,
+ height: height,
+ fill: config.fill
+ });
+ } else if (config.image) {
+ me.background = me.add({
+ type: 'image',
+ x: 0,
+ y: 0,
+ width: width,
+ height: height,
+ src: config.image
+ });
+ }
+ }
+ },
+
+ /**
+ * Sets the size of the surface. Accomodates the background (if any) to fit the new size too.
+ *
+ * For example:
+ *
+ * drawComponent.surface.setSize(500, 500);
+ *
+ * This method is generally called when also setting the size of the draw Component.
+ *
+ * @param {Number} w The new width of the canvas.
+ * @param {Number} h The new height of the canvas.
+ */
+ setSize: function(w, h) {
+ if (this.background) {
+ this.background.setAttributes({
+ width: w,
+ height: h,
+ hidden: false
+ }, true);
+ }
+ this.applyViewBox();
+ },
+
+ // @private
+ scrubAttrs: function(sprite) {
+ var i,
+ attrs = {},
+ exclude = {},
+ sattr = sprite.attr;
+ for (i in sattr) {
+ // Narrow down attributes to the main set
+ if (this.translateAttrs.hasOwnProperty(i)) {
+ // Translated attr
+ attrs[this.translateAttrs[i]] = sattr[i];
+ exclude[this.translateAttrs[i]] = true;
+ }
+ else if (this.availableAttrs.hasOwnProperty(i) && !exclude[i]) {
+ // Passtrhough attr
+ attrs[i] = sattr[i];
+ }
+ }
+ return attrs;
+ },
+
+ // @private
+ onClick: function(e) {
+ this.processEvent('click', e);
+ },
+
+ // @private
+ onMouseUp: function(e) {
+ this.processEvent('mouseup', e);
+ },
+
+ // @private
+ onMouseDown: function(e) {
+ this.processEvent('mousedown', e);
+ },
+
+ // @private
+ onMouseOver: function(e) {
+ this.processEvent('mouseover', e);
+ },
+
+ // @private
+ onMouseOut: function(e) {
+ this.processEvent('mouseout', e);
+ },
+
+ // @private
+ onMouseMove: function(e) {
+ this.fireEvent('mousemove', e);
+ },
+
+ // @private
+ onMouseEnter: Ext.emptyFn,
+
+ // @private
+ onMouseLeave: Ext.emptyFn,
+
+ /**
+ * Adds a gradient definition to the Surface. Note that in some surface engines, adding
+ * a gradient via this method will not take effect if the surface has already been rendered.
+ * Therefore, it is preferred to pass the gradients as an item to the surface config, rather
+ * than calling this method, especially if the surface is rendered immediately (e.g. due to
+ * 'renderTo' in its config). For more information on how to create gradients in the Chart
+ * configuration object please refer to {@link Ext.chart.Chart}.
+ *
+ * The gradient object to be passed into this method is composed by:
+ *
+ * - **id** - string - The unique name of the gradient.
+ * - **angle** - number, optional - The angle of the gradient in degrees.
+ * - **stops** - object - An object with numbers as keys (from 0 to 100) and style objects as values.
+ *
+ * For example:
+ *
+ * drawComponent.surface.addGradient({
+ * id: 'gradientId',
+ * angle: 45,
+ * stops: {
+ * 0: {
+ * color: '#555'
+ * },
+ * 100: {
+ * color: '#ddd'
+ * }
+ * }
+ * });
+ *
+ * @method
+ */
+ addGradient: Ext.emptyFn,
+
+ /**
+ * Adds a Sprite to the surface. See {@link Ext.draw.Sprite} for the configuration object to be
+ * passed into this method.
+ *
+ * For example:
+ *
+ * drawComponent.surface.add({
+ * type: 'circle',
+ * fill: '#ffc',
+ * radius: 100,
+ * x: 100,
+ * y: 100
+ * });
+ *
+ */
+ add: function() {
+ var args = Array.prototype.slice.call(arguments),
+ sprite,
+ index;
+
+ var hasMultipleArgs = args.length > 1;
+ if (hasMultipleArgs || Ext.isArray(args[0])) {
+ var items = hasMultipleArgs ? args : args[0],
+ results = [],
+ i, ln, item;
+
+ for (i = 0, ln = items.length; i < ln; i++) {
+ item = items[i];
+ item = this.add(item);
+ results.push(item);
+ }
+
+ return results;
+ }
+ sprite = this.prepareItems(args[0], true)[0];
+ this.insertByZIndex(sprite);
+ this.onAdd(sprite);
+ return sprite;
+ },
+
+ /**
+ * @private
+ * Inserts a given sprite into the correct position in the items collection, according to
+ * its zIndex. It will be inserted at the end of an existing series of sprites with the same or
+ * lower zIndex. By ensuring sprites are always ordered, this allows surface subclasses to render
+ * the sprites in the correct order for proper z-index stacking.
+ * @param {Ext.draw.Sprite} sprite
+ * @return {Number} the sprite's new index in the list
+ */
+ insertByZIndex: function(sprite) {
+ var me = this,
+ sprites = me.items.items,
+ len = sprites.length,
+ ceil = Math.ceil,
+ zIndex = sprite.attr.zIndex,
+ idx = len,
+ high = idx - 1,
+ low = 0,
+ otherZIndex;
+
+ if (me.orderSpritesByZIndex && len && zIndex < sprites[high].attr.zIndex) {
+ // Find the target index via a binary search for speed
+ while (low <= high) {
+ idx = ceil((low + high) / 2);
+ otherZIndex = sprites[idx].attr.zIndex;
+ if (otherZIndex > zIndex) {
+ high = idx - 1;
+ }
+ else if (otherZIndex < zIndex) {
+ low = idx + 1;
+ }
+ else {
+ break;
+ }
+ }
+ // Step forward to the end of a sequence of the same or lower z-index
+ while (idx < len && sprites[idx].attr.zIndex <= zIndex) {
+ idx++;
+ }
+ }
+
+ me.items.insert(idx, sprite);
+ return idx;
+ },
+
+ onAdd: function(sprite) {
+ var group = sprite.group,
+ draggable = sprite.draggable,
+ groups, ln, i;
+ if (group) {
+ groups = [].concat(group);
+ ln = groups.length;
+ for (i = 0; i < ln; i++) {
+ group = groups[i];
+ this.getGroup(group).add(sprite);
+ }
+ delete sprite.group;
+ }
+ if (draggable) {
+ sprite.initDraggable();
+ }
+ },
+
+ /**
+ * Removes a given sprite from the surface, optionally destroying the sprite in the process.
+ * You can also call the sprite own `remove` method.
+ *
+ * For example:
+ *
+ * drawComponent.surface.remove(sprite);
+ * //or...
+ * sprite.remove();
+ *
+ * @param {Ext.draw.Sprite} sprite
+ * @param {Boolean} destroySprite
+ * @return {Number} the sprite's new index in the list
+ */
+ remove: function(sprite, destroySprite) {
+ if (sprite) {
+ this.items.remove(sprite);
+ this.groups.each(function(item) {
+ item.remove(sprite);
+ });
+ sprite.onRemove();
+ if (destroySprite === true) {
+ sprite.destroy();
+ }
+ }
+ },
+
+ /**
+ * Removes all sprites from the surface, optionally destroying the sprites in the process.
+ *
+ * For example:
+ *
+ * drawComponent.surface.removeAll();
+ *
+ * @param {Boolean} destroySprites Whether to destroy all sprites when removing them.
+ * @return {Number} The sprite's new index in the list.
+ */
+ removeAll: function(destroySprites) {
+ var items = this.items.items,
+ ln = items.length,
+ i;
+ for (i = ln - 1; i > -1; i--) {
+ this.remove(items[i], destroySprites);
+ }
+ },
+
+ onRemove: Ext.emptyFn,
+
+ onDestroy: Ext.emptyFn,
+
+ /**
+ * @private Using the current viewBox property and the surface's width and height, calculate the
+ * appropriate viewBoxShift that will be applied as a persistent transform to all sprites.
+ */
+ applyViewBox: function() {
+ var me = this,
+ viewBox = me.viewBox,
+ width = me.width,
+ height = me.height,
+ viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight,
+ relativeHeight, relativeWidth, size;
+
+ if (viewBox && (width || height)) {
+ viewBoxX = viewBox.x;
+ viewBoxY = viewBox.y;
+ viewBoxWidth = viewBox.width;
+ viewBoxHeight = viewBox.height;
+ relativeHeight = height / viewBoxHeight;
+ relativeWidth = width / viewBoxWidth;
+
+ if (viewBoxWidth * relativeHeight < width) {
+ viewBoxX -= (width - viewBoxWidth * relativeHeight) / 2 / relativeHeight;
+ }
+ if (viewBoxHeight * relativeWidth < height) {
+ viewBoxY -= (height - viewBoxHeight * relativeWidth) / 2 / relativeWidth;
+ }
+
+ size = 1 / Math.min(viewBoxWidth, relativeHeight);
+
+ me.viewBoxShift = {
+ dx: -viewBoxX,
+ dy: -viewBoxY,
+ scale: size
+ };
+ }
+ },
+
+ transformToViewBox: function (x, y) {
+ if (this.viewBoxShift) {
+ var me = this, shift = me.viewBoxShift;
+ return [x * shift.scale - shift.dx, y * shift.scale - shift.dy];
+ } else {
+ return [x, y];
+ }
+ },
+
+ // @private
+ applyTransformations: function(sprite) {
+ sprite.bbox.transform = 0;
+ this.transform(sprite);
+
+ var me = this,
+ dirty = false,
+ attr = sprite.attr;
+
+ if (attr.translation.x != null || attr.translation.y != null) {
+ me.translate(sprite);
+ dirty = true;
+ }
+ if (attr.scaling.x != null || attr.scaling.y != null) {
+ me.scale(sprite);
+ dirty = true;
+ }
+ if (attr.rotation.degrees != null) {
+ me.rotate(sprite);
+ dirty = true;
+ }
+ if (dirty) {
+ sprite.bbox.transform = 0;
+ this.transform(sprite);
+ sprite.transformations = [];
+ }
+ },
+
+ // @private
+ rotate: function (sprite) {
+ var bbox,
+ deg = sprite.attr.rotation.degrees,
+ centerX = sprite.attr.rotation.x,
+ centerY = sprite.attr.rotation.y;
+ if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) {
+ bbox = this.getBBox(sprite);
+ centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX;
+ centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY;
+ }
+ sprite.transformations.push({
+ type: "rotate",
+ degrees: deg,
+ x: centerX,
+ y: centerY
+ });
+ },
+
+ // @private
+ translate: function(sprite) {
+ var x = sprite.attr.translation.x || 0,
+ y = sprite.attr.translation.y || 0;
+ sprite.transformations.push({
+ type: "translate",
+ x: x,
+ y: y
+ });
+ },
+
+ // @private
+ scale: function(sprite) {
+ var bbox,
+ x = sprite.attr.scaling.x || 1,
+ y = sprite.attr.scaling.y || 1,
+ centerX = sprite.attr.scaling.centerX,
+ centerY = sprite.attr.scaling.centerY;
+
+ if (!Ext.isNumber(centerX) || !Ext.isNumber(centerY)) {
+ bbox = this.getBBox(sprite);
+ centerX = !Ext.isNumber(centerX) ? bbox.x + bbox.width / 2 : centerX;
+ centerY = !Ext.isNumber(centerY) ? bbox.y + bbox.height / 2 : centerY;
+ }
+ sprite.transformations.push({
+ type: "scale",
+ x: x,
+ y: y,
+ centerX: centerX,
+ centerY: centerY
+ });
+ },
+
+ // @private
+ rectPath: function (x, y, w, h, r) {
+ if (r) {
+ return [["M", x + r, y], ["l", w - r * 2, 0], ["a", r, r, 0, 0, 1, r, r], ["l", 0, h - r * 2], ["a", r, r, 0, 0, 1, -r, r], ["l", r * 2 - w, 0], ["a", r, r, 0, 0, 1, -r, -r], ["l", 0, r * 2 - h], ["a", r, r, 0, 0, 1, r, -r], ["z"]];
+ }
+ return [["M", x, y], ["l", w, 0], ["l", 0, h], ["l", -w, 0], ["z"]];
+ },
+
+ // @private
+ ellipsePath: function (x, y, rx, ry) {
+ if (ry == null) {
+ ry = rx;
+ }
+ return [["M", x, y], ["m", 0, -ry], ["a", rx, ry, 0, 1, 1, 0, 2 * ry], ["a", rx, ry, 0, 1, 1, 0, -2 * ry], ["z"]];
+ },
+
+ // @private
+ getPathpath: function (el) {
+ return el.attr.path;
+ },
+
+ // @private
+ getPathcircle: function (el) {
+ var a = el.attr;
+ return this.ellipsePath(a.x, a.y, a.radius, a.radius);
+ },
+
+ // @private
+ getPathellipse: function (el) {
+ var a = el.attr;
+ return this.ellipsePath(a.x, a.y,
+ a.radiusX || (a.width / 2) || 0,
+ a.radiusY || (a.height / 2) || 0);
+ },
+
+ // @private
+ getPathrect: function (el) {
+ var a = el.attr;
+ return this.rectPath(a.x, a.y, a.width, a.height, a.r);
+ },
+
+ // @private
+ getPathimage: function (el) {
+ var a = el.attr;
+ return this.rectPath(a.x || 0, a.y || 0, a.width, a.height);
+ },
+
+ // @private
+ getPathtext: function (el) {
+ var bbox = this.getBBoxText(el);
+ return this.rectPath(bbox.x, bbox.y, bbox.width, bbox.height);
+ },
+
+ createGroup: function(id) {
+ var group = this.groups.get(id);
+ if (!group) {
+ group = Ext.create('Ext.draw.CompositeSprite', {
+ surface: this
+ });
+ group.id = id || Ext.id(null, 'ext-surface-group-');
+ this.groups.add(group);
+ }
+ return group;
+ },
+
+ /**
+ * Returns a new group or an existent group associated with the current surface.
+ * The group returned is a {@link Ext.draw.CompositeSprite} group.
+ *
+ * For example:
+ *
+ * var spriteGroup = drawComponent.surface.getGroup('someGroupId');
+ *
+ * @param {String} id The unique identifier of the group.
+ * @return {Object} The {@link Ext.draw.CompositeSprite}.
+ */
+ getGroup: function(id) {
+ if (typeof id == "string") {
+ var group = this.groups.get(id);
+ if (!group) {
+ group = this.createGroup(id);
+ }
+ } else {
+ group = id;
+ }
+ return group;
+ },
+
+ // @private
+ prepareItems: function(items, applyDefaults) {
+ items = [].concat(items);
+ // Make sure defaults are applied and item is initialized
+ var item, i, ln;
+ for (i = 0, ln = items.length; i < ln; i++) {
+ item = items[i];
+ if (!(item instanceof Ext.draw.Sprite)) {
+ // Temporary, just take in configs...
+ item.surface = this;
+ items[i] = this.createItem(item);
+ } else {
+ item.surface = this;
+ }
+ }
+ return items;
+ },
+
+ /**
+ * Changes the text in the sprite element. The sprite must be a `text` sprite.
+ * This method can also be called from {@link Ext.draw.Sprite}.
+ *
+ * For example:
+ *
+ * var spriteGroup = drawComponent.surface.setText(sprite, 'my new text');
+ *
+ * @param {Object} sprite The Sprite to change the text.
+ * @param {String} text The new text to be set.
+ * @method
+ */
+ setText: Ext.emptyFn,
+
+ //@private Creates an item and appends it to the surface. Called
+ //as an internal method when calling `add`.
+ createItem: Ext.emptyFn,
+
+ /**
+ * Retrieves the id of this component.
+ * Will autogenerate an id if one has not already been set.
+ */
+ getId: function() {
+ return this.id || (this.id = Ext.id(null, 'ext-surface-'));
+ },
+
+ /**
+ * Destroys the surface. This is done by removing all components from it and
+ * also removing its reference to a DOM element.
+ *
+ * For example:
+ *
+ * drawComponent.surface.destroy();
+ */
+ destroy: function() {
+ delete this.domRef;
+ this.removeAll();
+ }
+});
+/**
+ * @class Ext.layout.component.Draw
+ * @extends Ext.layout.component.Component
+ * @private
+ *
+ */
+
+Ext.define('Ext.layout.component.Draw', {
+
+ /* Begin Definitions */
+
+ alias: 'layout.draw',
+
+ extend: 'Ext.layout.component.Auto',
+
+ /* End Definitions */
+
+ type: 'draw',
+
+ onLayout : function(width, height) {
+ this.owner.surface.setSize(width, height);
+ this.callParent(arguments);
+ }
+});
+/**
+ * @class Ext.draw.Component
+ * @extends Ext.Component
+ *
+ * The Draw Component is a surface in which sprites can be rendered. The Draw Component
+ * manages and holds a `Surface` instance: an interface that has
+ * an SVG or VML implementation depending on the browser capabilities and where
+ * Sprites can be appended.
+ *
+ * One way to create a draw component is:
+ *
+ * @example
+ * var drawComponent = Ext.create('Ext.draw.Component', {
+ * viewBox: false,
+ * items: [{
+ * type: 'circle',
+ * fill: '#79BB3F',
+ * radius: 100,
+ * x: 100,
+ * y: 100
+ * }]
+ * });
+ *
+ * Ext.create('Ext.Window', {
+ * width: 215,
+ * height: 235,
+ * layout: 'fit',
+ * items: [drawComponent]
+ * }).show();
+ *
+ * In this case we created a draw component and added a sprite to it.
+ * The *type* of the sprite is *circle* so if you run this code you'll see a yellow-ish
+ * circle in a Window. When setting `viewBox` to `false` we are responsible for setting the object's position and
+ * dimensions accordingly.
+ *
+ * You can also add sprites by using the surface's add method:
+ *
+ * drawComponent.surface.add({
+ * type: 'circle',
+ * fill: '#79BB3F',
+ * radius: 100,
+ * x: 100,
+ * y: 100
+ * });
+ *
+ * For more information on Sprites, the core elements added to a draw component's surface,
+ * refer to the Ext.draw.Sprite documentation.
+ */
+Ext.define('Ext.draw.Component', {
+
+ /* Begin Definitions */
+
+ alias: 'widget.draw',
+
+ extend: 'Ext.Component',
+
+ requires: [
+ 'Ext.draw.Surface',
+ 'Ext.layout.component.Draw'
+ ],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String[]} enginePriority
+ * Defines the priority order for which Surface implementation to use. The first
+ * one supported by the current environment will be used.
+ */
+ enginePriority: ['Svg', 'Vml'],
+
+ baseCls: Ext.baseCSSPrefix + 'surface',
+
+ componentLayout: 'draw',
+
+ /**
+ * @cfg {Boolean} viewBox
+ * Turn on view box support which will scale and position items in the draw component to fit to the component while
+ * maintaining aspect ratio. Note that this scaling can override other sizing settings on yor items. Defaults to true.
+ */
+ viewBox: true,
+
+ /**
+ * @cfg {Boolean} autoSize
+ * Turn on autoSize support which will set the bounding div's size to the natural size of the contents. Defaults to false.
+ */
+ autoSize: false,
+
+ /**
+ * @cfg {Object[]} gradients (optional) Define a set of gradients that can be used as `fill` property in sprites.
+ * The gradients array is an array of objects with the following properties:
+ *
+ * - `id` - string - The unique name of the gradient.
+ * - `angle` - number, optional - The angle of the gradient in degrees.
+ * - `stops` - object - An object with numbers as keys (from 0 to 100) and style objects as values
+ *
+ * ## Example
+ *
+ * gradients: [{
+ * id: 'gradientId',
+ * angle: 45,
+ * stops: {
+ * 0: {
+ * color: '#555'
+ * },
+ * 100: {
+ * color: '#ddd'
+ * }
+ * }
+ * }, {
+ * id: 'gradientId2',
+ * angle: 0,
+ * stops: {
+ * 0: {
+ * color: '#590'
+ * },
+ * 20: {
+ * color: '#599'
+ * },
+ * 100: {
+ * color: '#ddd'
+ * }
+ * }
+ * }]
+ *
+ * Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example:
+ *
+ * sprite.setAttributes({
+ * fill: 'url(#gradientId)'
+ * }, true);
+ */
+ initComponent: function() {
+ this.callParent(arguments);
+
+ this.addEvents(
+ 'mousedown',
+ 'mouseup',
+ 'mousemove',
+ 'mouseenter',
+ 'mouseleave',
+ 'click'
+ );
+ },
+
+ /**
+ * @private
+ *
+ * Create the Surface on initial render
+ */
+ onRender: function() {
+ var me = this,
+ viewBox = me.viewBox,
+ autoSize = me.autoSize,
+ bbox, items, width, height, x, y;
+ me.callParent(arguments);
+
+ if (me.createSurface() !== false) {
+ items = me.surface.items;
+
+ if (viewBox || autoSize) {
+ bbox = items.getBBox();
+ width = bbox.width;
+ height = bbox.height;
+ x = bbox.x;
+ y = bbox.y;
+ if (me.viewBox) {
+ me.surface.setViewBox(x, y, width, height);
+ }
+ else {
+ // AutoSized
+ me.autoSizeSurface();
+ }
+ }
+ }
+ },
+
+ //@private
+ autoSizeSurface: function() {
+ var me = this,
+ items = me.surface.items,
+ bbox = items.getBBox(),
+ width = bbox.width,
+ height = bbox.height;
+ items.setAttributes({
+ translate: {
+ x: -bbox.x,
+ //Opera has a slight offset in the y axis.
+ y: -bbox.y + (+Ext.isOpera)
+ }
+ }, true);
+ if (me.rendered) {
+ me.setSize(width, height);
+ me.surface.setSize(width, height);
+ }
+ else {
+ me.surface.setSize(width, height);
+ }
+ me.el.setSize(width, height);
+ },
+
+ /**
+ * Create the Surface instance. Resolves the correct Surface implementation to
+ * instantiate based on the 'enginePriority' config. Once the Surface instance is
+ * created you can use the handle to that instance to add sprites. For example:
+ *
+ * drawComponent.surface.add(sprite);
+ */
+ createSurface: function() {
+ var surface = Ext.draw.Surface.create(Ext.apply({}, {
+ width: this.width,
+ height: this.height,
+ renderTo: this.el
+ }, this.initialConfig));
+ if (!surface) {
+ // In case we cannot create a surface, return false so we can stop
+ return false;
+ }
+ this.surface = surface;
+
+
+ function refire(eventName) {
+ return function(e) {
+ this.fireEvent(eventName, e);
+ };
+ }
+
+ surface.on({
+ scope: this,
+ mouseup: refire('mouseup'),
+ mousedown: refire('mousedown'),
+ mousemove: refire('mousemove'),
+ mouseenter: refire('mouseenter'),
+ mouseleave: refire('mouseleave'),
+ click: refire('click')
+ });
+ },
+
+
+ /**
+ * @private
+ *
+ * Clean up the Surface instance on component destruction
+ */
+ onDestroy: function() {
+ var surface = this.surface;
+ if (surface) {
+ surface.destroy();
+ }
+ this.callParent(arguments);
+ }
+
+});
+
+/**
+ * @class Ext.chart.LegendItem
+ * @extends Ext.draw.CompositeSprite
+ * A single item of a legend (marker plus label)
+ */
+Ext.define('Ext.chart.LegendItem', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.draw.CompositeSprite',
+
+ requires: ['Ext.chart.Shape'],
+
+ /* End Definitions */
+
+ // Position of the item, relative to the upper-left corner of the legend box
+ x: 0,
+ y: 0,
+ zIndex: 500,
+
+ constructor: function(config) {
+ this.callParent(arguments);
+ this.createLegend(config);
+ },
+
+ /**
+ * Creates all the individual sprites for this legend item
+ */
+ createLegend: function(config) {
+ var me = this,
+ index = config.yFieldIndex,
+ series = me.series,
+ seriesType = series.type,
+ idx = me.yFieldIndex,
+ legend = me.legend,
+ surface = me.surface,
+ refX = legend.x + me.x,
+ refY = legend.y + me.y,
+ bbox, z = me.zIndex,
+ markerConfig, label, mask,
+ radius, toggle = false,
+ seriesStyle = Ext.apply(series.seriesStyle, series.style);
+
+ function getSeriesProp(name) {
+ var val = series[name];
+ return (Ext.isArray(val) ? val[idx] : val);
+ }
+
+ label = me.add('label', surface.add({
+ type: 'text',
+ x: 20,
+ y: 0,
+ zIndex: z || 0,
+ font: legend.labelFont,
+ text: getSeriesProp('title') || getSeriesProp('yField')
+ }));
+
+ // Line series - display as short line with optional marker in the middle
+ if (seriesType === 'line' || seriesType === 'scatter') {
+ if(seriesType === 'line') {
+ me.add('line', surface.add({
+ type: 'path',
+ path: 'M0.5,0.5L16.5,0.5',
+ zIndex: z,
+ "stroke-width": series.lineWidth,
+ "stroke-linejoin": "round",
+ "stroke-dasharray": series.dash,
+ stroke: seriesStyle.stroke || '#000',
+ style: {
+ cursor: 'pointer'
+ }
+ }));
+ }
+ if (series.showMarkers || seriesType === 'scatter') {
+ markerConfig = Ext.apply(series.markerStyle, series.markerConfig || {});
+ me.add('marker', Ext.chart.Shape[markerConfig.type](surface, {
+ fill: markerConfig.fill,
+ x: 8.5,
+ y: 0.5,
+ zIndex: z,
+ radius: markerConfig.radius || markerConfig.size,
+ style: {
+ cursor: 'pointer'
+ }
+ }));
+ }
+ }
+ // All other series types - display as filled box
+ else {
+ me.add('box', surface.add({
+ type: 'rect',
+ zIndex: z,
+ x: 0,
+ y: 0,
+ width: 12,
+ height: 12,
+ fill: series.getLegendColor(index),
+ style: {
+ cursor: 'pointer'
+ }
+ }));
+ }
+
+ me.setAttributes({
+ hidden: false
+ }, true);
+
+ bbox = me.getBBox();
+
+ mask = me.add('mask', surface.add({
+ type: 'rect',
+ x: bbox.x,
+ y: bbox.y,
+ width: bbox.width || 20,
+ height: bbox.height || 20,
+ zIndex: (z || 0) + 1000,
+ fill: '#f00',
+ opacity: 0,
+ style: {
+ 'cursor': 'pointer'
+ }
+ }));
+
+ //add toggle listener
+ me.on('mouseover', function() {
+ label.setStyle({
+ 'font-weight': 'bold'
+ });
+ mask.setStyle({
+ 'cursor': 'pointer'
+ });
+ series._index = index;
+ series.highlightItem();
+ }, me);
+
+ me.on('mouseout', function() {
+ label.setStyle({
+ 'font-weight': 'normal'
+ });
+ series._index = index;
+ series.unHighlightItem();
+ }, me);
+
+ if (!series.visibleInLegend(index)) {
+ toggle = true;
+ label.setAttributes({
+ opacity: 0.5
+ }, true);
+ }
+
+ me.on('mousedown', function() {
+ if (!toggle) {
+ series.hideAll();
+ label.setAttributes({
+ opacity: 0.5
+ }, true);
+ } else {
+ series.showAll();
+ label.setAttributes({
+ opacity: 1
+ }, true);
+ }
+ toggle = !toggle;
+ }, me);
+ me.updatePosition({x:0, y:0}); //Relative to 0,0 at first so that the bbox is calculated correctly
+ },
+
+ /**
+ * Update the positions of all this item's sprites to match the root position
+ * of the legend box.
+ * @param {Object} relativeTo (optional) If specified, this object's 'x' and 'y' values will be used
+ * as the reference point for the relative positioning. Defaults to the Legend.
+ */
+ updatePosition: function(relativeTo) {
+ var me = this,
+ items = me.items,
+ ln = items.length,
+ i = 0,
+ item;
+ if (!relativeTo) {
+ relativeTo = me.legend;
+ }
+ for (; i < ln; i++) {
+ item = items[i];
+ switch (item.type) {
+ case 'text':
+ item.setAttributes({
+ x: 20 + relativeTo.x + me.x,
+ y: relativeTo.y + me.y
+ }, true);
+ break;
+ case 'rect':
+ item.setAttributes({
+ translate: {
+ x: relativeTo.x + me.x,
+ y: relativeTo.y + me.y - 6
+ }
+ }, true);
+ break;
+ default:
+ item.setAttributes({
+ translate: {
+ x: relativeTo.x + me.x,
+ y: relativeTo.y + me.y
+ }
+ }, true);
+ }
+ }
+ }
+});
+
+/**
+ * @class Ext.chart.Legend
+ *
+ * Defines a legend for a chart's series.
+ * The 'chart' member must be set prior to rendering.
+ * The legend class displays a list of legend items each of them related with a
+ * series being rendered. In order to render the legend item of the proper series
+ * the series configuration object must have `showInSeries` set to true.
+ *
+ * The legend configuration object accepts a `position` as parameter.
+ * The `position` parameter can be `left`, `right`
+ * `top` or `bottom`. For example:
+ *
+ * legend: {
+ * position: 'right'
+ * },
+ *
+ * ## Example
+ *
+ * @example
+ * var store = Ext.create('Ext.data.JsonStore', {
+ * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+ * data: [
+ * { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 },
+ * { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 },
+ * { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 },
+ * { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 },
+ * { 'name': 'metric five', 'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 300,
+ * animate: true,
+ * store: store,
+ * shadow: true,
+ * theme: 'Category1',
+ * legend: {
+ * position: 'top'
+ * },
+ * axes: [
+ * {
+ * type: 'Numeric',
+ * grid: true,
+ * position: 'left',
+ * fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
+ * title: 'Sample Values',
+ * grid: {
+ * odd: {
+ * opacity: 1,
+ * fill: '#ddd',
+ * stroke: '#bbb',
+ * 'stroke-width': 1
+ * }
+ * },
+ * minimum: 0,
+ * adjustMinimumByMajorUnit: 0
+ * },
+ * {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Sample Metrics',
+ * grid: true,
+ * label: {
+ * rotate: {
+ * degrees: 315
+ * }
+ * }
+ * }
+ * ],
+ * series: [{
+ * type: 'area',
+ * highlight: false,
+ * axis: 'left',
+ * xField: 'name',
+ * yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
+ * style: {
+ * opacity: 0.93
+ * }
+ * }]
+ * });
+ */
+Ext.define('Ext.chart.Legend', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.chart.LegendItem'],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Boolean} visible
+ * Whether or not the legend should be displayed.
+ */
+ visible: true,
+
+ /**
+ * @cfg {String} position
+ * The position of the legend in relation to the chart. One of: "top",
+ * "bottom", "left", "right", or "float". If set to "float", then the legend
+ * box will be positioned at the point denoted by the x and y parameters.
+ */
+ position: 'bottom',
+
+ /**
+ * @cfg {Number} x
+ * X-position of the legend box. Used directly if position is set to "float", otherwise
+ * it will be calculated dynamically.
+ */
+ x: 0,
+
+ /**
+ * @cfg {Number} y
+ * Y-position of the legend box. Used directly if position is set to "float", otherwise
+ * it will be calculated dynamically.
+ */
+ y: 0,
+
+ /**
+ * @cfg {String} labelFont
+ * Font to be used for the legend labels, eg '12px Helvetica'
+ */
+ labelFont: '12px Helvetica, sans-serif',
+
+ /**
+ * @cfg {String} boxStroke
+ * Style of the stroke for the legend box
+ */
+ boxStroke: '#000',
+
+ /**
+ * @cfg {String} boxStrokeWidth
+ * Width of the stroke for the legend box
+ */
+ boxStrokeWidth: 1,
+
+ /**
+ * @cfg {String} boxFill
+ * Fill style for the legend box
+ */
+ boxFill: '#FFF',
+
+ /**
+ * @cfg {Number} itemSpacing
+ * Amount of space between legend items
+ */
+ itemSpacing: 10,
+
+ /**
+ * @cfg {Number} padding
+ * Amount of padding between the legend box's border and its items
+ */
+ padding: 5,
+
+ // @private
+ width: 0,
+ // @private
+ height: 0,
+
+ /**
+ * @cfg {Number} boxZIndex
+ * Sets the z-index for the legend. Defaults to 100.
+ */
+ boxZIndex: 100,
+
+ /**
+ * Creates new Legend.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ var me = this;
+ if (config) {
+ Ext.apply(me, config);
+ }
+ me.items = [];
+ /**
+ * Whether the legend box is oriented vertically, i.e. if it is on the left or right side or floating.
+ * @type {Boolean}
+ */
+ me.isVertical = ("left|right|float".indexOf(me.position) !== -1);
+
+ // cache these here since they may get modified later on
+ me.origX = me.x;
+ me.origY = me.y;
+ },
+
+ /**
+ * @private Create all the sprites for the legend
+ */
+ create: function() {
+ var me = this;
+ me.createBox();
+ me.createItems();
+ if (!me.created && me.isDisplayed()) {
+ me.created = true;
+
+ // Listen for changes to series titles to trigger regeneration of the legend
+ me.chart.series.each(function(series) {
+ series.on('titlechange', function() {
+ me.create();
+ me.updatePosition();
+ });
+ });
+ }
+ },
+
+ /**
+ * @private Determine whether the legend should be displayed. Looks at the legend's 'visible' config,
+ * and also the 'showInLegend' config for each of the series.
+ */
+ isDisplayed: function() {
+ return this.visible && this.chart.series.findIndex('showInLegend', true) !== -1;
+ },
+
+ /**
+ * @private Create the series markers and labels
+ */
+ createItems: function() {
+ var me = this,
+ chart = me.chart,
+ surface = chart.surface,
+ items = me.items,
+ padding = me.padding,
+ itemSpacing = me.itemSpacing,
+ spacingOffset = 2,
+ maxWidth = 0,
+ maxHeight = 0,
+ totalWidth = 0,
+ totalHeight = 0,
+ vertical = me.isVertical,
+ math = Math,
+ mfloor = math.floor,
+ mmax = math.max,
+ index = 0,
+ i = 0,
+ len = items ? items.length : 0,
+ x, y, spacing, item, bbox, height, width;
+
+ //remove all legend items
+ if (len) {
+ for (; i < len; i++) {
+ items[i].destroy();
+ }
+ }
+ //empty array
+ items.length = [];
+ // Create all the item labels, collecting their dimensions and positioning each one
+ // properly in relation to the previous item
+ chart.series.each(function(series, i) {
+ if (series.showInLegend) {
+ Ext.each([].concat(series.yField), function(field, j) {
+ item = Ext.create('Ext.chart.LegendItem', {
+ legend: this,
+ series: series,
+ surface: chart.surface,
+ yFieldIndex: j
+ });
+ bbox = item.getBBox();
+
+ //always measure from x=0, since not all markers go all the way to the left
+ width = bbox.width;
+ height = bbox.height;
+
+ if (i + j === 0) {
+ spacing = vertical ? padding + height / 2 : padding;
+ }
+ else {
+ spacing = itemSpacing / (vertical ? 2 : 1);
+ }
+ // Set the item's position relative to the legend box
+ item.x = mfloor(vertical ? padding : totalWidth + spacing);
+ item.y = mfloor(vertical ? totalHeight + spacing : padding + height / 2);
+
+ // Collect cumulative dimensions
+ totalWidth += width + spacing;
+ totalHeight += height + spacing;
+ maxWidth = mmax(maxWidth, width);
+ maxHeight = mmax(maxHeight, height);
+
+ items.push(item);
+ }, this);
+ }
+ }, me);
+
+ // Store the collected dimensions for later
+ me.width = mfloor((vertical ? maxWidth : totalWidth) + padding * 2);
+ if (vertical && items.length === 1) {
+ spacingOffset = 1;
+ }
+ me.height = mfloor((vertical ? totalHeight - spacingOffset * spacing : maxHeight) + (padding * 2));
+ me.itemHeight = maxHeight;
+ },
+
+ /**
+ * @private Get the bounds for the legend's outer box
+ */
+ getBBox: function() {
+ var me = this;
+ return {
+ x: Math.round(me.x) - me.boxStrokeWidth / 2,
+ y: Math.round(me.y) - me.boxStrokeWidth / 2,
+ width: me.width,
+ height: me.height
+ };
+ },
+
+ /**
+ * @private Create the box around the legend items
+ */
+ createBox: function() {
+ var me = this,
+ box;
+
+ if (me.boxSprite) {
+ me.boxSprite.destroy();
+ }
+
+ box = me.boxSprite = me.chart.surface.add(Ext.apply({
+ type: 'rect',
+ stroke: me.boxStroke,
+ "stroke-width": me.boxStrokeWidth,
+ fill: me.boxFill,
+ zIndex: me.boxZIndex
+ }, me.getBBox()));
+
+ box.redraw();
+ },
+
+ /**
+ * @private Update the position of all the legend's sprites to match its current x/y values
+ */
+ updatePosition: function() {
+ var me = this,
+ x, y,
+ legendWidth = me.width,
+ legendHeight = me.height,
+ padding = me.padding,
+ chart = me.chart,
+ chartBBox = chart.chartBBox,
+ insets = chart.insetPadding,
+ chartWidth = chartBBox.width - (insets * 2),
+ chartHeight = chartBBox.height - (insets * 2),
+ chartX = chartBBox.x + insets,
+ chartY = chartBBox.y + insets,
+ surface = chart.surface,
+ mfloor = Math.floor;
+
+ if (me.isDisplayed()) {
+ // Find the position based on the dimensions
+ switch(me.position) {
+ case "left":
+ x = insets;
+ y = mfloor(chartY + chartHeight / 2 - legendHeight / 2);
+ break;
+ case "right":
+ x = mfloor(surface.width - legendWidth) - insets;
+ y = mfloor(chartY + chartHeight / 2 - legendHeight / 2);
+ break;
+ case "top":
+ x = mfloor(chartX + chartWidth / 2 - legendWidth / 2);
+ y = insets;
+ break;
+ case "bottom":
+ x = mfloor(chartX + chartWidth / 2 - legendWidth / 2);
+ y = mfloor(surface.height - legendHeight) - insets;
+ break;
+ default:
+ x = mfloor(me.origX) + insets;
+ y = mfloor(me.origY) + insets;
+ }
+ me.x = x;
+ me.y = y;
+
+ // Update the position of each item
+ Ext.each(me.items, function(item) {
+ item.updatePosition();
+ });
+ // Update the position of the outer box
+ me.boxSprite.setAttributes(me.getBBox(), true);
+ }
+ }
+});
+
+/**
+ * Charts provide a flexible way to achieve a wide range of data visualization capablitities.
+ * Each Chart gets its data directly from a {@link Ext.data.Store Store}, and automatically
+ * updates its display whenever data in the Store changes. In addition, the look and feel
+ * of a Chart can be customized using {@link Ext.chart.theme.Theme Theme}s.
+ *
+ * ## Creating a Simple Chart
+ *
+ * Every Chart has three key parts - a {@link Ext.data.Store Store} that contains the data,
+ * an array of {@link Ext.chart.axis.Axis Axes} which define the boundaries of the Chart,
+ * and one or more {@link Ext.chart.series.Series Series} to handle the visual rendering of the data points.
+ *
+ * ### 1. Creating a Store
+ *
+ * The first step is to create a {@link Ext.data.Model Model} that represents the type of
+ * data that will be displayed in the Chart. For example the data for a chart that displays
+ * a weather forecast could be represented as a series of "WeatherPoint" data points with
+ * two fields - "temperature", and "date":
+ *
+ * Ext.define('WeatherPoint', {
+ * extend: 'Ext.data.Model',
+ * fields: ['temperature', 'date']
+ * });
+ *
+ * Next a {@link Ext.data.Store Store} must be created. The store contains a collection of "WeatherPoint" Model instances.
+ * The data could be loaded dynamically, but for sake of ease this example uses inline data:
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'WeatherPoint',
+ * data: [
+ * { temperature: 58, date: new Date(2011, 1, 1, 8) },
+ * { temperature: 63, date: new Date(2011, 1, 1, 9) },
+ * { temperature: 73, date: new Date(2011, 1, 1, 10) },
+ * { temperature: 78, date: new Date(2011, 1, 1, 11) },
+ * { temperature: 81, date: new Date(2011, 1, 1, 12) }
+ * ]
+ * });
+ *
+ * For additional information on Models and Stores please refer to the [Data Guide](#/guide/data).
+ *
+ * ### 2. Creating the Chart object
+ *
+ * Now that a Store has been created it can be used in a Chart:
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 400,
+ * height: 300,
+ * store: store
+ * });
+ *
+ * That's all it takes to create a Chart instance that is backed by a Store.
+ * However, if the above code is run in a browser, a blank screen will be displayed.
+ * This is because the two pieces that are responsible for the visual display,
+ * the Chart's {@link #cfg-axes axes} and {@link #cfg-series series}, have not yet been defined.
+ *
+ * ### 3. Configuring the Axes
+ *
+ * {@link Ext.chart.axis.Axis Axes} are the lines that define the boundaries of the data points that a Chart can display.
+ * This example uses one of the most common Axes configurations - a horizontal "x" axis, and a vertical "y" axis:
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * ...
+ * axes: [
+ * {
+ * title: 'Temperature',
+ * type: 'Numeric',
+ * position: 'left',
+ * fields: ['temperature'],
+ * minimum: 0,
+ * maximum: 100
+ * },
+ * {
+ * title: 'Time',
+ * type: 'Time',
+ * position: 'bottom',
+ * fields: ['date'],
+ * dateFormat: 'ga'
+ * }
+ * ]
+ * });
+ *
+ * The "Temperature" axis is a vertical {@link Ext.chart.axis.Numeric Numeric Axis} and is positioned on the left edge of the Chart.
+ * It represents the bounds of the data contained in the "WeatherPoint" Model's "temperature" field that was
+ * defined above. The minimum value for this axis is "0", and the maximum is "100".
+ *
+ * The horizontal axis is a {@link Ext.chart.axis.Time Time Axis} and is positioned on the bottom edge of the Chart.
+ * It represents the bounds of the data contained in the "WeatherPoint" Model's "date" field.
+ * The {@link Ext.chart.axis.Time#cfg-dateFormat dateFormat}
+ * configuration tells the Time Axis how to format it's labels.
+ *
+ * Here's what the Chart looks like now that it has its Axes configured:
+ *
+ * {@img Ext.chart.Chart/Ext.chart.Chart1.png Chart Axes}
+ *
+ * ### 4. Configuring the Series
+ *
+ * The final step in creating a simple Chart is to configure one or more {@link Ext.chart.series.Series Series}.
+ * Series are responsible for the visual representation of the data points contained in the Store.
+ * This example only has one Series:
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * ...
+ * axes: [
+ * ...
+ * ],
+ * series: [
+ * {
+ * type: 'line',
+ * xField: 'date',
+ * yField: 'temperature'
+ * }
+ * ]
+ * });
+ *
+ * This Series is a {@link Ext.chart.series.Line Line Series}, and it uses the "date" and "temperature" fields
+ * from the "WeatherPoint" Models in the Store to plot its data points:
+ *
+ * {@img Ext.chart.Chart/Ext.chart.Chart2.png Line Series}
+ *
+ * See the [Simple Chart Example](doc-resources/Ext.chart.Chart/examples/simple_chart/index.html) for a live demo.
+ *
+ * ## Themes
+ *
+ * The color scheme for a Chart can be easily changed using the {@link #cfg-theme theme} configuration option:
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * ...
+ * theme: 'Green',
+ * ...
+ * });
+ *
+ * {@img Ext.chart.Chart/Ext.chart.Chart3.png Green Theme}
+ *
+ * For more information on Charts please refer to the [Drawing and Charting Guide](#/guide/drawing_and_charting).
+ *
+ */
+Ext.define('Ext.chart.Chart', {
+
+ /* Begin Definitions */
+
+ alias: 'widget.chart',
+
+ extend: 'Ext.draw.Component',
+
+ mixins: {
+ themeManager: 'Ext.chart.theme.Theme',
+ mask: 'Ext.chart.Mask',
+ navigation: 'Ext.chart.Navigation'
+ },
+
+ requires: [
+ 'Ext.util.MixedCollection',
+ 'Ext.data.StoreManager',
+ 'Ext.chart.Legend',
+ 'Ext.util.DelayedTask'
+ ],
+
+ /* End Definitions */
+
+ // @private
+ viewBox: false,
+
+ /**
+ * @cfg {String} theme
+ * The name of the theme to be used. A theme defines the colors and other visual displays of tick marks
+ * on axis, text, title text, line colors, marker colors and styles, etc. Possible theme values are 'Base', 'Green',
+ * 'Sky', 'Red', 'Purple', 'Blue', 'Yellow' and also six category themes 'Category1' to 'Category6'. Default value
+ * is 'Base'.
+ */
+
+ /**
+ * @cfg {Boolean/Object} animate
+ * True for the default animation (easing: 'ease' and duration: 500) or a standard animation config
+ * object to be used for default chart animations. Defaults to false.
+ */
+ animate: false,
+
+ /**
+ * @cfg {Boolean/Object} legend
+ * True for the default legend display or a legend config object. Defaults to false.
+ */
+ legend: false,
+
+ /**
+ * @cfg {Number} insetPadding
+ * The amount of inset padding in pixels for the chart. Defaults to 10.
+ */
+ insetPadding: 10,
+
+ /**
+ * @cfg {String[]} enginePriority
+ * Defines the priority order for which Surface implementation to use. The first one supported by the current
+ * environment will be used. Defaults to `['Svg', 'Vml']`.
+ */
+ enginePriority: ['Svg', 'Vml'],
+
+ /**
+ * @cfg {Object/Boolean} background
+ * The chart background. This can be a gradient object, image, or color. Defaults to false for no
+ * background. For example, if `background` were to be a color we could set the object as
+ *
+ * background: {
+ * //color string
+ * fill: '#ccc'
+ * }
+ *
+ * You can specify an image by using:
+ *
+ * background: {
+ * image: 'http://path.to.image/'
+ * }
+ *
+ * Also you can specify a gradient by using the gradient object syntax:
+ *
+ * background: {
+ * gradient: {
+ * id: 'gradientId',
+ * angle: 45,
+ * stops: {
+ * 0: {
+ * color: '#555'
+ * }
+ * 100: {
+ * color: '#ddd'
+ * }
+ * }
+ * }
+ * }
+ */
+ background: false,
+
+ /**
+ * @cfg {Object[]} gradients
+ * Define a set of gradients that can be used as `fill` property in sprites. The gradients array is an
+ * array of objects with the following properties:
+ *
+ * - **id** - string - The unique name of the gradient.
+ * - **angle** - number, optional - The angle of the gradient in degrees.
+ * - **stops** - object - An object with numbers as keys (from 0 to 100) and style objects as values
+ *
+ * For example:
+ *
+ * gradients: [{
+ * id: 'gradientId',
+ * angle: 45,
+ * stops: {
+ * 0: {
+ * color: '#555'
+ * },
+ * 100: {
+ * color: '#ddd'
+ * }
+ * }
+ * }, {
+ * id: 'gradientId2',
+ * angle: 0,
+ * stops: {
+ * 0: {
+ * color: '#590'
+ * },
+ * 20: {
+ * color: '#599'
+ * },
+ * 100: {
+ * color: '#ddd'
+ * }
+ * }
+ * }]
+ *
+ * Then the sprites can use `gradientId` and `gradientId2` by setting the fill attributes to those ids, for example:
+ *
+ * sprite.setAttributes({
+ * fill: 'url(#gradientId)'
+ * }, true);
+ */
+
+ /**
+ * @cfg {Ext.data.Store} store
+ * The store that supplies data to this chart.
+ */
+
+ /**
+ * @cfg {Ext.chart.series.Series[]} series
+ * Array of {@link Ext.chart.series.Series Series} instances or config objects. For example:
+ *
+ * series: [{
+ * type: 'column',
+ * axis: 'left',
+ * listeners: {
+ * 'afterrender': function() {
+ * console('afterrender');
+ * }
+ * },
+ * xField: 'category',
+ * yField: 'data1'
+ * }]
+ */
+
+ /**
+ * @cfg {Ext.chart.axis.Axis[]} axes
+ * Array of {@link Ext.chart.axis.Axis Axis} instances or config objects. For example:
+ *
+ * axes: [{
+ * type: 'Numeric',
+ * position: 'left',
+ * fields: ['data1'],
+ * title: 'Number of Hits',
+ * minimum: 0,
+ * //one minor tick between two major ticks
+ * minorTickSteps: 1
+ * }, {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Month of the Year'
+ * }]
+ */
+
+ constructor: function(config) {
+ var me = this,
+ defaultAnim;
+
+ config = Ext.apply({}, config);
+ me.initTheme(config.theme || me.theme);
+ if (me.gradients) {
+ Ext.apply(config, { gradients: me.gradients });
+ }
+ if (me.background) {
+ Ext.apply(config, { background: me.background });
+ }
+ if (config.animate) {
+ defaultAnim = {
+ easing: 'ease',
+ duration: 500
+ };
+ if (Ext.isObject(config.animate)) {
+ config.animate = Ext.applyIf(config.animate, defaultAnim);
+ }
+ else {
+ config.animate = defaultAnim;
+ }
+ }
+ me.mixins.mask.constructor.call(me, config);
+ me.mixins.navigation.constructor.call(me, config);
+ me.callParent([config]);
+ },
+
+ getChartStore: function(){
+ return this.substore || this.store;
+ },
+
+ initComponent: function() {
+ var me = this,
+ axes,
+ series;
+ me.callParent();
+ me.addEvents(
+ 'itemmousedown',
+ 'itemmouseup',
+ 'itemmouseover',
+ 'itemmouseout',
+ 'itemclick',
+ 'itemdoubleclick',
+ 'itemdragstart',
+ 'itemdrag',
+ '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 {Ext.chart.Chart} this
+ */
+ 'beforerefresh',
+ /**
+ * @event refresh
+ * Fires after the chart data has been refreshed.
+ * @param {Ext.chart.Chart} this
+ */
+ 'refresh'
+ );
+ Ext.applyIf(me, {
+ zoom: {
+ width: 1,
+ height: 1,
+ x: 0,
+ y: 0
+ }
+ });
+ me.maxGutter = [0, 0];
+ me.store = Ext.data.StoreManager.lookup(me.store);
+ axes = me.axes;
+ me.axes = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.position; });
+ if (axes) {
+ me.axes.addAll(axes);
+ }
+ series = me.series;
+ me.series = Ext.create('Ext.util.MixedCollection', false, function(a) { return a.seriesId || (a.seriesId = Ext.id(null, 'ext-chart-series-')); });
+ if (series) {
+ me.series.addAll(series);
+ }
+ if (me.legend !== false) {
+ me.legend = Ext.create('Ext.chart.Legend', Ext.applyIf({chart:me}, me.legend));
+ }
+
+ me.on({
+ mousemove: me.onMouseMove,
+ mouseleave: me.onMouseLeave,
+ mousedown: me.onMouseDown,
+ mouseup: me.onMouseUp,
+ scope: me
+ });
+ },
+
+ // @private overrides the component method to set the correct dimensions to the chart.
+ afterComponentLayout: function(width, height) {
+ var me = this;
+ if (Ext.isNumber(width) && Ext.isNumber(height)) {
+ me.curWidth = width;
+ me.curHeight = height;
+ me.redraw(true);
+ }
+ this.callParent(arguments);
+ },
+
+ /**
+ * Redraws the chart. If animations are set this will animate the chart too.
+ * @param {Boolean} resize (optional) flag which changes the default origin points of the chart for animations.
+ */
+ redraw: function(resize) {
+ var me = this,
+ chartBBox = me.chartBBox = {
+ x: 0,
+ y: 0,
+ height: me.curHeight,
+ width: me.curWidth
+ },
+ legend = me.legend;
+ me.surface.setSize(chartBBox.width, chartBBox.height);
+ // Instantiate Series and Axes
+ me.series.each(me.initializeSeries, me);
+ me.axes.each(me.initializeAxis, me);
+ //process all views (aggregated data etc) on stores
+ //before rendering.
+ me.axes.each(function(axis) {
+ axis.processView();
+ });
+ me.axes.each(function(axis) {
+ axis.drawAxis(true);
+ });
+
+ // Create legend if not already created
+ if (legend !== false) {
+ legend.create();
+ }
+
+ // Place axes properly, including influence from each other
+ me.alignAxes();
+
+ // Reposition legend based on new axis alignment
+ if (me.legend !== false) {
+ legend.updatePosition();
+ }
+
+ // Find the max gutter
+ me.getMaxGutter();
+
+ // Draw axes and series
+ me.resizing = !!resize;
+
+ me.axes.each(me.drawAxis, me);
+ me.series.each(me.drawCharts, me);
+ me.resizing = false;
+ },
+
+ // @private set the store after rendering the chart.
+ afterRender: function() {
+ var ref,
+ me = this;
+ this.callParent();
+
+ if (me.categoryNames) {
+ me.setCategoryNames(me.categoryNames);
+ }
+
+ if (me.tipRenderer) {
+ ref = me.getFunctionRef(me.tipRenderer);
+ me.setTipRenderer(ref.fn, ref.scope);
+ }
+ me.bindStore(me.store, true);
+ me.refresh();
+ },
+
+ // @private get x and y position of the mouse cursor.
+ getEventXY: function(e) {
+ var me = this,
+ box = this.surface.getRegion(),
+ pageXY = e.getXY(),
+ x = pageXY[0] - box.left,
+ y = pageXY[1] - box.top;
+ return [x, y];
+ },
+
+ // @private wrap the mouse down position to delegate the event to the series.
+ onClick: function(e) {
+ var me = this,
+ position = me.getEventXY(e),
+ item;
+
+ // Ask each series if it has an item corresponding to (not necessarily exactly
+ // on top of) the current mouse coords. Fire itemclick event.
+ me.series.each(function(series) {
+ if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
+ if (series.getItemForPoint) {
+ item = series.getItemForPoint(position[0], position[1]);
+ if (item) {
+ series.fireEvent('itemclick', item);
+ }
+ }
+ }
+ }, me);
+ },
+
+ // @private wrap the mouse down position to delegate the event to the series.
+ onMouseDown: function(e) {
+ var me = this,
+ position = me.getEventXY(e),
+ item;
+
+ if (me.mask) {
+ me.mixins.mask.onMouseDown.call(me, e);
+ }
+ // Ask each series if it has an item corresponding to (not necessarily exactly
+ // on top of) the current mouse coords. Fire mousedown event.
+ me.series.each(function(series) {
+ if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
+ if (series.getItemForPoint) {
+ item = series.getItemForPoint(position[0], position[1]);
+ if (item) {
+ series.fireEvent('itemmousedown', item);
+ }
+ }
+ }
+ }, me);
+ },
+
+ // @private wrap the mouse up event to delegate it to the series.
+ onMouseUp: function(e) {
+ var me = this,
+ position = me.getEventXY(e),
+ item;
+
+ if (me.mask) {
+ me.mixins.mask.onMouseUp.call(me, e);
+ }
+ // Ask each series if it has an item corresponding to (not necessarily exactly
+ // on top of) the current mouse coords. Fire mousedown event.
+ me.series.each(function(series) {
+ if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
+ if (series.getItemForPoint) {
+ item = series.getItemForPoint(position[0], position[1]);
+ if (item) {
+ series.fireEvent('itemmouseup', item);
+ }
+ }
+ }
+ }, me);
+ },
+
+ // @private wrap the mouse move event so it can be delegated to the series.
+ onMouseMove: function(e) {
+ var me = this,
+ position = me.getEventXY(e),
+ item, last, storeItem, storeField;
+
+ if (me.mask) {
+ me.mixins.mask.onMouseMove.call(me, e);
+ }
+ // Ask each series if it has an item corresponding to (not necessarily exactly
+ // on top of) the current mouse coords. Fire itemmouseover/out events.
+ me.series.each(function(series) {
+ if (Ext.draw.Draw.withinBox(position[0], position[1], series.bbox)) {
+ if (series.getItemForPoint) {
+ item = series.getItemForPoint(position[0], position[1]);
+ last = series._lastItemForPoint;
+ storeItem = series._lastStoreItem;
+ storeField = series._lastStoreField;
+
+
+ if (item !== last || item && (item.storeItem != storeItem || item.storeField != storeField)) {
+ if (last) {
+ series.fireEvent('itemmouseout', last);
+ delete series._lastItemForPoint;
+ delete series._lastStoreField;
+ delete series._lastStoreItem;
+ }
+ if (item) {
+ series.fireEvent('itemmouseover', item);
+ series._lastItemForPoint = item;
+ series._lastStoreItem = item.storeItem;
+ series._lastStoreField = item.storeField;
+ }
+ }
+ }
+ } else {
+ last = series._lastItemForPoint;
+ if (last) {
+ series.fireEvent('itemmouseout', last);
+ delete series._lastItemForPoint;
+ delete series._lastStoreField;
+ delete series._lastStoreItem;
+ }
+ }
+ }, me);
+ },
+
+ // @private handle mouse leave event.
+ onMouseLeave: function(e) {
+ var me = this;
+ if (me.mask) {
+ me.mixins.mask.onMouseLeave.call(me, e);
+ }
+ me.series.each(function(series) {
+ delete series._lastItemForPoint;
+ });
+ },
+
+ // @private buffered refresh for when we update the store
+ delayRefresh: function() {
+ var me = this;
+ if (!me.refreshTask) {
+ me.refreshTask = Ext.create('Ext.util.DelayedTask', me.refresh, me);
+ }
+ me.refreshTask.delay(me.refreshBuffer);
+ },
+
+ // @private
+ refresh: function() {
+ var me = this;
+ if (me.rendered && me.curWidth !== undefined && me.curHeight !== undefined) {
+ if (me.fireEvent('beforerefresh', me) !== false) {
+ me.redraw();
+ me.fireEvent('refresh', me);
+ }
+ }
+ },
+
+ /**
+ * Changes the data store bound to this chart and refreshes it.
+ * @param {Ext.data.Store} store The store to bind to this chart
+ */
+ bindStore: function(store, initial) {
+ var me = this;
+ if (!initial && me.store) {
+ if (store !== me.store && me.store.autoDestroy) {
+ me.store.destroyStore();
+ }
+ else {
+ me.store.un('datachanged', me.refresh, me);
+ me.store.un('add', me.delayRefresh, me);
+ me.store.un('remove', me.delayRefresh, me);
+ me.store.un('update', me.delayRefresh, me);
+ me.store.un('clear', me.refresh, me);
+ }
+ }
+ if (store) {
+ store = Ext.data.StoreManager.lookup(store);
+ store.on({
+ scope: me,
+ datachanged: me.refresh,
+ add: me.delayRefresh,
+ remove: me.delayRefresh,
+ update: me.delayRefresh,
+ clear: me.refresh
+ });
+ }
+ me.store = store;
+ if (store && !initial) {
+ me.refresh();
+ }
+ },
+
+ // @private Create Axis
+ initializeAxis: function(axis) {
+ var me = this,
+ chartBBox = me.chartBBox,
+ w = chartBBox.width,
+ h = chartBBox.height,
+ x = chartBBox.x,
+ y = chartBBox.y,
+ themeAttrs = me.themeAttrs,
+ config = {
+ chart: me
+ };
+ if (themeAttrs) {
+ config.axisStyle = Ext.apply({}, themeAttrs.axis);
+ config.axisLabelLeftStyle = Ext.apply({}, themeAttrs.axisLabelLeft);
+ config.axisLabelRightStyle = Ext.apply({}, themeAttrs.axisLabelRight);
+ config.axisLabelTopStyle = Ext.apply({}, themeAttrs.axisLabelTop);
+ config.axisLabelBottomStyle = Ext.apply({}, themeAttrs.axisLabelBottom);
+ config.axisTitleLeftStyle = Ext.apply({}, themeAttrs.axisTitleLeft);
+ config.axisTitleRightStyle = Ext.apply({}, themeAttrs.axisTitleRight);
+ config.axisTitleTopStyle = Ext.apply({}, themeAttrs.axisTitleTop);
+ config.axisTitleBottomStyle = Ext.apply({}, themeAttrs.axisTitleBottom);
+ }
+ switch (axis.position) {
+ case 'top':
+ Ext.apply(config, {
+ length: w,
+ width: h,
+ x: x,
+ y: y
+ });
+ break;
+ case 'bottom':
+ Ext.apply(config, {
+ length: w,
+ width: h,
+ x: x,
+ y: h
+ });
+ break;
+ case 'left':
+ Ext.apply(config, {
+ length: h,
+ width: w,
+ x: x,
+ y: h
+ });
+ break;
+ case 'right':
+ Ext.apply(config, {
+ length: h,
+ width: w,
+ x: w,
+ y: h
+ });
+ break;
+ }
+ if (!axis.chart) {
+ Ext.apply(config, axis);
+ axis = me.axes.replace(Ext.createByAlias('axis.' + axis.type.toLowerCase(), config));
+ }
+ else {
+ Ext.apply(axis, config);
+ }
+ },
+
+
+ /**
+ * @private Adjust the dimensions and positions of each axis and the chart body area after accounting
+ * for the space taken up on each side by the axes and legend.
+ */
+ alignAxes: function() {
+ var me = this,
+ axes = me.axes,
+ legend = me.legend,
+ edges = ['top', 'right', 'bottom', 'left'],
+ chartBBox,
+ insetPadding = me.insetPadding,
+ insets = {
+ top: insetPadding,
+ right: insetPadding,
+ bottom: insetPadding,
+ left: insetPadding
+ };
+
+ function getAxis(edge) {
+ var i = axes.findIndex('position', edge);
+ return (i < 0) ? null : axes.getAt(i);
+ }
+
+ // Find the space needed by axes and legend as a positive inset from each edge
+ Ext.each(edges, function(edge) {
+ var isVertical = (edge === 'left' || edge === 'right'),
+ axis = getAxis(edge),
+ bbox;
+
+ // Add legend size if it's on this edge
+ if (legend !== false) {
+ if (legend.position === edge) {
+ bbox = legend.getBBox();
+ insets[edge] += (isVertical ? bbox.width : bbox.height) + insets[edge];
+ }
+ }
+
+ // Add axis size if there's one on this edge only if it has been
+ //drawn before.
+ if (axis && axis.bbox) {
+ bbox = axis.bbox;
+ insets[edge] += (isVertical ? bbox.width : bbox.height);
+ }
+ });
+ // Build the chart bbox based on the collected inset values
+ chartBBox = {
+ x: insets.left,
+ y: insets.top,
+ width: me.curWidth - insets.left - insets.right,
+ height: me.curHeight - insets.top - insets.bottom
+ };
+ me.chartBBox = chartBBox;
+
+ // Go back through each axis and set its length and position based on the
+ // corresponding edge of the chartBBox
+ axes.each(function(axis) {
+ var pos = axis.position,
+ isVertical = (pos === 'left' || pos === 'right');
+
+ axis.x = (pos === 'right' ? chartBBox.x + chartBBox.width : chartBBox.x);
+ axis.y = (pos === 'top' ? chartBBox.y : chartBBox.y + chartBBox.height);
+ axis.width = (isVertical ? chartBBox.width : chartBBox.height);
+ axis.length = (isVertical ? chartBBox.height : chartBBox.width);
+ });
+ },
+
+ // @private initialize the series.
+ initializeSeries: function(series, idx) {
+ var me = this,
+ themeAttrs = me.themeAttrs,
+ seriesObj, markerObj, seriesThemes, st,
+ markerThemes, colorArrayStyle = [],
+ i = 0, l,
+ config = {
+ chart: me,
+ seriesId: series.seriesId
+ };
+ if (themeAttrs) {
+ seriesThemes = themeAttrs.seriesThemes;
+ markerThemes = themeAttrs.markerThemes;
+ seriesObj = Ext.apply({}, themeAttrs.series);
+ markerObj = Ext.apply({}, themeAttrs.marker);
+ config.seriesStyle = Ext.apply(seriesObj, seriesThemes[idx % seriesThemes.length]);
+ config.seriesLabelStyle = Ext.apply({}, themeAttrs.seriesLabel);
+ config.markerStyle = Ext.apply(markerObj, markerThemes[idx % markerThemes.length]);
+ if (themeAttrs.colors) {
+ config.colorArrayStyle = themeAttrs.colors;
+ } else {
+ colorArrayStyle = [];
+ for (l = seriesThemes.length; i < l; i++) {
+ st = seriesThemes[i];
+ if (st.fill || st.stroke) {
+ colorArrayStyle.push(st.fill || st.stroke);
+ }
+ }
+ if (colorArrayStyle.length) {
+ config.colorArrayStyle = colorArrayStyle;
+ }
+ }
+ config.seriesIdx = idx;
+ }
+ if (series instanceof Ext.chart.series.Series) {
+ Ext.apply(series, config);
+ } else {
+ Ext.applyIf(config, series);
+ series = me.series.replace(Ext.createByAlias('series.' + series.type.toLowerCase(), config));
+ }
+ if (series.initialize) {
+ series.initialize();
+ }
+ },
+
+ // @private
+ getMaxGutter: function() {
+ var me = this,
+ maxGutter = [0, 0];
+ me.series.each(function(s) {
+ var gutter = s.getGutters && s.getGutters() || [0, 0];
+ maxGutter[0] = Math.max(maxGutter[0], gutter[0]);
+ maxGutter[1] = Math.max(maxGutter[1], gutter[1]);
+ });
+ me.maxGutter = maxGutter;
+ },
+
+ // @private draw axis.
+ drawAxis: function(axis) {
+ axis.drawAxis();
+ },
+
+ // @private draw series.
+ drawCharts: function(series) {
+ series.triggerafterrender = false;
+ series.drawSeries();
+ if (!this.animate) {
+ series.fireEvent('afterrender');
+ }
+ },
+
+ // @private remove gently.
+ destroy: function() {
+ Ext.destroy(this.surface);
+ this.bindStore(null);
+ this.callParent(arguments);
+ }
+});
+
+/**
+ * @class Ext.chart.Highlight
+ * A mixin providing highlight functionality for Ext.chart.series.Series.
+ */
+Ext.define('Ext.chart.Highlight', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.fx.Anim'],
+
+ /* End Definitions */
+
+ /**
+ * Highlight the given series item.
+ * @param {Boolean/Object} Default's false. Can also be an object width style properties (i.e fill, stroke, radius)
+ * or just use default styles per series by setting highlight = true.
+ */
+ highlight: false,
+
+ highlightCfg : null,
+
+ constructor: function(config) {
+ if (config.highlight) {
+ if (config.highlight !== true) { //is an object
+ this.highlightCfg = Ext.apply({}, config.highlight);
+ }
+ else {
+ this.highlightCfg = {
+ fill: '#fdd',
+ radius: 20,
+ lineWidth: 5,
+ stroke: '#f55'
+ };
+ }
+ }
+ },
+
+ /**
+ * Highlight the given series item.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
+ */
+ highlightItem: function(item) {
+ if (!item) {
+ return;
+ }
+
+ var me = this,
+ sprite = item.sprite,
+ opts = me.highlightCfg,
+ surface = me.chart.surface,
+ animate = me.chart.animate,
+ p, from, to, pi;
+
+ if (!me.highlight || !sprite || sprite._highlighted) {
+ return;
+ }
+ if (sprite._anim) {
+ sprite._anim.paused = true;
+ }
+ sprite._highlighted = true;
+ if (!sprite._defaults) {
+ sprite._defaults = Ext.apply({}, sprite.attr);
+ from = {};
+ to = {};
+ for (p in opts) {
+ if (! (p in sprite._defaults)) {
+ sprite._defaults[p] = surface.availableAttrs[p];
+ }
+ from[p] = sprite._defaults[p];
+ to[p] = opts[p];
+ if (Ext.isObject(opts[p])) {
+ from[p] = {};
+ to[p] = {};
+ Ext.apply(sprite._defaults[p], sprite.attr[p]);
+ Ext.apply(from[p], sprite._defaults[p]);
+ for (pi in sprite._defaults[p]) {
+ if (! (pi in opts[p])) {
+ to[p][pi] = from[p][pi];
+ } else {
+ to[p][pi] = opts[p][pi];
+ }
+ }
+ for (pi in opts[p]) {
+ if (! (pi in to[p])) {
+ to[p][pi] = opts[p][pi];
+ }
+ }
+ }
+ }
+ sprite._from = from;
+ sprite._to = to;
+ sprite._endStyle = to;
+ }
+ if (animate) {
+ sprite._anim = Ext.create('Ext.fx.Anim', {
+ target: sprite,
+ from: sprite._from,
+ to: sprite._to,
+ duration: 150
+ });
+ } else {
+ sprite.setAttributes(sprite._to, true);
+ }
+ },
+
+ /**
+ * Un-highlight any existing highlights
+ */
+ unHighlightItem: function() {
+ if (!this.highlight || !this.items) {
+ return;
+ }
+
+ var me = this,
+ items = me.items,
+ len = items.length,
+ opts = me.highlightCfg,
+ animate = me.chart.animate,
+ i = 0,
+ obj, p, sprite;
+
+ for (; i < len; i++) {
+ if (!items[i]) {
+ continue;
+ }
+ sprite = items[i].sprite;
+ if (sprite && sprite._highlighted) {
+ if (sprite._anim) {
+ sprite._anim.paused = true;
+ }
+ obj = {};
+ for (p in opts) {
+ if (Ext.isObject(sprite._defaults[p])) {
+ obj[p] = {};
+ Ext.apply(obj[p], sprite._defaults[p]);
+ }
+ else {
+ obj[p] = sprite._defaults[p];
+ }
+ }
+ if (animate) {
+ //sprite._to = obj;
+ sprite._endStyle = obj;
+ sprite._anim = Ext.create('Ext.fx.Anim', {
+ target: sprite,
+ to: obj,
+ duration: 150
+ });
+ }
+ else {
+ sprite.setAttributes(obj, true);
+ }
+ delete sprite._highlighted;
+ //delete sprite._defaults;
+ }
+ }
+ },
+
+ cleanHighlights: function() {
+ if (!this.highlight) {
+ return;
+ }
+
+ var group = this.group,
+ markerGroup = this.markerGroup,
+ i = 0,
+ l;
+ for (l = group.getCount(); i < l; i++) {
+ delete group.getAt(i)._defaults;
+ }
+ if (markerGroup) {
+ for (l = markerGroup.getCount(); i < l; i++) {
+ delete markerGroup.getAt(i)._defaults;
+ }
+ }
+ }
+});
+/**
+ * @class Ext.chart.Label
+ *
+ * Labels is a mixin to the Series class. Labels methods are implemented
+ * in each of the Series (Pie, Bar, etc) for label creation and placement.
+ *
+ * The methods implemented by the Series are:
+ *
+ * - **`onCreateLabel(storeItem, item, i, display)`** Called each time a new label is created.
+ * The arguments of the method are:
+ * - *`storeItem`* The element of the store that is related to the label sprite.
+ * - *`item`* The item related to the label sprite. An item is an object containing the position of the shape
+ * used to describe the visualization and also pointing to the actual shape (circle, rectangle, path, etc).
+ * - *`i`* The index of the element created (i.e the first created label, second created label, etc)
+ * - *`display`* The display type. May be <b>false</b> if the label is hidden
+ *
+ * - **`onPlaceLabel(label, storeItem, item, i, display, animate)`** Called for updating the position of the label.
+ * The arguments of the method are:
+ * - *`label`* The sprite label.</li>
+ * - *`storeItem`* The element of the store that is related to the label sprite</li>
+ * - *`item`* The item related to the label sprite. An item is an object containing the position of the shape
+ * used to describe the visualization and also pointing to the actual shape (circle, rectangle, path, etc).
+ * - *`i`* The index of the element to be updated (i.e. whether it is the first, second, third from the labelGroup)
+ * - *`display`* The display type. May be <b>false</b> if the label is hidden.
+ * - *`animate`* A boolean value to set or unset animations for the labels.
+ */
+Ext.define('Ext.chart.Label', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.draw.Color'],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Object} label
+ * Object with the following properties:
+ *
+ * - **display** : String
+ *
+ * Specifies the presence and position of labels for each pie slice. Either "rotate", "middle", "insideStart",
+ * "insideEnd", "outside", "over", "under", or "none" to prevent label rendering.
+ * Default value: 'none'.
+ *
+ * - **color** : String
+ *
+ * The color of the label text.
+ * Default value: '#000' (black).
+ *
+ * - **contrast** : Boolean
+ *
+ * True to render the label in contrasting color with the backround.
+ * Default value: false.
+ *
+ * - **field** : String
+ *
+ * The name of the field to be displayed in the label.
+ * Default value: 'name'.
+ *
+ * - **minMargin** : Number
+ *
+ * Specifies the minimum distance from a label to the origin of the visualization.
+ * This parameter is useful when using PieSeries width variable pie slice lengths.
+ * Default value: 50.
+ *
+ * - **font** : String
+ *
+ * The font used for the labels.
+ * Default value: "11px Helvetica, sans-serif".
+ *
+ * - **orientation** : String
+ *
+ * Either "horizontal" or "vertical".
+ * Dafault value: "horizontal".
+ *
+ * - **renderer** : Function
+ *
+ * Optional function for formatting the label into a displayable value.
+ * Default value: function(v) { return v; }
+ */
+
+ //@private a regex to parse url type colors.
+ colorStringRe: /url\s*\(\s*#([^\/)]+)\s*\)/,
+
+ //@private the mixin constructor. Used internally by Series.
+ constructor: function(config) {
+ var me = this;
+ me.label = Ext.applyIf(me.label || {},
+ {
+ display: "none",
+ color: "#000",
+ field: "name",
+ minMargin: 50,
+ font: "11px Helvetica, sans-serif",
+ orientation: "horizontal",
+ renderer: function(v) {
+ return v;
+ }
+ });
+
+ if (me.label.display !== 'none') {
+ me.labelsGroup = me.chart.surface.getGroup(me.seriesId + '-labels');
+ }
+ },
+
+ //@private a method to render all labels in the labelGroup
+ renderLabels: function() {
+ var me = this,
+ chart = me.chart,
+ gradients = chart.gradients,
+ items = me.items,
+ animate = chart.animate,
+ config = me.label,
+ display = config.display,
+ color = config.color,
+ field = [].concat(config.field),
+ group = me.labelsGroup,
+ groupLength = (group || 0) && group.length,
+ store = me.chart.store,
+ len = store.getCount(),
+ itemLength = (items || 0) && items.length,
+ ratio = itemLength / len,
+ gradientsCount = (gradients || 0) && gradients.length,
+ Color = Ext.draw.Color,
+ hides = [],
+ gradient, i, count, groupIndex, index, j, k, colorStopTotal, colorStopIndex, colorStop, item, label,
+ storeItem, sprite, spriteColor, spriteBrightness, labelColor, colorString;
+
+ if (display == 'none') {
+ return;
+ }
+ // no items displayed, hide all labels
+ if(itemLength == 0){
+ while(groupLength--)
+ hides.push(groupLength);
+ }else{
+ for (i = 0, count = 0, groupIndex = 0; i < len; i++) {
+ index = 0;
+ for (j = 0; j < ratio; j++) {
+ item = items[count];
+ label = group.getAt(groupIndex);
+ storeItem = store.getAt(i);
+ //check the excludes
+ while(this.__excludes && this.__excludes[index] && ratio > 1) {
+ if(field[j]){
+ hides.push(groupIndex);
+ }
+ index++;
+
+ }
+
+ if (!item && label) {
+ label.hide(true);
+ groupIndex++;
+ }
+
+ if (item && field[j]) {
+ if (!label) {
+ label = me.onCreateLabel(storeItem, item, i, display, j, index);
+ }
+ me.onPlaceLabel(label, storeItem, item, i, display, animate, j, index);
+ groupIndex++;
+
+ //set contrast
+ if (config.contrast && item.sprite) {
+ sprite = item.sprite;
+ //set the color string to the color to be set.
+ if (sprite._endStyle) {
+ colorString = sprite._endStyle.fill;
+ }
+ else if (sprite._to) {
+ colorString = sprite._to.fill;
+ }
+ else {
+ colorString = sprite.attr.fill;
+ }
+ colorString = colorString || sprite.attr.fill;
+
+ spriteColor = Color.fromString(colorString);
+ //color wasn't parsed property maybe because it's a gradient id
+ if (colorString && !spriteColor) {
+ colorString = colorString.match(me.colorStringRe)[1];
+ for (k = 0; k < gradientsCount; k++) {
+ gradient = gradients[k];
+ if (gradient.id == colorString) {
+ //avg color stops
+ colorStop = 0; colorStopTotal = 0;
+ for (colorStopIndex in gradient.stops) {
+ colorStop++;
+ colorStopTotal += Color.fromString(gradient.stops[colorStopIndex].color).getGrayscale();
+ }
+ spriteBrightness = (colorStopTotal / colorStop) / 255;
+ break;
+ }
+ }
+ }
+ else {
+ spriteBrightness = spriteColor.getGrayscale() / 255;
+ }
+ if (label.isOutside) {
+ spriteBrightness = 1;
+ }
+ labelColor = Color.fromString(label.attr.color || label.attr.fill).getHSL();
+ labelColor[2] = spriteBrightness > 0.5 ? 0.2 : 0.8;
+ label.setAttributes({
+ fill: String(Color.fromHSL.apply({}, labelColor))
+ }, true);
+ }
+
+ }
+ count++;
+ index++;
+ }
+ }
+ }
+ me.hideLabels(hides);
+ },
+ hideLabels: function(hides){
+ var labelsGroup = this.labelsGroup,
+ hlen = hides.length;
+ while(hlen--)
+ labelsGroup.getAt(hides[hlen]).hide(true);
+ }
+});
+Ext.define('Ext.chart.MaskLayer', {
+ extend: 'Ext.Component',
+
+ constructor: function(config) {
+ config = Ext.apply(config || {}, {
+ style: 'position:absolute;background-color:#888;cursor:move;opacity:0.6;border:1px solid #222;'
+ });
+ this.callParent([config]);
+ },
+
+ initComponent: function() {
+ var me = this;
+ me.callParent(arguments);
+ me.addEvents(
+ 'mousedown',
+ 'mouseup',
+ 'mousemove',
+ 'mouseenter',
+ 'mouseleave'
+ );
+ },
+
+ initDraggable: function() {
+ this.callParent(arguments);
+ this.dd.onStart = function (e) {
+ var me = this,
+ comp = me.comp;
+
+ // Cache the start [X, Y] array
+ this.startPosition = comp.getPosition(true);
+
+ // If client Component has a ghost method to show a lightweight version of itself
+ // then use that as a drag proxy unless configured to liveDrag.
+ if (comp.ghost && !comp.liveDrag) {
+ me.proxy = comp.ghost();
+ me.dragTarget = me.proxy.header.el;
+ }
+
+ // Set the constrainTo Region before we start dragging.
+ if (me.constrain || me.constrainDelegate) {
+ me.constrainTo = me.calculateConstrainRegion();
+ }
+ };
+ }
+});
+/**
+ * @class Ext.chart.TipSurface
+ * @ignore
+ */
+Ext.define('Ext.chart.TipSurface', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.draw.Component',
+
+ /* End Definitions */
+
+ spriteArray: false,
+ renderFirst: true,
+
+ constructor: function(config) {
+ this.callParent([config]);
+ if (config.sprites) {
+ this.spriteArray = [].concat(config.sprites);
+ delete config.sprites;
+ }
+ },
+
+ onRender: function() {
+ var me = this,
+ i = 0,
+ l = 0,
+ sp,
+ sprites;
+ this.callParent(arguments);
+ sprites = me.spriteArray;
+ if (me.renderFirst && sprites) {
+ me.renderFirst = false;
+ for (l = sprites.length; i < l; i++) {
+ sp = me.surface.add(sprites[i]);
+ sp.setAttributes({
+ hidden: false
+ },
+ true);
+ }
+ }
+ }
+});
+
+/**
+ * @class Ext.chart.Tip
+ * Provides tips for Ext.chart.series.Series.
+ */
+Ext.define('Ext.chart.Tip', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.tip.ToolTip', 'Ext.chart.TipSurface'],
+
+ /* End Definitions */
+
+ constructor: function(config) {
+ var me = this,
+ surface,
+ sprites,
+ tipSurface;
+ if (config.tips) {
+ me.tipTimeout = null;
+ me.tipConfig = Ext.apply({}, config.tips, {
+ renderer: Ext.emptyFn,
+ constrainPosition: false
+ });
+ me.tooltip = Ext.create('Ext.tip.ToolTip', me.tipConfig);
+ me.chart.surface.on('mousemove', me.tooltip.onMouseMove, me.tooltip);
+ me.chart.surface.on('mouseleave', function() {
+ me.hideTip();
+ });
+ if (me.tipConfig.surface) {
+ //initialize a surface
+ surface = me.tipConfig.surface;
+ sprites = surface.sprites;
+ tipSurface = Ext.create('Ext.chart.TipSurface', {
+ id: 'tipSurfaceComponent',
+ sprites: sprites
+ });
+ if (surface.width && surface.height) {
+ tipSurface.setSize(surface.width, surface.height);
+ }
+ me.tooltip.add(tipSurface);
+ me.spriteTip = tipSurface;
+ }
+ }
+ },
+
+ showTip: function(item) {
+ var me = this;
+ if (!me.tooltip) {
+ return;
+ }
+ clearTimeout(me.tipTimeout);
+ var tooltip = me.tooltip,
+ spriteTip = me.spriteTip,
+ tipConfig = me.tipConfig,
+ trackMouse = tooltip.trackMouse,
+ sprite, surface, surfaceExt, pos, x, y;
+ if (!trackMouse) {
+ tooltip.trackMouse = true;
+ sprite = item.sprite;
+ surface = sprite.surface;
+ surfaceExt = Ext.get(surface.getId());
+ if (surfaceExt) {
+ pos = surfaceExt.getXY();
+ x = pos[0] + (sprite.attr.x || 0) + (sprite.attr.translation && sprite.attr.translation.x || 0);
+ y = pos[1] + (sprite.attr.y || 0) + (sprite.attr.translation && sprite.attr.translation.y || 0);
+ tooltip.targetXY = [x, y];
+ }
+ }
+ if (spriteTip) {
+ tipConfig.renderer.call(tooltip, item.storeItem, item, spriteTip.surface);
+ } else {
+ tipConfig.renderer.call(tooltip, item.storeItem, item);
+ }
+ tooltip.show();
+ tooltip.trackMouse = trackMouse;
+ },
+
+ hideTip: function(item) {
+ var tooltip = this.tooltip;
+ if (!tooltip) {
+ return;
+ }
+ clearTimeout(this.tipTimeout);
+ this.tipTimeout = setTimeout(function() {
+ tooltip.hide();
+ }, 0);
+ }
+});
+/**
+ * @class Ext.chart.axis.Abstract
+ * Base class for all axis classes.
+ * @private
+ */
+Ext.define('Ext.chart.axis.Abstract', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.chart.Chart'],
+
+ /* End Definitions */
+
+ /**
+ * Creates new Axis.
+ * @param {Object} config (optional) Config options.
+ */
+ constructor: function(config) {
+ config = config || {};
+
+ var me = this,
+ pos = config.position || 'left';
+
+ pos = pos.charAt(0).toUpperCase() + pos.substring(1);
+ //axisLabel(Top|Bottom|Right|Left)Style
+ config.label = Ext.apply(config['axisLabel' + pos + 'Style'] || {}, config.label || {});
+ config.axisTitleStyle = Ext.apply(config['axisTitle' + pos + 'Style'] || {}, config.labelTitle || {});
+ Ext.apply(me, config);
+ me.fields = [].concat(me.fields);
+ this.callParent();
+ me.labels = [];
+ me.getId();
+ me.labelGroup = me.chart.surface.getGroup(me.axisId + "-labels");
+ },
+
+ alignment: null,
+ grid: false,
+ steps: 10,
+ x: 0,
+ y: 0,
+ minValue: 0,
+ maxValue: 0,
+
+ getId: function() {
+ return this.axisId || (this.axisId = Ext.id(null, 'ext-axis-'));
+ },
+
+ /*
+ Called to process a view i.e to make aggregation and filtering over
+ a store creating a substore to be used to render the axis. Since many axes
+ may do different things on the data and we want the final result of all these
+ operations to be rendered we need to call processView on all axes before drawing
+ them.
+ */
+ processView: Ext.emptyFn,
+
+ drawAxis: Ext.emptyFn,
+ addDisplayAndLabels: Ext.emptyFn
+});
+
+/**
+ * @class Ext.chart.axis.Axis
+ * @extends Ext.chart.axis.Abstract
+ *
+ * Defines axis for charts. The axis position, type, style can be configured.
+ * The axes are defined in an axes array of configuration objects where the type,
+ * field, grid and other configuration options can be set. To know more about how
+ * to create a Chart please check the Chart class documentation. Here's an example for the axes part:
+ * An example of axis for a series (in this case for an area chart that has multiple layers of yFields) could be:
+ *
+ * axes: [{
+ * type: 'Numeric',
+ * grid: true,
+ * position: 'left',
+ * fields: ['data1', 'data2', 'data3'],
+ * title: 'Number of Hits',
+ * grid: {
+ * odd: {
+ * opacity: 1,
+ * fill: '#ddd',
+ * stroke: '#bbb',
+ * 'stroke-width': 1
+ * }
+ * },
+ * minimum: 0
+ * }, {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Month of the Year',
+ * grid: true,
+ * label: {
+ * rotate: {
+ * degrees: 315
+ * }
+ * }
+ * }]
+ *
+ * In this case we use a `Numeric` axis for displaying the values of the Area series and a `Category` axis for displaying the names of
+ * the store elements. The numeric axis is placed on the left of the screen, while the category axis is placed at the bottom of the chart.
+ * Both the category and numeric axes have `grid` set, which means that horizontal and vertical lines will cover the chart background. In the
+ * category axis the labels will be rotated so they can fit the space better.
+ */
+Ext.define('Ext.chart.axis.Axis', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.axis.Abstract',
+
+ alternateClassName: 'Ext.chart.Axis',
+
+ requires: ['Ext.draw.Draw'],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Boolean/Object} grid
+ * The grid configuration enables you to set a background grid for an axis.
+ * If set to *true* on a vertical axis, vertical lines will be drawn.
+ * If set to *true* on a horizontal axis, horizontal lines will be drawn.
+ * If both are set, a proper grid with horizontal and vertical lines will be drawn.
+ *
+ * You can set specific options for the grid configuration for odd and/or even lines/rows.
+ * Since the rows being drawn are rectangle sprites, you can set to an odd or even property
+ * all styles that apply to {@link Ext.draw.Sprite}. For more information on all the style
+ * properties you can set please take a look at {@link Ext.draw.Sprite}. Some useful style properties are `opacity`, `fill`, `stroke`, `stroke-width`, etc.
+ *
+ * The possible values for a grid option are then *true*, *false*, or an object with `{ odd, even }` properties
+ * where each property contains a sprite style descriptor object that is defined in {@link Ext.draw.Sprite}.
+ *
+ * For example:
+ *
+ * axes: [{
+ * type: 'Numeric',
+ * grid: true,
+ * position: 'left',
+ * fields: ['data1', 'data2', 'data3'],
+ * title: 'Number of Hits',
+ * grid: {
+ * odd: {
+ * opacity: 1,
+ * fill: '#ddd',
+ * stroke: '#bbb',
+ * 'stroke-width': 1
+ * }
+ * }
+ * }, {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Month of the Year',
+ * grid: true
+ * }]
+ *
+ */
+
+ /**
+ * @cfg {Number} majorTickSteps
+ * If `minimum` and `maximum` are specified it forces the number of major ticks to the specified value.
+ */
+
+ /**
+ * @cfg {Number} minorTickSteps
+ * The number of small ticks between two major ticks. Default is zero.
+ */
+
+ /**
+ * @cfg {String} title
+ * The title for the Axis
+ */
+
+ //@private force min/max values from store
+ forceMinMax: false,
+
+ /**
+ * @cfg {Number} dashSize
+ * The size of the dash marker. Default's 3.
+ */
+ dashSize: 3,
+
+ /**
+ * @cfg {String} position
+ * Where to set the axis. Available options are `left`, `bottom`, `right`, `top`. Default's `bottom`.
+ */
+ position: 'bottom',
+
+ // @private
+ skipFirst: false,
+
+ /**
+ * @cfg {Number} length
+ * Offset axis position. Default's 0.
+ */
+ length: 0,
+
+ /**
+ * @cfg {Number} width
+ * Offset axis width. Default's 0.
+ */
+ width: 0,
+
+ majorTickSteps: false,
+
+ // @private
+ applyData: Ext.emptyFn,
+
+ getRange: function () {
+ var me = this,
+ store = me.chart.getChartStore(),
+ fields = me.fields,
+ ln = fields.length,
+ math = Math,
+ mmax = math.max,
+ mmin = math.min,
+ aggregate = false,
+ min = isNaN(me.minimum) ? Infinity : me.minimum,
+ max = isNaN(me.maximum) ? -Infinity : me.maximum,
+ total = 0, i, l, value, values, rec,
+ excludes = [],
+ series = me.chart.series.items;
+
+ //if one series is stacked I have to aggregate the values
+ //for the scale.
+ // TODO(zhangbei): the code below does not support series that stack on 1 side but non-stacked axis
+ // listed in axis config. For example, a Area series whose axis : ['left', 'bottom'].
+ // Assuming only stack on y-axis.
+ for (i = 0, l = series.length; !aggregate && i < l; i++) {
+ aggregate = aggregate || (me.position == 'left' || me.position == 'right') && series[i].stacked;
+ excludes = series[i].__excludes || excludes;
+ }
+ store.each(function(record) {
+ if (aggregate) {
+ if (!isFinite(min)) {
+ min = 0;
+ }
+ for (values = [0, 0], i = 0; i < ln; i++) {
+ if (excludes[i]) {
+ continue;
+ }
+ rec = record.get(fields[i]);
+ values[+(rec > 0)] += math.abs(rec);
+ }
+ max = mmax(max, -values[0], +values[1]);
+ min = mmin(min, -values[0], +values[1]);
+ }
+ else {
+ for (i = 0; i < ln; i++) {
+ if (excludes[i]) {
+ continue;
+ }
+ value = record.get(fields[i]);
+ max = mmax(max, +value);
+ min = mmin(min, +value);
+ }
+ }
+ });
+ if (!isFinite(max)) {
+ max = me.prevMax || 0;
+ }
+ if (!isFinite(min)) {
+ min = me.prevMin || 0;
+ }
+ //normalize min max for snapEnds.
+ if (min != max && (max != Math.floor(max))) {
+ max = Math.floor(max) + 1;
+ }
+
+ if (!isNaN(me.minimum)) {
+ min = me.minimum;
+ }
+
+ if (!isNaN(me.maximum)) {
+ max = me.maximum;
+ }
+
+ return {min: min, max: max};
+ },
+
+ // @private creates a structure with start, end and step points.
+ calcEnds: function() {
+ var me = this,
+ fields = me.fields,
+ range = me.getRange(),
+ min = range.min,
+ max = range.max,
+ outfrom, outto, out;
+
+ out = Ext.draw.Draw.snapEnds(min, max, me.majorTickSteps !== false ? (me.majorTickSteps +1) : me.steps);
+ outfrom = out.from;
+ outto = out.to;
+ if (me.forceMinMax) {
+ if (!isNaN(max)) {
+ out.to = max;
+ }
+ if (!isNaN(min)) {
+ out.from = min;
+ }
+ }
+ if (!isNaN(me.maximum)) {
+ //TODO(nico) users are responsible for their own minimum/maximum values set.
+ //Clipping should be added to remove lines in the chart which are below the axis.
+ out.to = me.maximum;
+ }
+ if (!isNaN(me.minimum)) {
+ //TODO(nico) users are responsible for their own minimum/maximum values set.
+ //Clipping should be added to remove lines in the chart which are below the axis.
+ out.from = me.minimum;
+ }
+
+ //Adjust after adjusting minimum and maximum
+ out.step = (out.to - out.from) / (outto - outfrom) * out.step;
+
+ if (me.adjustMaximumByMajorUnit) {
+ out.to += out.step;
+ }
+ if (me.adjustMinimumByMajorUnit) {
+ out.from -= out.step;
+ }
+ me.prevMin = min == max? 0 : min;
+ me.prevMax = max;
+ return out;
+ },
+
+ /**
+ * Renders the axis into the screen and updates its position.
+ */
+ drawAxis: function (init) {
+ var me = this,
+ i, j,
+ x = me.x,
+ y = me.y,
+ gutterX = me.chart.maxGutter[0],
+ gutterY = me.chart.maxGutter[1],
+ dashSize = me.dashSize,
+ subDashesX = me.minorTickSteps || 0,
+ subDashesY = me.minorTickSteps || 0,
+ length = me.length,
+ position = me.position,
+ inflections = [],
+ calcLabels = false,
+ stepCalcs = me.applyData(),
+ step = stepCalcs.step,
+ steps = stepCalcs.steps,
+ from = stepCalcs.from,
+ to = stepCalcs.to,
+ trueLength,
+ currentX,
+ currentY,
+ path,
+ prev,
+ dashesX,
+ dashesY,
+ delta;
+
+ //If no steps are specified
+ //then don't draw the axis. This generally happens
+ //when an empty store.
+ if (me.hidden || isNaN(step) || (from == to)) {
+ return;
+ }
+
+ me.from = stepCalcs.from;
+ me.to = stepCalcs.to;
+ if (position == 'left' || position == 'right') {
+ currentX = Math.floor(x) + 0.5;
+ path = ["M", currentX, y, "l", 0, -length];
+ trueLength = length - (gutterY * 2);
+ }
+ else {
+ currentY = Math.floor(y) + 0.5;
+ path = ["M", x, currentY, "l", length, 0];
+ trueLength = length - (gutterX * 2);
+ }
+
+ delta = trueLength / (steps || 1);
+ dashesX = Math.max(subDashesX +1, 0);
+ dashesY = Math.max(subDashesY +1, 0);
+ if (me.type == 'Numeric' || me.type == 'Time') {
+ calcLabels = true;
+ me.labels = [stepCalcs.from];
+ }
+ if (position == 'right' || position == 'left') {
+ currentY = y - gutterY;
+ currentX = x - ((position == 'left') * dashSize * 2);
+ while (currentY >= y - gutterY - trueLength) {
+ path.push("M", currentX, Math.floor(currentY) + 0.5, "l", dashSize * 2 + 1, 0);
+ if (currentY != y - gutterY) {
+ for (i = 1; i < dashesY; i++) {
+ path.push("M", currentX + dashSize, Math.floor(currentY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0);
+ }
+ }
+ inflections.push([ Math.floor(x), Math.floor(currentY) ]);
+ currentY -= delta;
+ if (calcLabels) {
+ me.labels.push(me.labels[me.labels.length -1] + step);
+ }
+ if (delta === 0) {
+ break;
+ }
+ }
+ if (Math.round(currentY + delta - (y - gutterY - trueLength))) {
+ path.push("M", currentX, Math.floor(y - length + gutterY) + 0.5, "l", dashSize * 2 + 1, 0);
+ for (i = 1; i < dashesY; i++) {
+ path.push("M", currentX + dashSize, Math.floor(y - length + gutterY + delta * i / dashesY) + 0.5, "l", dashSize + 1, 0);
+ }
+ inflections.push([ Math.floor(x), Math.floor(currentY) ]);
+ if (calcLabels) {
+ me.labels.push(me.labels[me.labels.length -1] + step);
+ }
+ }
+ } else {
+ currentX = x + gutterX;
+ currentY = y - ((position == 'top') * dashSize * 2);
+ while (currentX <= x + gutterX + trueLength) {
+ path.push("M", Math.floor(currentX) + 0.5, currentY, "l", 0, dashSize * 2 + 1);
+ if (currentX != x + gutterX) {
+ for (i = 1; i < dashesX; i++) {
+ path.push("M", Math.floor(currentX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1);
+ }
+ }
+ inflections.push([ Math.floor(currentX), Math.floor(y) ]);
+ currentX += delta;
+ if (calcLabels) {
+ me.labels.push(me.labels[me.labels.length -1] + step);
+ }
+ if (delta === 0) {
+ break;
+ }
+ }
+ if (Math.round(currentX - delta - (x + gutterX + trueLength))) {
+ path.push("M", Math.floor(x + length - gutterX) + 0.5, currentY, "l", 0, dashSize * 2 + 1);
+ for (i = 1; i < dashesX; i++) {
+ path.push("M", Math.floor(x + length - gutterX - delta * i / dashesX) + 0.5, currentY, "l", 0, dashSize + 1);
+ }
+ inflections.push([ Math.floor(currentX), Math.floor(y) ]);
+ if (calcLabels) {
+ me.labels.push(me.labels[me.labels.length -1] + step);
+ }
+ }
+ }
+ if (!me.axis) {
+ me.axis = me.chart.surface.add(Ext.apply({
+ type: 'path',
+ path: path
+ }, me.axisStyle));
+ }
+ me.axis.setAttributes({
+ path: path
+ }, true);
+ me.inflections = inflections;
+ if (!init && me.grid) {
+ me.drawGrid();
+ }
+ me.axisBBox = me.axis.getBBox();
+ me.drawLabel();
+ },
+
+ /**
+ * Renders an horizontal and/or vertical grid into the Surface.
+ */
+ drawGrid: function() {
+ var me = this,
+ surface = me.chart.surface,
+ grid = me.grid,
+ odd = grid.odd,
+ even = grid.even,
+ inflections = me.inflections,
+ ln = inflections.length - ((odd || even)? 0 : 1),
+ position = me.position,
+ gutter = me.chart.maxGutter,
+ width = me.width - 2,
+ vert = false,
+ point, prevPoint,
+ i = 1,
+ path = [], styles, lineWidth, dlineWidth,
+ oddPath = [], evenPath = [];
+
+ if ((gutter[1] !== 0 && (position == 'left' || position == 'right')) ||
+ (gutter[0] !== 0 && (position == 'top' || position == 'bottom'))) {
+ i = 0;
+ ln++;
+ }
+ for (; i < ln; i++) {
+ point = inflections[i];
+ prevPoint = inflections[i - 1];
+ if (odd || even) {
+ path = (i % 2)? oddPath : evenPath;
+ styles = ((i % 2)? odd : even) || {};
+ lineWidth = (styles.lineWidth || styles['stroke-width'] || 0) / 2;
+ dlineWidth = 2 * lineWidth;
+ if (position == 'left') {
+ path.push("M", prevPoint[0] + 1 + lineWidth, prevPoint[1] + 0.5 - lineWidth,
+ "L", prevPoint[0] + 1 + width - lineWidth, prevPoint[1] + 0.5 - lineWidth,
+ "L", point[0] + 1 + width - lineWidth, point[1] + 0.5 + lineWidth,
+ "L", point[0] + 1 + lineWidth, point[1] + 0.5 + lineWidth, "Z");
+ }
+ else if (position == 'right') {
+ path.push("M", prevPoint[0] - lineWidth, prevPoint[1] + 0.5 - lineWidth,
+ "L", prevPoint[0] - width + lineWidth, prevPoint[1] + 0.5 - lineWidth,
+ "L", point[0] - width + lineWidth, point[1] + 0.5 + lineWidth,
+ "L", point[0] - lineWidth, point[1] + 0.5 + lineWidth, "Z");
+ }
+ else if (position == 'top') {
+ path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + lineWidth,
+ "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] + 1 + width - lineWidth,
+ "L", point[0] + 0.5 - lineWidth, point[1] + 1 + width - lineWidth,
+ "L", point[0] + 0.5 - lineWidth, point[1] + 1 + lineWidth, "Z");
+ }
+ else {
+ path.push("M", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - lineWidth,
+ "L", prevPoint[0] + 0.5 + lineWidth, prevPoint[1] - width + lineWidth,
+ "L", point[0] + 0.5 - lineWidth, point[1] - width + lineWidth,
+ "L", point[0] + 0.5 - lineWidth, point[1] - lineWidth, "Z");
+ }
+ } else {
+ if (position == 'left') {
+ path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", width, 0]);
+ }
+ else if (position == 'right') {
+ path = path.concat(["M", point[0] - 0.5, point[1] + 0.5, "l", -width, 0]);
+ }
+ else if (position == 'top') {
+ path = path.concat(["M", point[0] + 0.5, point[1] + 0.5, "l", 0, width]);
+ }
+ else {
+ path = path.concat(["M", point[0] + 0.5, point[1] - 0.5, "l", 0, -width]);
+ }
+ }
+ }
+ if (odd || even) {
+ if (oddPath.length) {
+ if (!me.gridOdd && oddPath.length) {
+ me.gridOdd = surface.add({
+ type: 'path',
+ path: oddPath
+ });
+ }
+ me.gridOdd.setAttributes(Ext.apply({
+ path: oddPath,
+ hidden: false
+ }, odd || {}), true);
+ }
+ if (evenPath.length) {
+ if (!me.gridEven) {
+ me.gridEven = surface.add({
+ type: 'path',
+ path: evenPath
+ });
+ }
+ me.gridEven.setAttributes(Ext.apply({
+ path: evenPath,
+ hidden: false
+ }, even || {}), true);
+ }
+ }
+ else {
+ if (path.length) {
+ if (!me.gridLines) {
+ me.gridLines = me.chart.surface.add({
+ type: 'path',
+ path: path,
+ "stroke-width": me.lineWidth || 1,
+ stroke: me.gridColor || '#ccc'
+ });
+ }
+ me.gridLines.setAttributes({
+ hidden: false,
+ path: path
+ }, true);
+ }
+ else if (me.gridLines) {
+ me.gridLines.hide(true);
+ }
+ }
+ },
+
+ //@private
+ getOrCreateLabel: function(i, text) {
+ var me = this,
+ labelGroup = me.labelGroup,
+ textLabel = labelGroup.getAt(i),
+ surface = me.chart.surface;
+ if (textLabel) {
+ if (text != textLabel.attr.text) {
+ textLabel.setAttributes(Ext.apply({
+ text: text
+ }, me.label), true);
+ textLabel._bbox = textLabel.getBBox();
+ }
+ }
+ else {
+ textLabel = surface.add(Ext.apply({
+ group: labelGroup,
+ type: 'text',
+ x: 0,
+ y: 0,
+ text: text
+ }, me.label));
+ surface.renderItem(textLabel);
+ textLabel._bbox = textLabel.getBBox();
+ }
+ //get untransformed bounding box
+ if (me.label.rotation) {
+ textLabel.setAttributes({
+ rotation: {
+ degrees: 0
+ }
+ }, true);
+ textLabel._ubbox = textLabel.getBBox();
+ textLabel.setAttributes(me.label, true);
+ } else {
+ textLabel._ubbox = textLabel._bbox;
+ }
+ return textLabel;
+ },
+
+ rect2pointArray: function(sprite) {
+ var surface = this.chart.surface,
+ rect = surface.getBBox(sprite, true),
+ p1 = [rect.x, rect.y],
+ p1p = p1.slice(),
+ p2 = [rect.x + rect.width, rect.y],
+ p2p = p2.slice(),
+ p3 = [rect.x + rect.width, rect.y + rect.height],
+ p3p = p3.slice(),
+ p4 = [rect.x, rect.y + rect.height],
+ p4p = p4.slice(),
+ matrix = sprite.matrix;
+ //transform the points
+ p1[0] = matrix.x.apply(matrix, p1p);
+ p1[1] = matrix.y.apply(matrix, p1p);
+
+ p2[0] = matrix.x.apply(matrix, p2p);
+ p2[1] = matrix.y.apply(matrix, p2p);
+
+ p3[0] = matrix.x.apply(matrix, p3p);
+ p3[1] = matrix.y.apply(matrix, p3p);
+
+ p4[0] = matrix.x.apply(matrix, p4p);
+ p4[1] = matrix.y.apply(matrix, p4p);
+ return [p1, p2, p3, p4];
+ },
+
+ intersect: function(l1, l2) {
+ var r1 = this.rect2pointArray(l1),
+ r2 = this.rect2pointArray(l2);
+ return !!Ext.draw.Draw.intersect(r1, r2).length;
+ },
+
+ drawHorizontalLabels: function() {
+ var me = this,
+ labelConf = me.label,
+ floor = Math.floor,
+ max = Math.max,
+ axes = me.chart.axes,
+ position = me.position,
+ inflections = me.inflections,
+ ln = inflections.length,
+ labels = me.labels,
+ labelGroup = me.labelGroup,
+ maxHeight = 0,
+ ratio,
+ gutterY = me.chart.maxGutter[1],
+ ubbox, bbox, point, prevX, prevLabel,
+ projectedWidth = 0,
+ textLabel, attr, textRight, text,
+ label, last, x, y, i, firstLabel;
+
+ last = ln - 1;
+ //get a reference to the first text label dimensions
+ point = inflections[0];
+ firstLabel = me.getOrCreateLabel(0, me.label.renderer(labels[0]));
+ ratio = Math.floor(Math.abs(Math.sin(labelConf.rotate && (labelConf.rotate.degrees * Math.PI / 180) || 0)));
+
+ for (i = 0; i < ln; i++) {
+ point = inflections[i];
+ text = me.label.renderer(labels[i]);
+ textLabel = me.getOrCreateLabel(i, text);
+ bbox = textLabel._bbox;
+ maxHeight = max(maxHeight, bbox.height + me.dashSize + me.label.padding);
+ x = floor(point[0] - (ratio? bbox.height : bbox.width) / 2);
+ if (me.chart.maxGutter[0] == 0) {
+ if (i == 0 && axes.findIndex('position', 'left') == -1) {
+ x = point[0];
+ }
+ else if (i == last && axes.findIndex('position', 'right') == -1) {
+ x = point[0] - bbox.width;
+ }
+ }
+ if (position == 'top') {
+ y = point[1] - (me.dashSize * 2) - me.label.padding - (bbox.height / 2);
+ }
+ else {
+ y = point[1] + (me.dashSize * 2) + me.label.padding + (bbox.height / 2);
+ }
+
+ textLabel.setAttributes({
+ hidden: false,
+ x: x,
+ y: y
+ }, true);
+
+ // Skip label if there isn't available minimum space
+ if (i != 0 && (me.intersect(textLabel, prevLabel)
+ || me.intersect(textLabel, firstLabel))) {
+ textLabel.hide(true);
+ continue;
+ }
+
+ prevLabel = textLabel;
+ }
+
+ return maxHeight;
+ },
+
+ drawVerticalLabels: function() {
+ var me = this,
+ inflections = me.inflections,
+ position = me.position,
+ ln = inflections.length,
+ labels = me.labels,
+ maxWidth = 0,
+ max = Math.max,
+ floor = Math.floor,
+ ceil = Math.ceil,
+ axes = me.chart.axes,
+ gutterY = me.chart.maxGutter[1],
+ ubbox, bbox, point, prevLabel,
+ projectedWidth = 0,
+ textLabel, attr, textRight, text,
+ label, last, x, y, i;
+
+ last = ln;
+ for (i = 0; i < last; i++) {
+ point = inflections[i];
+ text = me.label.renderer(labels[i]);
+ textLabel = me.getOrCreateLabel(i, text);
+ bbox = textLabel._bbox;
+
+ maxWidth = max(maxWidth, bbox.width + me.dashSize + me.label.padding);
+ y = point[1];
+ if (gutterY < bbox.height / 2) {
+ if (i == last - 1 && axes.findIndex('position', 'top') == -1) {
+ y = me.y - me.length + ceil(bbox.height / 2);
+ }
+ else if (i == 0 && axes.findIndex('position', 'bottom') == -1) {
+ y = me.y - floor(bbox.height / 2);
+ }
+ }
+ if (position == 'left') {
+ x = point[0] - bbox.width - me.dashSize - me.label.padding - 2;
+ }
+ else {
+ x = point[0] + me.dashSize + me.label.padding + 2;
+ }
+ textLabel.setAttributes(Ext.apply({
+ hidden: false,
+ x: x,
+ y: y
+ }, me.label), true);
+ // Skip label if there isn't available minimum space
+ if (i != 0 && me.intersect(textLabel, prevLabel)) {
+ textLabel.hide(true);
+ continue;
+ }
+ prevLabel = textLabel;
+ }
+
+ return maxWidth;
+ },
+
+ /**
+ * Renders the labels in the axes.
+ */
+ drawLabel: function() {
+ var me = this,
+ position = me.position,
+ labelGroup = me.labelGroup,
+ inflections = me.inflections,
+ maxWidth = 0,
+ maxHeight = 0,
+ ln, i;
+
+ if (position == 'left' || position == 'right') {
+ maxWidth = me.drawVerticalLabels();
+ } else {
+ maxHeight = me.drawHorizontalLabels();
+ }
+
+ // Hide unused bars
+ ln = labelGroup.getCount();
+ i = inflections.length;
+ for (; i < ln; i++) {
+ labelGroup.getAt(i).hide(true);
+ }
+
+ me.bbox = {};
+ Ext.apply(me.bbox, me.axisBBox);
+ me.bbox.height = maxHeight;
+ me.bbox.width = maxWidth;
+ if (Ext.isString(me.title)) {
+ me.drawTitle(maxWidth, maxHeight);
+ }
+ },
+
+ // @private creates the elipsis for the text.
+ elipsis: function(sprite, text, desiredWidth, minWidth, center) {
+ var bbox,
+ x;
+
+ if (desiredWidth < minWidth) {
+ sprite.hide(true);
+ return false;
+ }
+ while (text.length > 4) {
+ text = text.substr(0, text.length - 4) + "...";
+ sprite.setAttributes({
+ text: text
+ }, true);
+ bbox = sprite.getBBox();
+ if (bbox.width < desiredWidth) {
+ if (typeof center == 'number') {
+ sprite.setAttributes({
+ x: Math.floor(center - (bbox.width / 2))
+ }, true);
+ }
+ break;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * Updates the {@link #title} of this axis.
+ * @param {String} title
+ */
+ setTitle: function(title) {
+ this.title = title;
+ this.drawLabel();
+ },
+
+ // @private draws the title for the axis.
+ drawTitle: function(maxWidth, maxHeight) {
+ var me = this,
+ position = me.position,
+ surface = me.chart.surface,
+ displaySprite = me.displaySprite,
+ title = me.title,
+ rotate = (position == 'left' || position == 'right'),
+ x = me.x,
+ y = me.y,
+ base, bbox, pad;
+
+ if (displaySprite) {
+ displaySprite.setAttributes({text: title}, true);
+ } else {
+ base = {
+ type: 'text',
+ x: 0,
+ y: 0,
+ text: title
+ };
+ displaySprite = me.displaySprite = surface.add(Ext.apply(base, me.axisTitleStyle, me.labelTitle));
+ surface.renderItem(displaySprite);
+ }
+ bbox = displaySprite.getBBox();
+ pad = me.dashSize + me.label.padding;
+
+ if (rotate) {
+ y -= ((me.length / 2) - (bbox.height / 2));
+ if (position == 'left') {
+ x -= (maxWidth + pad + (bbox.width / 2));
+ }
+ else {
+ x += (maxWidth + pad + bbox.width - (bbox.width / 2));
+ }
+ me.bbox.width += bbox.width + 10;
+ }
+ else {
+ x += (me.length / 2) - (bbox.width * 0.5);
+ if (position == 'top') {
+ y -= (maxHeight + pad + (bbox.height * 0.3));
+ }
+ else {
+ y += (maxHeight + pad + (bbox.height * 0.8));
+ }
+ me.bbox.height += bbox.height + 10;
+ }
+ displaySprite.setAttributes({
+ translate: {
+ x: x,
+ y: y
+ }
+ }, true);
+ }
+});
+
+/**
+ * @class Ext.chart.axis.Category
+ * @extends Ext.chart.axis.Axis
+ *
+ * A type of axis that displays items in categories. This axis is generally used to
+ * display categorical information like names of items, month names, quarters, etc.
+ * but no quantitative values. For that other type of information `Number`
+ * axis are more suitable.
+ *
+ * As with other axis you can set the position of the axis and its title. For example:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.JsonStore', {
+ * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+ * data: [
+ * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
+ * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
+ * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
+ * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
+ * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
+ * ]
+ * });
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 300,
+ * store: store,
+ * axes: [{
+ * type: 'Numeric',
+ * grid: true,
+ * position: 'left',
+ * fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
+ * title: 'Sample Values',
+ * grid: {
+ * odd: {
+ * opacity: 1,
+ * fill: '#ddd',
+ * stroke: '#bbb',
+ * 'stroke-width': 1
+ * }
+ * },
+ * minimum: 0,
+ * adjustMinimumByMajorUnit: 0
+ * }, {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Sample Metrics',
+ * grid: true,
+ * label: {
+ * rotate: {
+ * degrees: 315
+ * }
+ * }
+ * }],
+ * series: [{
+ * type: 'area',
+ * highlight: false,
+ * axis: 'left',
+ * xField: 'name',
+ * yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
+ * style: {
+ * opacity: 0.93
+ * }
+ * }]
+ * });
+ *
+ * In this example with set the category axis to the bottom of the surface, bound the axis to
+ * the `name` property and set as title _Month of the Year_.
+ */
+Ext.define('Ext.chart.axis.Category', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.axis.Axis',
+
+ alternateClassName: 'Ext.chart.CategoryAxis',
+
+ alias: 'axis.category',
+
+ /* End Definitions */
+
+ /**
+ * A list of category names to display along this axis.
+ * @property {String} categoryNames
+ */
+ categoryNames: null,
+
+ /**
+ * Indicates whether or not to calculate the number of categories (ticks and
+ * labels) when there is not enough room to display all labels on the axis.
+ * If set to true, the axis will determine the number of categories to plot.
+ * If not, all categories will be plotted.
+ *
+ * @property calculateCategoryCount
+ * @type Boolean
+ */
+ calculateCategoryCount: false,
+
+ // @private creates an array of labels to be used when rendering.
+ setLabels: function() {
+ var store = this.chart.store,
+ fields = this.fields,
+ ln = fields.length,
+ i;
+
+ this.labels = [];
+ store.each(function(record) {
+ for (i = 0; i < ln; i++) {
+ this.labels.push(record.get(fields[i]));
+ }
+ }, this);
+ },
+
+ // @private calculates labels positions and marker positions for rendering.
+ applyData: function() {
+ this.callParent();
+ this.setLabels();
+ var count = this.chart.store.getCount();
+ return {
+ from: 0,
+ to: count,
+ power: 1,
+ step: 1,
+ steps: count - 1
+ };
+ }
+});
+
+/**
+ * @class Ext.chart.axis.Gauge
+ * @extends Ext.chart.axis.Abstract
+ *
+ * Gauge Axis is the axis to be used with a Gauge series. The Gauge axis
+ * displays numeric data from an interval defined by the `minimum`, `maximum` and
+ * `step` configuration properties. The placement of the numeric data can be changed
+ * by altering the `margin` option that is set to `10` by default.
+ *
+ * A possible configuration for this axis would look like:
+ *
+ * axes: [{
+ * type: 'gauge',
+ * position: 'gauge',
+ * minimum: 0,
+ * maximum: 100,
+ * steps: 10,
+ * margin: 7
+ * }],
+ */
+Ext.define('Ext.chart.axis.Gauge', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.axis.Abstract',
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Number} minimum (required)
+ * The minimum value of the interval to be displayed in the axis.
+ */
+
+ /**
+ * @cfg {Number} maximum (required)
+ * The maximum value of the interval to be displayed in the axis.
+ */
+
+ /**
+ * @cfg {Number} steps (required)
+ * The number of steps and tick marks to add to the interval.
+ */
+
+ /**
+ * @cfg {Number} [margin=10]
+ * The offset positioning of the tick marks and labels in pixels.
+ */
+
+ /**
+ * @cfg {String} title
+ * The title for the Axis.
+ */
+
+ position: 'gauge',
+
+ alias: 'axis.gauge',
+
+ drawAxis: function(init) {
+ var chart = this.chart,
+ surface = chart.surface,
+ bbox = chart.chartBBox,
+ centerX = bbox.x + (bbox.width / 2),
+ centerY = bbox.y + bbox.height,
+ margin = this.margin || 10,
+ rho = Math.min(bbox.width, 2 * bbox.height) /2 + margin,
+ sprites = [], sprite,
+ steps = this.steps,
+ i, pi = Math.PI,
+ cos = Math.cos,
+ sin = Math.sin;
+
+ if (this.sprites && !chart.resizing) {
+ this.drawLabel();
+ return;
+ }
+
+ if (this.margin >= 0) {
+ if (!this.sprites) {
+ //draw circles
+ for (i = 0; i <= steps; i++) {
+ sprite = surface.add({
+ type: 'path',
+ path: ['M', centerX + (rho - margin) * cos(i / steps * pi - pi),
+ centerY + (rho - margin) * sin(i / steps * pi - pi),
+ 'L', centerX + rho * cos(i / steps * pi - pi),
+ centerY + rho * sin(i / steps * pi - pi), 'Z'],
+ stroke: '#ccc'
+ });
+ sprite.setAttributes({
+ hidden: false
+ }, true);
+ sprites.push(sprite);
+ }
+ } else {
+ sprites = this.sprites;
+ //draw circles
+ for (i = 0; i <= steps; i++) {
+ sprites[i].setAttributes({
+ path: ['M', centerX + (rho - margin) * cos(i / steps * pi - pi),
+ centerY + (rho - margin) * sin(i / steps * pi - pi),
+ 'L', centerX + rho * cos(i / steps * pi - pi),
+ centerY + rho * sin(i / steps * pi - pi), 'Z'],
+ stroke: '#ccc'
+ }, true);
+ }
+ }
+ }
+ this.sprites = sprites;
+ this.drawLabel();
+ if (this.title) {
+ this.drawTitle();
+ }
+ },
+
+ drawTitle: function() {
+ var me = this,
+ chart = me.chart,
+ surface = chart.surface,
+ bbox = chart.chartBBox,
+ labelSprite = me.titleSprite,
+ labelBBox;
+
+ if (!labelSprite) {
+ me.titleSprite = labelSprite = surface.add({
+ type: 'text',
+ zIndex: 2
+ });
+ }
+ labelSprite.setAttributes(Ext.apply({
+ text: me.title
+ }, me.label || {}), true);
+ labelBBox = labelSprite.getBBox();
+ labelSprite.setAttributes({
+ x: bbox.x + (bbox.width / 2) - (labelBBox.width / 2),
+ y: bbox.y + bbox.height - (labelBBox.height / 2) - 4
+ }, true);
+ },
+
+ /**
+ * Updates the {@link #title} of this axis.
+ * @param {String} title
+ */
+ setTitle: function(title) {
+ this.title = title;
+ this.drawTitle();
+ },
+
+ drawLabel: function() {
+ var chart = this.chart,
+ surface = chart.surface,
+ bbox = chart.chartBBox,
+ centerX = bbox.x + (bbox.width / 2),
+ centerY = bbox.y + bbox.height,
+ margin = this.margin || 10,
+ rho = Math.min(bbox.width, 2 * bbox.height) /2 + 2 * margin,
+ round = Math.round,
+ labelArray = [], label,
+ maxValue = this.maximum || 0,
+ steps = this.steps, i = 0,
+ adjY,
+ pi = Math.PI,
+ cos = Math.cos,
+ sin = Math.sin,
+ labelConf = this.label,
+ renderer = labelConf.renderer || function(v) { return v; };
+
+ if (!this.labelArray) {
+ //draw scale
+ for (i = 0; i <= steps; i++) {
+ // TODO Adjust for height of text / 2 instead
+ adjY = (i === 0 || i === steps) ? 7 : 0;
+ label = surface.add({
+ type: 'text',
+ text: renderer(round(i / steps * maxValue)),
+ x: centerX + rho * cos(i / steps * pi - pi),
+ y: centerY + rho * sin(i / steps * pi - pi) - adjY,
+ 'text-anchor': 'middle',
+ 'stroke-width': 0.2,
+ zIndex: 10,
+ stroke: '#333'
+ });
+ label.setAttributes({
+ hidden: false
+ }, true);
+ labelArray.push(label);
+ }
+ }
+ else {
+ labelArray = this.labelArray;
+ //draw values
+ for (i = 0; i <= steps; i++) {
+ // TODO Adjust for height of text / 2 instead
+ adjY = (i === 0 || i === steps) ? 7 : 0;
+ labelArray[i].setAttributes({
+ text: renderer(round(i / steps * maxValue)),
+ x: centerX + rho * cos(i / steps * pi - pi),
+ y: centerY + rho * sin(i / steps * pi - pi) - adjY
+ }, true);
+ }
+ }
+ this.labelArray = labelArray;
+ }
+});
+/**
+ * @class Ext.chart.axis.Numeric
+ * @extends Ext.chart.axis.Axis
+ *
+ * An axis to handle numeric values. This axis is used for quantitative data as
+ * opposed to the category axis. You can set mininum and maximum values to the
+ * axis so that the values are bound to that. If no values are set, then the
+ * scale will auto-adjust to the values.
+ *
+ * @example
+ * var store = Ext.create('Ext.data.JsonStore', {
+ * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+ * data: [
+ * {'name':'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13},
+ * {'name':'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3},
+ * {'name':'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7},
+ * {'name':'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23},
+ * {'name':'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33}
+ * ]
+ * });
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 300,
+ * store: store,
+ * axes: [{
+ * type: 'Numeric',
+ * grid: true,
+ * position: 'left',
+ * fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
+ * title: 'Sample Values',
+ * grid: {
+ * odd: {
+ * opacity: 1,
+ * fill: '#ddd',
+ * stroke: '#bbb',
+ * 'stroke-width': 1
+ * }
+ * },
+ * minimum: 0,
+ * adjustMinimumByMajorUnit: 0
+ * }, {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Sample Metrics',
+ * grid: true,
+ * label: {
+ * rotate: {
+ * degrees: 315
+ * }
+ * }
+ * }],
+ * series: [{
+ * type: 'area',
+ * highlight: false,
+ * axis: 'left',
+ * xField: 'name',
+ * yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
+ * style: {
+ * opacity: 0.93
+ * }
+ * }]
+ * });
+ *
+ * In this example we create an axis of Numeric type. We set a minimum value so that
+ * even if all series have values greater than zero, the grid starts at zero. We bind
+ * the axis onto the left part of the surface by setting `position` to `left`.
+ * We bind three different store fields to this axis by setting `fields` to an array.
+ * We set the title of the axis to _Number of Hits_ by using the `title` property.
+ * We use a `grid` configuration to set odd background rows to a certain style and even rows
+ * to be transparent/ignored.
+ */
+Ext.define('Ext.chart.axis.Numeric', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.axis.Axis',
+
+ alternateClassName: 'Ext.chart.NumericAxis',
+
+ /* End Definitions */
+
+ type: 'numeric',
+
+ alias: 'axis.numeric',
+
+ constructor: function(config) {
+ var me = this,
+ hasLabel = !!(config.label && config.label.renderer),
+ label;
+
+ me.callParent([config]);
+ label = me.label;
+ if (me.roundToDecimal === false) {
+ return;
+ }
+ if (!hasLabel) {
+ label.renderer = function(v) {
+ return me.roundToDecimal(v, me.decimals);
+ };
+ }
+ },
+
+ roundToDecimal: function(v, dec) {
+ var val = Math.pow(10, dec || 0);
+ return Math.floor(v * val) / val;
+ },
+
+ /**
+ * The minimum value drawn by the axis. If not set explicitly, the axis
+ * minimum will be calculated automatically.
+ *
+ * @property {Number} minimum
+ */
+ minimum: NaN,
+
+ /**
+ * The maximum value drawn by the axis. If not set explicitly, the axis
+ * maximum will be calculated automatically.
+ *
+ * @property {Number} maximum
+ */
+ maximum: NaN,
+
+ /**
+ * The number of decimals to round the value to.
+ *
+ * @property {Number} decimals
+ */
+ decimals: 2,
+
+ /**
+ * The scaling algorithm to use on this axis. May be "linear" or
+ * "logarithmic". Currently only linear scale is implemented.
+ *
+ * @property {String} scale
+ * @private
+ */
+ scale: "linear",
+
+ /**
+ * Indicates the position of the axis relative to the chart
+ *
+ * @property {String} position
+ */
+ position: 'left',
+
+ /**
+ * Indicates whether to extend maximum beyond data's maximum to the nearest
+ * majorUnit.
+ *
+ * @property {Boolean} adjustMaximumByMajorUnit
+ */
+ adjustMaximumByMajorUnit: false,
+
+ /**
+ * Indicates whether to extend the minimum beyond data's minimum to the
+ * nearest majorUnit.
+ *
+ * @property {Boolean} adjustMinimumByMajorUnit
+ */
+ adjustMinimumByMajorUnit: false,
+
+ // @private apply data.
+ applyData: function() {
+ this.callParent();
+ return this.calcEnds();
+ }
+});
+
+/**
+ * @class Ext.chart.axis.Radial
+ * @extends Ext.chart.axis.Abstract
+ * @ignore
+ */
+Ext.define('Ext.chart.axis.Radial', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.axis.Abstract',
+
+ /* End Definitions */
+
+ position: 'radial',
+
+ alias: 'axis.radial',
+
+ drawAxis: function(init) {
+ var chart = this.chart,
+ surface = chart.surface,
+ bbox = chart.chartBBox,
+ store = chart.store,
+ l = store.getCount(),
+ centerX = bbox.x + (bbox.width / 2),
+ centerY = bbox.y + (bbox.height / 2),
+ rho = Math.min(bbox.width, bbox.height) /2,
+ sprites = [], sprite,
+ steps = this.steps,
+ i, j, pi2 = Math.PI * 2,
+ cos = Math.cos, sin = Math.sin;
+
+ if (this.sprites && !chart.resizing) {
+ this.drawLabel();
+ return;
+ }
+
+ if (!this.sprites) {
+ //draw circles
+ for (i = 1; i <= steps; i++) {
+ sprite = surface.add({
+ type: 'circle',
+ x: centerX,
+ y: centerY,
+ radius: Math.max(rho * i / steps, 0),
+ stroke: '#ccc'
+ });
+ sprite.setAttributes({
+ hidden: false
+ }, true);
+ sprites.push(sprite);
+ }
+ //draw lines
+ store.each(function(rec, i) {
+ sprite = surface.add({
+ type: 'path',
+ path: ['M', centerX, centerY, 'L', centerX + rho * cos(i / l * pi2), centerY + rho * sin(i / l * pi2), 'Z'],
+ stroke: '#ccc'
+ });
+ sprite.setAttributes({
+ hidden: false
+ }, true);
+ sprites.push(sprite);
+ });
+ } else {
+ sprites = this.sprites;
+ //draw circles
+ for (i = 0; i < steps; i++) {
+ sprites[i].setAttributes({
+ x: centerX,
+ y: centerY,
+ radius: Math.max(rho * (i + 1) / steps, 0),
+ stroke: '#ccc'
+ }, true);
+ }
+ //draw lines
+ store.each(function(rec, j) {
+ sprites[i + j].setAttributes({
+ path: ['M', centerX, centerY, 'L', centerX + rho * cos(j / l * pi2), centerY + rho * sin(j / l * pi2), 'Z'],
+ stroke: '#ccc'
+ }, true);
+ });
+ }
+ this.sprites = sprites;
+
+ this.drawLabel();
+ },
+
+ drawLabel: function() {
+ var chart = this.chart,
+ surface = chart.surface,
+ bbox = chart.chartBBox,
+ store = chart.store,
+ centerX = bbox.x + (bbox.width / 2),
+ centerY = bbox.y + (bbox.height / 2),
+ rho = Math.min(bbox.width, bbox.height) /2,
+ max = Math.max, round = Math.round,
+ labelArray = [], label,
+ fields = [], nfields,
+ categories = [], xField,
+ aggregate = !this.maximum,
+ maxValue = this.maximum || 0,
+ steps = this.steps, i = 0, j, dx, dy,
+ pi2 = Math.PI * 2,
+ cos = Math.cos, sin = Math.sin,
+ display = this.label.display,
+ draw = display !== 'none',
+ margin = 10;
+
+ if (!draw) {
+ return;
+ }
+
+ //get all rendered fields
+ chart.series.each(function(series) {
+ fields.push(series.yField);
+ xField = series.xField;
+ });
+
+ //get maxValue to interpolate
+ store.each(function(record, i) {
+ if (aggregate) {
+ for (i = 0, nfields = fields.length; i < nfields; i++) {
+ maxValue = max(+record.get(fields[i]), maxValue);
+ }
+ }
+ categories.push(record.get(xField));
+ });
+ if (!this.labelArray) {
+ if (display != 'categories') {
+ //draw scale
+ for (i = 1; i <= steps; i++) {
+ label = surface.add({
+ type: 'text',
+ text: round(i / steps * maxValue),
+ x: centerX,
+ y: centerY - rho * i / steps,
+ 'text-anchor': 'middle',
+ 'stroke-width': 0.1,
+ stroke: '#333'
+ });
+ label.setAttributes({
+ hidden: false
+ }, true);
+ labelArray.push(label);
+ }
+ }
+ if (display != 'scale') {
+ //draw text
+ for (j = 0, steps = categories.length; j < steps; j++) {
+ dx = cos(j / steps * pi2) * (rho + margin);
+ dy = sin(j / steps * pi2) * (rho + margin);
+ label = surface.add({
+ type: 'text',
+ text: categories[j],
+ x: centerX + dx,
+ y: centerY + dy,
+ 'text-anchor': dx * dx <= 0.001? 'middle' : (dx < 0? 'end' : 'start')
+ });
+ label.setAttributes({
+ hidden: false
+ }, true);
+ labelArray.push(label);
+ }
+ }
+ }
+ else {
+ labelArray = this.labelArray;
+ if (display != 'categories') {
+ //draw values
+ for (i = 0; i < steps; i++) {
+ labelArray[i].setAttributes({
+ text: round((i + 1) / steps * maxValue),
+ x: centerX,
+ y: centerY - rho * (i + 1) / steps,
+ 'text-anchor': 'middle',
+ 'stroke-width': 0.1,
+ stroke: '#333'
+ }, true);
+ }
+ }
+ if (display != 'scale') {
+ //draw text
+ for (j = 0, steps = categories.length; j < steps; j++) {
+ dx = cos(j / steps * pi2) * (rho + margin);
+ dy = sin(j / steps * pi2) * (rho + margin);
+ if (labelArray[i + j]) {
+ labelArray[i + j].setAttributes({
+ type: 'text',
+ text: categories[j],
+ x: centerX + dx,
+ y: centerY + dy,
+ 'text-anchor': dx * dx <= 0.001? 'middle' : (dx < 0? 'end' : 'start')
+ }, true);
+ }
+ }
+ }
+ }
+ this.labelArray = labelArray;
+ }
+});
+/**
+ * @author Ed Spencer
+ *
+ * AbstractStore is a superclass of {@link Ext.data.Store} and {@link Ext.data.TreeStore}. It's never used directly,
+ * but offers a set of methods used by both of those subclasses.
+ *
+ * We've left it here in the docs for reference purposes, but unless you need to make a whole new type of Store, what
+ * you're probably looking for is {@link Ext.data.Store}. If you're still interested, here's a brief description of what
+ * AbstractStore is and is not.
+ *
+ * AbstractStore provides the basic configuration for anything that can be considered a Store. It expects to be
+ * given a {@link Ext.data.Model Model} that represents the type of data in the Store. It also expects to be given a
+ * {@link Ext.data.proxy.Proxy Proxy} that handles the loading of data into the Store.
+ *
+ * AbstractStore provides a few helpful methods such as {@link #load} and {@link #sync}, which load and save data
+ * respectively, passing the requests through the configured {@link #proxy}. Both built-in Store subclasses add extra
+ * behavior to each of these functions. Note also that each AbstractStore subclass has its own way of storing data -
+ * in {@link Ext.data.Store} the data is saved as a flat {@link Ext.util.MixedCollection MixedCollection}, whereas in
+ * {@link Ext.data.TreeStore TreeStore} we use a {@link Ext.data.Tree} to maintain the data's hierarchy.
+ *
+ * The store provides filtering and sorting support. This sorting/filtering can happen on the client side
+ * or can be completed on the server. This is controlled by the {@link Ext.data.Store#remoteSort remoteSort} and
+ * {@link Ext.data.Store#remoteFilter remoteFilter} config options. For more information see the {@link #sort} and
+ * {@link Ext.data.Store#filter filter} methods.
+ */
+Ext.define('Ext.data.AbstractStore', {
+ requires: ['Ext.util.MixedCollection', 'Ext.data.Operation', 'Ext.util.Filter'],
+
+ mixins: {
+ observable: 'Ext.util.Observable',
+ sortable: 'Ext.util.Sortable'
+ },
+
+ statics: {
+ create: function(store){
+ if (!store.isStore) {
+ if (!store.type) {
+ store.type = 'store';
+ }
+ store = Ext.createByAlias('store.' + store.type, store);
+ }
+ return store;
+ }
+ },
+
+ remoteSort : false,
+ remoteFilter: false,
+
+ /**
+ * @cfg {String/Ext.data.proxy.Proxy/Object} proxy
+ * The Proxy to use for this Store. This can be either a string, a config object or a Proxy instance -
+ * see {@link #setProxy} for details.
+ */
+
+ /**
+ * @cfg {Boolean/Object} autoLoad
+ * If data is not specified, and if autoLoad is true or an Object, this store's load method is automatically called
+ * after creation. If the value of autoLoad is an Object, this Object will be passed to the store's load method.
+ * Defaults to false.
+ */
+ autoLoad: false,
+
+ /**
+ * @cfg {Boolean} autoSync
+ * True to automatically sync the Store with its Proxy after every edit to one of its Records. Defaults to false.
+ */
+ autoSync: false,
+
+ /**
+ * @property {String} batchUpdateMode
+ * Sets the updating behavior based on batch synchronization. 'operation' (the default) will update the Store's
+ * internal representation of the data after each operation of the batch has completed, 'complete' will wait until
+ * the entire batch has been completed before updating the Store's data. 'complete' is a good choice for local
+ * storage proxies, 'operation' is better for remote proxies, where there is a comparatively high latency.
+ */
+ batchUpdateMode: 'operation',
+
+ /**
+ * @property {Boolean} filterOnLoad
+ * If true, any filters attached to this Store will be run after loading data, before the datachanged event is fired.
+ * Defaults to true, ignored if {@link Ext.data.Store#remoteFilter remoteFilter} is true
+ */
+ filterOnLoad: true,
+
+ /**
+ * @property {Boolean} sortOnLoad
+ * If true, any sorters attached to this Store will be run after loading data, before the datachanged event is fired.
+ * Defaults to true, igored if {@link Ext.data.Store#remoteSort remoteSort} is true
+ */
+ sortOnLoad: true,
+
+ /**
+ * @property {Boolean} implicitModel
+ * True if a model was created implicitly for this Store. This happens if a fields array is passed to the Store's
+ * constructor instead of a model constructor or name.
+ * @private
+ */
+ implicitModel: false,
+
+ /**
+ * @property {String} defaultProxyType
+ * The string type of the Proxy to create if none is specified. This defaults to creating a
+ * {@link Ext.data.proxy.Memory memory proxy}.
+ */
+ defaultProxyType: 'memory',
+
+ /**
+ * @property {Boolean} isDestroyed
+ * True if the Store has already been destroyed. If this is true, the reference to Store should be deleted
+ * as it will not function correctly any more.
+ */
+ isDestroyed: false,
+
+ isStore: true,
+
+ /**
+ * @cfg {String} storeId
+ * Unique identifier for this store. If present, this Store will be registered with the {@link Ext.data.StoreManager},
+ * making it easy to reuse elsewhere. Defaults to undefined.
+ */
+
+ /**
+ * @cfg {Object[]} fields
+ * This may be used in place of specifying a {@link #model} configuration. The fields should be a
+ * set of {@link Ext.data.Field} configuration objects. The store will automatically create a {@link Ext.data.Model}
+ * with these fields. In general this configuration option should be avoided, it exists for the purposes of
+ * backwards compatibility. For anything more complicated, such as specifying a particular id property or
+ * assocations, a {@link Ext.data.Model} should be defined and specified for the {@link #model}
+ * config.
+ */
+
+ /**
+ * @cfg {String} model
+ * Name of the {@link Ext.data.Model Model} associated with this store.
+ * The string is used as an argument for {@link Ext.ModelManager#getModel}.
+ */
+
+ sortRoot: 'data',
+
+ //documented above
+ constructor: function(config) {
+ var me = this,
+ filters;
+
+ me.addEvents(
+ /**
+ * @event add
+ * Fired when a Model instance has been added to this Store
+ * @param {Ext.data.Store} store The store
+ * @param {Ext.data.Model[]} records The Model instances that were added
+ * @param {Number} index The index at which the instances were inserted
+ */
+ 'add',
+
+ /**
+ * @event remove
+ * Fired when a Model instance has been removed from this Store
+ * @param {Ext.data.Store} store The Store object
+ * @param {Ext.data.Model} record The record that was removed
+ * @param {Number} index The index of the record that was removed
+ */
+ 'remove',
+
+ /**
+ * @event update
+ * Fires when a Model instance has been updated
+ * @param {Ext.data.Store} this
+ * @param {Ext.data.Model} record The Model instance that was updated
+ * @param {String} operation The update operation being performed. Value may be one of:
+ *
+ * Ext.data.Model.EDIT
+ * Ext.data.Model.REJECT
+ * Ext.data.Model.COMMIT
+ */
+ 'update',
+
+ /**
+ * @event datachanged
+ * Fires whenever the records in the Store have changed in some way - this could include adding or removing
+ * records, or updating the data in existing records
+ * @param {Ext.data.Store} this The data store
+ */
+ 'datachanged',
+
+ /**
+ * @event beforeload
+ * Fires before a request is made for a new data object. If the beforeload handler returns false the load
+ * action will be canceled.
+ * @param {Ext.data.Store} store This Store
+ * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to
+ * load the Store
+ */
+ 'beforeload',
+
+ /**
+ * @event load
+ * Fires whenever the store reads data from a remote data source.
+ * @param {Ext.data.Store} this
+ * @param {Ext.data.Model[]} records An array of records
+ * @param {Boolean} successful True if the operation was successful.
+ */
+ 'load',
+
+ /**
+ * @event write
+ * Fires whenever a successful write has been made via the configured {@link #proxy Proxy}
+ * @param {Ext.data.Store} store This Store
+ * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object that was used in
+ * the write
+ */
+ 'write',
+
+ /**
+ * @event beforesync
+ * Fired before a call to {@link #sync} is executed. Return false from any listener to cancel the synv
+ * @param {Object} options Hash of all records to be synchronized, broken down into create, update and destroy
+ */
+ 'beforesync',
+ /**
+ * @event clear
+ * Fired after the {@link #removeAll} method is called.
+ * @param {Ext.data.Store} this
+ */
+ 'clear'
+ );
+
+ Ext.apply(me, config);
+ // don't use *config* anymore from here on... use *me* instead...
+
+ /**
+ * Temporary cache in which removed model instances are kept until successfully synchronised with a Proxy,
+ * at which point this is cleared.
+ * @private
+ * @property {Ext.data.Model[]} removed
+ */
+ me.removed = [];
+
+ me.mixins.observable.constructor.apply(me, arguments);
+ me.model = Ext.ModelManager.getModel(me.model);
+
+ /**
+ * @property {Object} modelDefaults
+ * @private
+ * A set of default values to be applied to every model instance added via {@link #insert} or created via {@link #create}.
+ * This is used internally by associations to set foreign keys and other fields. See the Association classes source code
+ * for examples. This should not need to be used by application developers.
+ */
+ Ext.applyIf(me, {
+ modelDefaults: {}
+ });
+
+ //Supports the 3.x style of simply passing an array of fields to the store, implicitly creating a model
+ if (!me.model && me.fields) {
+ me.model = Ext.define('Ext.data.Store.ImplicitModel-' + (me.storeId || Ext.id()), {
+ extend: 'Ext.data.Model',
+ fields: me.fields,
+ proxy: me.proxy || me.defaultProxyType
+ });
+
+ delete me.fields;
+
+ me.implicitModel = true;
+ }
+
+
+ //ensures that the Proxy is instantiated correctly
+ me.setProxy(me.proxy || me.model.getProxy());
+
+ if (me.id && !me.storeId) {
+ me.storeId = me.id;
+ delete me.id;
+ }
+
+ if (me.storeId) {
+ Ext.data.StoreManager.register(me);
+ }
+
+ me.mixins.sortable.initSortable.call(me);
+
+ /**
+ * @property {Ext.util.MixedCollection} filters
+ * The collection of {@link Ext.util.Filter Filters} currently applied to this Store
+ */
+ filters = me.decodeFilters(me.filters);
+ me.filters = Ext.create('Ext.util.MixedCollection');
+ me.filters.addAll(filters);
+ },
+
+ /**
+ * Sets the Store's Proxy by string, config object or Proxy instance
+ * @param {String/Object/Ext.data.proxy.Proxy} proxy The new Proxy, which can be either a type string, a configuration object
+ * or an Ext.data.proxy.Proxy instance
+ * @return {Ext.data.proxy.Proxy} The attached Proxy object
+ */
+ setProxy: function(proxy) {
+ var me = this;
+
+ if (proxy instanceof Ext.data.proxy.Proxy) {
+ proxy.setModel(me.model);
+ } else {
+ if (Ext.isString(proxy)) {
+ proxy = {
+ type: proxy
+ };
+ }
+ Ext.applyIf(proxy, {
+ model: me.model
+ });
+
+ proxy = Ext.createByAlias('proxy.' + proxy.type, proxy);
+ }
+
+ me.proxy = proxy;
+
+ return me.proxy;
+ },
+
+ /**
+ * Returns the proxy currently attached to this proxy instance
+ * @return {Ext.data.proxy.Proxy} The Proxy instance
+ */
+ getProxy: function() {
+ return this.proxy;
+ },
+
+ //saves any phantom records
+ create: function(data, options) {
+ var me = this,
+ instance = Ext.ModelManager.create(Ext.applyIf(data, me.modelDefaults), me.model.modelName),
+ operation;
+
+ options = options || {};
+
+ Ext.applyIf(options, {
+ action : 'create',
+ records: [instance]
+ });
+
+ operation = Ext.create('Ext.data.Operation', options);
+
+ me.proxy.create(operation, me.onProxyWrite, me);
+
+ return instance;
+ },
+
+ read: function() {
+ return this.load.apply(this, arguments);
+ },
+
+ onProxyRead: Ext.emptyFn,
+
+ update: function(options) {
+ var me = this,
+ operation;
+ options = options || {};
+
+ Ext.applyIf(options, {
+ action : 'update',
+ records: me.getUpdatedRecords()
+ });
+
+ operation = Ext.create('Ext.data.Operation', options);
+
+ return me.proxy.update(operation, me.onProxyWrite, me);
+ },
+
+ /**
+ * @private
+ * Callback for any write Operation over the Proxy. Updates the Store's MixedCollection to reflect
+ * the updates provided by the Proxy
+ */
+ onProxyWrite: function(operation) {
+ var me = this,
+ success = operation.wasSuccessful(),
+ records = operation.getRecords();
+
+ switch (operation.action) {
+ case 'create':
+ me.onCreateRecords(records, operation, success);
+ break;
+ case 'update':
+ me.onUpdateRecords(records, operation, success);
+ break;
+ case 'destroy':
+ me.onDestroyRecords(records, operation, success);
+ break;
+ }
+
+ if (success) {
+ me.fireEvent('write', me, operation);
+ me.fireEvent('datachanged', me);
+ }
+ //this is a callback that would have been passed to the 'create', 'update' or 'destroy' function and is optional
+ Ext.callback(operation.callback, operation.scope || me, [records, operation, success]);
+ },
+
+
+ //tells the attached proxy to destroy the given records
+ destroy: function(options) {
+ var me = this,
+ operation;
+
+ options = options || {};
+
+ Ext.applyIf(options, {
+ action : 'destroy',
+ records: me.getRemovedRecords()
+ });
+
+ operation = Ext.create('Ext.data.Operation', options);
+
+ return me.proxy.destroy(operation, me.onProxyWrite, me);
+ },
+
+ /**
+ * @private
+ * Attached as the 'operationcomplete' event listener to a proxy's Batch object. By default just calls through
+ * to onProxyWrite.
+ */
+ onBatchOperationComplete: function(batch, operation) {
+ return this.onProxyWrite(operation);
+ },
+
+ /**
+ * @private
+ * Attached as the 'complete' event listener to a proxy's Batch object. Iterates over the batch operations
+ * and updates the Store's internal data MixedCollection.
+ */
+ onBatchComplete: function(batch, operation) {
+ var me = this,
+ operations = batch.operations,
+ length = operations.length,
+ i;
+
+ me.suspendEvents();
+
+ for (i = 0; i < length; i++) {
+ me.onProxyWrite(operations[i]);
+ }
+
+ me.resumeEvents();
+
+ me.fireEvent('datachanged', me);
+ },
+
+ onBatchException: function(batch, operation) {
+ // //decide what to do... could continue with the next operation
+ // batch.start();
+ //
+ // //or retry the last operation
+ // batch.retry();
+ },
+
+ /**
+ * @private
+ * Filter function for new records.
+ */
+ filterNew: function(item) {
+ // only want phantom records that are valid
+ return item.phantom === true && item.isValid();
+ },
+
+ /**
+ * Returns all Model instances that are either currently a phantom (e.g. have no id), or have an ID but have not
+ * yet been saved on this Store (this happens when adding a non-phantom record from another Store into this one)
+ * @return {Ext.data.Model[]} The Model instances
+ */
+ getNewRecords: function() {
+ return [];
+ },
+
+ /**
+ * Returns all Model instances that have been updated in the Store but not yet synchronized with the Proxy
+ * @return {Ext.data.Model[]} The updated Model instances
+ */
+ getUpdatedRecords: function() {
+ return [];
+ },
+
+ /**
+ * @private
+ * Filter function for updated records.
+ */
+ filterUpdated: function(item) {
+ // only want dirty records, not phantoms that are valid
+ return item.dirty === true && item.phantom !== true && item.isValid();
+ },
+
+ /**
+ * Returns any records that have been removed from the store but not yet destroyed on the proxy.
+ * @return {Ext.data.Model[]} The removed Model instances
+ */
+ getRemovedRecords: function() {
+ return this.removed;
+ },
+
+ filter: function(filters, value) {
+
+ },
+
+ /**
+ * @private
+ * Normalizes an array of filter objects, ensuring that they are all Ext.util.Filter instances
+ * @param {Object[]} filters The filters array
+ * @return {Ext.util.Filter[]} Array of Ext.util.Filter objects
+ */
+ decodeFilters: function(filters) {
+ if (!Ext.isArray(filters)) {
+ if (filters === undefined) {
+ filters = [];
+ } else {
+ filters = [filters];
+ }
+ }
+
+ var length = filters.length,
+ Filter = Ext.util.Filter,
+ config, i;
+
+ for (i = 0; i < length; i++) {
+ config = filters[i];
+
+ if (!(config instanceof Filter)) {
+ Ext.apply(config, {
+ root: 'data'
+ });
+
+ //support for 3.x style filters where a function can be defined as 'fn'
+ if (config.fn) {
+ config.filterFn = config.fn;
+ }
+
+ //support a function to be passed as a filter definition
+ if (typeof config == 'function') {
+ config = {
+ filterFn: config
+ };
+ }
+
+ filters[i] = new Filter(config);
+ }
+ }
+
+ return filters;
+ },
+
+ clearFilter: function(supressEvent) {
+
+ },
+
+ isFiltered: function() {
+
+ },
+
+ filterBy: function(fn, scope) {
+
+ },
+
+ /**
+ * Synchronizes the Store with its Proxy. This asks the Proxy to batch together any new, updated
+ * and deleted records in the store, updating the Store's internal representation of the records
+ * as each operation completes.
+ */
+ sync: function() {
+ var me = this,
+ options = {},
+ toCreate = me.getNewRecords(),
+ toUpdate = me.getUpdatedRecords(),
+ toDestroy = me.getRemovedRecords(),
+ needsSync = false;
+
+ if (toCreate.length > 0) {
+ options.create = toCreate;
+ needsSync = true;
+ }
+
+ if (toUpdate.length > 0) {
+ options.update = toUpdate;
+ needsSync = true;
+ }
+
+ if (toDestroy.length > 0) {
+ options.destroy = toDestroy;
+ needsSync = true;
+ }
+
+ if (needsSync && me.fireEvent('beforesync', options) !== false) {
+ me.proxy.batch(options, me.getBatchListeners());
+ }
+ },
+
+
+ /**
+ * @private
+ * Returns an object which is passed in as the listeners argument to proxy.batch inside this.sync.
+ * This is broken out into a separate function to allow for customisation of the listeners
+ * @return {Object} The listeners object
+ */
+ getBatchListeners: function() {
+ var me = this,
+ listeners = {
+ scope: me,
+ exception: me.onBatchException
+ };
+
+ if (me.batchUpdateMode == 'operation') {
+ listeners.operationcomplete = me.onBatchOperationComplete;
+ } else {
+ listeners.complete = me.onBatchComplete;
+ }
+
+ return listeners;
+ },
+
+ //deprecated, will be removed in 5.0
+ save: function() {
+ return this.sync.apply(this, arguments);
+ },
+
+ /**
+ * Loads the Store using its configured {@link #proxy}.
+ * @param {Object} options (optional) config object. This is passed into the {@link Ext.data.Operation Operation}
+ * object that is created and then sent to the proxy's {@link Ext.data.proxy.Proxy#read} function
+ */
+ load: function(options) {
+ var me = this,
+ operation;
+
+ options = options || {};
+
+ Ext.applyIf(options, {
+ action : 'read',
+ filters: me.filters.items,
+ sorters: me.getSorters()
+ });
+
+ operation = Ext.create('Ext.data.Operation', options);
+
+ if (me.fireEvent('beforeload', me, operation) !== false) {
+ me.loading = true;
+ me.proxy.read(operation, me.onProxyLoad, me);
+ }
+
+ return me;
+ },
+
+ /**
+ * @private
+ * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
+ * @param {Ext.data.Model} record The model instance that was edited
+ */
+ afterEdit : function(record) {
+ var me = this;
+
+ if (me.autoSync) {
+ me.sync();
+ }
+
+ me.fireEvent('update', me, record, Ext.data.Model.EDIT);
+ },
+
+ /**
+ * @private
+ * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to..
+ * @param {Ext.data.Model} record The model instance that was edited
+ */
+ afterReject : function(record) {
+ this.fireEvent('update', this, record, Ext.data.Model.REJECT);
+ },
+
+ /**
+ * @private
+ * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
+ * @param {Ext.data.Model} record The model instance that was edited
+ */
+ afterCommit : function(record) {
+ this.fireEvent('update', this, record, Ext.data.Model.COMMIT);
+ },
+
+ clearData: Ext.emptyFn,
+
+ destroyStore: function() {
+ var me = this;
+
+ if (!me.isDestroyed) {
+ if (me.storeId) {
+ Ext.data.StoreManager.unregister(me);
+ }
+ me.clearData();
+ me.data = null;
+ me.tree = null;
+ // Ext.destroy(this.proxy);
+ me.reader = me.writer = null;
+ me.clearListeners();
+ me.isDestroyed = true;
+
+ if (me.implicitModel) {
+ Ext.destroy(me.model);
+ }
+ }
+ },
+
+ doSort: function(sorterFn) {
+ var me = this;
+ if (me.remoteSort) {
+ //the load function will pick up the new sorters and request the sorted data from the proxy
+ me.load();
+ } else {
+ me.data.sortBy(sorterFn);
+ me.fireEvent('datachanged', me);
+ }
+ },
+
+ getCount: Ext.emptyFn,
+
+ getById: Ext.emptyFn,
+
+ /**
+ * Removes all records from the store. This method does a "fast remove",
+ * individual remove events are not called. The {@link #clear} event is
+ * fired upon completion.
+ * @method
+ */
+ removeAll: Ext.emptyFn,
+ // individual substores should implement a "fast" remove
+ // and fire a clear event afterwards
+
+ /**
+ * Returns true if the Store is currently performing a load operation
+ * @return {Boolean} True if the Store is currently loading
+ */
+ isLoading: function() {
+ return !!this.loading;
+ }
+});
+
+/**
+ * @class Ext.util.Grouper
+ * @extends Ext.util.Sorter
+
+Represents a single grouper that can be applied to a Store. The grouper works
+in the same fashion as the {@link Ext.util.Sorter}.
+
+ * @markdown
+ */
+
+Ext.define('Ext.util.Grouper', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.util.Sorter',
+
+ /* End Definitions */
+
+ /**
+ * Returns the value for grouping to be used.
+ * @param {Ext.data.Model} instance The Model instance
+ * @return {String} The group string for this model
+ */
+ getGroupString: function(instance) {
+ return instance.get(this.property);
+ }
+});
+/**
+ * @author Ed Spencer
+ * @class Ext.data.Store
+ * @extends Ext.data.AbstractStore
+ *
+ * <p>The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects. Stores load
+ * data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions for {@link #sort sorting},
+ * {@link #filter filtering} and querying the {@link Ext.data.Model model} instances contained within it.</p>
+ *
+ * <p>Creating a Store is easy - we just tell it the Model and the Proxy to use to load and save its data:</p>
+ *
+<pre><code>
+// Set up a {@link Ext.data.Model model} to use in our Store
+Ext.define('User', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {name: 'firstName', type: 'string'},
+ {name: 'lastName', type: 'string'},
+ {name: 'age', type: 'int'},
+ {name: 'eyeColor', type: 'string'}
+ ]
+});
+
+var myStore = Ext.create('Ext.data.Store', {
+ model: 'User',
+ proxy: {
+ type: 'ajax',
+ url : '/users.json',
+ reader: {
+ type: 'json',
+ root: 'users'
+ }
+ },
+ autoLoad: true
+});
+</code></pre>
+
+ * <p>In the example above we configured an AJAX proxy to load data from the url '/users.json'. We told our Proxy
+ * to use a {@link Ext.data.reader.Json JsonReader} to parse the response from the server into Model object -
+ * {@link Ext.data.reader.Json see the docs on JsonReader} for details.</p>
+ *
+ * <p><u>Inline data</u></p>
+ *
+ * <p>Stores can also load data inline. Internally, Store converts each of the objects we pass in as {@link #data}
+ * into Model instances:</p>
+ *
+<pre><code>
+Ext.create('Ext.data.Store', {
+ model: 'User',
+ data : [
+ {firstName: 'Ed', lastName: 'Spencer'},
+ {firstName: 'Tommy', lastName: 'Maintz'},
+ {firstName: 'Aaron', lastName: 'Conran'},
+ {firstName: 'Jamie', lastName: 'Avins'}
+ ]
+});
+</code></pre>
+ *
+ * <p>Loading inline data using the method above is great if the data is in the correct format already (e.g. it doesn't need
+ * to be processed by a {@link Ext.data.reader.Reader reader}). If your inline data requires processing to decode the data structure,
+ * use a {@link Ext.data.proxy.Memory MemoryProxy} instead (see the {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).</p>
+ *
+ * <p>Additional data can also be loaded locally using {@link #add}.</p>
+ *
+ * <p><u>Loading Nested Data</u></p>
+ *
+ * <p>Applications often need to load sets of associated data - for example a CRM system might load a User and her Orders.
+ * Instead of issuing an AJAX request for the User and a series of additional AJAX requests for each Order, we can load a nested dataset
+ * and allow the Reader to automatically populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader} intro
+ * docs for a full explanation:</p>
+ *
+<pre><code>
+var store = Ext.create('Ext.data.Store', {
+ autoLoad: true,
+ model: "User",
+ proxy: {
+ type: 'ajax',
+ url : 'users.json',
+ reader: {
+ type: 'json',
+ root: 'users'
+ }
+ }
+});
+</code></pre>
+ *
+ * <p>Which would consume a response like this:</p>
+ *
+<pre><code>
+{
+ "users": [
+ {
+ "id": 1,
+ "name": "Ed",
+ "orders": [
+ {
+ "id": 10,
+ "total": 10.76,
+ "status": "invoiced"
+ },
+ {
+ "id": 11,
+ "total": 13.45,
+ "status": "shipped"
+ }
+ ]
+ }
+ ]
+}
+</code></pre>
+ *
+ * <p>See the {@link Ext.data.reader.Reader} intro docs for a full explanation.</p>
+ *
+ * <p><u>Filtering and Sorting</u></p>
+ *
+ * <p>Stores can be sorted and filtered - in both cases either remotely or locally. The {@link #sorters} and {@link #filters} are
+ * held inside {@link Ext.util.MixedCollection MixedCollection} instances to make them easy to manage. Usually it is sufficient to
+ * either just specify sorters and filters in the Store configuration or call {@link #sort} or {@link #filter}:
+ *
+<pre><code>
+var store = Ext.create('Ext.data.Store', {
+ model: 'User',
+ sorters: [
+ {
+ property : 'age',
+ direction: 'DESC'
+ },
+ {
+ property : 'firstName',
+ direction: 'ASC'
+ }
+ ],
+
+ filters: [
+ {
+ property: 'firstName',
+ value : /Ed/
+ }
+ ]
+});
+</code></pre>
+ *
+ * <p>The new Store will keep the configured sorters and filters in the MixedCollection instances mentioned above. By default, sorting
+ * and filtering are both performed locally by the Store - see {@link #remoteSort} and {@link #remoteFilter} to allow the server to
+ * perform these operations instead.</p>
+ *
+ * <p>Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter} adds another filter to the Store
+ * and automatically filters the dataset (calling {@link #filter} with no arguments simply re-applies all existing filters). Note that by
+ * default {@link #sortOnFilter} is set to true, which means that your sorters are automatically reapplied if using local sorting.</p>
+ *
+<pre><code>
+store.filter('eyeColor', 'Brown');
+</code></pre>
+ *
+ * <p>Change the sorting at any time by calling {@link #sort}:</p>
+ *
+<pre><code>
+store.sort('height', 'ASC');
+</code></pre>
+ *
+ * <p>Note that all existing sorters will be removed in favor of the new sorter data (if {@link #sort} is called with no arguments,
+ * the existing sorters are just reapplied instead of being removed). To keep existing sorters and add new ones, just add them
+ * to the MixedCollection:</p>
+ *
+<pre><code>
+store.sorters.add(new Ext.util.Sorter({
+ property : 'shoeSize',
+ direction: 'ASC'
+}));
+
+store.sort();
+</code></pre>
+ *
+ * <p><u>Registering with StoreManager</u></p>
+ *
+ * <p>Any Store that is instantiated with a {@link #storeId} will automatically be registed with the {@link Ext.data.StoreManager StoreManager}.
+ * This makes it easy to reuse the same store in multiple views:</p>
+ *
+ <pre><code>
+//this store can be used several times
+Ext.create('Ext.data.Store', {
+ model: 'User',
+ storeId: 'usersStore'
+});
+
+new Ext.List({
+ store: 'usersStore',
+
+ //other config goes here
+});
+
+new Ext.view.View({
+ store: 'usersStore',
+
+ //other config goes here
+});
+</code></pre>
+ *
+ * <p><u>Further Reading</u></p>
+ *
+ * <p>Stores are backed up by an ecosystem of classes that enables their operation. To gain a full understanding of these
+ * pieces and how they fit together, see:</p>
+ *
+ * <ul style="list-style-type: disc; padding-left: 25px">
+ * <li>{@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used</li>
+ * <li>{@link Ext.data.Model Model} - the core class in the data package</li>
+ * <li>{@link Ext.data.reader.Reader Reader} - used by any subclass of {@link Ext.data.proxy.Server ServerProxy} to read a response</li>
+ * </ul>
+ *
+ */
+Ext.define('Ext.data.Store', {
+ extend: 'Ext.data.AbstractStore',
+
+ alias: 'store.store',
+
+ requires: ['Ext.data.StoreManager', 'Ext.ModelManager', 'Ext.data.Model', 'Ext.util.Grouper'],
+ uses: ['Ext.data.proxy.Memory'],
+
+ /**
+ * @cfg {Boolean} remoteSort
+ * True to defer any sorting operation to the server. If false, sorting is done locally on the client. Defaults to <tt>false</tt>.
+ */
+ remoteSort: false,
+
+ /**
+ * @cfg {Boolean} remoteFilter
+ * True to defer any filtering operation to the server. If false, filtering is done locally on the client. Defaults to <tt>false</tt>.
+ */
+ remoteFilter: false,
+
+ /**
+ * @cfg {Boolean} remoteGroup
+ * True if the grouping should apply on the server side, false if it is local only. If the
+ * grouping is local, it can be applied immediately to the data. If it is remote, then it will simply act as a
+ * helper, automatically sending the grouping information to the server.
+ */
+ remoteGroup : false,
+
+ /**
+ * @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config
+ * object or a Proxy instance - see {@link #setProxy} for details.
+ */
+
+ /**
+ * @cfg {Object[]/Ext.data.Model[]} data Optional array of Model instances or data objects to load locally. See "Inline data" above for details.
+ */
+
+ /**
+ * @property {String} groupField
+ * The field by which to group data in the store. Internally, grouping is very similar to sorting - the
+ * groupField and {@link #groupDir} are injected as the first sorter (see {@link #sort}). Stores support a single
+ * level of grouping, and groups can be fetched via the {@link #getGroups} method.
+ */
+ groupField: undefined,
+
+ /**
+ * The direction in which sorting should be applied when grouping. Defaults to "ASC" - the other supported value is "DESC"
+ * @property groupDir
+ * @type String
+ */
+ groupDir: "ASC",
+
+ /**
+ * @cfg {Number} pageSize
+ * The number of records considered to form a 'page'. This is used to power the built-in
+ * paging using the nextPage and previousPage functions. Defaults to 25.
+ */
+ pageSize: 25,
+
+ /**
+ * The page that the Store has most recently loaded (see {@link #loadPage})
+ * @property currentPage
+ * @type Number
+ */
+ currentPage: 1,
+
+ /**
+ * @cfg {Boolean} clearOnPageLoad True to empty the store when loading another page via {@link #loadPage},
+ * {@link #nextPage} or {@link #previousPage}. Setting to false keeps existing records, allowing
+ * large data sets to be loaded one page at a time but rendered all together.
+ */
+ clearOnPageLoad: true,
+
+ /**
+ * @property {Boolean} loading
+ * True if the Store is currently loading via its Proxy
+ * @private
+ */
+ loading: false,
+
+ /**
+ * @cfg {Boolean} sortOnFilter For local filtering only, causes {@link #sort} to be called whenever {@link #filter} is called,
+ * causing the sorters to be reapplied after filtering. Defaults to true
+ */
+ sortOnFilter: true,
+
+ /**
+ * @cfg {Boolean} buffered
+ * Allow the store to buffer and pre-fetch pages of records. This is to be used in conjunction with a view will
+ * tell the store to pre-fetch records ahead of a time.
+ */
+ buffered: false,
+
+ /**
+ * @cfg {Number} purgePageCount
+ * The number of pages to keep in the cache before purging additional records. A value of 0 indicates to never purge the prefetched data.
+ * This option is only relevant when the {@link #buffered} option is set to true.
+ */
+ purgePageCount: 5,
+
+ isStore: true,
+
+ onClassExtended: function(cls, data) {
+ var model = data.model;
+
+ if (typeof model == 'string') {
+ var onBeforeClassCreated = data.onBeforeClassCreated;
+
+ data.onBeforeClassCreated = function(cls, data) {
+ var me = this;
+
+ Ext.require(model, function() {
+ onBeforeClassCreated.call(me, cls, data);
+ });
+ };
+ }
+ },
+
+ /**
+ * Creates the store.
+ * @param {Object} config (optional) Config object
+ */
+ constructor: function(config) {
+ // Clone the config so we don't modify the original config object
+ config = Ext.Object.merge({}, config);
+
+ var me = this,
+ groupers = config.groupers || me.groupers,
+ groupField = config.groupField || me.groupField,
+ proxy,
+ data;
+
+ if (config.buffered || me.buffered) {
+ me.prefetchData = Ext.create('Ext.util.MixedCollection', false, function(record) {
+ return record.index;
+ });
+ me.pendingRequests = [];
+ me.pagesRequested = [];
+
+ me.sortOnLoad = false;
+ me.filterOnLoad = false;
+ }
+
+ me.addEvents(
+ /**
+ * @event beforeprefetch
+ * Fires before a prefetch occurs. Return false to cancel.
+ * @param {Ext.data.Store} this
+ * @param {Ext.data.Operation} operation The associated operation
+ */
+ 'beforeprefetch',
+ /**
+ * @event groupchange
+ * Fired whenever the grouping in the grid changes
+ * @param {Ext.data.Store} store The store
+ * @param {Ext.util.Grouper[]} groupers The array of grouper objects
+ */
+ 'groupchange',
+ /**
+ * @event load
+ * Fires whenever records have been prefetched
+ * @param {Ext.data.Store} this
+ * @param {Ext.util.Grouper[]} records An array of records
+ * @param {Boolean} successful True if the operation was successful.
+ * @param {Ext.data.Operation} operation The associated operation
+ */
+ 'prefetch'
+ );
+ data = config.data || me.data;
+
+ /**
+ * The MixedCollection that holds this store's local cache of records
+ * @property data
+ * @type Ext.util.MixedCollection
+ */
+ me.data = Ext.create('Ext.util.MixedCollection', false, function(record) {
+ return record.internalId;
+ });
+
+ if (data) {
+ me.inlineData = data;
+ delete config.data;
+ }
+
+ if (!groupers && groupField) {
+ groupers = [{
+ property : groupField,
+ direction: config.groupDir || me.groupDir
+ }];
+ }
+ delete config.groupers;
+
+ /**
+ * The collection of {@link Ext.util.Grouper Groupers} currently applied to this Store
+ * @property groupers
+ * @type Ext.util.MixedCollection
+ */
+ me.groupers = Ext.create('Ext.util.MixedCollection');
+ me.groupers.addAll(me.decodeGroupers(groupers));
+
+ this.callParent([config]);
+ // don't use *config* anymore from here on... use *me* instead...
+
+ if (me.groupers.items.length) {
+ me.sort(me.groupers.items, 'prepend', false);
+ }
+
+ proxy = me.proxy;
+ data = me.inlineData;
+
+ if (data) {
+ if (proxy instanceof Ext.data.proxy.Memory) {
+ proxy.data = data;
+ me.read();
+ } else {
+ me.add.apply(me, data);
+ }
+
+ me.sort();
+ delete me.inlineData;
+ } else if (me.autoLoad) {
+ Ext.defer(me.load, 10, me, [typeof me.autoLoad === 'object' ? me.autoLoad: undefined]);
+ // Remove the defer call, we may need reinstate this at some point, but currently it's not obvious why it's here.
+ // this.load(typeof this.autoLoad == 'object' ? this.autoLoad : undefined);
+ }
+ },
+
+ onBeforeSort: function() {
+ var groupers = this.groupers;
+ if (groupers.getCount() > 0) {
+ this.sort(groupers.items, 'prepend', false);
+ }
+ },
+
+ /**
+ * @private
+ * Normalizes an array of grouper objects, ensuring that they are all Ext.util.Grouper instances
+ * @param {Object[]} groupers The groupers array
+ * @return {Ext.util.Grouper[]} Array of Ext.util.Grouper objects
+ */
+ decodeGroupers: function(groupers) {
+ if (!Ext.isArray(groupers)) {
+ if (groupers === undefined) {
+ groupers = [];
+ } else {
+ groupers = [groupers];
+ }
+ }
+
+ var length = groupers.length,
+ Grouper = Ext.util.Grouper,
+ config, i;
+
+ for (i = 0; i < length; i++) {
+ config = groupers[i];
+
+ if (!(config instanceof Grouper)) {
+ if (Ext.isString(config)) {
+ config = {
+ property: config
+ };
+ }
+
+ Ext.applyIf(config, {
+ root : 'data',
+ direction: "ASC"
+ });
+
+ //support for 3.x style sorters where a function can be defined as 'fn'
+ if (config.fn) {
+ config.sorterFn = config.fn;
+ }
+
+ //support a function to be passed as a sorter definition
+ if (typeof config == 'function') {
+ config = {
+ sorterFn: config
+ };
+ }
+
+ groupers[i] = new Grouper(config);
+ }
+ }
+
+ return groupers;
+ },
+
+ /**
+ * Group data in the store
+ * @param {String/Object[]} groupers Either a string name of one of the fields in this Store's configured {@link Ext.data.Model Model},
+ * or an Array of grouper configurations.
+ * @param {String} direction The overall direction to group the data by. Defaults to "ASC".
+ */
+ group: function(groupers, direction) {
+ var me = this,
+ hasNew = false,
+ grouper,
+ newGroupers;
+
+ if (Ext.isArray(groupers)) {
+ newGroupers = groupers;
+ } else if (Ext.isObject(groupers)) {
+ newGroupers = [groupers];
+ } else if (Ext.isString(groupers)) {
+ grouper = me.groupers.get(groupers);
+
+ if (!grouper) {
+ grouper = {
+ property : groupers,
+ direction: direction
+ };
+ newGroupers = [grouper];
+ } else if (direction === undefined) {
+ grouper.toggle();
+ } else {
+ grouper.setDirection(direction);
+ }
+ }
+
+ if (newGroupers && newGroupers.length) {
+ hasNew = true;
+ newGroupers = me.decodeGroupers(newGroupers);
+ me.groupers.clear();
+ me.groupers.addAll(newGroupers);
+ }
+
+ if (me.remoteGroup) {
+ me.load({
+ scope: me,
+ callback: me.fireGroupChange
+ });
+ } else {
+ // need to explicitly force a sort if we have groupers
+ me.sort(null, null, null, hasNew);
+ me.fireGroupChange();
+ }
+ },
+
+ /**
+ * Clear any groupers in the store
+ */
+ clearGrouping: function(){
+ var me = this;
+ // Clear any groupers we pushed on to the sorters
+ me.groupers.each(function(grouper){
+ me.sorters.remove(grouper);
+ });
+ me.groupers.clear();
+ if (me.remoteGroup) {
+ me.load({
+ scope: me,
+ callback: me.fireGroupChange
+ });
+ } else {
+ me.sort();
+ me.fireEvent('groupchange', me, me.groupers);
+ }
+ },
+
+ /**
+ * Checks if the store is currently grouped
+ * @return {Boolean} True if the store is grouped.
+ */
+ isGrouped: function() {
+ return this.groupers.getCount() > 0;
+ },
+
+ /**
+ * Fires the groupchange event. Abstracted out so we can use it
+ * as a callback
+ * @private
+ */
+ fireGroupChange: function(){
+ this.fireEvent('groupchange', this, this.groupers);
+ },
+
+ /**
+ * Returns an array containing the result of applying grouping to the records in this store. See {@link #groupField},
+ * {@link #groupDir} and {@link #getGroupString}. Example for a store containing records with a color field:
+<pre><code>
+var myStore = Ext.create('Ext.data.Store', {
+ groupField: 'color',
+ groupDir : 'DESC'
+});
+
+myStore.getGroups(); //returns:
+[
+ {
+ name: 'yellow',
+ children: [
+ //all records where the color field is 'yellow'
+ ]
+ },
+ {
+ name: 'red',
+ children: [
+ //all records where the color field is 'red'
+ ]
+ }
+]
+</code></pre>
+ * @param {String} groupName (Optional) Pass in an optional groupName argument to access a specific group as defined by {@link #getGroupString}
+ * @return {Object/Object[]} The grouped data
+ */
+ getGroups: function(requestGroupString) {
+ var records = this.data.items,
+ length = records.length,
+ groups = [],
+ pointers = {},
+ record,
+ groupStr,
+ group,
+ i;
+
+ for (i = 0; i < length; i++) {
+ record = records[i];
+ groupStr = this.getGroupString(record);
+ group = pointers[groupStr];
+
+ if (group === undefined) {
+ group = {
+ name: groupStr,
+ children: []
+ };
+
+ groups.push(group);
+ pointers[groupStr] = group;
+ }
+
+ group.children.push(record);
+ }
+
+ return requestGroupString ? pointers[requestGroupString] : groups;
+ },
+
+ /**
+ * @private
+ * For a given set of records and a Grouper, returns an array of arrays - each of which is the set of records
+ * matching a certain group.
+ */
+ getGroupsForGrouper: function(records, grouper) {
+ var length = records.length,
+ groups = [],
+ oldValue,
+ newValue,
+ record,
+ group,
+ i;
+
+ for (i = 0; i < length; i++) {
+ record = records[i];
+ newValue = grouper.getGroupString(record);
+
+ if (newValue !== oldValue) {
+ group = {
+ name: newValue,
+ grouper: grouper,
+ records: []
+ };
+ groups.push(group);
+ }
+
+ group.records.push(record);
+
+ oldValue = newValue;
+ }
+
+ return groups;
+ },
+
+ /**
+ * @private
+ * This is used recursively to gather the records into the configured Groupers. The data MUST have been sorted for
+ * this to work properly (see {@link #getGroupData} and {@link #getGroupsForGrouper}) Most of the work is done by
+ * {@link #getGroupsForGrouper} - this function largely just handles the recursion.
+ * @param {Ext.data.Model[]} records The set or subset of records to group
+ * @param {Number} grouperIndex The grouper index to retrieve
+ * @return {Object[]} The grouped records
+ */
+ getGroupsForGrouperIndex: function(records, grouperIndex) {
+ var me = this,
+ groupers = me.groupers,
+ grouper = groupers.getAt(grouperIndex),
+ groups = me.getGroupsForGrouper(records, grouper),
+ length = groups.length,
+ i;
+
+ if (grouperIndex + 1 < groupers.length) {
+ for (i = 0; i < length; i++) {
+ groups[i].children = me.getGroupsForGrouperIndex(groups[i].records, grouperIndex + 1);
+ }
+ }
+
+ for (i = 0; i < length; i++) {
+ groups[i].depth = grouperIndex;
+ }
+
+ return groups;
+ },
+
+ /**
+ * @private
+ * <p>Returns records grouped by the configured {@link #groupers grouper} configuration. Sample return value (in
+ * this case grouping by genre and then author in a fictional books dataset):</p>
+<pre><code>
+[
+ {
+ name: 'Fantasy',
+ depth: 0,
+ records: [
+ //book1, book2, book3, book4
+ ],
+ children: [
+ {
+ name: 'Rowling',
+ depth: 1,
+ records: [
+ //book1, book2
+ ]
+ },
+ {
+ name: 'Tolkein',
+ depth: 1,
+ records: [
+ //book3, book4
+ ]
+ }
+ ]
+ }
+]
+</code></pre>
+ * @param {Boolean} sort True to call {@link #sort} before finding groups. Sorting is required to make grouping
+ * function correctly so this should only be set to false if the Store is known to already be sorted correctly
+ * (defaults to true)
+ * @return {Object[]} The group data
+ */
+ getGroupData: function(sort) {
+ var me = this;
+ if (sort !== false) {
+ me.sort();
+ }
+
+ return me.getGroupsForGrouperIndex(me.data.items, 0);
+ },
+
+ /**
+ * <p>Returns the string to group on for a given model instance. The default implementation of this method returns
+ * the model's {@link #groupField}, but this can be overridden to group by an arbitrary string. For example, to
+ * group by the first letter of a model's 'name' field, use the following code:</p>
+<pre><code>
+Ext.create('Ext.data.Store', {
+ groupDir: 'ASC',
+ getGroupString: function(instance) {
+ return instance.get('name')[0];
+ }
+});
+</code></pre>
+ * @param {Ext.data.Model} instance The model instance
+ * @return {String} The string to compare when forming groups
+ */
+ getGroupString: function(instance) {
+ var group = this.groupers.first();
+ if (group) {
+ return instance.get(group.property);
+ }
+ return '';
+ },
+ /**
+ * Inserts Model instances into the Store at the given index and fires the {@link #add} event.
+ * See also <code>{@link #add}</code>.
+ * @param {Number} index The start index at which to insert the passed Records.
+ * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache.
+ */
+ insert: function(index, records) {
+ var me = this,
+ sync = false,
+ i,
+ record,
+ len;
+
+ records = [].concat(records);
+ for (i = 0, len = records.length; i < len; i++) {
+ record = me.createModel(records[i]);
+ record.set(me.modelDefaults);
+ // reassign the model in the array in case it wasn't created yet
+ records[i] = record;
+
+ me.data.insert(index + i, record);
+ record.join(me);
+
+ sync = sync || record.phantom === true;
+ }
+
+ if (me.snapshot) {
+ me.snapshot.addAll(records);
+ }
+
+ me.fireEvent('add', me, records, index);
+ me.fireEvent('datachanged', me);
+ if (me.autoSync && sync) {
+ me.sync();
+ }
+ },
+
+ /**
+ * Adds Model instance to the Store. This method accepts either:
+ *
+ * - An array of Model instances or Model configuration objects.
+ * - Any number of Model instance or Model configuration object arguments.
+ *
+ * The new Model instances will be added at the end of the existing collection.
+ *
+ * Sample usage:
+ *
+ * myStore.add({some: 'data'}, {some: 'other data'});
+ *
+ * @param {Ext.data.Model[]/Ext.data.Model...} model An array of Model instances
+ * or Model configuration objects, or variable number of Model instance or config arguments.
+ * @return {Ext.data.Model[]} The model instances that were added
+ */
+ add: function(records) {
+ //accept both a single-argument array of records, or any number of record arguments
+ if (!Ext.isArray(records)) {
+ records = Array.prototype.slice.apply(arguments);
+ }
+
+ var me = this,
+ i = 0,
+ length = records.length,
+ record;
+
+ for (; i < length; i++) {
+ record = me.createModel(records[i]);
+ // reassign the model in the array in case it wasn't created yet
+ records[i] = record;
+ }
+
+ me.insert(me.data.length, records);
+
+ return records;
+ },
+
+ /**
+ * Converts a literal to a model, if it's not a model already
+ * @private
+ * @param record {Ext.data.Model/Object} The record to create
+ * @return {Ext.data.Model}
+ */
+ createModel: function(record) {
+ if (!record.isModel) {
+ record = Ext.ModelManager.create(record, this.model);
+ }
+
+ return record;
+ },
+
+ /**
+ * Calls the specified function for each of the {@link Ext.data.Model Records} in the cache.
+ * @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed as the first parameter.
+ * Returning <tt>false</tt> aborts and exits the iteration.
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed.
+ * Defaults to the current {@link Ext.data.Model Record} in the iteration.
+ */
+ each: function(fn, scope) {
+ this.data.each(fn, scope);
+ },
+
+ /**
+ * Removes the given record from the Store, firing the 'remove' event for each instance that is removed, plus a single
+ * 'datachanged' event after removal.
+ * @param {Ext.data.Model/Ext.data.Model[]} records The Ext.data.Model instance or array of instances to remove
+ */
+ remove: function(records, /* private */ isMove) {
+ if (!Ext.isArray(records)) {
+ records = [records];
+ }
+
+ /*
+ * Pass the isMove parameter if we know we're going to be re-inserting this record
+ */
+ isMove = isMove === true;
+ var me = this,
+ sync = false,
+ i = 0,
+ length = records.length,
+ isPhantom,
+ index,
+ record;
+
+ for (; i < length; i++) {
+ record = records[i];
+ index = me.data.indexOf(record);
+
+ if (me.snapshot) {
+ me.snapshot.remove(record);
+ }
+
+ if (index > -1) {
+ isPhantom = record.phantom === true;
+ if (!isMove && !isPhantom) {
+ // don't push phantom records onto removed
+ me.removed.push(record);
+ }
+
+ record.unjoin(me);
+ me.data.remove(record);
+ sync = sync || !isPhantom;
+
+ me.fireEvent('remove', me, record, index);
+ }
+ }
+
+ me.fireEvent('datachanged', me);
+ if (!isMove && me.autoSync && sync) {
+ me.sync();
+ }
+ },
+
+ /**
+ * Removes the model instance at the given index
+ * @param {Number} index The record index
+ */
+ removeAt: function(index) {
+ var record = this.getAt(index);
+
+ if (record) {
+ this.remove(record);
+ }
+ },
+
+ /**
+ * <p>Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an
+ * asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved
+ * instances into the Store and calling an optional callback if required. Example usage:</p>
+ *
+<pre><code>
+store.load({
+ scope : this,
+ callback: function(records, operation, success) {
+ //the {@link Ext.data.Operation operation} object contains all of the details of the load operation
+ console.log(records);
+ }
+});
+</code></pre>
+ *
+ * <p>If the callback scope does not need to be set, a function can simply be passed:</p>
+ *
+<pre><code>
+store.load(function(records, operation, success) {
+ console.log('loaded records');
+});
+</code></pre>
+ *
+ * @param {Object/Function} options (Optional) config object, passed into the Ext.data.Operation object before loading.
+ */
+ load: function(options) {
+ var me = this;
+
+ options = options || {};
+
+ if (Ext.isFunction(options)) {
+ options = {
+ callback: options
+ };
+ }
+
+ Ext.applyIf(options, {
+ groupers: me.groupers.items,
+ page: me.currentPage,
+ start: (me.currentPage - 1) * me.pageSize,
+ limit: me.pageSize,
+ addRecords: false
+ });
+
+ return me.callParent([options]);
+ },
+
+ /**
+ * @private
+ * Called internally when a Proxy has completed a load request
+ */
+ onProxyLoad: function(operation) {
+ var me = this,
+ resultSet = operation.getResultSet(),
+ records = operation.getRecords(),
+ successful = operation.wasSuccessful();
+
+ if (resultSet) {
+ me.totalCount = resultSet.total;
+ }
+
+ if (successful) {
+ me.loadRecords(records, operation);
+ }
+
+ me.loading = false;
+ me.fireEvent('load', me, records, successful);
+
+ //TODO: deprecate this event, it should always have been 'load' instead. 'load' is now documented, 'read' is not.
+ //People are definitely using this so can't deprecate safely until 2.x
+ me.fireEvent('read', me, records, operation.wasSuccessful());
+
+ //this is a callback that would have been passed to the 'read' function and is optional
+ Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
+ },
+
+ /**
+ * Create any new records when a write is returned from the server.
+ * @private
+ * @param {Ext.data.Model[]} records The array of new records
+ * @param {Ext.data.Operation} operation The operation that just completed
+ * @param {Boolean} success True if the operation was successful
+ */
+ onCreateRecords: function(records, operation, success) {
+ if (success) {
+ var i = 0,
+ data = this.data,
+ snapshot = this.snapshot,
+ length = records.length,
+ originalRecords = operation.records,
+ record,
+ original,
+ index;
+
+ /*
+ * Loop over each record returned from the server. Assume they are
+ * returned in order of how they were sent. If we find a matching
+ * record, replace it with the newly created one.
+ */
+ for (; i < length; ++i) {
+ record = records[i];
+ original = originalRecords[i];
+ if (original) {
+ index = data.indexOf(original);
+ if (index > -1) {
+ data.removeAt(index);
+ data.insert(index, record);
+ }
+ if (snapshot) {
+ index = snapshot.indexOf(original);
+ if (index > -1) {
+ snapshot.removeAt(index);
+ snapshot.insert(index, record);
+ }
+ }
+ record.phantom = false;
+ record.join(this);
+ }
+ }
+ }
+ },
+
+ /**
+ * Update any records when a write is returned from the server.
+ * @private
+ * @param {Ext.data.Model[]} records The array of updated records
+ * @param {Ext.data.Operation} operation The operation that just completed
+ * @param {Boolean} success True if the operation was successful
+ */
+ onUpdateRecords: function(records, operation, success){
+ if (success) {
+ var i = 0,
+ length = records.length,
+ data = this.data,
+ snapshot = this.snapshot,
+ record;
+
+ for (; i < length; ++i) {
+ record = records[i];
+ data.replace(record);
+ if (snapshot) {
+ snapshot.replace(record);
+ }
+ record.join(this);
+ }
+ }
+ },
+
+ /**
+ * Remove any records when a write is returned from the server.
+ * @private
+ * @param {Ext.data.Model[]} records The array of removed records
+ * @param {Ext.data.Operation} operation The operation that just completed
+ * @param {Boolean} success True if the operation was successful
+ */
+ onDestroyRecords: function(records, operation, success){
+ if (success) {
+ var me = this,
+ i = 0,
+ length = records.length,
+ data = me.data,
+ snapshot = me.snapshot,
+ record;
+
+ for (; i < length; ++i) {
+ record = records[i];
+ record.unjoin(me);
+ data.remove(record);
+ if (snapshot) {
+ snapshot.remove(record);
+ }
+ }
+ me.removed = [];
+ }
+ },
+
+ //inherit docs
+ getNewRecords: function() {
+ return this.data.filterBy(this.filterNew).items;
+ },
+
+ //inherit docs
+ getUpdatedRecords: function() {
+ return this.data.filterBy(this.filterUpdated).items;
+ },
+
+ /**
+ * Filters the loaded set of records by a given set of filters.
+ *
+ * Filtering by single field:
+ *
+ * store.filter("email", /\.com$/);
+ *
+ * Using multiple filters:
+ *
+ * store.filter([
+ * {property: "email", value: /\.com$/},
+ * {filterFn: function(item) { return item.get("age") > 10; }}
+ * ]);
+ *
+ * Using Ext.util.Filter instances instead of config objects
+ * (note that we need to specify the {@link Ext.util.Filter#root root} config option in this case):
+ *
+ * store.filter([
+ * Ext.create('Ext.util.Filter', {property: "email", value: /\.com$/, root: 'data'}),
+ * Ext.create('Ext.util.Filter', {filterFn: function(item) { return item.get("age") > 10; }, root: 'data'})
+ * ]);
+ *
+ * @param {Object[]/Ext.util.Filter[]/String} filters The set of filters to apply to the data. These are stored internally on the store,
+ * but the filtering itself is done on the Store's {@link Ext.util.MixedCollection MixedCollection}. See
+ * MixedCollection's {@link Ext.util.MixedCollection#filter filter} method for filter syntax. Alternatively,
+ * pass in a property string
+ * @param {String} value (optional) value to filter by (only if using a property string as the first argument)
+ */
+ filter: function(filters, value) {
+ if (Ext.isString(filters)) {
+ filters = {
+ property: filters,
+ value: value
+ };
+ }
+
+ var me = this,
+ decoded = me.decodeFilters(filters),
+ i = 0,
+ doLocalSort = me.sortOnFilter && !me.remoteSort,
+ length = decoded.length;
+
+ for (; i < length; i++) {
+ me.filters.replace(decoded[i]);
+ }
+
+ if (me.remoteFilter) {
+ //the load function will pick up the new filters and request the filtered data from the proxy
+ me.load();
+ } else {
+ /**
+ * A pristine (unfiltered) collection of the records in this store. This is used to reinstate
+ * records when a filter is removed or changed
+ * @property snapshot
+ * @type Ext.util.MixedCollection
+ */
+ if (me.filters.getCount()) {
+ me.snapshot = me.snapshot || me.data.clone();
+ me.data = me.data.filter(me.filters.items);
+
+ if (doLocalSort) {
+ me.sort();
+ }
+ // fire datachanged event if it hasn't already been fired by doSort
+ if (!doLocalSort || me.sorters.length < 1) {
+ me.fireEvent('datachanged', me);
+ }
+ }
+ }
+ },
+
+ /**
+ * Revert to a view of the Record cache with no filtering applied.
+ * @param {Boolean} suppressEvent If <tt>true</tt> the filter is cleared silently without firing the
+ * {@link #datachanged} event.
+ */
+ clearFilter: function(suppressEvent) {
+ var me = this;
+
+ me.filters.clear();
+
+ if (me.remoteFilter) {
+ me.load();
+ } else if (me.isFiltered()) {
+ me.data = me.snapshot.clone();
+ delete me.snapshot;
+
+ if (suppressEvent !== true) {
+ me.fireEvent('datachanged', me);
+ }
+ }
+ },
+
+ /**
+ * Returns true if this store is currently filtered
+ * @return {Boolean}
+ */
+ isFiltered: function() {
+ var snapshot = this.snapshot;
+ return !! snapshot && snapshot !== this.data;
+ },
+
+ /**
+ * Filter by a function. The specified function will be called for each
+ * Record in this Store. If the function returns <tt>true</tt> the Record is included,
+ * otherwise it is filtered out.
+ * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
+ * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
+ * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
+ * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
+ * </ul>
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
+ */
+ filterBy: function(fn, scope) {
+ var me = this;
+
+ me.snapshot = me.snapshot || me.data.clone();
+ me.data = me.queryBy(fn, scope || me);
+ me.fireEvent('datachanged', me);
+ },
+
+ /**
+ * Query the cached records in this Store using a filtering function. The specified function
+ * will be called with each record in this Store. If the function returns <tt>true</tt> the record is
+ * included in the results.
+ * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
+ * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
+ * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
+ * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
+ * </ul>
+ * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to this Store.
+ * @return {Ext.util.MixedCollection} Returns an Ext.util.MixedCollection of the matched records
+ **/
+ queryBy: function(fn, scope) {
+ var me = this,
+ data = me.snapshot || me.data;
+ return data.filterBy(fn, scope || me);
+ },
+
+ /**
+ * Loads an array of data straight into the Store.
+ *
+ * Using this method is great if the data is in the correct format already (e.g. it doesn't need to be
+ * processed by a reader). If your data requires processing to decode the data structure, use a
+ * {@link Ext.data.proxy.Memory MemoryProxy} instead.
+ *
+ * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances will be cast
+ * into model instances.
+ * @param {Boolean} [append=false] True to add the records to the existing records in the store, false
+ * to remove the old ones first.
+ */
+ loadData: function(data, append) {
+ var model = this.model,
+ length = data.length,
+ newData = [],
+ i,
+ record;
+
+ //make sure each data element is an Ext.data.Model instance
+ for (i = 0; i < length; i++) {
+ record = data[i];
+
+ if (!(record instanceof Ext.data.Model)) {
+ record = Ext.ModelManager.create(record, model);
+ }
+ newData.push(record);
+ }
+
+ this.loadRecords(newData, {addRecords: append});
+ },
+
+
+ /**
+ * Loads data via the bound Proxy's reader
+ *
+ * Use this method if you are attempting to load data and want to utilize the configured data reader.
+ *
+ * @param {Object[]} data The full JSON object you'd like to load into the Data store.
+ * @param {Boolean} [append=false] True to add the records to the existing records in the store, false
+ * to remove the old ones first.
+ */
+ loadRawData : function(data, append) {
+ var me = this,
+ result = me.proxy.reader.read(data),
+ records = result.records;
+
+ if (result.success) {
+ me.loadRecords(records, { addRecords: append });
+ me.fireEvent('load', me, records, true);
+ }
+ },
+
+
+ /**
+ * Loads an array of {@link Ext.data.Model model} instances into the store, fires the datachanged event. This should only usually
+ * be called internally when loading from the {@link Ext.data.proxy.Proxy Proxy}, when adding records manually use {@link #add} instead
+ * @param {Ext.data.Model[]} records The array of records to load
+ * @param {Object} options {addRecords: true} to add these records to the existing records, false to remove the Store's existing records first
+ */
+ loadRecords: function(records, options) {
+ var me = this,
+ i = 0,
+ length = records.length;
+
+ options = options || {};
+
+
+ if (!options.addRecords) {
+ delete me.snapshot;
+ me.clearData();
+ }
+
+ me.data.addAll(records);
+
+ //FIXME: this is not a good solution. Ed Spencer is totally responsible for this and should be forced to fix it immediately.
+ for (; i < length; i++) {
+ if (options.start !== undefined) {
+ records[i].index = options.start + i;
+
+ }
+ records[i].join(me);
+ }
+
+ /*
+ * this rather inelegant suspension and resumption of events is required because both the filter and sort functions
+ * fire an additional datachanged event, which is not wanted. Ideally we would do this a different way. The first
+ * datachanged event is fired by the call to this.add, above.
+ */
+ me.suspendEvents();
+
+ if (me.filterOnLoad && !me.remoteFilter) {
+ me.filter();
+ }
+
+ if (me.sortOnLoad && !me.remoteSort) {
+ me.sort();
+ }
+
+ me.resumeEvents();
+ me.fireEvent('datachanged', me, records);
+ },
+
+ // PAGING METHODS
+ /**
+ * Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal
+ * load operation, passing in calculated 'start' and 'limit' params
+ * @param {Number} page The number of the page to load
+ * @param {Object} options See options for {@link #load}
+ */
+ loadPage: function(page, options) {
+ var me = this;
+ options = Ext.apply({}, options);
+
+ me.currentPage = page;
+
+ me.read(Ext.applyIf(options, {
+ page: page,
+ start: (page - 1) * me.pageSize,
+ limit: me.pageSize,
+ addRecords: !me.clearOnPageLoad
+ }));
+ },
+
+ /**
+ * Loads the next 'page' in the current data set
+ * @param {Object} options See options for {@link #load}
+ */
+ nextPage: function(options) {
+ this.loadPage(this.currentPage + 1, options);
+ },
+
+ /**
+ * Loads the previous 'page' in the current data set
+ * @param {Object} options See options for {@link #load}
+ */
+ previousPage: function(options) {
+ this.loadPage(this.currentPage - 1, options);
+ },
+
+ // private
+ clearData: function() {
+ var me = this;
+ me.data.each(function(record) {
+ record.unjoin(me);
+ });
+
+ me.data.clear();
+ },
+
+ // Buffering
+ /**
+ * Prefetches data into the store using its configured {@link #proxy}.
+ * @param {Object} options (Optional) config object, passed into the Ext.data.Operation object before loading.
+ * See {@link #load}
+ */
+ prefetch: function(options) {
+ var me = this,
+ operation,
+ requestId = me.getRequestId();
+
+ options = options || {};
+
+ Ext.applyIf(options, {
+ action : 'read',
+ filters: me.filters.items,
+ sorters: me.sorters.items,
+ requestId: requestId
+ });
+ me.pendingRequests.push(requestId);
+
+ operation = Ext.create('Ext.data.Operation', options);
+
+ // HACK to implement loadMask support.
+ //if (operation.blocking) {
+ // me.fireEvent('beforeload', me, operation);
+ //}
+ if (me.fireEvent('beforeprefetch', me, operation) !== false) {
+ me.loading = true;
+ me.proxy.read(operation, me.onProxyPrefetch, me);
+ }
+
+ return me;
+ },
+
+ /**
+ * Prefetches a page of data.
+ * @param {Number} page The page to prefetch
+ * @param {Object} options (Optional) config object, passed into the Ext.data.Operation object before loading.
+ * See {@link #load}
+ */
+ prefetchPage: function(page, options) {
+ var me = this,
+ pageSize = me.pageSize,
+ start = (page - 1) * me.pageSize,
+ end = start + pageSize;
+
+ // Currently not requesting this page and range isn't already satisified
+ if (Ext.Array.indexOf(me.pagesRequested, page) === -1 && !me.rangeSatisfied(start, end)) {
+ options = options || {};
+ me.pagesRequested.push(page);
+ Ext.applyIf(options, {
+ page : page,
+ start: start,
+ limit: pageSize,
+ callback: me.onWaitForGuarantee,
+ scope: me
+ });
+
+ me.prefetch(options);
+ }
+
+ },
+
+ /**
+ * Returns a unique requestId to track requests.
+ * @private
+ */
+ getRequestId: function() {
+ this.requestSeed = this.requestSeed || 1;
+ return this.requestSeed++;
+ },
+
+ /**
+ * Called after the configured proxy completes a prefetch operation.
+ * @private
+ * @param {Ext.data.Operation} operation The operation that completed
+ */
+ onProxyPrefetch: function(operation) {
+ var me = this,
+ resultSet = operation.getResultSet(),
+ records = operation.getRecords(),
+
+ successful = operation.wasSuccessful();
+
+ if (resultSet) {
+ me.totalCount = resultSet.total;
+ me.fireEvent('totalcountchange', me.totalCount);
+ }
+
+ if (successful) {
+ me.cacheRecords(records, operation);
+ }
+ Ext.Array.remove(me.pendingRequests, operation.requestId);
+ if (operation.page) {
+ Ext.Array.remove(me.pagesRequested, operation.page);
+ }
+
+ me.loading = false;
+ me.fireEvent('prefetch', me, records, successful, operation);
+
+ // HACK to support loadMask
+ if (operation.blocking) {
+ me.fireEvent('load', me, records, successful);
+ }
+
+ //this is a callback that would have been passed to the 'read' function and is optional
+ Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
+ },
+
+ /**
+ * Caches the records in the prefetch and stripes them with their server-side
+ * index.
+ * @private
+ * @param {Ext.data.Model[]} records The records to cache
+ * @param {Ext.data.Operation} The associated operation
+ */
+ cacheRecords: function(records, operation) {
+ var me = this,
+ i = 0,
+ length = records.length,
+ start = operation ? operation.start : 0;
+
+ if (!Ext.isDefined(me.totalCount)) {
+ me.totalCount = records.length;
+ me.fireEvent('totalcountchange', me.totalCount);
+ }
+
+ for (; i < length; i++) {
+ // this is the true index, not the viewIndex
+ records[i].index = start + i;
+ }
+
+ me.prefetchData.addAll(records);
+ if (me.purgePageCount) {
+ me.purgeRecords();
+ }
+
+ },
+
+
+ /**
+ * Purge the least recently used records in the prefetch if the purgeCount
+ * has been exceeded.
+ */
+ purgeRecords: function() {
+ var me = this,
+ prefetchCount = me.prefetchData.getCount(),
+ purgeCount = me.purgePageCount * me.pageSize,
+ numRecordsToPurge = prefetchCount - purgeCount - 1,
+ i = 0;
+
+ for (; i <= numRecordsToPurge; i++) {
+ me.prefetchData.removeAt(0);
+ }
+ },
+
+ /**
+ * Determines if the range has already been satisfied in the prefetchData.
+ * @private
+ * @param {Number} start The start index
+ * @param {Number} end The end index in the range
+ */
+ rangeSatisfied: function(start, end) {
+ var me = this,
+ i = start,
+ satisfied = true;
+
+ for (; i < end; i++) {
+ if (!me.prefetchData.getByKey(i)) {
+ satisfied = false;
+ break;
+ }
+ }
+ return satisfied;
+ },
+
+ /**
+ * Determines the page from a record index
+ * @param {Number} index The record index
+ * @return {Number} The page the record belongs to
+ */
+ getPageFromRecordIndex: function(index) {
+ return Math.floor(index / this.pageSize) + 1;
+ },
+
+ /**
+ * Handles a guaranteed range being loaded
+ * @private
+ */
+ onGuaranteedRange: function() {
+ var me = this,
+ totalCount = me.getTotalCount(),
+ start = me.requestStart,
+ end = ((totalCount - 1) < me.requestEnd) ? totalCount - 1 : me.requestEnd,
+ range = [],
+ record,
+ i = start;
+
+ end = Math.max(0, end);
+
+
+ if (start !== me.guaranteedStart && end !== me.guaranteedEnd) {
+ me.guaranteedStart = start;
+ me.guaranteedEnd = end;
+
+ for (; i <= end; i++) {
+ record = me.prefetchData.getByKey(i);
+ if (record) {
+ range.push(record);
+ }
+ }
+ me.fireEvent('guaranteedrange', range, start, end);
+ if (me.cb) {
+ me.cb.call(me.scope || me, range);
+ }
+ }
+
+ me.unmask();
+ },
+
+ // hack to support loadmask
+ mask: function() {
+ this.masked = true;
+ this.fireEvent('beforeload');
+ },
+
+ // hack to support loadmask
+ unmask: function() {
+ if (this.masked) {
+ this.fireEvent('load');
+ }
+ },
+
+ /**
+ * Returns the number of pending requests out.
+ */
+ hasPendingRequests: function() {
+ return this.pendingRequests.length;
+ },
+
+
+ // wait until all requests finish, until guaranteeing the range.
+ onWaitForGuarantee: function() {
+ if (!this.hasPendingRequests()) {
+ this.onGuaranteedRange();
+ }
+ },
+
+ /**
+ * Guarantee a specific range, this will load the store with a range (that
+ * must be the pageSize or smaller) and take care of any loading that may
+ * be necessary.
+ */
+ guaranteeRange: function(start, end, cb, scope) {
+
+ end = (end > this.totalCount) ? this.totalCount - 1 : end;
+
+ var me = this,
+ i = start,
+ prefetchData = me.prefetchData,
+ range = [],
+ startLoaded = !!prefetchData.getByKey(start),
+ endLoaded = !!prefetchData.getByKey(end),
+ startPage = me.getPageFromRecordIndex(start),
+ endPage = me.getPageFromRecordIndex(end);
+
+ me.cb = cb;
+ me.scope = scope;
+
+ me.requestStart = start;
+ me.requestEnd = end;
+ // neither beginning or end are loaded
+ if (!startLoaded || !endLoaded) {
+ // same page, lets load it
+ if (startPage === endPage) {
+ me.mask();
+ me.prefetchPage(startPage, {
+ //blocking: true,
+ callback: me.onWaitForGuarantee,
+ scope: me
+ });
+ // need to load two pages
+ } else {
+ me.mask();
+ me.prefetchPage(startPage, {
+ //blocking: true,
+ callback: me.onWaitForGuarantee,
+ scope: me
+ });
+ me.prefetchPage(endPage, {
+ //blocking: true,
+ callback: me.onWaitForGuarantee,
+ scope: me
+ });
+ }
+ // Request was already satisfied via the prefetch
+ } else {
+ me.onGuaranteedRange();
+ }
+ },
+
+ // because prefetchData is stored by index
+ // this invalidates all of the prefetchedData
+ sort: function() {
+ var me = this,
+ prefetchData = me.prefetchData,
+ sorters,
+ start,
+ end,
+ range;
+
+ if (me.buffered) {
+ if (me.remoteSort) {
+ prefetchData.clear();
+ me.callParent(arguments);
+ } else {
+ sorters = me.getSorters();
+ start = me.guaranteedStart;
+ end = me.guaranteedEnd;
+
+ if (sorters.length) {
+ prefetchData.sort(sorters);
+ range = prefetchData.getRange();
+ prefetchData.clear();
+ me.cacheRecords(range);
+ delete me.guaranteedStart;
+ delete me.guaranteedEnd;
+ me.guaranteeRange(start, end);
+ }
+ me.callParent(arguments);
+ }
+ } else {
+ me.callParent(arguments);
+ }
+ },
+
+ // overriden to provide striping of the indexes as sorting occurs.
+ // this cannot be done inside of sort because datachanged has already
+ // fired and will trigger a repaint of the bound view.
+ doSort: function(sorterFn) {
+ var me = this;
+ if (me.remoteSort) {
+ //the load function will pick up the new sorters and request the sorted data from the proxy
+ me.load();
+ } else {
+ me.data.sortBy(sorterFn);
+ if (!me.buffered) {
+ var range = me.getRange(),
+ ln = range.length,
+ i = 0;
+ for (; i < ln; i++) {
+ range[i].index = i;
+ }
+ }
+ me.fireEvent('datachanged', me);
+ }
+ },
+
+ /**
+ * 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
+ * @param {Boolean} exactMatch (optional) True to force exact match (^ and $ characters added to the regex). Defaults to false.
+ * @return {Number} The matched index or -1
+ */
+ find: function(property, value, start, anyMatch, caseSensitive, exactMatch) {
+ var fn = this.createFilterFn(property, value, anyMatch, caseSensitive, exactMatch);
+ return fn ? this.data.findIndexBy(fn, null, start) : -1;
+ },
+
+ /**
+ * Finds 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
+ * @param {Boolean} exactMatch (optional) True to force exact match (^ and $ characters added to the regex). Defaults to false.
+ * @return {Ext.data.Model} The matched record or null
+ */
+ findRecord: function() {
+ var me = this,
+ index = me.find.apply(me, arguments);
+ return index !== -1 ? me.getAt(index) : null;
+ },
+
+ /**
+ * @private
+ * Returns a filter function used to test a the given property's value. Defers most of the work to
+ * Ext.util.MixedCollection's createValueMatcher function
+ * @param {String} property The property to create the filter function for
+ * @param {String/RegExp} value The string/regex to compare the property value to
+ * @param {Boolean} [anyMatch=false] True if we don't care if the filter value is not the full value.
+ * @param {Boolean} [caseSensitive=false] True to create a case-sensitive regex.
+ * @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters added to the regex).
+ * Ignored if anyMatch is true.
+ */
+ createFilterFn: function(property, value, anyMatch, caseSensitive, exactMatch) {
+ if (Ext.isEmpty(value)) {
+ return false;
+ }
+ value = this.data.createValueMatcher(value, anyMatch, caseSensitive, exactMatch);
+ return function(r) {
+ return value.test(r.data[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 {Object} 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
+ */
+ findExact: function(property, value, start) {
+ return this.data.findIndexBy(function(rec) {
+ return rec.get(property) == value;
+ },
+ this, start);
+ },
+
+ /**
+ * Find the index of the first matching Record in this Store by a function.
+ * If the function returns <tt>true</tt> it is considered a match.
+ * @param {Function} fn The function to be called. It will be passed the following parameters:<ul>
+ * <li><b>record</b> : Ext.data.Model<p class="sub-desc">The {@link Ext.data.Model record}
+ * to test for filtering. Access field values using {@link Ext.data.Model#get}.</p></li>
+ * <li><b>id</b> : Object<p class="sub-desc">The ID of the Record passed.</p></li>
+ * </ul>
+ * @param {Object} scope (optional) The scope (<code>this</code> 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
+ */
+ findBy: function(fn, scope, start) {
+ return this.data.findIndexBy(fn, scope, start);
+ },
+
+ /**
+ * Collects unique values for a particular dataIndex from this store.
+ * @param {String} dataIndex The property to collect
+ * @param {Boolean} allowNull (optional) Pass true to allow null, undefined or empty string values
+ * @param {Boolean} bypassFilter (optional) Pass true to collect from all records, even ones which are filtered
+ * @return {Object[]} An array of the unique values
+ **/
+ collect: function(dataIndex, allowNull, bypassFilter) {
+ var me = this,
+ data = (bypassFilter === true && me.snapshot) ? me.snapshot: me.data;
+
+ return data.collect(dataIndex, 'data', allowNull);
+ },
+
+ /**
+ * Gets the number of cached records.
+ * <p>If using paging, this may not be the total size of the dataset. If the data object
+ * used by the Reader contains the dataset size, then the {@link #getTotalCount} function returns
+ * the dataset size. <b>Note</b>: see the Important note in {@link #load}.</p>
+ * @return {Number} The number of Records in the Store's cache.
+ */
+ getCount: function() {
+ return this.data.length || 0;
+ },
+
+ /**
+ * Returns the total number of {@link Ext.data.Model Model} instances that the {@link Ext.data.proxy.Proxy Proxy}
+ * indicates exist. This will usually differ from {@link #getCount} when using paging - getCount returns the
+ * number of records loaded into the Store at the moment, getTotalCount returns the number of records that
+ * could be loaded into the Store if the Store contained all data
+ * @return {Number} The total number of Model instances available via the Proxy
+ */
+ getTotalCount: function() {
+ return this.totalCount;
+ },
+
+ /**
+ * Get the Record at the specified index.
+ * @param {Number} index The index of the Record to find.
+ * @return {Ext.data.Model} The Record at the passed index. Returns undefined if not found.
+ */
+ getAt: function(index) {
+ return this.data.getAt(index);
+ },
+
+ /**
+ * Returns a range of Records between specified indices.
+ * @param {Number} [startIndex=0] The starting index
+ * @param {Number} [endIndex] The ending index. Defaults to the last Record in the Store.
+ * @return {Ext.data.Model[]} An array of Records
+ */
+ getRange: function(start, end) {
+ return this.data.getRange(start, end);
+ },
+
+ /**
+ * Get the Record with the specified id.
+ * @param {String} id The id of the Record to find.
+ * @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
+ */
+ getById: function(id) {
+ return (this.snapshot || this.data).findBy(function(record) {
+ return record.getId() === id;
+ });
+ },
+
+ /**
+ * Get the index within the cache of the passed Record.
+ * @param {Ext.data.Model} record The Ext.data.Model object to find.
+ * @return {Number} The index of the passed Record. Returns -1 if not found.
+ */
+ indexOf: function(record) {
+ return this.data.indexOf(record);
+ },
+
+
+ /**
+ * Get the index within the entire dataset. From 0 to the totalCount.
+ * @param {Ext.data.Model} record The Ext.data.Model object to find.
+ * @return {Number} The index of the passed Record. Returns -1 if not found.
+ */
+ indexOfTotal: function(record) {
+ var index = record.index;
+ if (index || index === 0) {
+ return index;
+ }
+ return this.indexOf(record);
+ },
+
+ /**
+ * Get the index within the cache of the Record with the passed id.
+ * @param {String} id The id of the Record to find.
+ * @return {Number} The index of the Record. Returns -1 if not found.
+ */
+ indexOfId: function(id) {
+ return this.indexOf(this.getById(id));
+ },
+
+ /**
+ * Remove all items from the store.
+ * @param {Boolean} silent Prevent the `clear` event from being fired.
+ */
+ removeAll: function(silent) {
+ var me = this;
+
+ me.clearData();
+ if (me.snapshot) {
+ me.snapshot.clear();
+ }
+ if (silent !== true) {
+ me.fireEvent('clear', me);
+ }
+ },
+
+ /*
+ * Aggregation methods
+ */
+
+ /**
+ * Convenience function for getting the first model instance in the store
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the first record being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Ext.data.Model/undefined} The first model instance in the store, or undefined
+ */
+ first: function(grouped) {
+ var me = this;
+
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(function(records) {
+ return records.length ? records[0] : undefined;
+ }, me, true);
+ } else {
+ return me.data.first();
+ }
+ },
+
+ /**
+ * Convenience function for getting the last model instance in the store
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the last record being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Ext.data.Model/undefined} The last model instance in the store, or undefined
+ */
+ last: function(grouped) {
+ var me = this;
+
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(function(records) {
+ var len = records.length;
+ return len ? records[len - 1] : undefined;
+ }, me, true);
+ } else {
+ return me.data.last();
+ }
+ },
+
+ /**
+ * Sums the value of <tt>property</tt> for each {@link Ext.data.Model record} between <tt>start</tt>
+ * and <tt>end</tt> and returns the result.
+ * @param {String} field A field in each record
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the sum for that group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Number} The sum
+ */
+ sum: function(field, grouped) {
+ var me = this;
+
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(me.getSum, me, true, [field]);
+ } else {
+ return me.getSum(me.data.items, field);
+ }
+ },
+
+ // @private, see sum
+ getSum: function(records, field) {
+ var total = 0,
+ i = 0,
+ len = records.length;
+
+ for (; i < len; ++i) {
+ total += records[i].get(field);
+ }
+
+ return total;
+ },
+
+ /**
+ * Gets the count of items in the store.
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the count for each group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Number} the count
+ */
+ count: function(grouped) {
+ var me = this;
+
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(function(records) {
+ return records.length;
+ }, me, true);
+ } else {
+ return me.getCount();
+ }
+ },
+
+ /**
+ * Gets the minimum value in the store.
+ * @param {String} field The field in each record
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the minimum in the group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Object} The minimum value, if no items exist, undefined.
+ */
+ min: function(field, grouped) {
+ var me = this;
+
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(me.getMin, me, true, [field]);
+ } else {
+ return me.getMin(me.data.items, field);
+ }
+ },
+
+ // @private, see min
+ getMin: function(records, field){
+ var i = 1,
+ len = records.length,
+ value, min;
+
+ if (len > 0) {
+ min = records[0].get(field);
+ }
+
+ for (; i < len; ++i) {
+ value = records[i].get(field);
+ if (value < min) {
+ min = value;
+ }
+ }
+ return min;
+ },
+
+ /**
+ * Gets the maximum value in the store.
+ * @param {String} field The field in each record
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the maximum in the group being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Object} The maximum value, if no items exist, undefined.
+ */
+ max: function(field, grouped) {
+ var me = this;
+
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(me.getMax, me, true, [field]);
+ } else {
+ return me.getMax(me.data.items, field);
+ }
+ },
+
+ // @private, see max
+ getMax: function(records, field) {
+ var i = 1,
+ len = records.length,
+ value,
+ max;
+
+ if (len > 0) {
+ max = records[0].get(field);
+ }
+
+ for (; i < len; ++i) {
+ value = records[i].get(field);
+ if (value > max) {
+ max = value;
+ }
+ }
+ return max;
+ },
+
+ /**
+ * Gets the average value in the store.
+ * @param {String} field The field in each record
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the group average being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @return {Object} The average value, if no items exist, 0.
+ */
+ average: function(field, grouped) {
+ var me = this;
+ if (grouped && me.isGrouped()) {
+ return me.aggregate(me.getAverage, me, true, [field]);
+ } else {
+ return me.getAverage(me.data.items, field);
+ }
+ },
+
+ // @private, see average
+ getAverage: function(records, field) {
+ var i = 0,
+ len = records.length,
+ sum = 0;
+
+ if (records.length > 0) {
+ for (; i < len; ++i) {
+ sum += records[i].get(field);
+ }
+ return sum / len;
+ }
+ return 0;
+ },
+
+ /**
+ * Runs the aggregate function for all the records in the store.
+ * @param {Function} fn The function to execute. The function is called with a single parameter,
+ * an array of records for that group.
+ * @param {Object} scope (optional) The scope to execute the function in. Defaults to the store.
+ * @param {Boolean} grouped (Optional) True to perform the operation for each group
+ * in the store. The value returned will be an object literal with the key being the group
+ * name and the group average being the value. The grouped parameter is only honored if
+ * the store has a groupField.
+ * @param {Array} args (optional) Any arguments to append to the function call
+ * @return {Object} An object literal with the group names and their appropriate values.
+ */
+ aggregate: function(fn, scope, grouped, args) {
+ args = args || [];
+ if (grouped && this.isGrouped()) {
+ var groups = this.getGroups(),
+ i = 0,
+ len = groups.length,
+ out = {},
+ group;
+
+ for (; i < len; ++i) {
+ group = groups[i];
+ out[group.name] = fn.apply(scope || this, [group.children].concat(args));
+ }
+ return out;
+ } else {
+ return fn.apply(scope || this, [this.data.items].concat(args));
+ }
+ }
+}, function() {
+ // A dummy empty store with a fieldless Model defined in it.
+ // Just for binding to Views which are instantiated with no Store defined.
+ // They will be able to run and render fine, and be bound to a generated Store later.
+ Ext.regStore('ext-empty-store', {fields: [], proxy: 'proxy'});
+});
+
+/**
+ * @author Ed Spencer
+ * @class Ext.data.JsonStore
+ * @extends Ext.data.Store
+ * @ignore
+ *
+ * <p>Small helper class to make creating {@link Ext.data.Store}s from JSON data easier.
+ * A JsonStore will be automatically configured with a {@link Ext.data.reader.Json}.</p>
+ *
+ * <p>A store configuration would be something like:</p>
+ *
+<pre><code>
+var store = new Ext.data.JsonStore({
+ // store configs
+ autoDestroy: true,
+ storeId: 'myStore',
+
+ proxy: {
+ type: 'ajax',
+ url: 'get-images.php',
+ reader: {
+ type: 'json',
+ root: 'images',
+ idProperty: 'name'
+ }
+ },
+
+ //alternatively, a {@link Ext.data.Model} name can be given (see {@link Ext.data.Store} for an example)
+ fields: ['name', 'url', {name:'size', type: 'float'}, {name:'lastmod', type:'date'}]
+});
+</code></pre>
+ *
+ * <p>This store is configured to consume a returned object of the form:<pre><code>
+{
+ images: [
+ {name: 'Image one', url:'/GetImage.php?id=1', size:46.5, lastmod: new Date(2007, 10, 29)},
+ {name: 'Image Two', url:'/GetImage.php?id=2', size:43.2, lastmod: new Date(2007, 10, 30)}
+ ]
+}
+</code></pre>
+ *
+ * <p>An object literal of this form could also be used as the {@link #data} config option.</p>
+ *
+ * @xtype jsonstore
+ */
+Ext.define('Ext.data.JsonStore', {
+ extend: 'Ext.data.Store',
+ alias: 'store.json',
+
+ /**
+ * @cfg {Ext.data.DataReader} reader @hide
+ */
+ constructor: function(config) {
+ config = config || {};
+
+ Ext.applyIf(config, {
+ proxy: {
+ type : 'ajax',
+ reader: 'json',
+ writer: 'json'
+ }
+ });
+
+ this.callParent([config]);
+ }
+});
+
+/**
+ * @class Ext.chart.axis.Time
+ * @extends Ext.chart.axis.Numeric
+ *
+ * A type of axis whose units are measured in time values. Use this axis
+ * for listing dates that you will want to group or dynamically change.
+ * If you just want to display dates as categories then use the
+ * Category class for axis instead.
+ *
+ * For example:
+ *
+ * axes: [{
+ * type: 'Time',
+ * position: 'bottom',
+ * fields: 'date',
+ * title: 'Day',
+ * dateFormat: 'M d',
+ *
+ * constrain: true,
+ * fromDate: new Date('1/1/11'),
+ * toDate: new Date('1/7/11')
+ * }]
+ *
+ * In this example we're creating a time axis that has as title *Day*.
+ * The field the axis is bound to is `date`.
+ * The date format to use to display the text for the axis labels is `M d`
+ * which is a three letter month abbreviation followed by the day number.
+ * The time axis will show values for dates between `fromDate` and `toDate`.
+ * Since `constrain` is set to true all other values for other dates not between
+ * the fromDate and toDate will not be displayed.
+ *
+ */
+Ext.define('Ext.chart.axis.Time', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.axis.Numeric',
+
+ alternateClassName: 'Ext.chart.TimeAxis',
+
+ alias: 'axis.time',
+
+ requires: ['Ext.data.Store', 'Ext.data.JsonStore'],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String/Boolean} dateFormat
+ * Indicates the format the date will be rendered on.
+ * For example: 'M d' will render the dates as 'Jan 30', etc.
+ * For a list of possible format strings see {@link Ext.Date Date}
+ */
+ dateFormat: false,
+
+ /**
+ * @cfg {Date} fromDate The starting date for the time axis.
+ */
+ fromDate: false,
+
+ /**
+ * @cfg {Date} toDate The ending date for the time axis.
+ */
+ toDate: false,
+
+ /**
+ * @cfg {Array/Boolean} step
+ * An array with two components: The first is the unit of the step (day, month, year, etc).
+ * The second one is the number of units for the step (1, 2, etc.).
+ * Defaults to `[Ext.Date.DAY, 1]`.
+ */
+ step: [Ext.Date.DAY, 1],
+
+ /**
+ * @cfg {Boolean} constrain
+ * If true, the values of the chart will be rendered only if they belong between the fromDate and toDate.
+ * If false, the time axis will adapt to the new values by adding/removing steps.
+ */
+ constrain: false,
+
+ // Avoid roundtoDecimal call in Numeric Axis's constructor
+ roundToDecimal: false,
+
+ constructor: function (config) {
+ var me = this, label, f, df;
+ me.callParent([config]);
+ label = me.label || {};
+ df = this.dateFormat;
+ if (df) {
+ if (label.renderer) {
+ f = label.renderer;
+ label.renderer = function(v) {
+ v = f(v);
+ return Ext.Date.format(new Date(f(v)), df);
+ };
+ } else {
+ label.renderer = function(v) {
+ return Ext.Date.format(new Date(v >> 0), df);
+ };
+ }
+ }
+ },
+
+ doConstrain: function () {
+ var me = this,
+ store = me.chart.store,
+ data = [],
+ series = me.chart.series.items,
+ math = Math,
+ mmax = math.max,
+ mmin = math.min,
+ fields = me.fields,
+ ln = fields.length,
+ range = me.getRange(),
+ min = range.min, max = range.max, i, l, excludes = [],
+ value, values, rec, data = [];
+ for (i = 0, l = series.length; i < l; i++) {
+ excludes[i] = series[i].__excludes;
+ }
+ store.each(function(record) {
+ for (i = 0; i < ln; i++) {
+ if (excludes[i]) {
+ continue;
+ }
+ value = record.get(fields[i]);
+ if (+value < +min) return;
+ if (+value > +max) return;
+ }
+ data.push(record);
+ })
+ me.chart.substore = Ext.create('Ext.data.JsonStore', { model: store.model, data: data });
+ },
+
+ // Before rendering, set current default step count to be number of records.
+ processView: function () {
+ var me = this;
+ if (me.fromDate) {
+ me.minimum = +me.fromDate;
+ }
+ if (me.toDate) {
+ me.maximum = +me.toDate;
+ }
+ if (me.constrain) {
+ me.doConstrain();
+ }
+ },
+
+ // @private modifies the store and creates the labels for the axes.
+ calcEnds: function() {
+ var me = this, range, step = me.step;
+ if (step) {
+ range = me.getRange();
+ range = Ext.draw.Draw.snapEndsByDateAndStep(new Date(range.min), new Date(range.max), Ext.isNumber(step) ? [Date.MILLI, step]: step);
+ if (me.minimum) {
+ range.from = me.minimum;
+ }
+ if (me.maximum) {
+ range.to = me.maximum;
+ }
+ range.step = (range.to - range.from) / range.steps;
+ return range;
+ } else {
+ return me.callParent(arguments);
+ }
+ }
+ });
+
+
+/**
+ * @class Ext.chart.series.Series
+ *
+ * Series is the abstract class containing the common logic to all chart series. Series includes
+ * methods from Labels, Highlights, Tips and Callouts mixins. This class implements the logic of handling
+ * mouse events, animating, hiding, showing all elements and returning the color of the series to be used as a legend item.
+ *
+ * ## Listeners
+ *
+ * The series class supports listeners via the Observable syntax. Some of these listeners are:
+ *
+ * - `itemmouseup` When the user interacts with a marker.
+ * - `itemmousedown` When the user interacts with a marker.
+ * - `itemmousemove` When the user iteracts with a marker.
+ * - `afterrender` Will be triggered when the animation ends or when the series has been rendered completely.
+ *
+ * For example:
+ *
+ * series: [{
+ * type: 'column',
+ * axis: 'left',
+ * listeners: {
+ * 'afterrender': function() {
+ * console('afterrender');
+ * }
+ * },
+ * xField: 'category',
+ * yField: 'data1'
+ * }]
+ */
+Ext.define('Ext.chart.series.Series', {
+
+ /* Begin Definitions */
+
+ mixins: {
+ observable: 'Ext.util.Observable',
+ labels: 'Ext.chart.Label',
+ highlights: 'Ext.chart.Highlight',
+ tips: 'Ext.chart.Tip',
+ callouts: 'Ext.chart.Callout'
+ },
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Boolean/Object} highlight
+ * If set to `true` it will highlight the markers or the series when hovering
+ * with the mouse. This parameter can also be an object with the same style
+ * properties you would apply to a {@link Ext.draw.Sprite} to apply custom
+ * styles to markers and series.
+ */
+
+ /**
+ * @cfg {Object} tips
+ * Add tooltips to the visualization's markers. The options for the tips are the
+ * same configuration used with {@link Ext.tip.ToolTip}. For example:
+ *
+ * tips: {
+ * trackMouse: true,
+ * width: 140,
+ * height: 28,
+ * renderer: function(storeItem, item) {
+ * this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' views');
+ * }
+ * },
+ */
+
+ /**
+ * @cfg {String} type
+ * The type of series. Set in subclasses.
+ */
+ type: null,
+
+ /**
+ * @cfg {String} title
+ * The human-readable name of the series.
+ */
+ title: null,
+
+ /**
+ * @cfg {Boolean} showInLegend
+ * Whether to show this series in the legend.
+ */
+ showInLegend: true,
+
+ /**
+ * @cfg {Function} renderer
+ * A function that can be overridden to set custom styling properties to each rendered element.
+ * Passes in (sprite, record, attributes, index, store) to the function.
+ */
+ renderer: function(sprite, record, attributes, index, store) {
+ return attributes;
+ },
+
+ /**
+ * @cfg {Array} shadowAttributes
+ * An array with shadow attributes
+ */
+ shadowAttributes: null,
+
+ //@private triggerdrawlistener flag
+ triggerAfterDraw: false,
+
+ /**
+ * @cfg {Object} listeners
+ * An (optional) object with event callbacks. All event callbacks get the target *item* as first parameter. The callback functions are:
+ *
+ * - itemmouseover
+ * - itemmouseout
+ * - itemmousedown
+ * - itemmouseup
+ */
+
+ constructor: function(config) {
+ var me = this;
+ if (config) {
+ Ext.apply(me, config);
+ }
+
+ me.shadowGroups = [];
+
+ me.mixins.labels.constructor.call(me, config);
+ me.mixins.highlights.constructor.call(me, config);
+ me.mixins.tips.constructor.call(me, config);
+ me.mixins.callouts.constructor.call(me, config);
+
+ me.addEvents({
+ scope: me,
+ itemmouseover: true,
+ itemmouseout: true,
+ itemmousedown: true,
+ itemmouseup: true,
+ mouseleave: true,
+ afterdraw: true,
+
+ /**
+ * @event titlechange
+ * Fires when the series title is changed via {@link #setTitle}.
+ * @param {String} title The new title value
+ * @param {Number} index The index in the collection of titles
+ */
+ titlechange: true
+ });
+
+ me.mixins.observable.constructor.call(me, config);
+
+ me.on({
+ scope: me,
+ itemmouseover: me.onItemMouseOver,
+ itemmouseout: me.onItemMouseOut,
+ mouseleave: me.onMouseLeave
+ });
+ },
+
+ /**
+ * Iterate over each of the records for this series. The default implementation simply iterates
+ * through the entire data store, but individual series implementations can override this to
+ * provide custom handling, e.g. adding/removing records.
+ * @param {Function} fn The function to execute for each record.
+ * @param {Object} scope Scope for the fn.
+ */
+ eachRecord: function(fn, scope) {
+ var chart = this.chart;
+ (chart.substore || chart.store).each(fn, scope);
+ },
+
+ /**
+ * Return the number of records being displayed in this series. Defaults to the number of
+ * records in the store; individual series implementations can override to provide custom handling.
+ */
+ getRecordCount: function() {
+ var chart = this.chart,
+ store = chart.substore || chart.store;
+ return store ? store.getCount() : 0;
+ },
+
+ /**
+ * Determines whether the series item at the given index has been excluded, i.e. toggled off in the legend.
+ * @param index
+ */
+ isExcluded: function(index) {
+ var excludes = this.__excludes;
+ return !!(excludes && excludes[index]);
+ },
+
+ // @private set the bbox and clipBox for the series
+ setBBox: function(noGutter) {
+ var me = this,
+ chart = me.chart,
+ chartBBox = chart.chartBBox,
+ gutterX = noGutter ? 0 : chart.maxGutter[0],
+ gutterY = noGutter ? 0 : chart.maxGutter[1],
+ clipBox, bbox;
+
+ clipBox = {
+ x: chartBBox.x,
+ y: chartBBox.y,
+ width: chartBBox.width,
+ height: chartBBox.height
+ };
+ me.clipBox = clipBox;
+
+ bbox = {
+ x: (clipBox.x + gutterX) - (chart.zoom.x * chart.zoom.width),
+ y: (clipBox.y + gutterY) - (chart.zoom.y * chart.zoom.height),
+ width: (clipBox.width - (gutterX * 2)) * chart.zoom.width,
+ height: (clipBox.height - (gutterY * 2)) * chart.zoom.height
+ };
+ me.bbox = bbox;
+ },
+
+ // @private set the animation for the sprite
+ onAnimate: function(sprite, attr) {
+ var me = this;
+ sprite.stopAnimation();
+ if (me.triggerAfterDraw) {
+ return sprite.animate(Ext.applyIf(attr, me.chart.animate));
+ } else {
+ me.triggerAfterDraw = true;
+ return sprite.animate(Ext.apply(Ext.applyIf(attr, me.chart.animate), {
+ listeners: {
+ 'afteranimate': function() {
+ me.triggerAfterDraw = false;
+ me.fireEvent('afterrender');
+ }
+ }
+ }));
+ }
+ },
+
+ // @private return the gutter.
+ getGutters: function() {
+ return [0, 0];
+ },
+
+ // @private wrapper for the itemmouseover event.
+ onItemMouseOver: function(item) {
+ var me = this;
+ if (item.series === me) {
+ if (me.highlight) {
+ me.highlightItem(item);
+ }
+ if (me.tooltip) {
+ me.showTip(item);
+ }
+ }
+ },
+
+ // @private wrapper for the itemmouseout event.
+ onItemMouseOut: function(item) {
+ var me = this;
+ if (item.series === me) {
+ me.unHighlightItem();
+ if (me.tooltip) {
+ me.hideTip(item);
+ }
+ }
+ },
+
+ // @private wrapper for the mouseleave event.
+ onMouseLeave: function() {
+ var me = this;
+ me.unHighlightItem();
+ if (me.tooltip) {
+ me.hideTip();
+ }
+ },
+
+ /**
+ * For a given x/y point relative to the Surface, find a corresponding item from this
+ * series, if any.
+ * @param {Number} x
+ * @param {Number} y
+ * @return {Object} An object describing the item, or null if there is no matching item.
+ * The exact contents of this object will vary by series type, but should always contain the following:
+ * @return {Ext.chart.series.Series} return.series the Series object to which the item belongs
+ * @return {Object} return.value the value(s) of the item's data point
+ * @return {Array} return.point the x/y coordinates relative to the chart box of a single point
+ * for this data item, which can be used as e.g. a tooltip anchor point.
+ * @return {Ext.draw.Sprite} return.sprite the item's rendering Sprite.
+ */
+ getItemForPoint: function(x, y) {
+ //if there are no items to query just return null.
+ if (!this.items || !this.items.length || this.seriesIsHidden) {
+ return null;
+ }
+ var me = this,
+ items = me.items,
+ bbox = me.bbox,
+ item, i, ln;
+ // Check bounds
+ if (!Ext.draw.Draw.withinBox(x, y, bbox)) {
+ return null;
+ }
+ for (i = 0, ln = items.length; i < ln; i++) {
+ if (items[i] && this.isItemInPoint(x, y, items[i], i)) {
+ return items[i];
+ }
+ }
+
+ return null;
+ },
+
+ isItemInPoint: function(x, y, item, i) {
+ return false;
+ },
+
+ /**
+ * Hides all the elements in the series.
+ */
+ hideAll: function() {
+ var me = this,
+ items = me.items,
+ item, len, i, j, l, sprite, shadows;
+
+ me.seriesIsHidden = true;
+ me._prevShowMarkers = me.showMarkers;
+
+ me.showMarkers = false;
+ //hide all labels
+ me.hideLabels(0);
+ //hide all sprites
+ for (i = 0, len = items.length; i < len; i++) {
+ item = items[i];
+ sprite = item.sprite;
+ if (sprite) {
+ sprite.setAttributes({
+ hidden: true
+ }, true);
+ }
+
+ if (sprite && sprite.shadows) {
+ shadows = sprite.shadows;
+ for (j = 0, l = shadows.length; j < l; ++j) {
+ shadows[j].setAttributes({
+ hidden: true
+ }, true);
+ }
+ }
+ }
+ },
+
+ /**
+ * Shows all the elements in the series.
+ */
+ showAll: function() {
+ var me = this,
+ prevAnimate = me.chart.animate;
+ me.chart.animate = false;
+ me.seriesIsHidden = false;
+ me.showMarkers = me._prevShowMarkers;
+ me.drawSeries();
+ me.chart.animate = prevAnimate;
+ },
+
+ /**
+ * Returns a string with the color to be used for the series legend item.
+ */
+ getLegendColor: function(index) {
+ var me = this, fill, stroke;
+ if (me.seriesStyle) {
+ fill = me.seriesStyle.fill;
+ stroke = me.seriesStyle.stroke;
+ if (fill && fill != 'none') {
+ return fill;
+ }
+ return stroke;
+ }
+ return '#000';
+ },
+
+ /**
+ * Checks whether the data field should be visible in the legend
+ * @private
+ * @param {Number} index The index of the current item
+ */
+ visibleInLegend: function(index){
+ var excludes = this.__excludes;
+ if (excludes) {
+ return !excludes[index];
+ }
+ return !this.seriesIsHidden;
+ },
+
+ /**
+ * Changes the value of the {@link #title} for the series.
+ * Arguments can take two forms:
+ * <ul>
+ * <li>A single String value: this will be used as the new single title for the series (applies
+ * to series with only one yField)</li>
+ * <li>A numeric index and a String value: this will set the title for a single indexed yField.</li>
+ * </ul>
+ * @param {Number} index
+ * @param {String} title
+ */
+ setTitle: function(index, title) {
+ var me = this,
+ oldTitle = me.title;
+
+ if (Ext.isString(index)) {
+ title = index;
+ index = 0;
+ }
+
+ if (Ext.isArray(oldTitle)) {
+ oldTitle[index] = title;
+ } else {
+ me.title = title;
+ }
+
+ me.fireEvent('titlechange', title, index);
+ }
+});
+
+/**
+ * @class Ext.chart.series.Cartesian
+ * @extends Ext.chart.series.Series
+ *
+ * Common base class for series implementations which plot values using x/y coordinates.
+ */
+Ext.define('Ext.chart.series.Cartesian', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.series.Series',
+
+ alternateClassName: ['Ext.chart.CartesianSeries', 'Ext.chart.CartesianChart'],
+
+ /* End Definitions */
+
+ /**
+ * The field used to access the x axis value from the items from the data
+ * source.
+ *
+ * @cfg xField
+ * @type String
+ */
+ xField: null,
+
+ /**
+ * The field used to access the y-axis value from the items from the data
+ * source.
+ *
+ * @cfg yField
+ * @type String
+ */
+ yField: null,
+
+ /**
+ * @cfg {String} axis
+ * The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'.
+ * You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a
+ * relative scale will be used.
+ */
+ axis: 'left',
+
+ getLegendLabels: function() {
+ var me = this,
+ labels = [],
+ combinations = me.combinations;
+
+ Ext.each([].concat(me.yField), function(yField, i) {
+ var title = me.title;
+ // Use the 'title' config if present, otherwise use the raw yField name
+ labels.push((Ext.isArray(title) ? title[i] : title) || yField);
+ });
+
+ // Handle yFields combined via legend drag-drop
+ if (combinations) {
+ Ext.each(combinations, function(combo) {
+ var label0 = labels[combo[0]],
+ label1 = labels[combo[1]];
+ labels[combo[1]] = label0 + ' & ' + label1;
+ labels.splice(combo[0], 1);
+ });
+ }
+
+ return labels;
+ },
+
+ /**
+ * @protected Iterates over a given record's values for each of this series's yFields,
+ * executing a given function for each value. Any yFields that have been combined
+ * via legend drag-drop will be treated as a single value.
+ * @param {Ext.data.Model} record
+ * @param {Function} fn
+ * @param {Object} scope
+ */
+ eachYValue: function(record, fn, scope) {
+ Ext.each(this.getYValueAccessors(), function(accessor, i) {
+ fn.call(scope, accessor(record), i);
+ });
+ },
+
+ /**
+ * @protected Returns the number of yField values, taking into account fields combined
+ * via legend drag-drop.
+ * @return {Number}
+ */
+ getYValueCount: function() {
+ return this.getYValueAccessors().length;
+ },
+
+ combine: function(index1, index2) {
+ var me = this,
+ accessors = me.getYValueAccessors(),
+ accessor1 = accessors[index1],
+ accessor2 = accessors[index2];
+
+ // Combine the yValue accessors for the two indexes into a single accessor that returns their sum
+ accessors[index2] = function(record) {
+ return accessor1(record) + accessor2(record);
+ };
+ accessors.splice(index1, 1);
+
+ me.callParent([index1, index2]);
+ },
+
+ clearCombinations: function() {
+ // Clear combined accessors, they'll get regenerated on next call to getYValueAccessors
+ delete this.yValueAccessors;
+ this.callParent();
+ },
+
+ /**
+ * @protected Returns an array of functions, each of which returns the value of the yField
+ * corresponding to function's index in the array, for a given record (each function takes the
+ * record as its only argument.) If yFields have been combined by the user via legend drag-drop,
+ * this list of accessors will be kept in sync with those combinations.
+ * @return {Array} array of accessor functions
+ */
+ getYValueAccessors: function() {
+ var me = this,
+ accessors = me.yValueAccessors;
+ if (!accessors) {
+ accessors = me.yValueAccessors = [];
+ Ext.each([].concat(me.yField), function(yField) {
+ accessors.push(function(record) {
+ return record.get(yField);
+ });
+ });
+ }
+ return accessors;
+ },
+
+ /**
+ * Calculate the min and max values for this series's xField.
+ * @return {Array} [min, max]
+ */
+ getMinMaxXValues: function() {
+ var me = this,
+ min, max,
+ xField = me.xField;
+
+ if (me.getRecordCount() > 0) {
+ min = Infinity;
+ max = -min;
+ me.eachRecord(function(record) {
+ var xValue = record.get(xField);
+ if (xValue > max) {
+ max = xValue;
+ }
+ if (xValue < min) {
+ min = xValue;
+ }
+ });
+ } else {
+ min = max = 0;
+ }
+ return [min, max];
+ },
+
+ /**
+ * Calculate the min and max values for this series's yField(s). Takes into account yField
+ * combinations, exclusions, and stacking.
+ * @return {Array} [min, max]
+ */
+ getMinMaxYValues: function() {
+ var me = this,
+ stacked = me.stacked,
+ min, max,
+ positiveTotal, negativeTotal;
+
+ function eachYValueStacked(yValue, i) {
+ if (!me.isExcluded(i)) {
+ if (yValue < 0) {
+ negativeTotal += yValue;
+ } else {
+ positiveTotal += yValue;
+ }
+ }
+ }
+
+ function eachYValue(yValue, i) {
+ if (!me.isExcluded(i)) {
+ if (yValue > max) {
+ max = yValue;
+ }
+ if (yValue < min) {
+ min = yValue;
+ }
+ }
+ }
+
+ if (me.getRecordCount() > 0) {
+ min = Infinity;
+ max = -min;
+ me.eachRecord(function(record) {
+ if (stacked) {
+ positiveTotal = 0;
+ negativeTotal = 0;
+ me.eachYValue(record, eachYValueStacked);
+ if (positiveTotal > max) {
+ max = positiveTotal;
+ }
+ if (negativeTotal < min) {
+ min = negativeTotal;
+ }
+ } else {
+ me.eachYValue(record, eachYValue);
+ }
+ });
+ } else {
+ min = max = 0;
+ }
+ return [min, max];
+ },
+
+ getAxesForXAndYFields: function() {
+ var me = this,
+ axes = me.chart.axes,
+ axis = [].concat(me.axis),
+ xAxis, yAxis;
+
+ if (Ext.Array.indexOf(axis, 'top') > -1) {
+ xAxis = 'top';
+ } else if (Ext.Array.indexOf(axis, 'bottom') > -1) {
+ xAxis = 'bottom';
+ } else {
+ if (axes.get('top')) {
+ xAxis = 'top';
+ } else if (axes.get('bottom')) {
+ xAxis = 'bottom';
+ }
+ }
+
+ if (Ext.Array.indexOf(axis, 'left') > -1) {
+ yAxis = 'left';
+ } else if (Ext.Array.indexOf(axis, 'right') > -1) {
+ yAxis = 'right';
+ } else {
+ if (axes.get('left')) {
+ yAxis = 'left';
+ } else if (axes.get('right')) {
+ yAxis = 'right';
+ }
+ }
+
+ return {
+ xAxis: xAxis,
+ yAxis: yAxis
+ };
+ }
+
+
+});
+
+/**
+ * @class Ext.chart.series.Area
+ * @extends Ext.chart.series.Cartesian
+ *
+ * Creates a Stacked Area Chart. The stacked area chart is useful when displaying multiple aggregated layers of information.
+ * As with all other series, the Area Series must be appended in the *series* Chart array configuration. See the Chart
+ * documentation for more information. A typical configuration object for the area series could be:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.JsonStore', {
+ * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+ * data: [
+ * { 'name': 'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13 },
+ * { 'name': 'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3 },
+ * { 'name': 'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7 },
+ * { 'name': 'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23 },
+ * { 'name': 'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33 }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 300,
+ * store: store,
+ * axes: [
+ * {
+ * type: 'Numeric',
+ * grid: true,
+ * position: 'left',
+ * fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
+ * title: 'Sample Values',
+ * grid: {
+ * odd: {
+ * opacity: 1,
+ * fill: '#ddd',
+ * stroke: '#bbb',
+ * 'stroke-width': 1
+ * }
+ * },
+ * minimum: 0,
+ * adjustMinimumByMajorUnit: 0
+ * },
+ * {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Sample Metrics',
+ * grid: true,
+ * label: {
+ * rotate: {
+ * degrees: 315
+ * }
+ * }
+ * }
+ * ],
+ * series: [{
+ * type: 'area',
+ * highlight: false,
+ * axis: 'left',
+ * xField: 'name',
+ * yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
+ * style: {
+ * opacity: 0.93
+ * }
+ * }]
+ * });
+ *
+ * In this configuration we set `area` as the type for the series, set highlighting options to true for highlighting elements on hover,
+ * take the left axis to measure the data in the area series, set as xField (x values) the name field of each element in the store,
+ * and as yFields (aggregated layers) seven data fields from the same store. Then we override some theming styles by adding some opacity
+ * to the style object.
+ *
+ * @xtype area
+ */
+Ext.define('Ext.chart.series.Area', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.series.Cartesian',
+
+ alias: 'series.area',
+
+ requires: ['Ext.chart.axis.Axis', 'Ext.draw.Color', 'Ext.fx.Anim'],
+
+ /* End Definitions */
+
+ type: 'area',
+
+ // @private Area charts are alyways stacked
+ stacked: true,
+
+ /**
+ * @cfg {Object} style
+ * Append styling properties to this object for it to override theme properties.
+ */
+ style: {},
+
+ constructor: function(config) {
+ this.callParent(arguments);
+ var me = this,
+ surface = me.chart.surface,
+ i, l;
+ Ext.apply(me, config, {
+ __excludes: [],
+ highlightCfg: {
+ lineWidth: 3,
+ stroke: '#55c',
+ opacity: 0.8,
+ color: '#f00'
+ }
+ });
+ if (me.highlight) {
+ me.highlightSprite = surface.add({
+ type: 'path',
+ path: ['M', 0, 0],
+ zIndex: 1000,
+ opacity: 0.3,
+ lineWidth: 5,
+ hidden: true,
+ stroke: '#444'
+ });
+ }
+ me.group = surface.getGroup(me.seriesId);
+ },
+
+ // @private Shrinks dataSets down to a smaller size
+ shrink: function(xValues, yValues, size) {
+ var len = xValues.length,
+ ratio = Math.floor(len / size),
+ i, j,
+ xSum = 0,
+ yCompLen = this.areas.length,
+ ySum = [],
+ xRes = [],
+ yRes = [];
+ //initialize array
+ for (j = 0; j < yCompLen; ++j) {
+ ySum[j] = 0;
+ }
+ for (i = 0; i < len; ++i) {
+ xSum += xValues[i];
+ for (j = 0; j < yCompLen; ++j) {
+ ySum[j] += yValues[i][j];
+ }
+ if (i % ratio == 0) {
+ //push averages
+ xRes.push(xSum/ratio);
+ for (j = 0; j < yCompLen; ++j) {
+ ySum[j] /= ratio;
+ }
+ yRes.push(ySum);
+ //reset sum accumulators
+ xSum = 0;
+ for (j = 0, ySum = []; j < yCompLen; ++j) {
+ ySum[j] = 0;
+ }
+ }
+ }
+ return {
+ x: xRes,
+ y: yRes
+ };
+ },
+
+ // @private Get chart and data boundaries
+ getBounds: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ areas = [].concat(me.yField),
+ areasLen = areas.length,
+ xValues = [],
+ yValues = [],
+ infinity = Infinity,
+ minX = infinity,
+ minY = infinity,
+ maxX = -infinity,
+ maxY = -infinity,
+ math = Math,
+ mmin = math.min,
+ mmax = math.max,
+ bbox, xScale, yScale, xValue, yValue, areaIndex, acumY, ln, sumValues, clipBox, areaElem;
+
+ me.setBBox();
+ bbox = me.bbox;
+
+ // Run through the axis
+ if (me.axis) {
+ axis = chart.axes.get(me.axis);
+ if (axis) {
+ out = axis.calcEnds();
+ minY = out.from || axis.prevMin;
+ maxY = mmax(out.to || axis.prevMax, 0);
+ }
+ }
+
+ if (me.yField && !Ext.isNumber(minY)) {
+ axis = Ext.create('Ext.chart.axis.Axis', {
+ chart: chart,
+ fields: [].concat(me.yField)
+ });
+ out = axis.calcEnds();
+ minY = out.from || axis.prevMin;
+ maxY = mmax(out.to || axis.prevMax, 0);
+ }
+
+ if (!Ext.isNumber(minY)) {
+ minY = 0;
+ }
+ if (!Ext.isNumber(maxY)) {
+ maxY = 0;
+ }
+
+ store.each(function(record, i) {
+ xValue = record.get(me.xField);
+ yValue = [];
+ if (typeof xValue != 'number') {
+ xValue = i;
+ }
+ xValues.push(xValue);
+ acumY = 0;
+ for (areaIndex = 0; areaIndex < areasLen; areaIndex++) {
+ areaElem = record.get(areas[areaIndex]);
+ if (typeof areaElem == 'number') {
+ minY = mmin(minY, areaElem);
+ yValue.push(areaElem);
+ acumY += areaElem;
+ }
+ }
+ minX = mmin(minX, xValue);
+ maxX = mmax(maxX, xValue);
+ maxY = mmax(maxY, acumY);
+ yValues.push(yValue);
+ }, me);
+
+ xScale = bbox.width / ((maxX - minX) || 1);
+ yScale = bbox.height / ((maxY - minY) || 1);
+
+ ln = xValues.length;
+ if ((ln > bbox.width) && me.areas) {
+ sumValues = me.shrink(xValues, yValues, bbox.width);
+ xValues = sumValues.x;
+ yValues = sumValues.y;
+ }
+
+ return {
+ bbox: bbox,
+ minX: minX,
+ minY: minY,
+ xValues: xValues,
+ yValues: yValues,
+ xScale: xScale,
+ yScale: yScale,
+ areasLen: areasLen
+ };
+ },
+
+ // @private Build an array of paths for the chart
+ getPaths: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ first = true,
+ bounds = me.getBounds(),
+ bbox = bounds.bbox,
+ items = me.items = [],
+ componentPaths = [],
+ componentPath,
+ paths = [],
+ i, ln, x, y, xValue, yValue, acumY, areaIndex, prevAreaIndex, areaElem, path;
+
+ ln = bounds.xValues.length;
+ // Start the path
+ for (i = 0; i < ln; i++) {
+ xValue = bounds.xValues[i];
+ yValue = bounds.yValues[i];
+ x = bbox.x + (xValue - bounds.minX) * bounds.xScale;
+ acumY = 0;
+ for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
+ // Excluded series
+ if (me.__excludes[areaIndex]) {
+ continue;
+ }
+ if (!componentPaths[areaIndex]) {
+ componentPaths[areaIndex] = [];
+ }
+ areaElem = yValue[areaIndex];
+ acumY += areaElem;
+ y = bbox.y + bbox.height - (acumY - bounds.minY) * bounds.yScale;
+ if (!paths[areaIndex]) {
+ paths[areaIndex] = ['M', x, y];
+ componentPaths[areaIndex].push(['L', x, y]);
+ } else {
+ paths[areaIndex].push('L', x, y);
+ componentPaths[areaIndex].push(['L', x, y]);
+ }
+ if (!items[areaIndex]) {
+ items[areaIndex] = {
+ pointsUp: [],
+ pointsDown: [],
+ series: me
+ };
+ }
+ items[areaIndex].pointsUp.push([x, y]);
+ }
+ }
+
+ // Close the paths
+ for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
+ // Excluded series
+ if (me.__excludes[areaIndex]) {
+ continue;
+ }
+ path = paths[areaIndex];
+ // Close bottom path to the axis
+ if (areaIndex == 0 || first) {
+ first = false;
+ path.push('L', x, bbox.y + bbox.height,
+ 'L', bbox.x, bbox.y + bbox.height,
+ 'Z');
+ }
+ // Close other paths to the one before them
+ else {
+ componentPath = componentPaths[prevAreaIndex];
+ componentPath.reverse();
+ path.push('L', x, componentPath[0][2]);
+ for (i = 0; i < ln; i++) {
+ path.push(componentPath[i][0],
+ componentPath[i][1],
+ componentPath[i][2]);
+ items[areaIndex].pointsDown[ln -i -1] = [componentPath[i][1], componentPath[i][2]];
+ }
+ path.push('L', bbox.x, path[2], 'Z');
+ }
+ prevAreaIndex = areaIndex;
+ }
+ return {
+ paths: paths,
+ areasLen: bounds.areasLen
+ };
+ },
+
+ /**
+ * Draws the series for the current chart.
+ */
+ drawSeries: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ surface = chart.surface,
+ animate = chart.animate,
+ group = me.group,
+ endLineStyle = Ext.apply(me.seriesStyle, me.style),
+ colorArrayStyle = me.colorArrayStyle,
+ colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
+ areaIndex, areaElem, paths, path, rendererAttributes;
+
+ me.unHighlightItem();
+ me.cleanHighlights();
+
+ if (!store || !store.getCount()) {
+ return;
+ }
+
+ paths = me.getPaths();
+
+ if (!me.areas) {
+ me.areas = [];
+ }
+
+ for (areaIndex = 0; areaIndex < paths.areasLen; areaIndex++) {
+ // Excluded series
+ if (me.__excludes[areaIndex]) {
+ continue;
+ }
+ if (!me.areas[areaIndex]) {
+ me.items[areaIndex].sprite = me.areas[areaIndex] = surface.add(Ext.apply({}, {
+ type: 'path',
+ group: group,
+ // 'clip-rect': me.clipBox,
+ path: paths.paths[areaIndex],
+ stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength],
+ fill: colorArrayStyle[areaIndex % colorArrayLength]
+ }, endLineStyle || {}));
+ }
+ areaElem = me.areas[areaIndex];
+ path = paths.paths[areaIndex];
+ if (animate) {
+ //Add renderer to line. There is not a unique record associated with this.
+ rendererAttributes = me.renderer(areaElem, false, {
+ path: path,
+ // 'clip-rect': me.clipBox,
+ fill: colorArrayStyle[areaIndex % colorArrayLength],
+ stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
+ }, areaIndex, store);
+ //fill should not be used here but when drawing the special fill path object
+ me.animation = me.onAnimate(areaElem, {
+ to: rendererAttributes
+ });
+ } else {
+ rendererAttributes = me.renderer(areaElem, false, {
+ path: path,
+ // 'clip-rect': me.clipBox,
+ hidden: false,
+ fill: colorArrayStyle[areaIndex % colorArrayLength],
+ stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
+ }, areaIndex, store);
+ me.areas[areaIndex].setAttributes(rendererAttributes, true);
+ }
+ }
+ me.renderLabels();
+ me.renderCallouts();
+ },
+
+ // @private
+ onAnimate: function(sprite, attr) {
+ sprite.show();
+ return this.callParent(arguments);
+ },
+
+ // @private
+ onCreateLabel: function(storeItem, item, i, display) {
+ var me = this,
+ group = me.labelsGroup,
+ config = me.label,
+ bbox = me.bbox,
+ endLabelStyle = Ext.apply(config, me.seriesLabelStyle);
+
+ return me.chart.surface.add(Ext.apply({
+ 'type': 'text',
+ 'text-anchor': 'middle',
+ 'group': group,
+ 'x': item.point[0],
+ 'y': bbox.y + bbox.height / 2
+ }, endLabelStyle || {}));
+ },
+
+ // @private
+ onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
+ var me = this,
+ chart = me.chart,
+ resizing = chart.resizing,
+ config = me.label,
+ format = config.renderer,
+ field = config.field,
+ bbox = me.bbox,
+ x = item.point[0],
+ y = item.point[1],
+ bb, width, height;
+
+ label.setAttributes({
+ text: format(storeItem.get(field[index])),
+ hidden: true
+ }, true);
+
+ bb = label.getBBox();
+ width = bb.width / 2;
+ height = bb.height / 2;
+
+ x = x - width < bbox.x? bbox.x + width : x;
+ x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
+ y = y - height < bbox.y? bbox.y + height : y;
+ y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
+
+ if (me.chart.animate && !me.chart.resizing) {
+ label.show(true);
+ me.onAnimate(label, {
+ to: {
+ x: x,
+ y: y
+ }
+ });
+ } else {
+ label.setAttributes({
+ x: x,
+ y: y
+ }, true);
+ if (resizing) {
+ me.animation.on('afteranimate', function() {
+ label.show(true);
+ });
+ } else {
+ label.show(true);
+ }
+ }
+ },
+
+ // @private
+ onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
+ var me = this,
+ chart = me.chart,
+ surface = chart.surface,
+ resizing = chart.resizing,
+ config = me.callouts,
+ items = me.items,
+ prev = (i == 0) ? false : items[i -1].point,
+ next = (i == items.length -1) ? false : items[i +1].point,
+ cur = item.point,
+ dir, norm, normal, a, aprev, anext,
+ bbox = callout.label.getBBox(),
+ offsetFromViz = 30,
+ offsetToSide = 10,
+ offsetBox = 3,
+ boxx, boxy, boxw, boxh,
+ p, clipRect = me.clipRect,
+ x, y;
+
+ //get the right two points
+ if (!prev) {
+ prev = cur;
+ }
+ if (!next) {
+ next = cur;
+ }
+ a = (next[1] - prev[1]) / (next[0] - prev[0]);
+ aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]);
+ anext = (next[1] - cur[1]) / (next[0] - cur[0]);
+
+ norm = Math.sqrt(1 + a * a);
+ dir = [1 / norm, a / norm];
+ normal = [-dir[1], dir[0]];
+
+ //keep the label always on the outer part of the "elbow"
+ if (aprev > 0 && anext < 0 && normal[1] < 0 || aprev < 0 && anext > 0 && normal[1] > 0) {
+ normal[0] *= -1;
+ normal[1] *= -1;
+ } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) {
+ normal[0] *= -1;
+ normal[1] *= -1;
+ }
+
+ //position
+ x = cur[0] + normal[0] * offsetFromViz;
+ y = cur[1] + normal[1] * offsetFromViz;
+
+ //box position and dimensions
+ boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
+ boxy = y - bbox.height /2 - offsetBox;
+ boxw = bbox.width + 2 * offsetBox;
+ boxh = bbox.height + 2 * offsetBox;
+
+ //now check if we're out of bounds and invert the normal vector correspondingly
+ //this may add new overlaps between labels (but labels won't be out of bounds).
+ if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
+ normal[0] *= -1;
+ }
+ if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
+ normal[1] *= -1;
+ }
+
+ //update positions
+ x = cur[0] + normal[0] * offsetFromViz;
+ y = cur[1] + normal[1] * offsetFromViz;
+
+ //update box position and dimensions
+ boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
+ boxy = y - bbox.height /2 - offsetBox;
+ boxw = bbox.width + 2 * offsetBox;
+ boxh = bbox.height + 2 * offsetBox;
+
+ //set the line from the middle of the pie to the box.
+ callout.lines.setAttributes({
+ path: ["M", cur[0], cur[1], "L", x, y, "Z"]
+ }, true);
+ //set box position
+ callout.box.setAttributes({
+ x: boxx,
+ y: boxy,
+ width: boxw,
+ height: boxh
+ }, true);
+ //set text position
+ callout.label.setAttributes({
+ x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
+ y: y
+ }, true);
+ for (p in callout) {
+ callout[p].show(true);
+ }
+ },
+
+ isItemInPoint: function(x, y, item, i) {
+ var me = this,
+ pointsUp = item.pointsUp,
+ pointsDown = item.pointsDown,
+ abs = Math.abs,
+ dist = Infinity, p, pln, point;
+
+ for (p = 0, pln = pointsUp.length; p < pln; p++) {
+ point = [pointsUp[p][0], pointsUp[p][1]];
+ if (dist > abs(x - point[0])) {
+ dist = abs(x - point[0]);
+ } else {
+ point = pointsUp[p -1];
+ if (y >= point[1] && (!pointsDown.length || y <= (pointsDown[p -1][1]))) {
+ item.storeIndex = p -1;
+ item.storeField = me.yField[i];
+ item.storeItem = me.chart.store.getAt(p -1);
+ item._points = pointsDown.length? [point, pointsDown[p -1]] : [point];
+ return true;
+ } else {
+ break;
+ }
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Highlight this entire series.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
+ */
+ highlightSeries: function() {
+ var area, to, fillColor;
+ if (this._index !== undefined) {
+ area = this.areas[this._index];
+ if (area.__highlightAnim) {
+ area.__highlightAnim.paused = true;
+ }
+ area.__highlighted = true;
+ area.__prevOpacity = area.__prevOpacity || area.attr.opacity || 1;
+ area.__prevFill = area.__prevFill || area.attr.fill;
+ area.__prevLineWidth = area.__prevLineWidth || area.attr.lineWidth;
+ fillColor = Ext.draw.Color.fromString(area.__prevFill);
+ to = {
+ lineWidth: (area.__prevLineWidth || 0) + 2
+ };
+ if (fillColor) {
+ to.fill = fillColor.getLighter(0.2).toString();
+ }
+ else {
+ to.opacity = Math.max(area.__prevOpacity - 0.3, 0);
+ }
+ if (this.chart.animate) {
+ area.__highlightAnim = Ext.create('Ext.fx.Anim', Ext.apply({
+ target: area,
+ to: to
+ }, this.chart.animate));
+ }
+ else {
+ area.setAttributes(to, true);
+ }
+ }
+ },
+
+ /**
+ * UnHighlight this entire series.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
+ */
+ unHighlightSeries: function() {
+ var area;
+ if (this._index !== undefined) {
+ area = this.areas[this._index];
+ if (area.__highlightAnim) {
+ area.__highlightAnim.paused = true;
+ }
+ if (area.__highlighted) {
+ area.__highlighted = false;
+ area.__highlightAnim = Ext.create('Ext.fx.Anim', {
+ target: area,
+ to: {
+ fill: area.__prevFill,
+ opacity: area.__prevOpacity,
+ lineWidth: area.__prevLineWidth
+ }
+ });
+ }
+ }
+ },
+
+ /**
+ * Highlight the specified item. If no item is provided the whole series will be highlighted.
+ * @param item {Object} Info about the item; same format as returned by #getItemForPoint
+ */
+ highlightItem: function(item) {
+ var me = this,
+ points, path;
+ if (!item) {
+ this.highlightSeries();
+ return;
+ }
+ points = item._points;
+ path = points.length == 2? ['M', points[0][0], points[0][1], 'L', points[1][0], points[1][1]]
+ : ['M', points[0][0], points[0][1], 'L', points[0][0], me.bbox.y + me.bbox.height];
+ me.highlightSprite.setAttributes({
+ path: path,
+ hidden: false
+ }, true);
+ },
+
+ /**
+ * Un-highlights the specified item. If no item is provided it will un-highlight the entire series.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint
+ */
+ unHighlightItem: function(item) {
+ if (!item) {
+ this.unHighlightSeries();
+ }
+
+ if (this.highlightSprite) {
+ this.highlightSprite.hide(true);
+ }
+ },
+
+ // @private
+ hideAll: function() {
+ if (!isNaN(this._index)) {
+ this.__excludes[this._index] = true;
+ this.areas[this._index].hide(true);
+ this.drawSeries();
+ }
+ },
+
+ // @private
+ showAll: function() {
+ if (!isNaN(this._index)) {
+ this.__excludes[this._index] = false;
+ this.areas[this._index].show(true);
+ this.drawSeries();
+ }
+ },
+
+ /**
+ * Returns the color of the series (to be displayed as color for the series legend item).
+ * @param item {Object} Info about the item; same format as returned by #getItemForPoint
+ */
+ getLegendColor: function(index) {
+ var me = this;
+ return me.colorArrayStyle[index % me.colorArrayStyle.length];
+ }
+});
+/**
+ * @class Ext.chart.series.Area
+ * @extends Ext.chart.series.Cartesian
+ *
+ * Creates a Stacked Area Chart. The stacked area chart is useful when displaying multiple aggregated layers of information.
+ * As with all other series, the Area Series must be appended in the *series* Chart array configuration. See the Chart
+ * documentation for more information. A typical configuration object for the area series could be:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.JsonStore', {
+ * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+ * data: [
+ * { 'name': 'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13 },
+ * { 'name': 'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3 },
+ * { 'name': 'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7 },
+ * { 'name': 'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23 },
+ * { 'name': 'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33 }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 300,
+ * store: store,
+ * axes: [
+ * {
+ * type: 'Numeric',
+ * grid: true,
+ * position: 'left',
+ * fields: ['data1', 'data2', 'data3', 'data4', 'data5'],
+ * title: 'Sample Values',
+ * grid: {
+ * odd: {
+ * opacity: 1,
+ * fill: '#ddd',
+ * stroke: '#bbb',
+ * 'stroke-width': 1
+ * }
+ * },
+ * minimum: 0,
+ * adjustMinimumByMajorUnit: 0
+ * },
+ * {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Sample Metrics',
+ * grid: true,
+ * label: {
+ * rotate: {
+ * degrees: 315
+ * }
+ * }
+ * }
+ * ],
+ * series: [{
+ * type: 'area',
+ * highlight: false,
+ * axis: 'left',
+ * xField: 'name',
+ * yField: ['data1', 'data2', 'data3', 'data4', 'data5'],
+ * style: {
+ * opacity: 0.93
+ * }
+ * }]
+ * });
+ *
+ * In this configuration we set `area` as the type for the series, set highlighting options to true for highlighting elements on hover,
+ * take the left axis to measure the data in the area series, set as xField (x values) the name field of each element in the store,
+ * and as yFields (aggregated layers) seven data fields from the same store. Then we override some theming styles by adding some opacity
+ * to the style object.
+ *
+ * @xtype area
+ */
+Ext.define('Ext.chart.series.Area', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.series.Cartesian',
+
+ alias: 'series.area',
+
+ requires: ['Ext.chart.axis.Axis', 'Ext.draw.Color', 'Ext.fx.Anim'],
+
+ /* End Definitions */
+
+ type: 'area',
+
+ // @private Area charts are alyways stacked
+ stacked: true,
+
+ /**
+ * @cfg {Object} style
+ * Append styling properties to this object for it to override theme properties.
+ */
+ style: {},
+
+ constructor: function(config) {
+ this.callParent(arguments);
+ var me = this,
+ surface = me.chart.surface,
+ i, l;
+ Ext.apply(me, config, {
+ __excludes: [],
+ highlightCfg: {
+ lineWidth: 3,
+ stroke: '#55c',
+ opacity: 0.8,
+ color: '#f00'
+ }
+ });
+ if (me.highlight) {
+ me.highlightSprite = surface.add({
+ type: 'path',
+ path: ['M', 0, 0],
+ zIndex: 1000,
+ opacity: 0.3,
+ lineWidth: 5,
+ hidden: true,
+ stroke: '#444'
+ });
+ }
+ me.group = surface.getGroup(me.seriesId);
+ },
+
+ // @private Shrinks dataSets down to a smaller size
+ shrink: function(xValues, yValues, size) {
+ var len = xValues.length,
+ ratio = Math.floor(len / size),
+ i, j,
+ xSum = 0,
+ yCompLen = this.areas.length,
+ ySum = [],
+ xRes = [],
+ yRes = [];
+ //initialize array
+ for (j = 0; j < yCompLen; ++j) {
+ ySum[j] = 0;
+ }
+ for (i = 0; i < len; ++i) {
+ xSum += xValues[i];
+ for (j = 0; j < yCompLen; ++j) {
+ ySum[j] += yValues[i][j];
+ }
+ if (i % ratio == 0) {
+ //push averages
+ xRes.push(xSum/ratio);
+ for (j = 0; j < yCompLen; ++j) {
+ ySum[j] /= ratio;
+ }
+ yRes.push(ySum);
+ //reset sum accumulators
+ xSum = 0;
+ for (j = 0, ySum = []; j < yCompLen; ++j) {
+ ySum[j] = 0;
+ }
+ }
+ }
+ return {
+ x: xRes,
+ y: yRes
+ };
+ },
+
+ // @private Get chart and data boundaries
+ getBounds: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ areas = [].concat(me.yField),
+ areasLen = areas.length,
+ xValues = [],
+ yValues = [],
+ infinity = Infinity,
+ minX = infinity,
+ minY = infinity,
+ maxX = -infinity,
+ maxY = -infinity,
+ math = Math,
+ mmin = math.min,
+ mmax = math.max,
+ bbox, xScale, yScale, xValue, yValue, areaIndex, acumY, ln, sumValues, clipBox, areaElem;
+
+ me.setBBox();
+ bbox = me.bbox;
+
+ // Run through the axis
+ if (me.axis) {
+ axis = chart.axes.get(me.axis);
+ if (axis) {
+ out = axis.calcEnds();
+ minY = out.from || axis.prevMin;
+ maxY = mmax(out.to || axis.prevMax, 0);
+ }
+ }
+
+ if (me.yField && !Ext.isNumber(minY)) {
+ axis = Ext.create('Ext.chart.axis.Axis', {
+ chart: chart,
+ fields: [].concat(me.yField)
+ });
+ out = axis.calcEnds();
+ minY = out.from || axis.prevMin;
+ maxY = mmax(out.to || axis.prevMax, 0);
+ }
+
+ if (!Ext.isNumber(minY)) {
+ minY = 0;
+ }
+ if (!Ext.isNumber(maxY)) {
+ maxY = 0;
+ }
+
+ store.each(function(record, i) {
+ xValue = record.get(me.xField);
+ yValue = [];
+ if (typeof xValue != 'number') {
+ xValue = i;
+ }
+ xValues.push(xValue);
+ acumY = 0;
+ for (areaIndex = 0; areaIndex < areasLen; areaIndex++) {
+ areaElem = record.get(areas[areaIndex]);
+ if (typeof areaElem == 'number') {
+ minY = mmin(minY, areaElem);
+ yValue.push(areaElem);
+ acumY += areaElem;
+ }
+ }
+ minX = mmin(minX, xValue);
+ maxX = mmax(maxX, xValue);
+ maxY = mmax(maxY, acumY);
+ yValues.push(yValue);
+ }, me);
+
+ xScale = bbox.width / ((maxX - minX) || 1);
+ yScale = bbox.height / ((maxY - minY) || 1);
+
+ ln = xValues.length;
+ if ((ln > bbox.width) && me.areas) {
+ sumValues = me.shrink(xValues, yValues, bbox.width);
+ xValues = sumValues.x;
+ yValues = sumValues.y;
+ }
+
+ return {
+ bbox: bbox,
+ minX: minX,
+ minY: minY,
+ xValues: xValues,
+ yValues: yValues,
+ xScale: xScale,
+ yScale: yScale,
+ areasLen: areasLen
+ };
+ },
+
+ // @private Build an array of paths for the chart
+ getPaths: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ first = true,
+ bounds = me.getBounds(),
+ bbox = bounds.bbox,
+ items = me.items = [],
+ componentPaths = [],
+ componentPath,
+ paths = [],
+ i, ln, x, y, xValue, yValue, acumY, areaIndex, prevAreaIndex, areaElem, path;
+
+ ln = bounds.xValues.length;
+ // Start the path
+ for (i = 0; i < ln; i++) {
+ xValue = bounds.xValues[i];
+ yValue = bounds.yValues[i];
+ x = bbox.x + (xValue - bounds.minX) * bounds.xScale;
+ acumY = 0;
+ for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
+ // Excluded series
+ if (me.__excludes[areaIndex]) {
+ continue;
+ }
+ if (!componentPaths[areaIndex]) {
+ componentPaths[areaIndex] = [];
+ }
+ areaElem = yValue[areaIndex];
+ acumY += areaElem;
+ y = bbox.y + bbox.height - (acumY - bounds.minY) * bounds.yScale;
+ if (!paths[areaIndex]) {
+ paths[areaIndex] = ['M', x, y];
+ componentPaths[areaIndex].push(['L', x, y]);
+ } else {
+ paths[areaIndex].push('L', x, y);
+ componentPaths[areaIndex].push(['L', x, y]);
+ }
+ if (!items[areaIndex]) {
+ items[areaIndex] = {
+ pointsUp: [],
+ pointsDown: [],
+ series: me
+ };
+ }
+ items[areaIndex].pointsUp.push([x, y]);
+ }
+ }
+
+ // Close the paths
+ for (areaIndex = 0; areaIndex < bounds.areasLen; areaIndex++) {
+ // Excluded series
+ if (me.__excludes[areaIndex]) {
+ continue;
+ }
+ path = paths[areaIndex];
+ // Close bottom path to the axis
+ if (areaIndex == 0 || first) {
+ first = false;
+ path.push('L', x, bbox.y + bbox.height,
+ 'L', bbox.x, bbox.y + bbox.height,
+ 'Z');
+ }
+ // Close other paths to the one before them
+ else {
+ componentPath = componentPaths[prevAreaIndex];
+ componentPath.reverse();
+ path.push('L', x, componentPath[0][2]);
+ for (i = 0; i < ln; i++) {
+ path.push(componentPath[i][0],
+ componentPath[i][1],
+ componentPath[i][2]);
+ items[areaIndex].pointsDown[ln -i -1] = [componentPath[i][1], componentPath[i][2]];
+ }
+ path.push('L', bbox.x, path[2], 'Z');
+ }
+ prevAreaIndex = areaIndex;
+ }
+ return {
+ paths: paths,
+ areasLen: bounds.areasLen
+ };
+ },
+
+ /**
+ * Draws the series for the current chart.
+ */
+ drawSeries: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ surface = chart.surface,
+ animate = chart.animate,
+ group = me.group,
+ endLineStyle = Ext.apply(me.seriesStyle, me.style),
+ colorArrayStyle = me.colorArrayStyle,
+ colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
+ areaIndex, areaElem, paths, path, rendererAttributes;
+
+ me.unHighlightItem();
+ me.cleanHighlights();
+
+ if (!store || !store.getCount()) {
+ return;
+ }
+
+ paths = me.getPaths();
+
+ if (!me.areas) {
+ me.areas = [];
+ }
+
+ for (areaIndex = 0; areaIndex < paths.areasLen; areaIndex++) {
+ // Excluded series
+ if (me.__excludes[areaIndex]) {
+ continue;
+ }
+ if (!me.areas[areaIndex]) {
+ me.items[areaIndex].sprite = me.areas[areaIndex] = surface.add(Ext.apply({}, {
+ type: 'path',
+ group: group,
+ // 'clip-rect': me.clipBox,
+ path: paths.paths[areaIndex],
+ stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength],
+ fill: colorArrayStyle[areaIndex % colorArrayLength]
+ }, endLineStyle || {}));
+ }
+ areaElem = me.areas[areaIndex];
+ path = paths.paths[areaIndex];
+ if (animate) {
+ //Add renderer to line. There is not a unique record associated with this.
+ rendererAttributes = me.renderer(areaElem, false, {
+ path: path,
+ // 'clip-rect': me.clipBox,
+ fill: colorArrayStyle[areaIndex % colorArrayLength],
+ stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
+ }, areaIndex, store);
+ //fill should not be used here but when drawing the special fill path object
+ me.animation = me.onAnimate(areaElem, {
+ to: rendererAttributes
+ });
+ } else {
+ rendererAttributes = me.renderer(areaElem, false, {
+ path: path,
+ // 'clip-rect': me.clipBox,
+ hidden: false,
+ fill: colorArrayStyle[areaIndex % colorArrayLength],
+ stroke: endLineStyle.stroke || colorArrayStyle[areaIndex % colorArrayLength]
+ }, areaIndex, store);
+ me.areas[areaIndex].setAttributes(rendererAttributes, true);
+ }
+ }
+ me.renderLabels();
+ me.renderCallouts();
+ },
+
+ // @private
+ onAnimate: function(sprite, attr) {
+ sprite.show();
+ return this.callParent(arguments);
+ },
+
+ // @private
+ onCreateLabel: function(storeItem, item, i, display) {
+ var me = this,
+ group = me.labelsGroup,
+ config = me.label,
+ bbox = me.bbox,
+ endLabelStyle = Ext.apply(config, me.seriesLabelStyle);
+
+ return me.chart.surface.add(Ext.apply({
+ 'type': 'text',
+ 'text-anchor': 'middle',
+ 'group': group,
+ 'x': item.point[0],
+ 'y': bbox.y + bbox.height / 2
+ }, endLabelStyle || {}));
+ },
+
+ // @private
+ onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
+ var me = this,
+ chart = me.chart,
+ resizing = chart.resizing,
+ config = me.label,
+ format = config.renderer,
+ field = config.field,
+ bbox = me.bbox,
+ x = item.point[0],
+ y = item.point[1],
+ bb, width, height;
+
+ label.setAttributes({
+ text: format(storeItem.get(field[index])),
+ hidden: true
+ }, true);
+
+ bb = label.getBBox();
+ width = bb.width / 2;
+ height = bb.height / 2;
+
+ x = x - width < bbox.x? bbox.x + width : x;
+ x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
+ y = y - height < bbox.y? bbox.y + height : y;
+ y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
+
+ if (me.chart.animate && !me.chart.resizing) {
+ label.show(true);
+ me.onAnimate(label, {
+ to: {
+ x: x,
+ y: y
+ }
+ });
+ } else {
+ label.setAttributes({
+ x: x,
+ y: y
+ }, true);
+ if (resizing) {
+ me.animation.on('afteranimate', function() {
+ label.show(true);
+ });
+ } else {
+ label.show(true);
+ }
+ }
+ },
+
+ // @private
+ onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
+ var me = this,
+ chart = me.chart,
+ surface = chart.surface,
+ resizing = chart.resizing,
+ config = me.callouts,
+ items = me.items,
+ prev = (i == 0) ? false : items[i -1].point,
+ next = (i == items.length -1) ? false : items[i +1].point,
+ cur = item.point,
+ dir, norm, normal, a, aprev, anext,
+ bbox = callout.label.getBBox(),
+ offsetFromViz = 30,
+ offsetToSide = 10,
+ offsetBox = 3,
+ boxx, boxy, boxw, boxh,
+ p, clipRect = me.clipRect,
+ x, y;
+
+ //get the right two points
+ if (!prev) {
+ prev = cur;
+ }
+ if (!next) {
+ next = cur;
+ }
+ a = (next[1] - prev[1]) / (next[0] - prev[0]);
+ aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]);
+ anext = (next[1] - cur[1]) / (next[0] - cur[0]);
+
+ norm = Math.sqrt(1 + a * a);
+ dir = [1 / norm, a / norm];
+ normal = [-dir[1], dir[0]];
+
+ //keep the label always on the outer part of the "elbow"
+ if (aprev > 0 && anext < 0 && normal[1] < 0 || aprev < 0 && anext > 0 && normal[1] > 0) {
+ normal[0] *= -1;
+ normal[1] *= -1;
+ } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0 || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) {
+ normal[0] *= -1;
+ normal[1] *= -1;
+ }
+
+ //position
+ x = cur[0] + normal[0] * offsetFromViz;
+ y = cur[1] + normal[1] * offsetFromViz;
+
+ //box position and dimensions
+ boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
+ boxy = y - bbox.height /2 - offsetBox;
+ boxw = bbox.width + 2 * offsetBox;
+ boxh = bbox.height + 2 * offsetBox;
+
+ //now check if we're out of bounds and invert the normal vector correspondingly
+ //this may add new overlaps between labels (but labels won't be out of bounds).
+ if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
+ normal[0] *= -1;
+ }
+ if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
+ normal[1] *= -1;
+ }
+
+ //update positions
+ x = cur[0] + normal[0] * offsetFromViz;
+ y = cur[1] + normal[1] * offsetFromViz;
+
+ //update box position and dimensions
+ boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
+ boxy = y - bbox.height /2 - offsetBox;
+ boxw = bbox.width + 2 * offsetBox;
+ boxh = bbox.height + 2 * offsetBox;
+
+ //set the line from the middle of the pie to the box.
+ callout.lines.setAttributes({
+ path: ["M", cur[0], cur[1], "L", x, y, "Z"]
+ }, true);
+ //set box position
+ callout.box.setAttributes({
+ x: boxx,
+ y: boxy,
+ width: boxw,
+ height: boxh
+ }, true);
+ //set text position
+ callout.label.setAttributes({
+ x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
+ y: y
+ }, true);
+ for (p in callout) {
+ callout[p].show(true);
+ }
+ },
+
+ isItemInPoint: function(x, y, item, i) {
+ var me = this,
+ pointsUp = item.pointsUp,
+ pointsDown = item.pointsDown,
+ abs = Math.abs,
+ dist = Infinity, p, pln, point;
+
+ for (p = 0, pln = pointsUp.length; p < pln; p++) {
+ point = [pointsUp[p][0], pointsUp[p][1]];
+ if (dist > abs(x - point[0])) {
+ dist = abs(x - point[0]);
+ } else {
+ point = pointsUp[p -1];
+ if (y >= point[1] && (!pointsDown.length || y <= (pointsDown[p -1][1]))) {
+ item.storeIndex = p -1;
+ item.storeField = me.yField[i];
+ item.storeItem = me.chart.store.getAt(p -1);
+ item._points = pointsDown.length? [point, pointsDown[p -1]] : [point];
+ return true;
+ } else {
+ break;
+ }
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Highlight this entire series.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
+ */
+ highlightSeries: function() {
+ var area, to, fillColor;
+ if (this._index !== undefined) {
+ area = this.areas[this._index];
+ if (area.__highlightAnim) {
+ area.__highlightAnim.paused = true;
+ }
+ area.__highlighted = true;
+ area.__prevOpacity = area.__prevOpacity || area.attr.opacity || 1;
+ area.__prevFill = area.__prevFill || area.attr.fill;
+ area.__prevLineWidth = area.__prevLineWidth || area.attr.lineWidth;
+ fillColor = Ext.draw.Color.fromString(area.__prevFill);
+ to = {
+ lineWidth: (area.__prevLineWidth || 0) + 2
+ };
+ if (fillColor) {
+ to.fill = fillColor.getLighter(0.2).toString();
+ }
+ else {
+ to.opacity = Math.max(area.__prevOpacity - 0.3, 0);
+ }
+ if (this.chart.animate) {
+ area.__highlightAnim = Ext.create('Ext.fx.Anim', Ext.apply({
+ target: area,
+ to: to
+ }, this.chart.animate));
+ }
+ else {
+ area.setAttributes(to, true);
+ }
+ }
+ },
+
+ /**
+ * UnHighlight this entire series.
+ * @param {Object} item Info about the item; same format as returned by #getItemForPoint.
+ */
+ unHighlightSeries: function() {
+ var area;
+ if (this._index !== undefined) {
+ area = this.areas[this._index];
+ if (area.__highlightAnim) {
+ area.__highlightAnim.paused = true;
+ }
+ if (area.__highlighted) {
+ area.__highlighted = false;
+ area.__highlightAnim = Ext.create('Ext.fx.Anim', {
+ target: area,
+ to: {
+ fill: area.__prevFill,
+ opacity: area.__prevOpacity,
+ lineWidth: area.__prevLineWidth
+ }
+ });
+ }
+ }
+ },
+
+ /**
+ * Highlight the specified item. If no item is provided the whole series will be highlighted.
+ * @param item {Object} Info about the item; same format as returned by #getItemForPoint
+ */
+ highlightItem: function(item) {
+ var me = this,
+ points, path;
+ if (!item) {
+ this.highlightSeries();
+ return;
+ }
+ points = item._points;
+ path = points.length == 2? ['M', points[0][0], points[0][1], 'L', points[1][0], points[1][1]]
+ : ['M', points[0][0], points[0][1], 'L', points[0][0], me.bbox.y + me.bbox.height];
+ me.highlightSprite.setAttributes({
+ path: path,
+ hidden: false
+ }, true);
+ },
+
+ /**
+ * un-highlights the specified item. If no item is provided it will un-highlight the entire series.
+ * @param item {Object} Info about the item; same format as returned by #getItemForPoint
+ */
+ unHighlightItem: function(item) {
+ if (!item) {
+ this.unHighlightSeries();
+ }
+
+ if (this.highlightSprite) {
+ this.highlightSprite.hide(true);
+ }
+ },
+
+ // @private
+ hideAll: function() {
+ if (!isNaN(this._index)) {
+ this.__excludes[this._index] = true;
+ this.areas[this._index].hide(true);
+ this.drawSeries();
+ }
+ },
+
+ // @private
+ showAll: function() {
+ if (!isNaN(this._index)) {
+ this.__excludes[this._index] = false;
+ this.areas[this._index].show(true);
+ this.drawSeries();
+ }
+ },
+
+ /**
+ * Returns the color of the series (to be displayed as color for the series legend item).
+ * @param item {Object} Info about the item; same format as returned by #getItemForPoint
+ */
+ getLegendColor: function(index) {
+ var me = this;
+ return me.colorArrayStyle[index % me.colorArrayStyle.length];
+ }
+});
+
+/**
+ * Creates a Bar Chart. A Bar Chart is a useful visualization technique to display quantitative information for
+ * different categories that can show some progression (or regression) in the dataset. As with all other series, the Bar
+ * Series must be appended in the *series* Chart array configuration. See the Chart documentation for more information.
+ * A typical configuration object for the bar series could be:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.JsonStore', {
+ * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+ * data: [
+ * { 'name': 'metric one', 'data1':10, 'data2':12, 'data3':14, 'data4':8, 'data5':13 },
+ * { 'name': 'metric two', 'data1':7, 'data2':8, 'data3':16, 'data4':10, 'data5':3 },
+ * { 'name': 'metric three', 'data1':5, 'data2':2, 'data3':14, 'data4':12, 'data5':7 },
+ * { 'name': 'metric four', 'data1':2, 'data2':14, 'data3':6, 'data4':1, 'data5':23 },
+ * { 'name': 'metric five', 'data1':27, 'data2':38, 'data3':36, 'data4':13, 'data5':33 }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 300,
+ * animate: true,
+ * store: store,
+ * axes: [{
+ * type: 'Numeric',
+ * position: 'bottom',
+ * fields: ['data1'],
+ * label: {
+ * renderer: Ext.util.Format.numberRenderer('0,0')
+ * },
+ * title: 'Sample Values',
+ * grid: true,
+ * minimum: 0
+ * }, {
+ * type: 'Category',
+ * position: 'left',
+ * fields: ['name'],
+ * title: 'Sample Metrics'
+ * }],
+ * series: [{
+ * type: 'bar',
+ * axis: 'bottom',
+ * highlight: true,
+ * tips: {
+ * trackMouse: true,
+ * width: 140,
+ * height: 28,
+ * renderer: function(storeItem, item) {
+ * this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' views');
+ * }
+ * },
+ * label: {
+ * display: 'insideEnd',
+ * field: 'data1',
+ * renderer: Ext.util.Format.numberRenderer('0'),
+ * orientation: 'horizontal',
+ * color: '#333',
+ * 'text-anchor': 'middle'
+ * },
+ * xField: 'name',
+ * yField: ['data1']
+ * }]
+ * });
+ *
+ * In this configuration we set `bar` as the series type, bind the values of the bar to the bottom axis and set the
+ * xField or category field to the `name` parameter of the store. We also set `highlight` to true which enables smooth
+ * animations when bars are hovered. We also set some configuration for the bar labels to be displayed inside the bar,
+ * to display the information found in the `data1` property of each element store, to render a formated text with the
+ * `Ext.util.Format` we pass in, to have an `horizontal` orientation (as opposed to a vertical one) and we also set
+ * other styles like `color`, `text-anchor`, etc.
+ */
+Ext.define('Ext.chart.series.Bar', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.series.Cartesian',
+
+ alternateClassName: ['Ext.chart.BarSeries', 'Ext.chart.BarChart', 'Ext.chart.StackedBarChart'],
+
+ requires: ['Ext.chart.axis.Axis', 'Ext.fx.Anim'],
+
+ /* End Definitions */
+
+ type: 'bar',
+
+ alias: 'series.bar',
+ /**
+ * @cfg {Boolean} column Whether to set the visualization as column chart or horizontal bar chart.
+ */
+ column: false,
+
+ /**
+ * @cfg style Style properties that will override the theming series styles.
+ */
+ style: {},
+
+ /**
+ * @cfg {Number} gutter The gutter space between single bars, as a percentage of the bar width
+ */
+ gutter: 38.2,
+
+ /**
+ * @cfg {Number} groupGutter The gutter space between groups of bars, as a percentage of the bar width
+ */
+ groupGutter: 38.2,
+
+ /**
+ * @cfg {Number} xPadding Padding between the left/right axes and the bars
+ */
+ xPadding: 0,
+
+ /**
+ * @cfg {Number} yPadding Padding between the top/bottom axes and the bars
+ */
+ yPadding: 10,
+
+ constructor: function(config) {
+ this.callParent(arguments);
+ var me = this,
+ surface = me.chart.surface,
+ shadow = me.chart.shadow,
+ i, l;
+ Ext.apply(me, config, {
+ highlightCfg: {
+ lineWidth: 3,
+ stroke: '#55c',
+ opacity: 0.8,
+ color: '#f00'
+ },
+
+ shadowAttributes: [{
+ "stroke-width": 6,
+ "stroke-opacity": 0.05,
+ stroke: 'rgb(200, 200, 200)',
+ translate: {
+ x: 1.2,
+ y: 1.2
+ }
+ }, {
+ "stroke-width": 4,
+ "stroke-opacity": 0.1,
+ stroke: 'rgb(150, 150, 150)',
+ translate: {
+ x: 0.9,
+ y: 0.9
+ }
+ }, {
+ "stroke-width": 2,
+ "stroke-opacity": 0.15,
+ stroke: 'rgb(100, 100, 100)',
+ translate: {
+ x: 0.6,
+ y: 0.6
+ }
+ }]
+ });
+ me.group = surface.getGroup(me.seriesId + '-bars');
+ if (shadow) {
+ for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
+ me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
+ }
+ }
+ },
+
+ // @private sets the bar girth.
+ getBarGirth: function() {
+ var me = this,
+ store = me.chart.getChartStore(),
+ column = me.column,
+ ln = store.getCount(),
+ gutter = me.gutter / 100;
+
+ return (me.chart.chartBBox[column ? 'width' : 'height'] - me[column ? 'xPadding' : 'yPadding'] * 2) / (ln * (gutter + 1) - gutter);
+ },
+
+ // @private returns the gutters.
+ getGutters: function() {
+ var me = this,
+ column = me.column,
+ gutter = Math.ceil(me[column ? 'xPadding' : 'yPadding'] + me.getBarGirth() / 2);
+ return me.column ? [gutter, 0] : [0, gutter];
+ },
+
+ // @private Get chart and data boundaries
+ getBounds: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ bars = [].concat(me.yField),
+ barsLen = bars.length,
+ groupBarsLen = barsLen,
+ groupGutter = me.groupGutter / 100,
+ column = me.column,
+ xPadding = me.xPadding,
+ yPadding = me.yPadding,
+ stacked = me.stacked,
+ barWidth = me.getBarGirth(),
+ math = Math,
+ mmax = math.max,
+ mabs = math.abs,
+ groupBarWidth, bbox, minY, maxY, axis, out,
+ scale, zero, total, rec, j, plus, minus;
+
+ me.setBBox(true);
+ bbox = me.bbox;
+
+ //Skip excluded series
+ if (me.__excludes) {
+ for (j = 0, total = me.__excludes.length; j < total; j++) {
+ if (me.__excludes[j]) {
+ groupBarsLen--;
+ }
+ }
+ }
+
+ if (me.axis) {
+ axis = chart.axes.get(me.axis);
+ if (axis) {
+ out = axis.calcEnds();
+ minY = out.from;
+ maxY = out.to;
+ }
+ }
+
+ if (me.yField && !Ext.isNumber(minY)) {
+ axis = Ext.create('Ext.chart.axis.Axis', {
+ chart: chart,
+ fields: [].concat(me.yField)
+ });
+ out = axis.calcEnds();
+ minY = out.from;
+ maxY = out.to;
+ }
+
+ if (!Ext.isNumber(minY)) {
+ minY = 0;
+ }
+ if (!Ext.isNumber(maxY)) {
+ maxY = 0;
+ }
+ scale = (column ? bbox.height - yPadding * 2 : bbox.width - xPadding * 2) / (maxY - minY);
+ groupBarWidth = barWidth / ((stacked ? 1 : groupBarsLen) * (groupGutter + 1) - groupGutter);
+ zero = (column) ? bbox.y + bbox.height - yPadding : bbox.x + xPadding;
+
+ if (stacked) {
+ total = [[], []];
+ store.each(function(record, i) {
+ total[0][i] = total[0][i] || 0;
+ total[1][i] = total[1][i] || 0;
+ for (j = 0; j < barsLen; j++) {
+ if (me.__excludes && me.__excludes[j]) {
+ continue;
+ }
+ rec = record.get(bars[j]);
+ total[+(rec > 0)][i] += mabs(rec);
+ }
+ });
+ total[+(maxY > 0)].push(mabs(maxY));
+ total[+(minY > 0)].push(mabs(minY));
+ minus = mmax.apply(math, total[0]);
+ plus = mmax.apply(math, total[1]);
+ scale = (column ? bbox.height - yPadding * 2 : bbox.width - xPadding * 2) / (plus + minus);
+ zero = zero + minus * scale * (column ? -1 : 1);
+ }
+ else if (minY / maxY < 0) {
+ zero = zero - minY * scale * (column ? -1 : 1);
+ }
+ return {
+ bars: bars,
+ bbox: bbox,
+ barsLen: barsLen,
+ groupBarsLen: groupBarsLen,
+ barWidth: barWidth,
+ groupBarWidth: groupBarWidth,
+ scale: scale,
+ zero: zero,
+ xPadding: xPadding,
+ yPadding: yPadding,
+ signed: minY / maxY < 0,
+ minY: minY,
+ maxY: maxY
+ };
+ },
+
+ // @private Build an array of paths for the chart
+ getPaths: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ bounds = me.bounds = me.getBounds(),
+ items = me.items = [],
+ gutter = me.gutter / 100,
+ groupGutter = me.groupGutter / 100,
+ animate = chart.animate,
+ column = me.column,
+ group = me.group,
+ enableShadows = chart.shadow,
+ shadowGroups = me.shadowGroups,
+ shadowAttributes = me.shadowAttributes,
+ shadowGroupsLn = shadowGroups.length,
+ bbox = bounds.bbox,
+ xPadding = me.xPadding,
+ yPadding = me.yPadding,
+ stacked = me.stacked,
+ barsLen = bounds.barsLen,
+ colors = me.colorArrayStyle,
+ colorLength = colors && colors.length || 0,
+ math = Math,
+ mmax = math.max,
+ mmin = math.min,
+ mabs = math.abs,
+ j, yValue, height, totalDim, totalNegDim, bottom, top, hasShadow, barAttr, attrs, counter,
+ shadowIndex, shadow, sprite, offset, floorY;
+
+ store.each(function(record, i, total) {
+ bottom = bounds.zero;
+ top = bounds.zero;
+ totalDim = 0;
+ totalNegDim = 0;
+ hasShadow = false;
+ for (j = 0, counter = 0; j < barsLen; j++) {
+ // Excluded series
+ if (me.__excludes && me.__excludes[j]) {
+ continue;
+ }
+ yValue = record.get(bounds.bars[j]);
+ height = Math.round((yValue - mmax(bounds.minY, 0)) * bounds.scale);
+ barAttr = {
+ fill: colors[(barsLen > 1 ? j : 0) % colorLength]
+ };
+ if (column) {
+ Ext.apply(barAttr, {
+ height: height,
+ width: mmax(bounds.groupBarWidth, 0),
+ x: (bbox.x + xPadding + i * bounds.barWidth * (1 + gutter) + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked),
+ y: bottom - height
+ });
+ }
+ else {
+ // draw in reverse order
+ offset = (total - 1) - i;
+ Ext.apply(barAttr, {
+ height: mmax(bounds.groupBarWidth, 0),
+ width: height + (bottom == bounds.zero),
+ x: bottom + (bottom != bounds.zero),
+ y: (bbox.y + yPadding + offset * bounds.barWidth * (1 + gutter) + counter * bounds.groupBarWidth * (1 + groupGutter) * !stacked + 1)
+ });
+ }
+ if (height < 0) {
+ if (column) {
+ barAttr.y = top;
+ barAttr.height = mabs(height);
+ } else {
+ barAttr.x = top + height;
+ barAttr.width = mabs(height);
+ }
+ }
+ if (stacked) {
+ if (height < 0) {
+ top += height * (column ? -1 : 1);
+ } else {
+ bottom += height * (column ? -1 : 1);
+ }
+ totalDim += mabs(height);
+ if (height < 0) {
+ totalNegDim += mabs(height);
+ }
+ }
+ barAttr.x = Math.floor(barAttr.x) + 1;
+ floorY = Math.floor(barAttr.y);
+ if (!Ext.isIE9 && barAttr.y > floorY) {
+ floorY--;
+ }
+ barAttr.y = floorY;
+ barAttr.width = Math.floor(barAttr.width);
+ barAttr.height = Math.floor(barAttr.height);
+ items.push({
+ series: me,
+ storeItem: record,
+ value: [record.get(me.xField), yValue],
+ attr: barAttr,
+ point: column ? [barAttr.x + barAttr.width / 2, yValue >= 0 ? barAttr.y : barAttr.y + barAttr.height] :
+ [yValue >= 0 ? barAttr.x + barAttr.width : barAttr.x, barAttr.y + barAttr.height / 2]
+ });
+ // When resizing, reset before animating
+ if (animate && chart.resizing) {
+ attrs = column ? {
+ x: barAttr.x,
+ y: bounds.zero,
+ width: barAttr.width,
+ height: 0
+ } : {
+ x: bounds.zero,
+ y: barAttr.y,
+ width: 0,
+ height: barAttr.height
+ };
+ if (enableShadows && (stacked && !hasShadow || !stacked)) {
+ hasShadow = true;
+ //update shadows
+ for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
+ shadow = shadowGroups[shadowIndex].getAt(stacked ? i : (i * barsLen + j));
+ if (shadow) {
+ shadow.setAttributes(attrs, true);
+ }
+ }
+ }
+ //update sprite position and width/height
+ sprite = group.getAt(i * barsLen + j);
+ if (sprite) {
+ sprite.setAttributes(attrs, true);
+ }
+ }
+ counter++;
+ }
+ if (stacked && items.length) {
+ items[i * counter].totalDim = totalDim;
+ items[i * counter].totalNegDim = totalNegDim;
+ }
+ }, me);
+ },
+
+ // @private render/setAttributes on the shadows
+ renderShadows: function(i, barAttr, baseAttrs, bounds) {
+ var me = this,
+ chart = me.chart,
+ surface = chart.surface,
+ animate = chart.animate,
+ stacked = me.stacked,
+ shadowGroups = me.shadowGroups,
+ shadowAttributes = me.shadowAttributes,
+ shadowGroupsLn = shadowGroups.length,
+ store = chart.getChartStore(),
+ column = me.column,
+ items = me.items,
+ shadows = [],
+ zero = bounds.zero,
+ shadowIndex, shadowBarAttr, shadow, totalDim, totalNegDim, j, rendererAttributes;
+
+ if ((stacked && (i % bounds.groupBarsLen === 0)) || !stacked) {
+ j = i / bounds.groupBarsLen;
+ //create shadows
+ for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
+ shadowBarAttr = Ext.apply({}, shadowAttributes[shadowIndex]);
+ shadow = shadowGroups[shadowIndex].getAt(stacked ? j : i);
+ Ext.copyTo(shadowBarAttr, barAttr, 'x,y,width,height');
+ if (!shadow) {
+ shadow = surface.add(Ext.apply({
+ type: 'rect',
+ group: shadowGroups[shadowIndex]
+ }, Ext.apply({}, baseAttrs, shadowBarAttr)));
+ }
+ if (stacked) {
+ totalDim = items[i].totalDim;
+ totalNegDim = items[i].totalNegDim;
+ if (column) {
+ shadowBarAttr.y = zero - totalNegDim;
+ shadowBarAttr.height = totalDim;
+ }
+ else {
+ shadowBarAttr.x = zero - totalNegDim;
+ shadowBarAttr.width = totalDim;
+ }
+ }
+ if (animate) {
+ if (!stacked) {
+ rendererAttributes = me.renderer(shadow, store.getAt(j), shadowBarAttr, i, store);
+ me.onAnimate(shadow, { to: rendererAttributes });
+ }
+ else {
+ rendererAttributes = me.renderer(shadow, store.getAt(j), Ext.apply(shadowBarAttr, { hidden: true }), i, store);
+ shadow.setAttributes(rendererAttributes, true);
+ }
+ }
+ else {
+ rendererAttributes = me.renderer(shadow, store.getAt(j), Ext.apply(shadowBarAttr, { hidden: false }), i, store);
+ shadow.setAttributes(rendererAttributes, true);
+ }
+ shadows.push(shadow);
+ }
+ }
+ return shadows;
+ },
+
+ /**
+ * Draws the series for the current chart.
+ */
+ drawSeries: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ surface = chart.surface,
+ animate = chart.animate,
+ stacked = me.stacked,
+ column = me.column,
+ enableShadows = chart.shadow,
+ shadowGroups = me.shadowGroups,
+ shadowGroupsLn = shadowGroups.length,
+ group = me.group,
+ seriesStyle = me.seriesStyle,
+ items, ln, i, j, baseAttrs, sprite, rendererAttributes, shadowIndex, shadowGroup,
+ bounds, endSeriesStyle, barAttr, attrs, anim;
+
+ if (!store || !store.getCount()) {
+ return;
+ }
+
+ //fill colors are taken from the colors array.
+ delete seriesStyle.fill;
+ endSeriesStyle = Ext.apply(seriesStyle, this.style);
+ me.unHighlightItem();
+ me.cleanHighlights();
+
+ me.getPaths();
+ bounds = me.bounds;
+ items = me.items;
+
+ baseAttrs = column ? {
+ y: bounds.zero,
+ height: 0
+ } : {
+ x: bounds.zero,
+ width: 0
+ };
+ ln = items.length;
+ // Create new or reuse sprites and animate/display
+ for (i = 0; i < ln; i++) {
+ sprite = group.getAt(i);
+ barAttr = items[i].attr;
+
+ if (enableShadows) {
+ items[i].shadows = me.renderShadows(i, barAttr, baseAttrs, bounds);
+ }
+
+ // Create a new sprite if needed (no height)
+ if (!sprite) {
+ attrs = Ext.apply({}, baseAttrs, barAttr);
+ attrs = Ext.apply(attrs, endSeriesStyle || {});
+ sprite = surface.add(Ext.apply({}, {
+ type: 'rect',
+ group: group
+ }, attrs));
+ }
+ if (animate) {
+ rendererAttributes = me.renderer(sprite, store.getAt(i), barAttr, i, store);
+ sprite._to = rendererAttributes;
+ anim = me.onAnimate(sprite, { to: Ext.apply(rendererAttributes, endSeriesStyle) });
+ if (enableShadows && stacked && (i % bounds.barsLen === 0)) {
+ j = i / bounds.barsLen;
+ for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
+ anim.on('afteranimate', function() {
+ this.show(true);
+ }, shadowGroups[shadowIndex].getAt(j));
+ }
+ }
+ }
+ else {
+ rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(barAttr, { hidden: false }), i, store);
+ sprite.setAttributes(Ext.apply(rendererAttributes, endSeriesStyle), true);
+ }
+ items[i].sprite = sprite;
+ }
+
+ // Hide unused sprites
+ ln = group.getCount();
+ for (j = i; j < ln; j++) {
+ group.getAt(j).hide(true);
+ }
+ // Hide unused shadows
+ if (enableShadows) {
+ for (shadowIndex = 0; shadowIndex < shadowGroupsLn; shadowIndex++) {
+ shadowGroup = shadowGroups[shadowIndex];
+ ln = shadowGroup.getCount();
+ for (j = i; j < ln; j++) {
+ shadowGroup.getAt(j).hide(true);
+ }
+ }
+ }
+ me.renderLabels();
+ },
+
+ // @private handled when creating a label.
+ onCreateLabel: function(storeItem, item, i, display) {
+ var me = this,
+ surface = me.chart.surface,
+ group = me.labelsGroup,
+ config = me.label,
+ endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle || {}),
+ sprite;
+ return surface.add(Ext.apply({
+ type: 'text',
+ group: group
+ }, endLabelStyle || {}));
+ },
+
+ // @private callback used when placing a label.
+ onPlaceLabel: function(label, storeItem, item, i, display, animate, j, index) {
+ // Determine the label's final position. Starts with the configured preferred value but
+ // may get flipped from inside to outside or vice-versa depending on space.
+ var me = this,
+ opt = me.bounds,
+ groupBarWidth = opt.groupBarWidth,
+ column = me.column,
+ chart = me.chart,
+ chartBBox = chart.chartBBox,
+ resizing = chart.resizing,
+ xValue = item.value[0],
+ yValue = item.value[1],
+ attr = item.attr,
+ config = me.label,
+ rotate = config.orientation == 'vertical',
+ field = [].concat(config.field),
+ format = config.renderer,
+ text = format(storeItem.get(field[index])),
+ size = me.getLabelSize(text),
+ width = size.width,
+ height = size.height,
+ zero = opt.zero,
+ outside = 'outside',
+ insideStart = 'insideStart',
+ insideEnd = 'insideEnd',
+ offsetX = 10,
+ offsetY = 6,
+ signed = opt.signed,
+ x, y, finalAttr;
+
+ label.setAttributes({
+ text: text
+ });
+
+ label.isOutside = false;
+ if (column) {
+ if (display == outside) {
+ if (height + offsetY + attr.height > (yValue >= 0 ? zero - chartBBox.y : chartBBox.y + chartBBox.height - zero)) {
+ display = insideEnd;
+ }
+ } else {
+ if (height + offsetY > attr.height) {
+ display = outside;
+ label.isOutside = true;
+ }
+ }
+ x = attr.x + groupBarWidth / 2;
+ y = display == insideStart ?
+ (zero + ((height / 2 + 3) * (yValue >= 0 ? -1 : 1))) :
+ (yValue >= 0 ? (attr.y + ((height / 2 + 3) * (display == outside ? -1 : 1))) :
+ (attr.y + attr.height + ((height / 2 + 3) * (display === outside ? 1 : -1))));
+ }
+ else {
+ if (display == outside) {
+ if (width + offsetX + attr.width > (yValue >= 0 ? chartBBox.x + chartBBox.width - zero : zero - chartBBox.x)) {
+ display = insideEnd;
+ }
+ }
+ else {
+ if (width + offsetX > attr.width) {
+ display = outside;
+ label.isOutside = true;
+ }
+ }
+ x = display == insideStart ?
+ (zero + ((width / 2 + 5) * (yValue >= 0 ? 1 : -1))) :
+ (yValue >= 0 ? (attr.x + attr.width + ((width / 2 + 5) * (display === outside ? 1 : -1))) :
+ (attr.x + ((width / 2 + 5) * (display === outside ? -1 : 1))));
+ y = attr.y + groupBarWidth / 2;
+ }
+ //set position
+ finalAttr = {
+ x: x,
+ y: y
+ };
+ //rotate
+ if (rotate) {
+ finalAttr.rotate = {
+ x: x,
+ y: y,
+ degrees: 270
+ };
+ }
+ //check for resizing
+ if (animate && resizing) {
+ if (column) {
+ x = attr.x + attr.width / 2;
+ y = zero;
+ } else {
+ x = zero;
+ y = attr.y + attr.height / 2;
+ }
+ label.setAttributes({
+ x: x,
+ y: y
+ }, true);
+ if (rotate) {
+ label.setAttributes({
+ rotate: {
+ x: x,
+ y: y,
+ degrees: 270
+ }
+ }, true);
+ }
+ }
+ //handle animation
+ if (animate) {
+ me.onAnimate(label, { to: finalAttr });
+ }
+ else {
+ label.setAttributes(Ext.apply(finalAttr, {
+ hidden: false
+ }), true);
+ }
+ },
+
+ /* @private
+ * Gets the dimensions of a given bar label. Uses a single hidden sprite to avoid
+ * changing visible sprites.
+ * @param value
+ */
+ getLabelSize: function(value) {
+ var tester = this.testerLabel,
+ config = this.label,
+ endLabelStyle = Ext.apply({}, config, this.seriesLabelStyle || {}),
+ rotated = config.orientation === 'vertical',
+ bbox, w, h,
+ undef;
+ if (!tester) {
+ tester = this.testerLabel = this.chart.surface.add(Ext.apply({
+ type: 'text',
+ opacity: 0
+ }, endLabelStyle));
+ }
+ tester.setAttributes({
+ text: value
+ }, true);
+
+ // Flip the width/height if rotated, as getBBox returns the pre-rotated dimensions
+ bbox = tester.getBBox();
+ w = bbox.width;
+ h = bbox.height;
+ return {
+ width: rotated ? h : w,
+ height: rotated ? w : h
+ };
+ },
+
+ // @private used to animate label, markers and other sprites.
+ onAnimate: function(sprite, attr) {
+ sprite.show();
+ return this.callParent(arguments);
+ },
+
+ isItemInPoint: function(x, y, item) {
+ var bbox = item.sprite.getBBox();
+ return bbox.x <= x && bbox.y <= y
+ && (bbox.x + bbox.width) >= x
+ && (bbox.y + bbox.height) >= y;
+ },
+
+ // @private hide all markers
+ hideAll: function() {
+ var axes = this.chart.axes;
+ if (!isNaN(this._index)) {
+ if (!this.__excludes) {
+ this.__excludes = [];
+ }
+ this.__excludes[this._index] = true;
+ this.drawSeries();
+ axes.each(function(axis) {
+ axis.drawAxis();
+ });
+ }
+ },
+
+ // @private show all markers
+ showAll: function() {
+ var axes = this.chart.axes;
+ if (!isNaN(this._index)) {
+ if (!this.__excludes) {
+ this.__excludes = [];
+ }
+ this.__excludes[this._index] = false;
+ this.drawSeries();
+ axes.each(function(axis) {
+ axis.drawAxis();
+ });
+ }
+ },
+
+ /**
+ * Returns a string with the color to be used for the series legend item.
+ * @param index
+ */
+ getLegendColor: function(index) {
+ var me = this,
+ colorLength = me.colorArrayStyle.length;
+
+ if (me.style && me.style.fill) {
+ return me.style.fill;
+ } else {
+ return me.colorArrayStyle[index % colorLength];
+ }
+ },
+
+ highlightItem: function(item) {
+ this.callParent(arguments);
+ this.renderLabels();
+ },
+
+ unHighlightItem: function() {
+ this.callParent(arguments);
+ this.renderLabels();
+ },
+
+ cleanHighlights: function() {
+ this.callParent(arguments);
+ this.renderLabels();
+ }
+});
+/**
+ * @class Ext.chart.series.Column
+ * @extends Ext.chart.series.Bar
+ *
+ * Creates a Column Chart. Much of the methods are inherited from Bar. A Column Chart is a useful
+ * visualization technique to display quantitative information for different categories that can
+ * show some progression (or regression) in the data set. As with all other series, the Column Series
+ * must be appended in the *series* Chart array configuration. See the Chart documentation for more
+ * information. A typical configuration object for the column series could be:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.JsonStore', {
+ * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+ * data: [
+ * { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 },
+ * { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 },
+ * { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 },
+ * { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 },
+ * { 'name': 'metric five', 'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 300,
+ * animate: true,
+ * store: store,
+ * axes: [
+ * {
+ * type: 'Numeric',
+ * position: 'left',
+ * fields: ['data1'],
+ * label: {
+ * renderer: Ext.util.Format.numberRenderer('0,0')
+ * },
+ * title: 'Sample Values',
+ * grid: true,
+ * minimum: 0
+ * },
+ * {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Sample Metrics'
+ * }
+ * ],
+ * series: [
+ * {
+ * type: 'column',
+ * axis: 'left',
+ * highlight: true,
+ * tips: {
+ * trackMouse: true,
+ * width: 140,
+ * height: 28,
+ * renderer: function(storeItem, item) {
+ * this.setTitle(storeItem.get('name') + ': ' + storeItem.get('data1') + ' $');
+ * }
+ * },
+ * label: {
+ * display: 'insideEnd',
+ * 'text-anchor': 'middle',
+ * field: 'data1',
+ * renderer: Ext.util.Format.numberRenderer('0'),
+ * orientation: 'vertical',
+ * color: '#333'
+ * },
+ * xField: 'name',
+ * yField: 'data1'
+ * }
+ * ]
+ * });
+ *
+ * In this configuration we set `column` as the series type, bind the values of the bars to the bottom axis,
+ * set `highlight` to true so that bars are smoothly highlighted when hovered and bind the `xField` or category
+ * field to the data store `name` property and the `yField` as the data1 property of a store element.
+ */
+Ext.define('Ext.chart.series.Column', {
+
+ /* Begin Definitions */
+
+ alternateClassName: ['Ext.chart.ColumnSeries', 'Ext.chart.ColumnChart', 'Ext.chart.StackedColumnChart'],
+
+ extend: 'Ext.chart.series.Bar',
+
+ /* End Definitions */
+
+ type: 'column',
+ alias: 'series.column',
+
+ column: true,
+
+ /**
+ * @cfg {Number} xPadding
+ * Padding between the left/right axes and the bars
+ */
+ xPadding: 10,
+
+ /**
+ * @cfg {Number} yPadding
+ * Padding between the top/bottom axes and the bars
+ */
+ yPadding: 0
+});
+/**
+ * @class Ext.chart.series.Gauge
+ * @extends Ext.chart.series.Series
+ *
+ * Creates a Gauge Chart. Gauge Charts are used to show progress in a certain variable. There are two ways of using the Gauge chart.
+ * One is setting a store element into the Gauge and selecting the field to be used from that store. Another one is instantiating the
+ * visualization and using the `setValue` method to adjust the value you want.
+ *
+ * A chart/series configuration for the Gauge visualization could look like this:
+ *
+ * {
+ * xtype: 'chart',
+ * store: store,
+ * axes: [{
+ * type: 'gauge',
+ * position: 'gauge',
+ * minimum: 0,
+ * maximum: 100,
+ * steps: 10,
+ * margin: -10
+ * }],
+ * series: [{
+ * type: 'gauge',
+ * field: 'data1',
+ * donut: false,
+ * colorSet: ['#F49D10', '#ddd']
+ * }]
+ * }
+ *
+ * In this configuration we create a special Gauge axis to be used with the gauge visualization (describing half-circle markers), and also we're
+ * setting a maximum, minimum and steps configuration options into the axis. The Gauge series configuration contains the store field to be bound to
+ * the visual display and the color set to be used with the visualization.
+ *
+ * @xtype gauge
+ */
+Ext.define('Ext.chart.series.Gauge', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.series.Series',
+
+ /* End Definitions */
+
+ type: "gauge",
+ alias: 'series.gauge',
+
+ rad: Math.PI / 180,
+
+ /**
+ * @cfg {Number} highlightDuration
+ * The duration for the pie slice highlight effect.
+ */
+ highlightDuration: 150,
+
+ /**
+ * @cfg {String} angleField (required)
+ * The store record field name to be used for the pie angles.
+ * The values bound to this field name must be positive real numbers.
+ */
+ angleField: false,
+
+ /**
+ * @cfg {Boolean} needle
+ * Use the Gauge Series as an area series or add a needle to it. Default's false.
+ */
+ needle: false,
+
+ /**
+ * @cfg {Boolean/Number} donut
+ * Use the entire disk or just a fraction of it for the gauge. Default's false.
+ */
+ donut: false,
+
+ /**
+ * @cfg {Boolean} showInLegend
+ * Whether to add the pie chart elements as legend items. Default's false.
+ */
+ showInLegend: false,
+
+ /**
+ * @cfg {Object} style
+ * An object containing styles for overriding series styles from Theming.
+ */
+ style: {},
+
+ constructor: function(config) {
+ this.callParent(arguments);
+ var me = this,
+ chart = me.chart,
+ surface = chart.surface,
+ store = chart.store,
+ shadow = chart.shadow, i, l, cfg;
+ Ext.apply(me, config, {
+ shadowAttributes: [{
+ "stroke-width": 6,
+ "stroke-opacity": 1,
+ stroke: 'rgb(200, 200, 200)',
+ translate: {
+ x: 1.2,
+ y: 2
+ }
+ },
+ {
+ "stroke-width": 4,
+ "stroke-opacity": 1,
+ stroke: 'rgb(150, 150, 150)',
+ translate: {
+ x: 0.9,
+ y: 1.5
+ }
+ },
+ {
+ "stroke-width": 2,
+ "stroke-opacity": 1,
+ stroke: 'rgb(100, 100, 100)',
+ translate: {
+ x: 0.6,
+ y: 1
+ }
+ }]
+ });
+ me.group = surface.getGroup(me.seriesId);
+ if (shadow) {
+ for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
+ me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
+ }
+ }
+ surface.customAttributes.segment = function(opt) {
+ return me.getSegment(opt);
+ };
+ },
+
+ //@private updates some onbefore render parameters.
+ initialize: function() {
+ var me = this,
+ store = me.chart.getChartStore();
+ //Add yFields to be used in Legend.js
+ me.yField = [];
+ if (me.label.field) {
+ store.each(function(rec) {
+ me.yField.push(rec.get(me.label.field));
+ });
+ }
+ },
+
+ // @private returns an object with properties for a Slice
+ getSegment: function(opt) {
+ var me = this,
+ rad = me.rad,
+ cos = Math.cos,
+ sin = Math.sin,
+ abs = Math.abs,
+ x = me.centerX,
+ y = me.centerY,
+ x1 = 0, x2 = 0, x3 = 0, x4 = 0,
+ y1 = 0, y2 = 0, y3 = 0, y4 = 0,
+ delta = 1e-2,
+ r = opt.endRho - opt.startRho,
+ startAngle = opt.startAngle,
+ endAngle = opt.endAngle,
+ midAngle = (startAngle + endAngle) / 2 * rad,
+ margin = opt.margin || 0,
+ flag = abs(endAngle - startAngle) > 180,
+ a1 = Math.min(startAngle, endAngle) * rad,
+ a2 = Math.max(startAngle, endAngle) * rad,
+ singleSlice = false;
+
+ x += margin * cos(midAngle);
+ y += margin * sin(midAngle);
+
+ x1 = x + opt.startRho * cos(a1);
+ y1 = y + opt.startRho * sin(a1);
+
+ x2 = x + opt.endRho * cos(a1);
+ y2 = y + opt.endRho * sin(a1);
+
+ x3 = x + opt.startRho * cos(a2);
+ y3 = y + opt.startRho * sin(a2);
+
+ x4 = x + opt.endRho * cos(a2);
+ y4 = y + opt.endRho * sin(a2);
+
+ if (abs(x1 - x3) <= delta && abs(y1 - y3) <= delta) {
+ singleSlice = true;
+ }
+ //Solves mysterious clipping bug with IE
+ if (singleSlice) {
+ return {
+ path: [
+ ["M", x1, y1],
+ ["L", x2, y2],
+ ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
+ ["Z"]]
+ };
+ } else {
+ return {
+ path: [
+ ["M", x1, y1],
+ ["L", x2, y2],
+ ["A", opt.endRho, opt.endRho, 0, +flag, 1, x4, y4],
+ ["L", x3, y3],
+ ["A", opt.startRho, opt.startRho, 0, +flag, 0, x1, y1],
+ ["Z"]]
+ };
+ }
+ },
+
+ // @private utility function to calculate the middle point of a pie slice.
+ calcMiddle: function(item) {
+ var me = this,
+ rad = me.rad,
+ slice = item.slice,
+ x = me.centerX,
+ y = me.centerY,
+ startAngle = slice.startAngle,
+ endAngle = slice.endAngle,
+ radius = Math.max(('rho' in slice) ? slice.rho: me.radius, me.label.minMargin),
+ donut = +me.donut,
+ a1 = Math.min(startAngle, endAngle) * rad,
+ a2 = Math.max(startAngle, endAngle) * rad,
+ midAngle = -(a1 + (a2 - a1) / 2),
+ xm = x + (item.endRho + item.startRho) / 2 * Math.cos(midAngle),
+ ym = y - (item.endRho + item.startRho) / 2 * Math.sin(midAngle);
+
+ item.middle = {
+ x: xm,
+ y: ym
+ };
+ },
+
+ /**
+ * Draws the series for the current chart.
+ */
+ drawSeries: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ group = me.group,
+ animate = me.chart.animate,
+ axis = me.chart.axes.get(0),
+ minimum = axis && axis.minimum || me.minimum || 0,
+ maximum = axis && axis.maximum || me.maximum || 0,
+ field = me.angleField || me.field || me.xField,
+ surface = chart.surface,
+ chartBBox = chart.chartBBox,
+ rad = me.rad,
+ donut = +me.donut,
+ values = {},
+ items = [],
+ seriesStyle = me.seriesStyle,
+ seriesLabelStyle = me.seriesLabelStyle,
+ colorArrayStyle = me.colorArrayStyle,
+ colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
+ gutterX = chart.maxGutter[0],
+ gutterY = chart.maxGutter[1],
+ cos = Math.cos,
+ sin = Math.sin,
+ rendererAttributes, centerX, centerY, slice, slices, sprite, value,
+ item, ln, record, i, j, startAngle, endAngle, middleAngle, sliceLength, path,
+ p, spriteOptions, bbox, splitAngle, sliceA, sliceB;
+
+ Ext.apply(seriesStyle, me.style || {});
+
+ me.setBBox();
+ bbox = me.bbox;
+
+ //override theme colors
+ if (me.colorSet) {
+ colorArrayStyle = me.colorSet;
+ colorArrayLength = colorArrayStyle.length;
+ }
+
+ //if not store or store is empty then there's nothing to draw
+ if (!store || !store.getCount()) {
+ return;
+ }
+
+ centerX = me.centerX = chartBBox.x + (chartBBox.width / 2);
+ centerY = me.centerY = chartBBox.y + chartBBox.height;
+ me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y);
+ me.slices = slices = [];
+ me.items = items = [];
+
+ if (!me.value) {
+ record = store.getAt(0);
+ me.value = record.get(field);
+ }
+
+ value = me.value;
+ if (me.needle) {
+ sliceA = {
+ series: me,
+ value: value,
+ startAngle: -180,
+ endAngle: 0,
+ rho: me.radius
+ };
+ splitAngle = -180 * (1 - (value - minimum) / (maximum - minimum));
+ slices.push(sliceA);
+ } else {
+ splitAngle = -180 * (1 - (value - minimum) / (maximum - minimum));
+ sliceA = {
+ series: me,
+ value: value,
+ startAngle: -180,
+ endAngle: splitAngle,
+ rho: me.radius
+ };
+ sliceB = {
+ series: me,
+ value: me.maximum - value,
+ startAngle: splitAngle,
+ endAngle: 0,
+ rho: me.radius
+ };
+ slices.push(sliceA, sliceB);
+ }
+
+ //do pie slices after.
+ for (i = 0, ln = slices.length; i < ln; i++) {
+ slice = slices[i];
+ sprite = group.getAt(i);
+ //set pie slice properties
+ rendererAttributes = Ext.apply({
+ segment: {
+ startAngle: slice.startAngle,
+ endAngle: slice.endAngle,
+ margin: 0,
+ rho: slice.rho,
+ startRho: slice.rho * +donut / 100,
+ endRho: slice.rho
+ }
+ }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[i % colorArrayLength] } || {}));
+
+ item = Ext.apply({},
+ rendererAttributes.segment, {
+ slice: slice,
+ series: me,
+ storeItem: record,
+ index: i
+ });
+ items[i] = item;
+ // Create a new sprite if needed (no height)
+ if (!sprite) {
+ spriteOptions = Ext.apply({
+ type: "path",
+ group: group
+ }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[i % colorArrayLength] } || {}));
+ sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes));
+ }
+ slice.sprite = slice.sprite || [];
+ item.sprite = sprite;
+ slice.sprite.push(sprite);
+ if (animate) {
+ rendererAttributes = me.renderer(sprite, record, rendererAttributes, i, store);
+ sprite._to = rendererAttributes;
+ me.onAnimate(sprite, {
+ to: rendererAttributes
+ });
+ } else {
+ rendererAttributes = me.renderer(sprite, record, Ext.apply(rendererAttributes, {
+ hidden: false
+ }), i, store);
+ sprite.setAttributes(rendererAttributes, true);
+ }
+ }
+
+ if (me.needle) {
+ splitAngle = splitAngle * Math.PI / 180;
+
+ if (!me.needleSprite) {
+ me.needleSprite = me.chart.surface.add({
+ type: 'path',
+ path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle),
+ centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)),
+ 'L', centerX + me.radius * cos(splitAngle),
+ centerY + -Math.abs(me.radius * sin(splitAngle))],
+ 'stroke-width': 4,
+ 'stroke': '#222'
+ });
+ } else {
+ if (animate) {
+ me.onAnimate(me.needleSprite, {
+ to: {
+ path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle),
+ centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)),
+ 'L', centerX + me.radius * cos(splitAngle),
+ centerY + -Math.abs(me.radius * sin(splitAngle))]
+ }
+ });
+ } else {
+ me.needleSprite.setAttributes({
+ type: 'path',
+ path: ['M', centerX + (me.radius * +donut / 100) * cos(splitAngle),
+ centerY + -Math.abs((me.radius * +donut / 100) * sin(splitAngle)),
+ 'L', centerX + me.radius * cos(splitAngle),
+ centerY + -Math.abs(me.radius * sin(splitAngle))]
+ });
+ }
+ }
+ me.needleSprite.setAttributes({
+ hidden: false
+ }, true);
+ }
+
+ delete me.value;
+ },
+
+ /**
+ * Sets the Gauge chart to the current specified value.
+ */
+ setValue: function (value) {
+ this.value = value;
+ this.drawSeries();
+ },
+
+ // @private callback for when creating a label sprite.
+ onCreateLabel: function(storeItem, item, i, display) {},
+
+ // @private callback for when placing a label sprite.
+ onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {},
+
+ // @private callback for when placing a callout.
+ onPlaceCallout: function() {},
+
+ // @private handles sprite animation for the series.
+ onAnimate: function(sprite, attr) {
+ sprite.show();
+ return this.callParent(arguments);
+ },
+
+ isItemInPoint: function(x, y, item, i) {
+ return false;
+ },
+
+ // @private shows all elements in the series.
+ showAll: function() {
+ if (!isNaN(this._index)) {
+ this.__excludes[this._index] = false;
+ this.drawSeries();
+ }
+ },
+
+ /**
+ * Returns the color of the series (to be displayed as color for the series legend item).
+ * @param item {Object} Info about the item; same format as returned by #getItemForPoint
+ */
+ getLegendColor: function(index) {
+ var me = this;
+ return me.colorArrayStyle[index % me.colorArrayStyle.length];
+ }
+});
+
+
+/**
+ * @class Ext.chart.series.Line
+ * @extends Ext.chart.series.Cartesian
+ *
+ * Creates a Line Chart. A Line Chart is a useful visualization technique to display quantitative information for different
+ * categories or other real values (as opposed to the bar chart), that can show some progression (or regression) in the dataset.
+ * As with all other series, the Line Series must be appended in the *series* Chart array configuration. See the Chart
+ * documentation for more information. A typical configuration object for the line series could be:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.JsonStore', {
+ * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+ * data: [
+ * { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 },
+ * { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 },
+ * { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 },
+ * { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 },
+ * { 'name': 'metric five', 'data1': 4, 'data2': 4, 'data3': 36, 'data4': 13, 'data5': 33 }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 300,
+ * animate: true,
+ * store: store,
+ * axes: [
+ * {
+ * type: 'Numeric',
+ * position: 'left',
+ * fields: ['data1', 'data2'],
+ * label: {
+ * renderer: Ext.util.Format.numberRenderer('0,0')
+ * },
+ * title: 'Sample Values',
+ * grid: true,
+ * minimum: 0
+ * },
+ * {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Sample Metrics'
+ * }
+ * ],
+ * series: [
+ * {
+ * type: 'line',
+ * highlight: {
+ * size: 7,
+ * radius: 7
+ * },
+ * axis: 'left',
+ * xField: 'name',
+ * yField: 'data1',
+ * markerConfig: {
+ * type: 'cross',
+ * size: 4,
+ * radius: 4,
+ * 'stroke-width': 0
+ * }
+ * },
+ * {
+ * type: 'line',
+ * highlight: {
+ * size: 7,
+ * radius: 7
+ * },
+ * axis: 'left',
+ * fill: true,
+ * xField: 'name',
+ * yField: 'data2',
+ * markerConfig: {
+ * type: 'circle',
+ * size: 4,
+ * radius: 4,
+ * 'stroke-width': 0
+ * }
+ * }
+ * ]
+ * });
+ *
+ * In this configuration we're adding two series (or lines), one bound to the `data1`
+ * property of the store and the other to `data3`. The type for both configurations is
+ * `line`. The `xField` for both series is the same, the name propert of the store.
+ * Both line series share the same axis, the left axis. You can set particular marker
+ * configuration by adding properties onto the markerConfig object. Both series have
+ * an object as highlight so that markers animate smoothly to the properties in highlight
+ * when hovered. The second series has `fill=true` which means that the line will also
+ * have an area below it of the same color.
+ *
+ * **Note:** In the series definition remember to explicitly set the axis to bind the
+ * values of the line series to. This can be done by using the `axis` configuration property.
+ */
+Ext.define('Ext.chart.series.Line', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.series.Cartesian',
+
+ alternateClassName: ['Ext.chart.LineSeries', 'Ext.chart.LineChart'],
+
+ requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.draw.Draw', 'Ext.fx.Anim'],
+
+ /* End Definitions */
+
+ type: 'line',
+
+ alias: 'series.line',
+
+ /**
+ * @cfg {String} axis
+ * The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'.
+ * You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a
+ * relative scale will be used.
+ */
+
+ /**
+ * @cfg {Number} selectionTolerance
+ * The offset distance from the cursor position to the line series to trigger events (then used for highlighting series, etc).
+ */
+ selectionTolerance: 20,
+
+ /**
+ * @cfg {Boolean} showMarkers
+ * Whether markers should be displayed at the data points along the line. If true,
+ * then the {@link #markerConfig} config item will determine the markers' styling.
+ */
+ showMarkers: true,
+
+ /**
+ * @cfg {Object} markerConfig
+ * The display style for the markers. Only used if {@link #showMarkers} is true.
+ * The markerConfig is a configuration object containing the same set of properties defined in
+ * the Sprite class. For example, if we were to set red circles as markers to the line series we could
+ * pass the object:
+ *
+ <pre><code>
+ markerConfig: {
+ type: 'circle',
+ radius: 4,
+ 'fill': '#f00'
+ }
+ </code></pre>
+
+ */
+ markerConfig: {},
+
+ /**
+ * @cfg {Object} style
+ * An object containing style properties for the visualization lines and fill.
+ * These styles will override the theme styles. The following are valid style properties:
+ *
+ * - `stroke` - an rgb or hex color string for the background color of the line
+ * - `stroke-width` - the width of the stroke (integer)
+ * - `fill` - the background fill color string (hex or rgb), only works if {@link #fill} is `true`
+ * - `opacity` - the opacity of the line and the fill color (decimal)
+ *
+ * Example usage:
+ *
+ * style: {
+ * stroke: '#00ff00',
+ * 'stroke-width': 10,
+ * fill: '#80A080',
+ * opacity: 0.2
+ * }
+ */
+ style: {},
+
+ /**
+ * @cfg {Boolean/Number} smooth
+ * If set to `true` or a non-zero number, the line will be smoothed/rounded around its points; otherwise
+ * straight line segments will be drawn.
+ *
+ * A numeric value is interpreted as a divisor of the horizontal distance between consecutive points in
+ * the line; larger numbers result in sharper curves while smaller numbers result in smoother curves.
+ *
+ * If set to `true` then a default numeric value of 3 will be used. Defaults to `false`.
+ */
+ smooth: false,
+
+ /**
+ * @private Default numeric smoothing value to be used when {@link #smooth} = true.
+ */
+ defaultSmoothness: 3,
+
+ /**
+ * @cfg {Boolean} fill
+ * If true, the area below the line will be filled in using the {@link #style eefill} and
+ * {@link #style opacity} config properties. Defaults to false.
+ */
+ fill: false,
+
+ constructor: function(config) {
+ this.callParent(arguments);
+ var me = this,
+ surface = me.chart.surface,
+ shadow = me.chart.shadow,
+ i, l;
+ Ext.apply(me, config, {
+ highlightCfg: {
+ 'stroke-width': 3
+ },
+ shadowAttributes: [{
+ "stroke-width": 6,
+ "stroke-opacity": 0.05,
+ stroke: 'rgb(0, 0, 0)',
+ translate: {
+ x: 1,
+ y: 1
+ }
+ }, {
+ "stroke-width": 4,
+ "stroke-opacity": 0.1,
+ stroke: 'rgb(0, 0, 0)',
+ translate: {
+ x: 1,
+ y: 1
+ }
+ }, {
+ "stroke-width": 2,
+ "stroke-opacity": 0.15,
+ stroke: 'rgb(0, 0, 0)',
+ translate: {
+ x: 1,
+ y: 1
+ }
+ }]
+ });
+ me.group = surface.getGroup(me.seriesId);
+ if (me.showMarkers) {
+ me.markerGroup = surface.getGroup(me.seriesId + '-markers');
+ }
+ if (shadow) {
+ for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
+ me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
+ }
+ }
+ },
+
+ // @private makes an average of points when there are more data points than pixels to be rendered.
+ shrink: function(xValues, yValues, size) {
+ // Start at the 2nd point...
+ var len = xValues.length,
+ ratio = Math.floor(len / size),
+ i = 1,
+ xSum = 0,
+ ySum = 0,
+ xRes = [xValues[0]],
+ yRes = [yValues[0]];
+
+ for (; i < len; ++i) {
+ xSum += xValues[i] || 0;
+ ySum += yValues[i] || 0;
+ if (i % ratio == 0) {
+ xRes.push(xSum/ratio);
+ yRes.push(ySum/ratio);
+ xSum = 0;
+ ySum = 0;
+ }
+ }
+ return {
+ x: xRes,
+ y: yRes
+ };
+ },
+
+ /**
+ * Draws the series for the current chart.
+ */
+ drawSeries: function() {
+ var me = this,
+ chart = me.chart,
+ chartAxes = chart.axes,
+ store = chart.getChartStore(),
+ storeCount = store.getCount(),
+ surface = me.chart.surface,
+ bbox = {},
+ group = me.group,
+ showMarkers = me.showMarkers,
+ markerGroup = me.markerGroup,
+ enableShadows = chart.shadow,
+ shadowGroups = me.shadowGroups,
+ shadowAttributes = me.shadowAttributes,
+ smooth = me.smooth,
+ lnsh = shadowGroups.length,
+ dummyPath = ["M"],
+ path = ["M"],
+ renderPath = ["M"],
+ smoothPath = ["M"],
+ markerIndex = chart.markerIndex,
+ axes = [].concat(me.axis),
+ shadowBarAttr,
+ xValues = [],
+ xValueMap = {},
+ yValues = [],
+ yValueMap = {},
+ onbreak = false,
+ storeIndices = [],
+ markerStyle = me.markerStyle,
+ seriesStyle = me.style,
+ colorArrayStyle = me.colorArrayStyle,
+ colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
+ isNumber = Ext.isNumber,
+ seriesIdx = me.seriesIdx,
+ boundAxes = me.getAxesForXAndYFields(),
+ boundXAxis = boundAxes.xAxis,
+ boundYAxis = boundAxes.yAxis,
+ shadows, shadow, shindex, fromPath, fill, fillPath, rendererAttributes,
+ x, y, prevX, prevY, firstX, firstY, markerCount, i, j, ln, axis, ends, marker, markerAux, item, xValue,
+ yValue, coords, xScale, yScale, minX, maxX, minY, maxY, line, animation, endMarkerStyle,
+ endLineStyle, type, count, items;
+
+ if (me.fireEvent('beforedraw', me) === false) {
+ return;
+ }
+
+ //if store is empty or the series is excluded in the legend then there's nothing to draw.
+ if (!storeCount || me.seriesIsHidden) {
+ items = this.items;
+ if (items) {
+ for (i = 0, ln = items.length; i < ln; ++i) {
+ if (items[i].sprite) {
+ items[i].sprite.hide(true);
+ }
+ }
+ }
+ return;
+ }
+
+ //prepare style objects for line and markers
+ endMarkerStyle = Ext.apply(markerStyle || {}, me.markerConfig);
+ type = endMarkerStyle.type;
+ delete endMarkerStyle.type;
+ endLineStyle = seriesStyle;
+ //if no stroke with is specified force it to 0.5 because this is
+ //about making *lines*
+ if (!endLineStyle['stroke-width']) {
+ endLineStyle['stroke-width'] = 0.5;
+ }
+ //If we're using a time axis and we need to translate the points,
+ //then reuse the first markers as the last markers.
+ if (markerIndex && markerGroup && markerGroup.getCount()) {
+ for (i = 0; i < markerIndex; i++) {
+ marker = markerGroup.getAt(i);
+ markerGroup.remove(marker);
+ markerGroup.add(marker);
+ markerAux = markerGroup.getAt(markerGroup.getCount() - 2);
+ marker.setAttributes({
+ x: 0,
+ y: 0,
+ translate: {
+ x: markerAux.attr.translation.x,
+ y: markerAux.attr.translation.y
+ }
+ }, true);
+ }
+ }
+
+ me.unHighlightItem();
+ me.cleanHighlights();
+
+ me.setBBox();
+ bbox = me.bbox;
+ me.clipRect = [bbox.x, bbox.y, bbox.width, bbox.height];
+ for (i = 0, ln = axes.length; i < ln; i++) {
+ axis = chartAxes.get(axes[i]);
+ if (axis) {
+ ends = axis.calcEnds();
+ if (axis.position == 'top' || axis.position == 'bottom') {
+ minX = ends.from;
+ maxX = ends.to;
+ }
+ else {
+ minY = ends.from;
+ maxY = ends.to;
+ }
+ }
+ }
+ // If a field was specified without a corresponding axis, create one to get bounds
+ //only do this for the axis where real values are bound (that's why we check for
+ //me.axis)
+ if (me.xField && !isNumber(minX) &&
+ (boundXAxis == 'bottom' || boundXAxis == 'top') &&
+ !chartAxes.get(boundXAxis)) {
+ axis = Ext.create('Ext.chart.axis.Axis', {
+ chart: chart,
+ fields: [].concat(me.xField)
+ }).calcEnds();
+ minX = axis.from;
+ maxX = axis.to;
+ }
+ if (me.yField && !isNumber(minY) &&
+ (boundYAxis == 'right' || boundYAxis == 'left') &&
+ !chartAxes.get(boundYAxis)) {
+ axis = Ext.create('Ext.chart.axis.Axis', {
+ chart: chart,
+ fields: [].concat(me.yField)
+ }).calcEnds();
+ minY = axis.from;
+ maxY = axis.to;
+ }
+ if (isNaN(minX)) {
+ minX = 0;
+ xScale = bbox.width / ((storeCount - 1) || 1);
+ }
+ else {
+ xScale = bbox.width / ((maxX - minX) || (storeCount -1) || 1);
+ }
+
+ if (isNaN(minY)) {
+ minY = 0;
+ yScale = bbox.height / ((storeCount - 1) || 1);
+ }
+ else {
+ yScale = bbox.height / ((maxY - minY) || (storeCount - 1) || 1);
+ }
+
+ // Extract all x and y values from the store
+ me.eachRecord(function(record, i) {
+ xValue = record.get(me.xField);
+
+ // Ensure a value
+ if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue)
+ //set as uniform distribution if the axis is a category axis.
+ || boundXAxis && chartAxes.get(boundXAxis) && chartAxes.get(boundXAxis).type == 'Category') {
+ if (xValue in xValueMap) {
+ xValue = xValueMap[xValue];
+ } else {
+ xValue = xValueMap[xValue] = i;
+ }
+ }
+
+ // Filter out values that don't fit within the pan/zoom buffer area
+ yValue = record.get(me.yField);
+ //skip undefined values
+ if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
+ return;
+ }
+ // Ensure a value
+ if (typeof yValue == 'string' || typeof yValue == 'object' && !Ext.isDate(yValue)
+ //set as uniform distribution if the axis is a category axis.
+ || boundYAxis && chartAxes.get(boundYAxis) && chartAxes.get(boundYAxis).type == 'Category') {
+ yValue = i;
+ }
+ storeIndices.push(i);
+ xValues.push(xValue);
+ yValues.push(yValue);
+ });
+
+ ln = xValues.length;
+ if (ln > bbox.width) {
+ coords = me.shrink(xValues, yValues, bbox.width);
+ xValues = coords.x;
+ yValues = coords.y;
+ }
+
+ me.items = [];
+
+ count = 0;
+ ln = xValues.length;
+ for (i = 0; i < ln; i++) {
+ xValue = xValues[i];
+ yValue = yValues[i];
+ if (yValue === false) {
+ if (path.length == 1) {
+ path = [];
+ }
+ onbreak = true;
+ me.items.push(false);
+ continue;
+ } else {
+ x = (bbox.x + (xValue - minX) * xScale).toFixed(2);
+ y = ((bbox.y + bbox.height) - (yValue - minY) * yScale).toFixed(2);
+ if (onbreak) {
+ onbreak = false;
+ path.push('M');
+ }
+ path = path.concat([x, y]);
+ }
+ if ((typeof firstY == 'undefined') && (typeof y != 'undefined')) {
+ firstY = y;
+ firstX = x;
+ }
+ // If this is the first line, create a dummypath to animate in from.
+ if (!me.line || chart.resizing) {
+ dummyPath = dummyPath.concat([x, bbox.y + bbox.height / 2]);
+ }
+
+ // When resizing, reset before animating
+ if (chart.animate && chart.resizing && me.line) {
+ me.line.setAttributes({
+ path: dummyPath
+ }, true);
+ if (me.fillPath) {
+ me.fillPath.setAttributes({
+ path: dummyPath,
+ opacity: 0.2
+ }, true);
+ }
+ if (me.line.shadows) {
+ shadows = me.line.shadows;
+ for (j = 0, lnsh = shadows.length; j < lnsh; j++) {
+ shadow = shadows[j];
+ shadow.setAttributes({
+ path: dummyPath
+ }, true);
+ }
+ }
+ }
+ if (showMarkers) {
+ marker = markerGroup.getAt(count++);
+ if (!marker) {
+ marker = Ext.chart.Shape[type](surface, Ext.apply({
+ group: [group, markerGroup],
+ x: 0, y: 0,
+ translate: {
+ x: +(prevX || x),
+ y: prevY || (bbox.y + bbox.height / 2)
+ },
+ value: '"' + xValue + ', ' + yValue + '"',
+ zIndex: 4000
+ }, endMarkerStyle));
+ marker._to = {
+ translate: {
+ x: +x,
+ y: +y
+ }
+ };
+ } else {
+ marker.setAttributes({
+ value: '"' + xValue + ', ' + yValue + '"',
+ x: 0, y: 0,
+ hidden: false
+ }, true);
+ marker._to = {
+ translate: {
+ x: +x,
+ y: +y
+ }
+ };
+ }
+ }
+ me.items.push({
+ series: me,
+ value: [xValue, yValue],
+ point: [x, y],
+ sprite: marker,
+ storeItem: store.getAt(storeIndices[i])
+ });
+ prevX = x;
+ prevY = y;
+ }
+
+ if (path.length <= 1) {
+ //nothing to be rendered
+ return;
+ }
+
+ if (me.smooth) {
+ smoothPath = Ext.draw.Draw.smooth(path, isNumber(smooth) ? smooth : me.defaultSmoothness);
+ }
+
+ renderPath = smooth ? smoothPath : path;
+
+ //Correct path if we're animating timeAxis intervals
+ if (chart.markerIndex && me.previousPath) {
+ fromPath = me.previousPath;
+ if (!smooth) {
+ Ext.Array.erase(fromPath, 1, 2);
+ }
+ } else {
+ fromPath = path;
+ }
+
+ // Only create a line if one doesn't exist.
+ if (!me.line) {
+ me.line = surface.add(Ext.apply({
+ type: 'path',
+ group: group,
+ path: dummyPath,
+ stroke: endLineStyle.stroke || endLineStyle.fill
+ }, endLineStyle || {}));
+
+ if (enableShadows) {
+ me.line.setAttributes(Ext.apply({}, me.shadowOptions), true);
+ }
+
+ //unset fill here (there's always a default fill withing the themes).
+ me.line.setAttributes({
+ fill: 'none',
+ zIndex: 3000
+ });
+ if (!endLineStyle.stroke && colorArrayLength) {
+ me.line.setAttributes({
+ stroke: colorArrayStyle[seriesIdx % colorArrayLength]
+ }, true);
+ }
+ if (enableShadows) {
+ //create shadows
+ shadows = me.line.shadows = [];
+ for (shindex = 0; shindex < lnsh; shindex++) {
+ shadowBarAttr = shadowAttributes[shindex];
+ shadowBarAttr = Ext.apply({}, shadowBarAttr, { path: dummyPath });
+ shadow = surface.add(Ext.apply({}, {
+ type: 'path',
+ group: shadowGroups[shindex]
+ }, shadowBarAttr));
+ shadows.push(shadow);
+ }
+ }
+ }
+ if (me.fill) {
+ fillPath = renderPath.concat([
+ ["L", x, bbox.y + bbox.height],
+ ["L", firstX, bbox.y + bbox.height],
+ ["L", firstX, firstY]
+ ]);
+ if (!me.fillPath) {
+ me.fillPath = surface.add({
+ group: group,
+ type: 'path',
+ opacity: endLineStyle.opacity || 0.3,
+ fill: endLineStyle.fill || colorArrayStyle[seriesIdx % colorArrayLength],
+ path: dummyPath
+ });
+ }
+ }
+ markerCount = showMarkers && markerGroup.getCount();
+ if (chart.animate) {
+ fill = me.fill;
+ line = me.line;
+ //Add renderer to line. There is not unique record associated with this.
+ rendererAttributes = me.renderer(line, false, { path: renderPath }, i, store);
+ Ext.apply(rendererAttributes, endLineStyle || {}, {
+ stroke: endLineStyle.stroke || endLineStyle.fill
+ });
+ //fill should not be used here but when drawing the special fill path object
+ delete rendererAttributes.fill;
+ line.show(true);
+ if (chart.markerIndex && me.previousPath) {
+ me.animation = animation = me.onAnimate(line, {
+ to: rendererAttributes,
+ from: {
+ path: fromPath
+ }
+ });
+ } else {
+ me.animation = animation = me.onAnimate(line, {
+ to: rendererAttributes
+ });
+ }
+ //animate shadows
+ if (enableShadows) {
+ shadows = line.shadows;
+ for(j = 0; j < lnsh; j++) {
+ shadows[j].show(true);
+ if (chart.markerIndex && me.previousPath) {
+ me.onAnimate(shadows[j], {
+ to: { path: renderPath },
+ from: { path: fromPath }
+ });
+ } else {
+ me.onAnimate(shadows[j], {
+ to: { path: renderPath }
+ });
+ }
+ }
+ }
+ //animate fill path
+ if (fill) {
+ me.fillPath.show(true);
+ me.onAnimate(me.fillPath, {
+ to: Ext.apply({}, {
+ path: fillPath,
+ fill: endLineStyle.fill || colorArrayStyle[seriesIdx % colorArrayLength],
+ 'stroke-width': 0
+ }, endLineStyle || {})
+ });
+ }
+ //animate markers
+ if (showMarkers) {
+ count = 0;
+ for(i = 0; i < ln; i++) {
+ if (me.items[i]) {
+ item = markerGroup.getAt(count++);
+ if (item) {
+ rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
+ me.onAnimate(item, {
+ to: Ext.apply(rendererAttributes, endMarkerStyle || {})
+ });
+ item.show(true);
+ }
+ }
+ }
+ for(; count < markerCount; count++) {
+ item = markerGroup.getAt(count);
+ item.hide(true);
+ }
+// for(i = 0; i < (chart.markerIndex || 0)-1; i++) {
+// item = markerGroup.getAt(i);
+// item.hide(true);
+// }
+ }
+ } else {
+ rendererAttributes = me.renderer(me.line, false, { path: renderPath, hidden: false }, i, store);
+ Ext.apply(rendererAttributes, endLineStyle || {}, {
+ stroke: endLineStyle.stroke || endLineStyle.fill
+ });
+ //fill should not be used here but when drawing the special fill path object
+ delete rendererAttributes.fill;
+ me.line.setAttributes(rendererAttributes, true);
+ //set path for shadows
+ if (enableShadows) {
+ shadows = me.line.shadows;
+ for(j = 0; j < lnsh; j++) {
+ shadows[j].setAttributes({
+ path: renderPath,
+ hidden: false
+ }, true);
+ }
+ }
+ if (me.fill) {
+ me.fillPath.setAttributes({
+ path: fillPath,
+ hidden: false
+ }, true);
+ }
+ if (showMarkers) {
+ count = 0;
+ for(i = 0; i < ln; i++) {
+ if (me.items[i]) {
+ item = markerGroup.getAt(count++);
+ if (item) {
+ rendererAttributes = me.renderer(item, store.getAt(i), item._to, i, store);
+ item.setAttributes(Ext.apply(endMarkerStyle || {}, rendererAttributes || {}), true);
+ item.show(true);
+ }
+ }
+ }
+ for(; count < markerCount; count++) {
+ item = markerGroup.getAt(count);
+ item.hide(true);
+ }
+ }
+ }
+
+ if (chart.markerIndex) {
+ if (me.smooth) {
+ Ext.Array.erase(path, 1, 2);
+ } else {
+ Ext.Array.splice(path, 1, 0, path[1], path[2]);
+ }
+ me.previousPath = path;
+ }
+ me.renderLabels();
+ me.renderCallouts();
+
+ me.fireEvent('draw', me);
+ },
+
+ // @private called when a label is to be created.
+ onCreateLabel: function(storeItem, item, i, display) {
+ var me = this,
+ group = me.labelsGroup,
+ config = me.label,
+ bbox = me.bbox,
+ endLabelStyle = Ext.apply(config, me.seriesLabelStyle);
+
+ return me.chart.surface.add(Ext.apply({
+ 'type': 'text',
+ 'text-anchor': 'middle',
+ 'group': group,
+ 'x': item.point[0],
+ 'y': bbox.y + bbox.height / 2
+ }, endLabelStyle || {}));
+ },
+
+ // @private called when a label is to be created.
+ onPlaceLabel: function(label, storeItem, item, i, display, animate) {
+ var me = this,
+ chart = me.chart,
+ resizing = chart.resizing,
+ config = me.label,
+ format = config.renderer,
+ field = config.field,
+ bbox = me.bbox,
+ x = item.point[0],
+ y = item.point[1],
+ radius = item.sprite.attr.radius,
+ bb, width, height;
+
+ label.setAttributes({
+ text: format(storeItem.get(field)),
+ hidden: true
+ }, true);
+
+ if (display == 'rotate') {
+ label.setAttributes({
+ 'text-anchor': 'start',
+ 'rotation': {
+ x: x,
+ y: y,
+ degrees: -45
+ }
+ }, true);
+ //correct label position to fit into the box
+ bb = label.getBBox();
+ width = bb.width;
+ height = bb.height;
+ x = x < bbox.x? bbox.x : x;
+ x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x;
+ y = (y - height < bbox.y)? bbox.y + height : y;
+
+ } else if (display == 'under' || display == 'over') {
+ //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
+ bb = item.sprite.getBBox();
+ bb.width = bb.width || (radius * 2);
+ bb.height = bb.height || (radius * 2);
+ y = y + (display == 'over'? -bb.height : bb.height);
+ //correct label position to fit into the box
+ bb = label.getBBox();
+ width = bb.width/2;
+ height = bb.height/2;
+ x = x - width < bbox.x? bbox.x + width : x;
+ x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
+ y = y - height < bbox.y? bbox.y + height : y;
+ y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
+ }
+
+ if (me.chart.animate && !me.chart.resizing) {
+ label.show(true);
+ me.onAnimate(label, {
+ to: {
+ x: x,
+ y: y
+ }
+ });
+ } else {
+ label.setAttributes({
+ x: x,
+ y: y
+ }, true);
+ if (resizing && me.animation) {
+ me.animation.on('afteranimate', function() {
+ label.show(true);
+ });
+ } else {
+ label.show(true);
+ }
+ }
+ },
+
+ //@private Overriding highlights.js highlightItem method.
+ highlightItem: function() {
+ var me = this;
+ me.callParent(arguments);
+ if (me.line && !me.highlighted) {
+ if (!('__strokeWidth' in me.line)) {
+ me.line.__strokeWidth = me.line.attr['stroke-width'] || 0;
+ }
+ if (me.line.__anim) {
+ me.line.__anim.paused = true;
+ }
+ me.line.__anim = Ext.create('Ext.fx.Anim', {
+ target: me.line,
+ to: {
+ 'stroke-width': me.line.__strokeWidth + 3
+ }
+ });
+ me.highlighted = true;
+ }
+ },
+
+ //@private Overriding highlights.js unHighlightItem method.
+ unHighlightItem: function() {
+ var me = this;
+ me.callParent(arguments);
+ if (me.line && me.highlighted) {
+ me.line.__anim = Ext.create('Ext.fx.Anim', {
+ target: me.line,
+ to: {
+ 'stroke-width': me.line.__strokeWidth
+ }
+ });
+ me.highlighted = false;
+ }
+ },
+
+ //@private called when a callout needs to be placed.
+ onPlaceCallout : function(callout, storeItem, item, i, display, animate, index) {
+ if (!display) {
+ return;
+ }
+
+ var me = this,
+ chart = me.chart,
+ surface = chart.surface,
+ resizing = chart.resizing,
+ config = me.callouts,
+ items = me.items,
+ prev = i == 0? false : items[i -1].point,
+ next = (i == items.length -1)? false : items[i +1].point,
+ cur = [+item.point[0], +item.point[1]],
+ dir, norm, normal, a, aprev, anext,
+ offsetFromViz = config.offsetFromViz || 30,
+ offsetToSide = config.offsetToSide || 10,
+ offsetBox = config.offsetBox || 3,
+ boxx, boxy, boxw, boxh,
+ p, clipRect = me.clipRect,
+ bbox = {
+ width: config.styles.width || 10,
+ height: config.styles.height || 10
+ },
+ x, y;
+
+ //get the right two points
+ if (!prev) {
+ prev = cur;
+ }
+ if (!next) {
+ next = cur;
+ }
+ a = (next[1] - prev[1]) / (next[0] - prev[0]);
+ aprev = (cur[1] - prev[1]) / (cur[0] - prev[0]);
+ anext = (next[1] - cur[1]) / (next[0] - cur[0]);
+
+ norm = Math.sqrt(1 + a * a);
+ dir = [1 / norm, a / norm];
+ normal = [-dir[1], dir[0]];
+
+ //keep the label always on the outer part of the "elbow"
+ if (aprev > 0 && anext < 0 && normal[1] < 0
+ || aprev < 0 && anext > 0 && normal[1] > 0) {
+ normal[0] *= -1;
+ normal[1] *= -1;
+ } else if (Math.abs(aprev) < Math.abs(anext) && normal[0] < 0
+ || Math.abs(aprev) > Math.abs(anext) && normal[0] > 0) {
+ normal[0] *= -1;
+ normal[1] *= -1;
+ }
+ //position
+ x = cur[0] + normal[0] * offsetFromViz;
+ y = cur[1] + normal[1] * offsetFromViz;
+
+ //box position and dimensions
+ boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
+ boxy = y - bbox.height /2 - offsetBox;
+ boxw = bbox.width + 2 * offsetBox;
+ boxh = bbox.height + 2 * offsetBox;
+
+ //now check if we're out of bounds and invert the normal vector correspondingly
+ //this may add new overlaps between labels (but labels won't be out of bounds).
+ if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
+ normal[0] *= -1;
+ }
+ if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
+ normal[1] *= -1;
+ }
+
+ //update positions
+ x = cur[0] + normal[0] * offsetFromViz;
+ y = cur[1] + normal[1] * offsetFromViz;
+
+ //update box position and dimensions
+ boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
+ boxy = y - bbox.height /2 - offsetBox;
+ boxw = bbox.width + 2 * offsetBox;
+ boxh = bbox.height + 2 * offsetBox;
+
+ if (chart.animate) {
+ //set the line from the middle of the pie to the box.
+ me.onAnimate(callout.lines, {
+ to: {
+ path: ["M", cur[0], cur[1], "L", x, y, "Z"]
+ }
+ });
+ //set component position
+ if (callout.panel) {
+ callout.panel.setPosition(boxx, boxy, true);
+ }
+ }
+ else {
+ //set the line from the middle of the pie to the box.
+ callout.lines.setAttributes({
+ path: ["M", cur[0], cur[1], "L", x, y, "Z"]
+ }, true);
+ //set component position
+ if (callout.panel) {
+ callout.panel.setPosition(boxx, boxy);
+ }
+ }
+ for (p in callout) {
+ callout[p].show(true);
+ }
+ },
+
+ isItemInPoint: function(x, y, item, i) {
+ var me = this,
+ items = me.items,
+ tolerance = me.selectionTolerance,
+ result = null,
+ prevItem,
+ nextItem,
+ prevPoint,
+ nextPoint,
+ ln,
+ x1,
+ y1,
+ x2,
+ y2,
+ xIntersect,
+ yIntersect,
+ dist1, dist2, dist, midx, midy,
+ sqrt = Math.sqrt, abs = Math.abs;
+
+ nextItem = items[i];
+ prevItem = i && items[i - 1];
+
+ if (i >= ln) {
+ prevItem = items[ln - 1];
+ }
+ prevPoint = prevItem && prevItem.point;
+ nextPoint = nextItem && nextItem.point;
+ x1 = prevItem ? prevPoint[0] : nextPoint[0] - tolerance;
+ y1 = prevItem ? prevPoint[1] : nextPoint[1];
+ x2 = nextItem ? nextPoint[0] : prevPoint[0] + tolerance;
+ y2 = nextItem ? nextPoint[1] : prevPoint[1];
+ dist1 = sqrt((x - x1) * (x - x1) + (y - y1) * (y - y1));
+ dist2 = sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2));
+ dist = Math.min(dist1, dist2);
+
+ if (dist <= tolerance) {
+ return dist == dist1? prevItem : nextItem;
+ }
+ return false;
+ },
+
+ // @private toggle visibility of all series elements (markers, sprites).
+ toggleAll: function(show) {
+ var me = this,
+ i, ln, shadow, shadows;
+ if (!show) {
+ Ext.chart.series.Cartesian.prototype.hideAll.call(me);
+ }
+ else {
+ Ext.chart.series.Cartesian.prototype.showAll.call(me);
+ }
+ if (me.line) {
+ me.line.setAttributes({
+ hidden: !show
+ }, true);
+ //hide shadows too
+ if (me.line.shadows) {
+ for (i = 0, shadows = me.line.shadows, ln = shadows.length; i < ln; i++) {
+ shadow = shadows[i];
+ shadow.setAttributes({
+ hidden: !show
+ }, true);
+ }
+ }
+ }
+ if (me.fillPath) {
+ me.fillPath.setAttributes({
+ hidden: !show
+ }, true);
+ }
+ },
+
+ // @private hide all series elements (markers, sprites).
+ hideAll: function() {
+ this.toggleAll(false);
+ },
+
+ // @private hide all series elements (markers, sprites).
+ showAll: function() {
+ this.toggleAll(true);
+ }
+});
+
+/**
+ * @class Ext.chart.series.Pie
+ * @extends Ext.chart.series.Series
+ *
+ * Creates a Pie Chart. A Pie Chart is a useful visualization technique to display quantitative information for different
+ * categories that also have a meaning as a whole.
+ * As with all other series, the Pie Series must be appended in the *series* Chart array configuration. See the Chart
+ * documentation for more information. A typical configuration object for the pie series could be:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.JsonStore', {
+ * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+ * data: [
+ * { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 },
+ * { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 },
+ * { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 },
+ * { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 },
+ * { 'name': 'metric five', 'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 350,
+ * animate: true,
+ * store: store,
+ * theme: 'Base:gradients',
+ * series: [{
+ * type: 'pie',
+ * field: 'data1',
+ * showInLegend: true,
+ * tips: {
+ * trackMouse: true,
+ * width: 140,
+ * height: 28,
+ * renderer: function(storeItem, item) {
+ * // calculate and display percentage on hover
+ * var total = 0;
+ * store.each(function(rec) {
+ * total += rec.get('data1');
+ * });
+ * this.setTitle(storeItem.get('name') + ': ' + Math.round(storeItem.get('data1') / total * 100) + '%');
+ * }
+ * },
+ * highlight: {
+ * segment: {
+ * margin: 20
+ * }
+ * },
+ * label: {
+ * field: 'name',
+ * display: 'rotate',
+ * contrast: true,
+ * font: '18px Arial'
+ * }
+ * }]
+ * });
+ *
+ * In this configuration we set `pie` as the type for the series, set an object with specific style properties for highlighting options
+ * (triggered when hovering elements). We also set true to `showInLegend` so all the pie slices can be represented by a legend item.
+ *
+ * We set `data1` as the value of the field to determine the angle span for each pie slice. We also set a label configuration object
+ * where we set the field name of the store field to be renderer as text for the label. The labels will also be displayed rotated.
+ *
+ * We set `contrast` to `true` to flip the color of the label if it is to similar to the background color. Finally, we set the font family
+ * and size through the `font` parameter.
+ *
+ * @xtype pie
+ */
+Ext.define('Ext.chart.series.Pie', {
+
+ /* Begin Definitions */
+
+ alternateClassName: ['Ext.chart.PieSeries', 'Ext.chart.PieChart'],
+
+ extend: 'Ext.chart.series.Series',
+
+ /* End Definitions */
+
+ type: "pie",
+
+ alias: 'series.pie',
+
+ rad: Math.PI / 180,
+
+ /**
+ * @cfg {Number} highlightDuration
+ * The duration for the pie slice highlight effect.
+ */
+ highlightDuration: 150,
+
+ /**
+ * @cfg {String} angleField (required)
+ * The store record field name to be used for the pie angles.
+ * The values bound to this field name must be positive real numbers.
+ */
+ angleField: false,
+
+ /**
+ * @cfg {String} lengthField
+ * The store record field name to be used for the pie slice lengths.
+ * The values bound to this field name must be positive real numbers.
+ */
+ lengthField: false,
+
+ /**
+ * @cfg {Boolean/Number} donut
+ * Whether to set the pie chart as donut chart.
+ * Default's false. Can be set to a particular percentage to set the radius
+ * of the donut chart.
+ */
+ donut: false,
+
+ /**
+ * @cfg {Boolean} showInLegend
+ * Whether to add the pie chart elements as legend items. Default's false.
+ */
+ showInLegend: false,
+
+ /**
+ * @cfg {Array} colorSet
+ * An array of color values which will be used, in order, as the pie slice fill colors.
+ */
+
+ /**
+ * @cfg {Object} style
+ * An object containing styles for overriding series styles from Theming.
+ */
+ style: {},
+
+ constructor: function(config) {
+ this.callParent(arguments);
+ var me = this,
+ chart = me.chart,
+ surface = chart.surface,
+ store = chart.store,
+ shadow = chart.shadow, i, l, cfg;
+ Ext.applyIf(me, {
+ highlightCfg: {
+ segment: {
+ margin: 20
+ }
+ }
+ });
+ Ext.apply(me, config, {
+ shadowAttributes: [{
+ "stroke-width": 6,
+ "stroke-opacity": 1,
+ stroke: 'rgb(200, 200, 200)',
+ translate: {
+ x: 1.2,
+ y: 2
+ }
+ },
+ {
+ "stroke-width": 4,
+ "stroke-opacity": 1,
+ stroke: 'rgb(150, 150, 150)',
+ translate: {
+ x: 0.9,
+ y: 1.5
+ }
+ },
+ {
+ "stroke-width": 2,
+ "stroke-opacity": 1,
+ stroke: 'rgb(100, 100, 100)',
+ translate: {
+ x: 0.6,
+ y: 1
+ }
+ }]
+ });
+ me.group = surface.getGroup(me.seriesId);
+ if (shadow) {
+ for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
+ me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
+ }
+ }
+ surface.customAttributes.segment = function(opt) {
+ return me.getSegment(opt);
+ };
+ me.__excludes = me.__excludes || [];
+ },
+
+ //@private updates some onbefore render parameters.
+ initialize: function() {
+ var me = this,
+ store = me.chart.getChartStore();
+ //Add yFields to be used in Legend.js
+ me.yField = [];
+ if (me.label.field) {
+ store.each(function(rec) {
+ me.yField.push(rec.get(me.label.field));
+ });
+ }
+ },
+
+ // @private returns an object with properties for a PieSlice.
+ getSegment: function(opt) {
+ var me = this,
+ rad = me.rad,
+ cos = Math.cos,
+ sin = Math.sin,
+ x = me.centerX,
+ y = me.centerY,
+ x1 = 0, x2 = 0, x3 = 0, x4 = 0,
+ y1 = 0, y2 = 0, y3 = 0, y4 = 0,
+ x5 = 0, y5 = 0, x6 = 0, y6 = 0,
+ delta = 1e-2,
+ startAngle = opt.startAngle,
+ endAngle = opt.endAngle,
+ midAngle = (startAngle + endAngle) / 2 * rad,
+ margin = opt.margin || 0,
+ a1 = Math.min(startAngle, endAngle) * rad,
+ a2 = Math.max(startAngle, endAngle) * rad,
+ c1 = cos(a1), s1 = sin(a1),
+ c2 = cos(a2), s2 = sin(a2),
+ cm = cos(midAngle), sm = sin(midAngle),
+ flag = 0, hsqr2 = 0.7071067811865476; // sqrt(0.5)
+
+ if (a2 - a1 < delta) {
+ return {path: ""};
+ }
+
+ if (margin !== 0) {
+ x += margin * cm;
+ y += margin * sm;
+ }
+
+ x2 = x + opt.endRho * c1;
+ y2 = y + opt.endRho * s1;
+
+ x4 = x + opt.endRho * c2;
+ y4 = y + opt.endRho * s2;
+
+ if (Math.abs(x2 - x4) + Math.abs(y2 - y4) < delta) {
+ cm = hsqr2;
+ sm = -hsqr2;
+ flag = 1;
+ }
+
+ x6 = x + opt.endRho * cm;
+ y6 = y + opt.endRho * sm;
+
+ // TODO(bei): It seems that the canvas engine cannot render half circle command correctly on IE.
+ // Better fix the VML engine for half circles.
+
+ if (opt.startRho !== 0) {
+ x1 = x + opt.startRho * c1;
+ y1 = y + opt.startRho * s1;
+
+ x3 = x + opt.startRho * c2;
+ y3 = y + opt.startRho * s2;
+
+ x5 = x + opt.startRho * cm;
+ y5 = y + opt.startRho * sm;
+
+ return {
+ path: [
+ ["M", x2, y2],
+ ["A", opt.endRho, opt.endRho, 0, 0, 1, x6, y6], ["L", x6, y6],
+ ["A", opt.endRho, opt.endRho, 0, flag, 1, x4, y4], ["L", x4, y4],
+ ["L", x3, y3],
+ ["A", opt.startRho, opt.startRho, 0, flag, 0, x5, y5], ["L", x5, y5],
+ ["A", opt.startRho, opt.startRho, 0, 0, 0, x1, y1], ["L", x1, y1],
+ ["Z"]
+ ]
+ };
+ } else {
+ return {
+ path: [
+ ["M", x, y],
+ ["L", x2, y2],
+ ["A", opt.endRho, opt.endRho, 0, 0, 1, x6, y6], ["L", x6, y6],
+ ["A", opt.endRho, opt.endRho, 0, flag, 1, x4, y4], ["L", x4, y4],
+ ["L", x, y],
+ ["Z"]
+ ]
+ };
+ }
+ },
+
+ // @private utility function to calculate the middle point of a pie slice.
+ calcMiddle: function(item) {
+ var me = this,
+ rad = me.rad,
+ slice = item.slice,
+ x = me.centerX,
+ y = me.centerY,
+ startAngle = slice.startAngle,
+ endAngle = slice.endAngle,
+ donut = +me.donut,
+ midAngle = -(startAngle + endAngle) * rad / 2,
+ r = (item.endRho + item.startRho) / 2,
+ xm = x + r * Math.cos(midAngle),
+ ym = y - r * Math.sin(midAngle);
+
+ item.middle = {
+ x: xm,
+ y: ym
+ };
+ },
+
+ /**
+ * Draws the series for the current chart.
+ */
+ drawSeries: function() {
+ var me = this,
+ store = me.chart.getChartStore(),
+ group = me.group,
+ animate = me.chart.animate,
+ field = me.angleField || me.field || me.xField,
+ lenField = [].concat(me.lengthField),
+ totalLenField = 0,
+ colors = me.colorSet,
+ chart = me.chart,
+ surface = chart.surface,
+ chartBBox = chart.chartBBox,
+ enableShadows = chart.shadow,
+ shadowGroups = me.shadowGroups,
+ shadowAttributes = me.shadowAttributes,
+ lnsh = shadowGroups.length,
+ rad = me.rad,
+ layers = lenField.length,
+ rhoAcum = 0,
+ donut = +me.donut,
+ layerTotals = [],
+ values = {},
+ fieldLength,
+ items = [],
+ passed = false,
+ totalField = 0,
+ maxLenField = 0,
+ cut = 9,
+ defcut = true,
+ angle = 0,
+ seriesStyle = me.seriesStyle,
+ seriesLabelStyle = me.seriesLabelStyle,
+ colorArrayStyle = me.colorArrayStyle,
+ colorArrayLength = colorArrayStyle && colorArrayStyle.length || 0,
+ gutterX = chart.maxGutter[0],
+ gutterY = chart.maxGutter[1],
+ abs = Math.abs,
+ rendererAttributes,
+ shadowGroup,
+ shadowAttr,
+ shadows,
+ shadow,
+ shindex,
+ centerX,
+ centerY,
+ deltaRho,
+ first = 0,
+ slice,
+ slices,
+ sprite,
+ value,
+ item,
+ lenValue,
+ ln,
+ record,
+ i,
+ j,
+ startAngle,
+ endAngle,
+ middleAngle,
+ sliceLength,
+ path,
+ p,
+ spriteOptions, bbox;
+
+ Ext.apply(seriesStyle, me.style || {});
+
+ me.setBBox();
+ bbox = me.bbox;
+
+ //override theme colors
+ if (me.colorSet) {
+ colorArrayStyle = me.colorSet;
+ colorArrayLength = colorArrayStyle.length;
+ }
+
+ //if not store or store is empty then there's nothing to draw
+ if (!store || !store.getCount()) {
+ return;
+ }
+
+ me.unHighlightItem();
+ me.cleanHighlights();
+
+ centerX = me.centerX = chartBBox.x + (chartBBox.width / 2);
+ centerY = me.centerY = chartBBox.y + (chartBBox.height / 2);
+ me.radius = Math.min(centerX - chartBBox.x, centerY - chartBBox.y);
+ me.slices = slices = [];
+ me.items = items = [];
+
+ store.each(function(record, i) {
+ if (this.__excludes && this.__excludes[i]) {
+ //hidden series
+ return;
+ }
+ totalField += +record.get(field);
+ if (lenField[0]) {
+ for (j = 0, totalLenField = 0; j < layers; j++) {
+ totalLenField += +record.get(lenField[j]);
+ }
+ layerTotals[i] = totalLenField;
+ maxLenField = Math.max(maxLenField, totalLenField);
+ }
+ }, this);
+
+ totalField = totalField || 1;
+ store.each(function(record, i) {
+ if (this.__excludes && this.__excludes[i]) {
+ value = 0;
+ } else {
+ value = record.get(field);
+ if (first == 0) {
+ first = 1;
+ }
+ }
+
+ // First slice
+ if (first == 1) {
+ first = 2;
+ me.firstAngle = angle = 360 * value / totalField / 2;
+ for (j = 0; j < i; j++) {
+ slices[j].startAngle = slices[j].endAngle = me.firstAngle;
+ }
+ }
+
+ endAngle = angle - 360 * value / totalField;
+ slice = {
+ series: me,
+ value: value,
+ startAngle: angle,
+ endAngle: endAngle,
+ storeItem: record
+ };
+ if (lenField[0]) {
+ lenValue = layerTotals[i];
+ slice.rho = me.radius * (lenValue / maxLenField);
+ } else {
+ slice.rho = me.radius;
+ }
+ slices[i] = slice;
+ angle = endAngle;
+ }, me);
+ //do all shadows first.
+ if (enableShadows) {
+ for (i = 0, ln = slices.length; i < ln; i++) {
+ slice = slices[i];
+ slice.shadowAttrs = [];
+ for (j = 0, rhoAcum = 0, shadows = []; j < layers; j++) {
+ sprite = group.getAt(i * layers + j);
+ deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho;
+ //set pie slice properties
+ rendererAttributes = {
+ segment: {
+ startAngle: slice.startAngle,
+ endAngle: slice.endAngle,
+ margin: 0,
+ rho: slice.rho,
+ startRho: rhoAcum + (deltaRho * donut / 100),
+ endRho: rhoAcum + deltaRho
+ },
+ hidden: !slice.value && (slice.startAngle % 360) == (slice.endAngle % 360)
+ };
+ //create shadows
+ for (shindex = 0, shadows = []; shindex < lnsh; shindex++) {
+ shadowAttr = shadowAttributes[shindex];
+ shadow = shadowGroups[shindex].getAt(i);
+ if (!shadow) {
+ shadow = chart.surface.add(Ext.apply({}, {
+ type: 'path',
+ group: shadowGroups[shindex],
+ strokeLinejoin: "round"
+ }, rendererAttributes, shadowAttr));
+ }
+ if (animate) {
+ shadowAttr = me.renderer(shadow, store.getAt(i), Ext.apply({}, rendererAttributes, shadowAttr), i, store);
+ me.onAnimate(shadow, {
+ to: shadowAttr
+ });
+ } else {
+ shadowAttr = me.renderer(shadow, store.getAt(i), shadowAttr, i, store);
+ shadow.setAttributes(shadowAttr, true);
+ }
+ shadows.push(shadow);
+ }
+ slice.shadowAttrs[j] = shadows;
+ }
+ }
+ }
+ //do pie slices after.
+ for (i = 0, ln = slices.length; i < ln; i++) {
+ slice = slices[i];
+ for (j = 0, rhoAcum = 0; j < layers; j++) {
+ sprite = group.getAt(i * layers + j);
+ deltaRho = lenField[j] ? store.getAt(i).get(lenField[j]) / layerTotals[i] * slice.rho: slice.rho;
+ //set pie slice properties
+ rendererAttributes = Ext.apply({
+ segment: {
+ startAngle: slice.startAngle,
+ endAngle: slice.endAngle,
+ margin: 0,
+ rho: slice.rho,
+ startRho: rhoAcum + (deltaRho * donut / 100),
+ endRho: rhoAcum + deltaRho
+ },
+ hidden: (!slice.value && (slice.startAngle % 360) == (slice.endAngle % 360))
+ }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
+ item = Ext.apply({},
+ rendererAttributes.segment, {
+ slice: slice,
+ series: me,
+ storeItem: slice.storeItem,
+ index: i
+ });
+ me.calcMiddle(item);
+ if (enableShadows) {
+ item.shadows = slice.shadowAttrs[j];
+ }
+ items[i] = item;
+ // Create a new sprite if needed (no height)
+ if (!sprite) {
+ spriteOptions = Ext.apply({
+ type: "path",
+ group: group,
+ middle: item.middle
+ }, Ext.apply(seriesStyle, colorArrayStyle && { fill: colorArrayStyle[(layers > 1? j : i) % colorArrayLength] } || {}));
+ sprite = surface.add(Ext.apply(spriteOptions, rendererAttributes));
+ }
+ slice.sprite = slice.sprite || [];
+ item.sprite = sprite;
+ slice.sprite.push(sprite);
+ slice.point = [item.middle.x, item.middle.y];
+ if (animate) {
+ rendererAttributes = me.renderer(sprite, store.getAt(i), rendererAttributes, i, store);
+ sprite._to = rendererAttributes;
+ sprite._animating = true;
+ me.onAnimate(sprite, {
+ to: rendererAttributes,
+ listeners: {
+ afteranimate: {
+ fn: function() {
+ this._animating = false;
+ },
+ scope: sprite
+ }
+ }
+ });
+ } else {
+ rendererAttributes = me.renderer(sprite, store.getAt(i), Ext.apply(rendererAttributes, {
+ hidden: false
+ }), i, store);
+ sprite.setAttributes(rendererAttributes, true);
+ }
+ rhoAcum += deltaRho;
+ }
+ }
+
+ // Hide unused bars
+ ln = group.getCount();
+ for (i = 0; i < ln; i++) {
+ if (!slices[(i / layers) >> 0] && group.getAt(i)) {
+ group.getAt(i).hide(true);
+ }
+ }
+ if (enableShadows) {
+ lnsh = shadowGroups.length;
+ for (shindex = 0; shindex < ln; shindex++) {
+ if (!slices[(shindex / layers) >> 0]) {
+ for (j = 0; j < lnsh; j++) {
+ if (shadowGroups[j].getAt(shindex)) {
+ shadowGroups[j].getAt(shindex).hide(true);
+ }
+ }
+ }
+ }
+ }
+ me.renderLabels();
+ me.renderCallouts();
+ },
+
+ // @private callback for when creating a label sprite.
+ onCreateLabel: function(storeItem, item, i, display) {
+ var me = this,
+ group = me.labelsGroup,
+ config = me.label,
+ centerX = me.centerX,
+ centerY = me.centerY,
+ middle = item.middle,
+ endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config || {});
+
+ return me.chart.surface.add(Ext.apply({
+ 'type': 'text',
+ 'text-anchor': 'middle',
+ 'group': group,
+ 'x': middle.x,
+ 'y': middle.y
+ }, endLabelStyle));
+ },
+
+ // @private callback for when placing a label sprite.
+ onPlaceLabel: function(label, storeItem, item, i, display, animate, index) {
+ var me = this,
+ chart = me.chart,
+ resizing = chart.resizing,
+ config = me.label,
+ format = config.renderer,
+ field = [].concat(config.field),
+ centerX = me.centerX,
+ centerY = me.centerY,
+ middle = item.middle,
+ opt = {
+ x: middle.x,
+ y: middle.y
+ },
+ x = middle.x - centerX,
+ y = middle.y - centerY,
+ from = {},
+ rho = 1,
+ theta = Math.atan2(y, x || 1),
+ dg = theta * 180 / Math.PI,
+ prevDg;
+ if (this.__excludes && this.__excludes[i]) {
+ opt.hidden = true;
+ }
+ function fixAngle(a) {
+ if (a < 0) {
+ a += 360;
+ }
+ return a % 360;
+ }
+
+ label.setAttributes({
+ text: format(storeItem.get(field[index]))
+ }, true);
+
+ switch (display) {
+ case 'outside':
+ rho = Math.sqrt(x * x + y * y) * 2;
+ //update positions
+ opt.x = rho * Math.cos(theta) + centerX;
+ opt.y = rho * Math.sin(theta) + centerY;
+ break;
+
+ case 'rotate':
+ dg = fixAngle(dg);
+ dg = (dg > 90 && dg < 270) ? dg + 180: dg;
+
+ prevDg = label.attr.rotation.degrees;
+ if (prevDg != null && Math.abs(prevDg - dg) > 180) {
+ if (dg > prevDg) {
+ dg -= 360;
+ } else {
+ dg += 360;
+ }
+ dg = dg % 360;
+ } else {
+ dg = fixAngle(dg);
+ }
+ //update rotation angle
+ opt.rotate = {
+ degrees: dg,
+ x: opt.x,
+ y: opt.y
+ };
+ break;
+
+ default:
+ break;
+ }
+ //ensure the object has zero translation
+ opt.translate = {
+ x: 0, y: 0
+ };
+ if (animate && !resizing && (display != 'rotate' || prevDg != null)) {
+ me.onAnimate(label, {
+ to: opt
+ });
+ } else {
+ label.setAttributes(opt, true);
+ }
+ label._from = from;
+ },
+
+ // @private callback for when placing a callout sprite.
+ onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
+ var me = this,
+ chart = me.chart,
+ resizing = chart.resizing,
+ config = me.callouts,
+ centerX = me.centerX,
+ centerY = me.centerY,
+ middle = item.middle,
+ opt = {
+ x: middle.x,
+ y: middle.y
+ },
+ x = middle.x - centerX,
+ y = middle.y - centerY,
+ rho = 1,
+ rhoCenter,
+ theta = Math.atan2(y, x || 1),
+ bbox = callout.label.getBBox(),
+ offsetFromViz = 20,
+ offsetToSide = 10,
+ offsetBox = 10,
+ p;
+
+ //should be able to config this.
+ rho = item.endRho + offsetFromViz;
+ rhoCenter = (item.endRho + item.startRho) / 2 + (item.endRho - item.startRho) / 3;
+ //update positions
+ opt.x = rho * Math.cos(theta) + centerX;
+ opt.y = rho * Math.sin(theta) + centerY;
+
+ x = rhoCenter * Math.cos(theta);
+ y = rhoCenter * Math.sin(theta);
+
+ if (chart.animate) {
+ //set the line from the middle of the pie to the box.
+ me.onAnimate(callout.lines, {
+ to: {
+ path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"]
+ }
+ });
+ //set box position
+ me.onAnimate(callout.box, {
+ to: {
+ x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)),
+ y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)),
+ width: bbox.width + 2 * offsetBox,
+ height: bbox.height + 2 * offsetBox
+ }
+ });
+ //set text position
+ me.onAnimate(callout.label, {
+ to: {
+ x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)),
+ y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4)
+ }
+ });
+ } else {
+ //set the line from the middle of the pie to the box.
+ callout.lines.setAttributes({
+ path: ["M", x + centerX, y + centerY, "L", opt.x, opt.y, "Z", "M", opt.x, opt.y, "l", x > 0 ? offsetToSide: -offsetToSide, 0, "z"]
+ },
+ true);
+ //set box position
+ callout.box.setAttributes({
+ x: opt.x + (x > 0 ? offsetToSide: -(offsetToSide + bbox.width + 2 * offsetBox)),
+ y: opt.y + (y > 0 ? ( - bbox.height - offsetBox / 2) : ( - bbox.height - offsetBox / 2)),
+ width: bbox.width + 2 * offsetBox,
+ height: bbox.height + 2 * offsetBox
+ },
+ true);
+ //set text position
+ callout.label.setAttributes({
+ x: opt.x + (x > 0 ? (offsetToSide + offsetBox) : -(offsetToSide + bbox.width + offsetBox)),
+ y: opt.y + (y > 0 ? -bbox.height / 4: -bbox.height / 4)
+ },
+ true);
+ }
+ for (p in callout) {
+ callout[p].show(true);
+ }
+ },
+
+ // @private handles sprite animation for the series.
+ onAnimate: function(sprite, attr) {
+ sprite.show();
+ return this.callParent(arguments);
+ },
+
+ isItemInPoint: function(x, y, item, i) {
+ var me = this,
+ cx = me.centerX,
+ cy = me.centerY,
+ abs = Math.abs,
+ dx = abs(x - cx),
+ dy = abs(y - cy),
+ startAngle = item.startAngle,
+ endAngle = item.endAngle,
+ rho = Math.sqrt(dx * dx + dy * dy),
+ angle = Math.atan2(y - cy, x - cx) / me.rad;
+
+ // normalize to the same range of angles created by drawSeries
+ if (angle > me.firstAngle) {
+ angle -= 360;
+ }
+ return (angle <= startAngle && angle > endAngle
+ && rho >= item.startRho && rho <= item.endRho);
+ },
+
+ // @private hides all elements in the series.
+ hideAll: function() {
+ var i, l, shadow, shadows, sh, lsh, sprite;
+ if (!isNaN(this._index)) {
+ this.__excludes = this.__excludes || [];
+ this.__excludes[this._index] = true;
+ sprite = this.slices[this._index].sprite;
+ for (sh = 0, lsh = sprite.length; sh < lsh; sh++) {
+ sprite[sh].setAttributes({
+ hidden: true
+ }, true);
+ }
+ if (this.slices[this._index].shadowAttrs) {
+ for (i = 0, shadows = this.slices[this._index].shadowAttrs, l = shadows.length; i < l; i++) {
+ shadow = shadows[i];
+ for (sh = 0, lsh = shadow.length; sh < lsh; sh++) {
+ shadow[sh].setAttributes({
+ hidden: true
+ }, true);
+ }
+ }
+ }
+ this.drawSeries();
+ }
+ },
+
+ // @private shows all elements in the series.
+ showAll: function() {
+ if (!isNaN(this._index)) {
+ this.__excludes[this._index] = false;
+ this.drawSeries();
+ }
+ },
+
+ /**
+ * Highlight the specified item. If no item is provided the whole series will be highlighted.
+ * @param item {Object} Info about the item; same format as returned by #getItemForPoint
+ */
+ highlightItem: function(item) {
+ var me = this,
+ rad = me.rad;
+ item = item || this.items[this._index];
+
+ //TODO(nico): sometimes in IE itemmouseover is triggered
+ //twice without triggering itemmouseout in between. This
+ //fixes the highlighting bug. Eventually, events should be
+ //changed to trigger one itemmouseout between two itemmouseovers.
+ this.unHighlightItem();
+
+ if (!item || item.sprite && item.sprite._animating) {
+ return;
+ }
+ me.callParent([item]);
+ if (!me.highlight) {
+ return;
+ }
+ if ('segment' in me.highlightCfg) {
+ var highlightSegment = me.highlightCfg.segment,
+ animate = me.chart.animate,
+ attrs, i, shadows, shadow, ln, to, itemHighlightSegment, prop;
+ //animate labels
+ if (me.labelsGroup) {
+ var group = me.labelsGroup,
+ display = me.label.display,
+ label = group.getAt(item.index),
+ middle = (item.startAngle + item.endAngle) / 2 * rad,
+ r = highlightSegment.margin || 0,
+ x = r * Math.cos(middle),
+ y = r * Math.sin(middle);
+
+ //TODO(nico): rounding to 1e-10
+ //gives the right translation. Translation
+ //was buggy for very small numbers. In this
+ //case we're not looking to translate to very small
+ //numbers but not to translate at all.
+ if (Math.abs(x) < 1e-10) {
+ x = 0;
+ }
+ if (Math.abs(y) < 1e-10) {
+ y = 0;
+ }
+
+ if (animate) {
+ label.stopAnimation();
+ label.animate({
+ to: {
+ translate: {
+ x: x,
+ y: y
+ }
+ },
+ duration: me.highlightDuration
+ });
+ }
+ else {
+ label.setAttributes({
+ translate: {
+ x: x,
+ y: y
+ }
+ }, true);
+ }
+ }
+ //animate shadows
+ if (me.chart.shadow && item.shadows) {
+ i = 0;
+ shadows = item.shadows;
+ ln = shadows.length;
+ for (; i < ln; i++) {
+ shadow = shadows[i];
+ to = {};
+ itemHighlightSegment = item.sprite._from.segment;
+ for (prop in itemHighlightSegment) {
+ if (! (prop in highlightSegment)) {
+ to[prop] = itemHighlightSegment[prop];
+ }
+ }
+ attrs = {
+ segment: Ext.applyIf(to, me.highlightCfg.segment)
+ };
+ if (animate) {
+ shadow.stopAnimation();
+ shadow.animate({
+ to: attrs,
+ duration: me.highlightDuration
+ });
+ }
+ else {
+ shadow.setAttributes(attrs, true);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Un-highlights the specified item. If no item is provided it will un-highlight the entire series.
+ * @param item {Object} Info about the item; same format as returned by #getItemForPoint
+ */
+ unHighlightItem: function() {
+ var me = this;
+ if (!me.highlight) {
+ return;
+ }
+
+ if (('segment' in me.highlightCfg) && me.items) {
+ var items = me.items,
+ animate = me.chart.animate,
+ shadowsEnabled = !!me.chart.shadow,
+ group = me.labelsGroup,
+ len = items.length,
+ i = 0,
+ j = 0,
+ display = me.label.display,
+ shadowLen, p, to, ihs, hs, sprite, shadows, shadow, item, label, attrs;
+
+ for (; i < len; i++) {
+ item = items[i];
+ if (!item) {
+ continue;
+ }
+ sprite = item.sprite;
+ if (sprite && sprite._highlighted) {
+ //animate labels
+ if (group) {
+ label = group.getAt(item.index);
+ attrs = Ext.apply({
+ translate: {
+ x: 0,
+ y: 0
+ }
+ },
+ display == 'rotate' ? {
+ rotate: {
+ x: label.attr.x,
+ y: label.attr.y,
+ degrees: label.attr.rotation.degrees
+ }
+ }: {});
+ if (animate) {
+ label.stopAnimation();
+ label.animate({
+ to: attrs,
+ duration: me.highlightDuration
+ });
+ }
+ else {
+ label.setAttributes(attrs, true);
+ }
+ }
+ if (shadowsEnabled) {
+ shadows = item.shadows;
+ shadowLen = shadows.length;
+ for (; j < shadowLen; j++) {
+ to = {};
+ ihs = item.sprite._to.segment;
+ hs = item.sprite._from.segment;
+ Ext.apply(to, hs);
+ for (p in ihs) {
+ if (! (p in hs)) {
+ to[p] = ihs[p];
+ }
+ }
+ shadow = shadows[j];
+ if (animate) {
+ shadow.stopAnimation();
+ shadow.animate({
+ to: {
+ segment: to
+ },
+ duration: me.highlightDuration
+ });
+ }
+ else {
+ shadow.setAttributes({ segment: to }, true);
+ }
+ }
+ }
+ }
+ }
+ }
+ me.callParent(arguments);
+ },
+
+ /**
+ * Returns the color of the series (to be displayed as color for the series legend item).
+ * @param item {Object} Info about the item; same format as returned by #getItemForPoint
+ */
+ getLegendColor: function(index) {
+ var me = this;
+ return (me.colorSet && me.colorSet[index % me.colorSet.length]) || me.colorArrayStyle[index % me.colorArrayStyle.length];
+ }
+});
+
+
+/**
+ * @class Ext.chart.series.Radar
+ * @extends Ext.chart.series.Series
+ *
+ * Creates a Radar Chart. A Radar Chart is a useful visualization technique for comparing different quantitative values for
+ * a constrained number of categories.
+ *
+ * As with all other series, the Radar series must be appended in the *series* Chart array configuration. See the Chart
+ * documentation for more information. A typical configuration object for the radar series could be:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.JsonStore', {
+ * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+ * data: [
+ * { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 },
+ * { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 },
+ * { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 },
+ * { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 },
+ * { 'name': 'metric five', 'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 300,
+ * animate: true,
+ * theme:'Category2',
+ * store: store,
+ * axes: [{
+ * type: 'Radial',
+ * position: 'radial',
+ * label: {
+ * display: true
+ * }
+ * }],
+ * series: [{
+ * type: 'radar',
+ * xField: 'name',
+ * yField: 'data3',
+ * showInLegend: true,
+ * showMarkers: true,
+ * markerConfig: {
+ * radius: 5,
+ * size: 5
+ * },
+ * style: {
+ * 'stroke-width': 2,
+ * fill: 'none'
+ * }
+ * },{
+ * type: 'radar',
+ * xField: 'name',
+ * yField: 'data2',
+ * showMarkers: true,
+ * showInLegend: true,
+ * markerConfig: {
+ * radius: 5,
+ * size: 5
+ * },
+ * style: {
+ * 'stroke-width': 2,
+ * fill: 'none'
+ * }
+ * },{
+ * type: 'radar',
+ * xField: 'name',
+ * yField: 'data5',
+ * showMarkers: true,
+ * showInLegend: true,
+ * markerConfig: {
+ * radius: 5,
+ * size: 5
+ * },
+ * style: {
+ * 'stroke-width': 2,
+ * fill: 'none'
+ * }
+ * }]
+ * });
+ *
+ * In this configuration we add three series to the chart. Each of these series is bound to the same
+ * categories field, `name` but bound to different properties for each category, `data1`, `data2` and
+ * `data3` respectively. All series display markers by having `showMarkers` enabled. The configuration
+ * for the markers of each series can be set by adding properties onto the markerConfig object.
+ * Finally we override some theme styling properties by adding properties to the `style` object.
+ *
+ * @xtype radar
+ */
+Ext.define('Ext.chart.series.Radar', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.series.Series',
+
+ requires: ['Ext.chart.Shape', 'Ext.fx.Anim'],
+
+ /* End Definitions */
+
+ type: "radar",
+ alias: 'series.radar',
+
+
+ rad: Math.PI / 180,
+
+ showInLegend: false,
+
+ /**
+ * @cfg {Object} style
+ * An object containing styles for overriding series styles from Theming.
+ */
+ style: {},
+
+ constructor: function(config) {
+ this.callParent(arguments);
+ var me = this,
+ surface = me.chart.surface, i, l;
+ me.group = surface.getGroup(me.seriesId);
+ if (me.showMarkers) {
+ me.markerGroup = surface.getGroup(me.seriesId + '-markers');
+ }
+ },
+
+ /**
+ * Draws the series for the current chart.
+ */
+ drawSeries: function() {
+ var me = this,
+ store = me.chart.getChartStore(),
+ group = me.group,
+ sprite,
+ chart = me.chart,
+ animate = chart.animate,
+ field = me.field || me.yField,
+ surface = chart.surface,
+ chartBBox = chart.chartBBox,
+ rendererAttributes,
+ centerX, centerY,
+ items,
+ radius,
+ maxValue = 0,
+ fields = [],
+ max = Math.max,
+ cos = Math.cos,
+ sin = Math.sin,
+ pi2 = Math.PI * 2,
+ l = store.getCount(),
+ startPath, path, x, y, rho,
+ i, nfields,
+ seriesStyle = me.seriesStyle,
+ seriesLabelStyle = me.seriesLabelStyle,
+ first = chart.resizing || !me.radar,
+ axis = chart.axes && chart.axes.get(0),
+ aggregate = !(axis && axis.maximum);
+
+ me.setBBox();
+
+ maxValue = aggregate? 0 : (axis.maximum || 0);
+
+ Ext.apply(seriesStyle, me.style || {});
+
+ //if the store is empty then there's nothing to draw
+ if (!store || !store.getCount()) {
+ return;
+ }
+
+ me.unHighlightItem();
+ me.cleanHighlights();
+
+ centerX = me.centerX = chartBBox.x + (chartBBox.width / 2);
+ centerY = me.centerY = chartBBox.y + (chartBBox.height / 2);
+ me.radius = radius = Math.min(chartBBox.width, chartBBox.height) /2;
+ me.items = items = [];
+
+ if (aggregate) {
+ //get all renderer fields
+ chart.series.each(function(series) {
+ fields.push(series.yField);
+ });
+ //get maxValue to interpolate
+ store.each(function(record, i) {
+ for (i = 0, nfields = fields.length; i < nfields; i++) {
+ maxValue = max(+record.get(fields[i]), maxValue);
+ }
+ });
+ }
+ //ensure non-zero value.
+ maxValue = maxValue || 1;
+ //create path and items
+ startPath = []; path = [];
+ store.each(function(record, i) {
+ rho = radius * record.get(field) / maxValue;
+ x = rho * cos(i / l * pi2);
+ y = rho * sin(i / l * pi2);
+ if (i == 0) {
+ path.push('M', x + centerX, y + centerY);
+ startPath.push('M', 0.01 * x + centerX, 0.01 * y + centerY);
+ } else {
+ path.push('L', x + centerX, y + centerY);
+ startPath.push('L', 0.01 * x + centerX, 0.01 * y + centerY);
+ }
+ items.push({
+ sprite: false, //TODO(nico): add markers
+ point: [centerX + x, centerY + y],
+ series: me
+ });
+ });
+ path.push('Z');
+ //create path sprite
+ if (!me.radar) {
+ me.radar = surface.add(Ext.apply({
+ type: 'path',
+ group: group,
+ path: startPath
+ }, seriesStyle || {}));
+ }
+ //reset on resizing
+ if (chart.resizing) {
+ me.radar.setAttributes({
+ path: startPath
+ }, true);
+ }
+ //render/animate
+ if (chart.animate) {
+ me.onAnimate(me.radar, {
+ to: Ext.apply({
+ path: path
+ }, seriesStyle || {})
+ });
+ } else {
+ me.radar.setAttributes(Ext.apply({
+ path: path
+ }, seriesStyle || {}), true);
+ }
+ //render markers, labels and callouts
+ if (me.showMarkers) {
+ me.drawMarkers();
+ }
+ me.renderLabels();
+ me.renderCallouts();
+ },
+
+ // @private draws the markers for the lines (if any).
+ drawMarkers: function() {
+ var me = this,
+ chart = me.chart,
+ surface = chart.surface,
+ markerStyle = Ext.apply({}, me.markerStyle || {}),
+ endMarkerStyle = Ext.apply(markerStyle, me.markerConfig),
+ items = me.items,
+ type = endMarkerStyle.type,
+ markerGroup = me.markerGroup,
+ centerX = me.centerX,
+ centerY = me.centerY,
+ item, i, l, marker;
+
+ delete endMarkerStyle.type;
+
+ for (i = 0, l = items.length; i < l; i++) {
+ item = items[i];
+ marker = markerGroup.getAt(i);
+ if (!marker) {
+ marker = Ext.chart.Shape[type](surface, Ext.apply({
+ group: markerGroup,
+ x: 0,
+ y: 0,
+ translate: {
+ x: centerX,
+ y: centerY
+ }
+ }, endMarkerStyle));
+ }
+ else {
+ marker.show();
+ }
+ if (chart.resizing) {
+ marker.setAttributes({
+ x: 0,
+ y: 0,
+ translate: {
+ x: centerX,
+ y: centerY
+ }
+ }, true);
+ }
+ marker._to = {
+ translate: {
+ x: item.point[0],
+ y: item.point[1]
+ }
+ };
+ //render/animate
+ if (chart.animate) {
+ me.onAnimate(marker, {
+ to: marker._to
+ });
+ }
+ else {
+ marker.setAttributes(Ext.apply(marker._to, endMarkerStyle || {}), true);
+ }
+ }
+ },
+
+ isItemInPoint: function(x, y, item) {
+ var point,
+ tolerance = 10,
+ abs = Math.abs;
+ point = item.point;
+ return (abs(point[0] - x) <= tolerance &&
+ abs(point[1] - y) <= tolerance);
+ },
+
+ // @private callback for when creating a label sprite.
+ onCreateLabel: function(storeItem, item, i, display) {
+ var me = this,
+ group = me.labelsGroup,
+ config = me.label,
+ centerX = me.centerX,
+ centerY = me.centerY,
+ point = item.point,
+ endLabelStyle = Ext.apply(me.seriesLabelStyle || {}, config);
+
+ return me.chart.surface.add(Ext.apply({
+ 'type': 'text',
+ 'text-anchor': 'middle',
+ 'group': group,
+ 'x': centerX,
+ 'y': centerY
+ }, config || {}));
+ },
+
+ // @private callback for when placing a label sprite.
+ onPlaceLabel: function(label, storeItem, item, i, display, animate) {
+ var me = this,
+ chart = me.chart,
+ resizing = chart.resizing,
+ config = me.label,
+ format = config.renderer,
+ field = config.field,
+ centerX = me.centerX,
+ centerY = me.centerY,
+ opt = {
+ x: item.point[0],
+ y: item.point[1]
+ },
+ x = opt.x - centerX,
+ y = opt.y - centerY;
+
+ label.setAttributes({
+ text: format(storeItem.get(field)),
+ hidden: true
+ },
+ true);
+
+ if (resizing) {
+ label.setAttributes({
+ x: centerX,
+ y: centerY
+ }, true);
+ }
+
+ if (animate) {
+ label.show(true);
+ me.onAnimate(label, {
+ to: opt
+ });
+ } else {
+ label.setAttributes(opt, true);
+ label.show(true);
+ }
+ },
+
+ // @private for toggling (show/hide) series.
+ toggleAll: function(show) {
+ var me = this,
+ i, ln, shadow, shadows;
+ if (!show) {
+ Ext.chart.series.Radar.superclass.hideAll.call(me);
+ }
+ else {
+ Ext.chart.series.Radar.superclass.showAll.call(me);
+ }
+ if (me.radar) {
+ me.radar.setAttributes({
+ hidden: !show
+ }, true);
+ //hide shadows too
+ if (me.radar.shadows) {
+ for (i = 0, shadows = me.radar.shadows, ln = shadows.length; i < ln; i++) {
+ shadow = shadows[i];
+ shadow.setAttributes({
+ hidden: !show
+ }, true);
+ }
+ }
+ }
+ },
+
+ // @private hide all elements in the series.
+ hideAll: function() {
+ this.toggleAll(false);
+ this.hideMarkers(0);
+ },
+
+ // @private show all elements in the series.
+ showAll: function() {
+ this.toggleAll(true);
+ },
+
+ // @private hide all markers that belong to `markerGroup`
+ hideMarkers: function(index) {
+ var me = this,
+ count = me.markerGroup && me.markerGroup.getCount() || 0,
+ i = index || 0;
+ for (; i < count; i++) {
+ me.markerGroup.getAt(i).hide(true);
+ }
+ }
+});
+
+
+/**
+ * @class Ext.chart.series.Scatter
+ * @extends Ext.chart.series.Cartesian
+ *
+ * Creates a Scatter Chart. The scatter plot is useful when trying to display more than two variables in the same visualization.
+ * These variables can be mapped into x, y coordinates and also to an element's radius/size, color, etc.
+ * As with all other series, the Scatter Series must be appended in the *series* Chart array configuration. See the Chart
+ * documentation for more information on creating charts. A typical configuration object for the scatter could be:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.JsonStore', {
+ * fields: ['name', 'data1', 'data2', 'data3', 'data4', 'data5'],
+ * data: [
+ * { 'name': 'metric one', 'data1': 10, 'data2': 12, 'data3': 14, 'data4': 8, 'data5': 13 },
+ * { 'name': 'metric two', 'data1': 7, 'data2': 8, 'data3': 16, 'data4': 10, 'data5': 3 },
+ * { 'name': 'metric three', 'data1': 5, 'data2': 2, 'data3': 14, 'data4': 12, 'data5': 7 },
+ * { 'name': 'metric four', 'data1': 2, 'data2': 14, 'data3': 6, 'data4': 1, 'data5': 23 },
+ * { 'name': 'metric five', 'data1': 27, 'data2': 38, 'data3': 36, 'data4': 13, 'data5': 33 }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.chart.Chart', {
+ * renderTo: Ext.getBody(),
+ * width: 500,
+ * height: 300,
+ * animate: true,
+ * theme:'Category2',
+ * store: store,
+ * axes: [{
+ * type: 'Numeric',
+ * position: 'left',
+ * fields: ['data2', 'data3'],
+ * title: 'Sample Values',
+ * grid: true,
+ * minimum: 0
+ * }, {
+ * type: 'Category',
+ * position: 'bottom',
+ * fields: ['name'],
+ * title: 'Sample Metrics'
+ * }],
+ * series: [{
+ * type: 'scatter',
+ * markerConfig: {
+ * radius: 5,
+ * size: 5
+ * },
+ * axis: 'left',
+ * xField: 'name',
+ * yField: 'data2'
+ * }, {
+ * type: 'scatter',
+ * markerConfig: {
+ * radius: 5,
+ * size: 5
+ * },
+ * axis: 'left',
+ * xField: 'name',
+ * yField: 'data3'
+ * }]
+ * });
+ *
+ * In this configuration we add three different categories of scatter series. Each of them is bound to a different field of the same data store,
+ * `data1`, `data2` and `data3` respectively. All x-fields for the series must be the same field, in this case `name`.
+ * Each scatter series has a different styling configuration for markers, specified by the `markerConfig` object. Finally we set the left axis as
+ * axis to show the current values of the elements.
+ *
+ * @xtype scatter
+ */
+Ext.define('Ext.chart.series.Scatter', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.chart.series.Cartesian',
+
+ requires: ['Ext.chart.axis.Axis', 'Ext.chart.Shape', 'Ext.fx.Anim'],
+
+ /* End Definitions */
+
+ type: 'scatter',
+ alias: 'series.scatter',
+
+ /**
+ * @cfg {Object} markerConfig
+ * The display style for the scatter series markers.
+ */
+
+ /**
+ * @cfg {Object} style
+ * Append styling properties to this object for it to override theme properties.
+ */
+
+ /**
+ * @cfg {String/Array} axis
+ * The position of the axis to bind the values to. Possible values are 'left', 'bottom', 'top' and 'right'.
+ * You must explicitly set this value to bind the values of the line series to the ones in the axis, otherwise a
+ * relative scale will be used. If multiple axes are being used, they should both be specified in in the configuration.
+ */
+
+ constructor: function(config) {
+ this.callParent(arguments);
+ var me = this,
+ shadow = me.chart.shadow,
+ surface = me.chart.surface, i, l;
+ Ext.apply(me, config, {
+ style: {},
+ markerConfig: {},
+ shadowAttributes: [{
+ "stroke-width": 6,
+ "stroke-opacity": 0.05,
+ stroke: 'rgb(0, 0, 0)'
+ }, {
+ "stroke-width": 4,
+ "stroke-opacity": 0.1,
+ stroke: 'rgb(0, 0, 0)'
+ }, {
+ "stroke-width": 2,
+ "stroke-opacity": 0.15,
+ stroke: 'rgb(0, 0, 0)'
+ }]
+ });
+ me.group = surface.getGroup(me.seriesId);
+ if (shadow) {
+ for (i = 0, l = me.shadowAttributes.length; i < l; i++) {
+ me.shadowGroups.push(surface.getGroup(me.seriesId + '-shadows' + i));
+ }
+ }
+ },
+
+ // @private Get chart and data boundaries
+ getBounds: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ axes = [].concat(me.axis),
+ bbox, xScale, yScale, ln, minX, minY, maxX, maxY, i, axis, ends;
+
+ me.setBBox();
+ bbox = me.bbox;
+
+ for (i = 0, ln = axes.length; i < ln; i++) {
+ axis = chart.axes.get(axes[i]);
+ if (axis) {
+ ends = axis.calcEnds();
+ if (axis.position == 'top' || axis.position == 'bottom') {
+ minX = ends.from;
+ maxX = ends.to;
+ }
+ else {
+ minY = ends.from;
+ maxY = ends.to;
+ }
+ }
+ }
+ // If a field was specified without a corresponding axis, create one to get bounds
+ if (me.xField && !Ext.isNumber(minX)) {
+ axis = Ext.create('Ext.chart.axis.Axis', {
+ chart: chart,
+ fields: [].concat(me.xField)
+ }).calcEnds();
+ minX = axis.from;
+ maxX = axis.to;
+ }
+ if (me.yField && !Ext.isNumber(minY)) {
+ axis = Ext.create('Ext.chart.axis.Axis', {
+ chart: chart,
+ fields: [].concat(me.yField)
+ }).calcEnds();
+ minY = axis.from;
+ maxY = axis.to;
+ }
+
+ if (isNaN(minX)) {
+ minX = 0;
+ maxX = store.getCount() - 1;
+ xScale = bbox.width / (store.getCount() - 1);
+ }
+ else {
+ xScale = bbox.width / (maxX - minX);
+ }
+
+ if (isNaN(minY)) {
+ minY = 0;
+ maxY = store.getCount() - 1;
+ yScale = bbox.height / (store.getCount() - 1);
+ }
+ else {
+ yScale = bbox.height / (maxY - minY);
+ }
+
+ return {
+ bbox: bbox,
+ minX: minX,
+ minY: minY,
+ xScale: xScale,
+ yScale: yScale
+ };
+ },
+
+ // @private Build an array of paths for the chart
+ getPaths: function() {
+ var me = this,
+ chart = me.chart,
+ enableShadows = chart.shadow,
+ store = chart.getChartStore(),
+ group = me.group,
+ bounds = me.bounds = me.getBounds(),
+ bbox = me.bbox,
+ xScale = bounds.xScale,
+ yScale = bounds.yScale,
+ minX = bounds.minX,
+ minY = bounds.minY,
+ boxX = bbox.x,
+ boxY = bbox.y,
+ boxHeight = bbox.height,
+ items = me.items = [],
+ attrs = [],
+ x, y, xValue, yValue, sprite;
+
+ store.each(function(record, i) {
+ xValue = record.get(me.xField);
+ yValue = record.get(me.yField);
+ //skip undefined values
+ if (typeof yValue == 'undefined' || (typeof yValue == 'string' && !yValue)) {
+ return;
+ }
+ // Ensure a value
+ if (typeof xValue == 'string' || typeof xValue == 'object' && !Ext.isDate(xValue)) {
+ xValue = i;
+ }
+ if (typeof yValue == 'string' || typeof yValue == 'object' && !Ext.isDate(yValue)) {
+ yValue = i;
+ }
+ x = boxX + (xValue - minX) * xScale;
+ y = boxY + boxHeight - (yValue - minY) * yScale;
+ attrs.push({
+ x: x,
+ y: y
+ });
+
+ me.items.push({
+ series: me,
+ value: [xValue, yValue],
+ point: [x, y],
+ storeItem: record
+ });
+
+ // When resizing, reset before animating
+ if (chart.animate && chart.resizing) {
+ sprite = group.getAt(i);
+ if (sprite) {
+ me.resetPoint(sprite);
+ if (enableShadows) {
+ me.resetShadow(sprite);
+ }
+ }
+ }
+ });
+ return attrs;
+ },
+
+ // @private translate point to the center
+ resetPoint: function(sprite) {
+ var bbox = this.bbox;
+ sprite.setAttributes({
+ translate: {
+ x: (bbox.x + bbox.width) / 2,
+ y: (bbox.y + bbox.height) / 2
+ }
+ }, true);
+ },
+
+ // @private translate shadows of a sprite to the center
+ resetShadow: function(sprite) {
+ var me = this,
+ shadows = sprite.shadows,
+ shadowAttributes = me.shadowAttributes,
+ ln = me.shadowGroups.length,
+ bbox = me.bbox,
+ i, attr;
+ for (i = 0; i < ln; i++) {
+ attr = Ext.apply({}, shadowAttributes[i]);
+ if (attr.translate) {
+ attr.translate.x += (bbox.x + bbox.width) / 2;
+ attr.translate.y += (bbox.y + bbox.height) / 2;
+ }
+ else {
+ attr.translate = {
+ x: (bbox.x + bbox.width) / 2,
+ y: (bbox.y + bbox.height) / 2
+ };
+ }
+ shadows[i].setAttributes(attr, true);
+ }
+ },
+
+ // @private create a new point
+ createPoint: function(attr, type) {
+ var me = this,
+ chart = me.chart,
+ group = me.group,
+ bbox = me.bbox;
+
+ return Ext.chart.Shape[type](chart.surface, Ext.apply({}, {
+ x: 0,
+ y: 0,
+ group: group,
+ translate: {
+ x: (bbox.x + bbox.width) / 2,
+ y: (bbox.y + bbox.height) / 2
+ }
+ }, attr));
+ },
+
+ // @private create a new set of shadows for a sprite
+ createShadow: function(sprite, endMarkerStyle, type) {
+ var me = this,
+ chart = me.chart,
+ shadowGroups = me.shadowGroups,
+ shadowAttributes = me.shadowAttributes,
+ lnsh = shadowGroups.length,
+ bbox = me.bbox,
+ i, shadow, shadows, attr;
+
+ sprite.shadows = shadows = [];
+
+ for (i = 0; i < lnsh; i++) {
+ attr = Ext.apply({}, shadowAttributes[i]);
+ if (attr.translate) {
+ attr.translate.x += (bbox.x + bbox.width) / 2;
+ attr.translate.y += (bbox.y + bbox.height) / 2;
+ }
+ else {
+ Ext.apply(attr, {
+ translate: {
+ x: (bbox.x + bbox.width) / 2,
+ y: (bbox.y + bbox.height) / 2
+ }
+ });
+ }
+ Ext.apply(attr, endMarkerStyle);
+ shadow = Ext.chart.Shape[type](chart.surface, Ext.apply({}, {
+ x: 0,
+ y: 0,
+ group: shadowGroups[i]
+ }, attr));
+ shadows.push(shadow);
+ }
+ },
+
+ /**
+ * Draws the series for the current chart.
+ */
+ drawSeries: function() {
+ var me = this,
+ chart = me.chart,
+ store = chart.getChartStore(),
+ group = me.group,
+ enableShadows = chart.shadow,
+ shadowGroups = me.shadowGroups,
+ shadowAttributes = me.shadowAttributes,
+ lnsh = shadowGroups.length,
+ sprite, attrs, attr, ln, i, endMarkerStyle, shindex, type, shadows,
+ rendererAttributes, shadowAttribute;
+
+ endMarkerStyle = Ext.apply(me.markerStyle, me.markerConfig);
+ type = endMarkerStyle.type;
+ delete endMarkerStyle.type;
+
+ //if the store is empty then there's nothing to be rendered
+ if (!store || !store.getCount()) {
+ return;
+ }
+
+ me.unHighlightItem();
+ me.cleanHighlights();
+
+ attrs = me.getPaths();
+ ln = attrs.length;
+ for (i = 0; i < ln; i++) {
+ attr = attrs[i];
+ sprite = group.getAt(i);
+ Ext.apply(attr, endMarkerStyle);
+
+ // Create a new sprite if needed (no height)
+ if (!sprite) {
+ sprite = me.createPoint(attr, type);
+ if (enableShadows) {
+ me.createShadow(sprite, endMarkerStyle, type);
+ }
+ }
+
+ shadows = sprite.shadows;
+ if (chart.animate) {
+ rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store);
+ sprite._to = rendererAttributes;
+ me.onAnimate(sprite, {
+ to: rendererAttributes
+ });
+ //animate shadows
+ for (shindex = 0; shindex < lnsh; shindex++) {
+ shadowAttribute = Ext.apply({}, shadowAttributes[shindex]);
+ rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, {
+ hidden: false,
+ translate: {
+ x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0),
+ y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0)
+ }
+ }, shadowAttribute), i, store);
+ me.onAnimate(shadows[shindex], { to: rendererAttributes });
+ }
+ }
+ else {
+ rendererAttributes = me.renderer(sprite, store.getAt(i), { translate: attr }, i, store);
+ sprite._to = rendererAttributes;
+ sprite.setAttributes(rendererAttributes, true);
+ //animate shadows
+ for (shindex = 0; shindex < lnsh; shindex++) {
+ shadowAttribute = Ext.apply({}, shadowAttributes[shindex]);
+ rendererAttributes = me.renderer(shadows[shindex], store.getAt(i), Ext.apply({}, {
+ hidden: false,
+ translate: {
+ x: attr.x + (shadowAttribute.translate? shadowAttribute.translate.x : 0),
+ y: attr.y + (shadowAttribute.translate? shadowAttribute.translate.y : 0)
+ }
+ }, shadowAttribute), i, store);
+ shadows[shindex].setAttributes(rendererAttributes, true);
+ }
+ }
+ me.items[i].sprite = sprite;
+ }
+
+ // Hide unused sprites
+ ln = group.getCount();
+ for (i = attrs.length; i < ln; i++) {
+ group.getAt(i).hide(true);
+ }
+ me.renderLabels();
+ me.renderCallouts();
+ },
+
+ // @private callback for when creating a label sprite.
+ onCreateLabel: function(storeItem, item, i, display) {
+ var me = this,
+ group = me.labelsGroup,
+ config = me.label,
+ endLabelStyle = Ext.apply({}, config, me.seriesLabelStyle),
+ bbox = me.bbox;
+
+ return me.chart.surface.add(Ext.apply({
+ type: 'text',
+ group: group,
+ x: item.point[0],
+ y: bbox.y + bbox.height / 2
+ }, endLabelStyle));
+ },
+
+ // @private callback for when placing a label sprite.
+ onPlaceLabel: function(label, storeItem, item, i, display, animate) {
+ var me = this,
+ chart = me.chart,
+ resizing = chart.resizing,
+ config = me.label,
+ format = config.renderer,
+ field = config.field,
+ bbox = me.bbox,
+ x = item.point[0],
+ y = item.point[1],
+ radius = item.sprite.attr.radius,
+ bb, width, height, anim;
+
+ label.setAttributes({
+ text: format(storeItem.get(field)),
+ hidden: true
+ }, true);
+
+ if (display == 'rotate') {
+ label.setAttributes({
+ 'text-anchor': 'start',
+ 'rotation': {
+ x: x,
+ y: y,
+ degrees: -45
+ }
+ }, true);
+ //correct label position to fit into the box
+ bb = label.getBBox();
+ width = bb.width;
+ height = bb.height;
+ x = x < bbox.x? bbox.x : x;
+ x = (x + width > bbox.x + bbox.width)? (x - (x + width - bbox.x - bbox.width)) : x;
+ y = (y - height < bbox.y)? bbox.y + height : y;
+
+ } else if (display == 'under' || display == 'over') {
+ //TODO(nicolas): find out why width/height values in circle bounding boxes are undefined.
+ bb = item.sprite.getBBox();
+ bb.width = bb.width || (radius * 2);
+ bb.height = bb.height || (radius * 2);
+ y = y + (display == 'over'? -bb.height : bb.height);
+ //correct label position to fit into the box
+ bb = label.getBBox();
+ width = bb.width/2;
+ height = bb.height/2;
+ x = x - width < bbox.x ? bbox.x + width : x;
+ x = (x + width > bbox.x + bbox.width) ? (x - (x + width - bbox.x - bbox.width)) : x;
+ y = y - height < bbox.y? bbox.y + height : y;
+ y = (y + height > bbox.y + bbox.height) ? (y - (y + height - bbox.y - bbox.height)) : y;
+ }
+
+ if (!chart.animate) {
+ label.setAttributes({
+ x: x,
+ y: y
+ }, true);
+ label.show(true);
+ }
+ else {
+ if (resizing) {
+ anim = item.sprite.getActiveAnimation();
+ if (anim) {
+ anim.on('afteranimate', function() {
+ label.setAttributes({
+ x: x,
+ y: y
+ }, true);
+ label.show(true);
+ });
+ }
+ else {
+ label.show(true);
+ }
+ }
+ else {
+ me.onAnimate(label, {
+ to: {
+ x: x,
+ y: y
+ }
+ });
+ }
+ }
+ },
+
+ // @private callback for when placing a callout sprite.
+ onPlaceCallout: function(callout, storeItem, item, i, display, animate, index) {
+ var me = this,
+ chart = me.chart,
+ surface = chart.surface,
+ resizing = chart.resizing,
+ config = me.callouts,
+ items = me.items,
+ cur = item.point,
+ normal,
+ bbox = callout.label.getBBox(),
+ offsetFromViz = 30,
+ offsetToSide = 10,
+ offsetBox = 3,
+ boxx, boxy, boxw, boxh,
+ p, clipRect = me.bbox,
+ x, y;
+
+ //position
+ normal = [Math.cos(Math.PI /4), -Math.sin(Math.PI /4)];
+ x = cur[0] + normal[0] * offsetFromViz;
+ y = cur[1] + normal[1] * offsetFromViz;
+
+ //box position and dimensions
+ boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
+ boxy = y - bbox.height /2 - offsetBox;
+ boxw = bbox.width + 2 * offsetBox;
+ boxh = bbox.height + 2 * offsetBox;
+
+ //now check if we're out of bounds and invert the normal vector correspondingly
+ //this may add new overlaps between labels (but labels won't be out of bounds).
+ if (boxx < clipRect[0] || (boxx + boxw) > (clipRect[0] + clipRect[2])) {
+ normal[0] *= -1;
+ }
+ if (boxy < clipRect[1] || (boxy + boxh) > (clipRect[1] + clipRect[3])) {
+ normal[1] *= -1;
+ }
+
+ //update positions
+ x = cur[0] + normal[0] * offsetFromViz;
+ y = cur[1] + normal[1] * offsetFromViz;
+
+ //update box position and dimensions
+ boxx = x + (normal[0] > 0? 0 : -(bbox.width + 2 * offsetBox));
+ boxy = y - bbox.height /2 - offsetBox;
+ boxw = bbox.width + 2 * offsetBox;
+ boxh = bbox.height + 2 * offsetBox;
+
+ if (chart.animate) {
+ //set the line from the middle of the pie to the box.
+ me.onAnimate(callout.lines, {
+ to: {
+ path: ["M", cur[0], cur[1], "L", x, y, "Z"]
+ }
+ }, true);
+ //set box position
+ me.onAnimate(callout.box, {
+ to: {
+ x: boxx,
+ y: boxy,
+ width: boxw,
+ height: boxh
+ }
+ }, true);
+ //set text position
+ me.onAnimate(callout.label, {
+ to: {
+ x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
+ y: y
+ }
+ }, true);
+ } else {
+ //set the line from the middle of the pie to the box.
+ callout.lines.setAttributes({
+ path: ["M", cur[0], cur[1], "L", x, y, "Z"]
+ }, true);
+ //set box position
+ callout.box.setAttributes({
+ x: boxx,
+ y: boxy,
+ width: boxw,
+ height: boxh
+ }, true);
+ //set text position
+ callout.label.setAttributes({
+ x: x + (normal[0] > 0? offsetBox : -(bbox.width + offsetBox)),
+ y: y
+ }, true);
+ }
+ for (p in callout) {
+ callout[p].show(true);
+ }
+ },
+
+ // @private handles sprite animation for the series.
+ onAnimate: function(sprite, attr) {
+ sprite.show();
+ return this.callParent(arguments);
+ },
+
+ isItemInPoint: function(x, y, item) {
+ var point,
+ tolerance = 10,
+ abs = Math.abs;
+
+ function dist(point) {
+ var dx = abs(point[0] - x),
+ dy = abs(point[1] - y);
+ return Math.sqrt(dx * dx + dy * dy);
+ }
+ point = item.point;
+ return (point[0] - tolerance <= x && point[0] + tolerance >= x &&
+ point[1] - tolerance <= y && point[1] + tolerance >= y);
+ }
+});
+
+
+/**
+ * @class Ext.chart.theme.Base
+ * Provides default colors for non-specified things. Should be sub-classed when creating new themes.
+ * @ignore
+ */
+Ext.define('Ext.chart.theme.Base', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.chart.theme.Theme'],
+
+ /* End Definitions */
+
+ constructor: function(config) {
+ Ext.chart.theme.call(this, config, {
+ background: false,
+ axis: {
+ stroke: '#444',
+ 'stroke-width': 1
+ },
+ axisLabelTop: {
+ fill: '#444',
+ font: '12px Arial, Helvetica, sans-serif',
+ spacing: 2,
+ padding: 5,
+ renderer: function(v) { return v; }
+ },
+ axisLabelRight: {
+ fill: '#444',
+ font: '12px Arial, Helvetica, sans-serif',
+ spacing: 2,
+ padding: 5,
+ renderer: function(v) { return v; }
+ },
+ axisLabelBottom: {
+ fill: '#444',
+ font: '12px Arial, Helvetica, sans-serif',
+ spacing: 2,
+ padding: 5,
+ renderer: function(v) { return v; }
+ },
+ axisLabelLeft: {
+ fill: '#444',
+ font: '12px Arial, Helvetica, sans-serif',
+ spacing: 2,
+ padding: 5,
+ renderer: function(v) { return v; }
+ },
+ axisTitleTop: {
+ font: 'bold 18px Arial',
+ fill: '#444'
+ },
+ axisTitleRight: {
+ font: 'bold 18px Arial',
+ fill: '#444',
+ rotate: {
+ x:0, y:0,
+ degrees: 270
+ }
+ },
+ axisTitleBottom: {
+ font: 'bold 18px Arial',
+ fill: '#444'
+ },
+ axisTitleLeft: {
+ font: 'bold 18px Arial',
+ fill: '#444',
+ rotate: {
+ x:0, y:0,
+ degrees: 270
+ }
+ },
+ series: {
+ 'stroke-width': 0
+ },
+ seriesLabel: {
+ font: '12px Arial',
+ fill: '#333'
+ },
+ marker: {
+ stroke: '#555',
+ fill: '#000',
+ radius: 3,
+ size: 3
+ },
+ colors: [ "#94ae0a", "#115fa6","#a61120", "#ff8809", "#ffd13e", "#a61187", "#24ad9a", "#7c7474", "#a66111"],
+ seriesThemes: [{
+ fill: "#115fa6"
+ }, {
+ fill: "#94ae0a"
+ }, {
+ fill: "#a61120"
+ }, {
+ fill: "#ff8809"
+ }, {
+ fill: "#ffd13e"
+ }, {
+ fill: "#a61187"
+ }, {
+ fill: "#24ad9a"
+ }, {
+ fill: "#7c7474"
+ }, {
+ fill: "#a66111"
+ }],
+ markerThemes: [{
+ fill: "#115fa6",
+ type: 'circle'
+ }, {
+ fill: "#94ae0a",
+ type: 'cross'
+ }, {
+ fill: "#a61120",
+ type: 'plus'
+ }]
+ });
+ }
+}, function() {
+ var palette = ['#b1da5a', '#4ce0e7', '#e84b67', '#da5abd', '#4d7fe6', '#fec935'],
+ names = ['Green', 'Sky', 'Red', 'Purple', 'Blue', 'Yellow'],
+ i = 0, j = 0, l = palette.length, themes = Ext.chart.theme,
+ categories = [['#f0a50a', '#c20024', '#2044ba', '#810065', '#7eae29'],
+ ['#6d9824', '#87146e', '#2a9196', '#d39006', '#1e40ac'],
+ ['#fbbc29', '#ce2e4e', '#7e0062', '#158b90', '#57880e'],
+ ['#ef5773', '#fcbd2a', '#4f770d', '#1d3eaa', '#9b001f'],
+ ['#7eae29', '#fdbe2a', '#910019', '#27b4bc', '#d74dbc'],
+ ['#44dce1', '#0b2592', '#996e05', '#7fb325', '#b821a1']],
+ cats = categories.length;
+
+ //Create themes from base colors
+ for (; i < l; i++) {
+ themes[names[i]] = (function(color) {
+ return Ext.extend(themes.Base, {
+ constructor: function(config) {
+ themes.Base.prototype.constructor.call(this, Ext.apply({
+ baseColor: color
+ }, config));
+ }
+ });
+ })(palette[i]);
+ }
+
+ //Create theme from color array
+ for (i = 0; i < cats; i++) {
+ themes['Category' + (i + 1)] = (function(category) {
+ return Ext.extend(themes.Base, {
+ constructor: function(config) {
+ themes.Base.prototype.constructor.call(this, Ext.apply({
+ colors: category
+ }, config));
+ }
+ });
+ })(categories[i]);
+ }
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * Small helper class to make creating {@link Ext.data.Store}s from Array data easier. An ArrayStore will be
+ * automatically configured with a {@link Ext.data.reader.Array}.
+ *
+ * A store configuration would be something like:
+ *
+ * var store = Ext.create('Ext.data.ArrayStore', {
+ * // store configs
+ * autoDestroy: true,
+ * storeId: 'myStore',
+ * // reader configs
+ * idIndex: 0,
+ * fields: [
+ * 'company',
+ * {name: 'price', type: 'float'},
+ * {name: 'change', type: 'float'},
+ * {name: 'pctChange', type: 'float'},
+ * {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
+ * ]
+ * });
+ *
+ * This store is configured to consume a returned object of the form:
+ *
+ * var myData = [
+ * ['3m Co',71.72,0.02,0.03,'9/1 12:00am'],
+ * ['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am'],
+ * ['Boeing Co.',75.43,0.53,0.71,'9/1 12:00am'],
+ * ['Hewlett-Packard Co.',36.53,-0.03,-0.08,'9/1 12:00am'],
+ * ['Wal-Mart Stores, Inc.',45.45,0.73,1.63,'9/1 12:00am']
+ * ];
+ *
+ * An object literal of this form could also be used as the {@link #data} config option.
+ *
+ * **Note:** This class accepts all of the configuration options of {@link Ext.data.reader.Array ArrayReader}.
+ */
+Ext.define('Ext.data.ArrayStore', {
+ extend: 'Ext.data.Store',
+ alias: 'store.array',
+ uses: ['Ext.data.reader.Array'],
+
+ constructor: function(config) {
+ config = config || {};
+
+ Ext.applyIf(config, {
+ proxy: {
+ type: 'memory',
+ reader: 'array'
+ }
+ });
+
+ this.callParent([config]);
+ },
+
+ loadData: function(data, append) {
+ if (this.expandData === true) {
+ var r = [],
+ i = 0,
+ ln = data.length;
+
+ for (; i < ln; i++) {
+ r[r.length] = [data[i]];
+ }
+
+ data = r;
+ }
+
+ this.callParent([data, append]);
+ }
+}, function() {
+ // backwards compat
+ Ext.data.SimpleStore = Ext.data.ArrayStore;
+ // Ext.reg('simplestore', Ext.data.SimpleStore);
+});
+
+/**
+ * @author Ed Spencer
+ * @class Ext.data.Batch
+ *
+ * <p>Provides a mechanism to run one or more {@link Ext.data.Operation operations} in a given order. Fires the 'operationcomplete' event
+ * after the completion of each Operation, and the 'complete' event when all Operations have been successfully executed. Fires an 'exception'
+ * event if any of the Operations encounter an exception.</p>
+ *
+ * <p>Usually these are only used internally by {@link Ext.data.proxy.Proxy} classes</p>
+ *
+ */
+Ext.define('Ext.data.Batch', {
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ /**
+ * @property {Boolean} autoStart
+ * True to immediately start processing the batch as soon as it is constructed.
+ */
+ autoStart: false,
+
+ /**
+ * @property {Number} current
+ * The index of the current operation being executed
+ */
+ current: -1,
+
+ /**
+ * @property {Number} total
+ * The total number of operations in this batch. Read only
+ */
+ total: 0,
+
+ /**
+ * @property {Boolean} isRunning
+ * True if the batch is currently running
+ */
+ isRunning: false,
+
+ /**
+ * @property {Boolean} isComplete
+ * True if this batch has been executed completely
+ */
+ isComplete: false,
+
+ /**
+ * @property {Boolean} hasException
+ * True if this batch has encountered an exception. This is cleared at the start of each operation
+ */
+ hasException: false,
+
+ /**
+ * @property {Boolean} pauseOnException
+ * True to automatically pause the execution of the batch if any operation encounters an exception
+ */
+ pauseOnException: true,
+
+ /**
+ * Creates new Batch object.
+ * @param {Object} [config] Config object
+ */
+ constructor: function(config) {
+ var me = this;
+
+ me.addEvents(
+ /**
+ * @event complete
+ * Fired when all operations of this batch have been completed
+ * @param {Ext.data.Batch} batch The batch object
+ * @param {Object} operation The last operation that was executed
+ */
+ 'complete',
+
+ /**
+ * @event exception
+ * Fired when a operation encountered an exception
+ * @param {Ext.data.Batch} batch The batch object
+ * @param {Object} operation The operation that encountered the exception
+ */
+ 'exception',
+
+ /**
+ * @event operationcomplete
+ * Fired when each operation of the batch completes
+ * @param {Ext.data.Batch} batch The batch object
+ * @param {Object} operation The operation that just completed
+ */
+ 'operationcomplete'
+ );
+
+ me.mixins.observable.constructor.call(me, config);
+
+ /**
+ * Ordered array of operations that will be executed by this batch
+ * @property {Ext.data.Operation[]} operations
+ */
+ me.operations = [];
+ },
+
+ /**
+ * Adds a new operation to this batch
+ * @param {Object} operation The {@link Ext.data.Operation Operation} object
+ */
+ add: function(operation) {
+ this.total++;
+
+ operation.setBatch(this);
+
+ this.operations.push(operation);
+ },
+
+ /**
+ * Kicks off the execution of the batch, continuing from the next operation if the previous
+ * operation encountered an exception, or if execution was paused
+ */
+ start: function() {
+ this.hasException = false;
+ this.isRunning = true;
+
+ this.runNextOperation();
+ },
+
+ /**
+ * @private
+ * Runs the next operation, relative to this.current.
+ */
+ runNextOperation: function() {
+ this.runOperation(this.current + 1);
+ },
+
+ /**
+ * Pauses execution of the batch, but does not cancel the current operation
+ */
+ pause: function() {
+ this.isRunning = false;
+ },
+
+ /**
+ * Executes a operation by its numeric index
+ * @param {Number} index The operation index to run
+ */
+ runOperation: function(index) {
+ var me = this,
+ operations = me.operations,
+ operation = operations[index],
+ onProxyReturn;
+
+ if (operation === undefined) {
+ me.isRunning = false;
+ me.isComplete = true;
+ me.fireEvent('complete', me, operations[operations.length - 1]);
+ } else {
+ me.current = index;
+
+ onProxyReturn = function(operation) {
+ var hasException = operation.hasException();
+
+ if (hasException) {
+ me.hasException = true;
+ me.fireEvent('exception', me, operation);
+ } else {
+ me.fireEvent('operationcomplete', me, operation);
+ }
+
+ if (hasException && me.pauseOnException) {
+ me.pause();
+ } else {
+ operation.setCompleted();
+ me.runNextOperation();
+ }
+ };
+
+ operation.setStarted();
+
+ me.proxy[operation.action](operation, onProxyReturn, me);
+ }
+ }
+});
+/**
+ * @author Ed Spencer
+ * @class Ext.data.BelongsToAssociation
+ * @extends Ext.data.Association
+ *
+ * Represents a many to one association with another model. The owner model is expected to have
+ * a foreign key which references the primary key of the associated model:
+ *
+ * Ext.define('Category', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'id', type: 'int' },
+ * { name: 'name', type: 'string' }
+ * ]
+ * });
+ *
+ * Ext.define('Product', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name: 'id', type: 'int' },
+ * { name: 'category_id', type: 'int' },
+ * { name: 'name', type: 'string' }
+ * ],
+ * // we can use the belongsTo shortcut on the model to create a belongsTo association
+ * associations: [
+ * { type: 'belongsTo', model: 'Category' }
+ * ]
+ * });
+ *
+ * In the example above we have created models for Products and Categories, and linked them together
+ * by saying that each Product belongs to a Category. This automatically links each Product to a Category
+ * based on the Product's category_id, and provides new functions on the Product model:
+ *
+ * ## Generated getter function
+ *
+ * The first function that is added to the owner model is a getter function:
+ *
+ * var product = new Product({
+ * id: 100,
+ * category_id: 20,
+ * name: 'Sneakers'
+ * });
+ *
+ * product.getCategory(function(category, operation) {
+ * // do something with the category object
+ * alert(category.get('id')); // alerts 20
+ * }, this);
+ *
+ * The getCategory function was created on the Product model when we defined the association. This uses the
+ * Category's configured {@link Ext.data.proxy.Proxy proxy} to load the Category asynchronously, calling the provided
+ * callback when it has loaded.
+ *
+ * The new getCategory function will also accept an object containing success, failure and callback properties
+ * - callback will always be called, success will only be called if the associated model was loaded successfully
+ * and failure will only be called if the associatied model could not be loaded:
+ *
+ * product.getCategory({
+ * callback: function(category, operation) {}, // a function that will always be called
+ * success : function(category, operation) {}, // a function that will only be called if the load succeeded
+ * failure : function(category, operation) {}, // a function that will only be called if the load did not succeed
+ * scope : this // optionally pass in a scope object to execute the callbacks in
+ * });
+ *
+ * In each case above the callbacks are called with two arguments - the associated model instance and the
+ * {@link Ext.data.Operation operation} object that was executed to load that instance. The Operation object is
+ * useful when the instance could not be loaded.
+ *
+ * ## Generated setter function
+ *
+ * The second generated function sets the associated model instance - if only a single argument is passed to
+ * the setter then the following two calls are identical:
+ *
+ * // this call...
+ * product.setCategory(10);
+ *
+ * // is equivalent to this call:
+ * product.set('category_id', 10);
+ *
+ * If we pass in a second argument, the model will be automatically saved and the second argument passed to
+ * the owner model's {@link Ext.data.Model#save save} method:
+ *
+ * product.setCategory(10, function(product, operation) {
+ * // the product has been saved
+ * alert(product.get('category_id')); //now alerts 10
+ * });
+ *
+ * //alternative syntax:
+ * product.setCategory(10, {
+ * callback: function(product, operation), // a function that will always be called
+ * success : function(product, operation), // a function that will only be called if the load succeeded
+ * failure : function(product, operation), // a function that will only be called if the load did not succeed
+ * scope : this //optionally pass in a scope object to execute the callbacks in
+ * })
+ *
+ * ## Customisation
+ *
+ * Associations reflect on the models they are linking to automatically set up properties such as the
+ * {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified:
+ *
+ * Ext.define('Product', {
+ * fields: [...],
+ *
+ * associations: [
+ * { type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id' }
+ * ]
+ * });
+ *
+ * Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'category_id')
+ * with our own settings. Usually this will not be needed.
+ */
+Ext.define('Ext.data.BelongsToAssociation', {
+ extend: 'Ext.data.Association',
+
+ alias: 'association.belongsto',
+
+ /**
+ * @cfg {String} foreignKey The name of the foreign key on the owner model that links it to the associated
+ * model. Defaults to the lowercased name of the associated model plus "_id", e.g. an association with a
+ * model called Product would set up a product_id foreign key.
+ *
+ * Ext.define('Order', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'date'],
+ * hasMany: 'Product'
+ * });
+ *
+ * Ext.define('Product', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', 'order_id'], // refers to the id of the order that this product belongs to
+ * belongsTo: 'Group'
+ * });
+ * var product = new Product({
+ * id: 1,
+ * name: 'Product 1',
+ * order_id: 22
+ * }, 1);
+ * product.getOrder(); // Will make a call to the server asking for order_id 22
+ *
+ */
+
+ /**
+ * @cfg {String} getterName The name of the getter function that will be added to the local model's prototype.
+ * Defaults to 'get' + the name of the foreign model, e.g. getCategory
+ */
+
+ /**
+ * @cfg {String} setterName The name of the setter function that will be added to the local model's prototype.
+ * Defaults to 'set' + the name of the foreign model, e.g. setCategory
+ */
+
+ /**
+ * @cfg {String} type The type configuration can be used when creating associations using a configuration object.
+ * Use 'belongsTo' to create a HasManyAssocation
+ *
+ * associations: [{
+ * type: 'belongsTo',
+ * model: 'User'
+ * }]
+ */
+ constructor: function(config) {
+ this.callParent(arguments);
+
+ var me = this,
+ ownerProto = me.ownerModel.prototype,
+ associatedName = me.associatedName,
+ getterName = me.getterName || 'get' + associatedName,
+ setterName = me.setterName || 'set' + associatedName;
+
+ Ext.applyIf(me, {
+ name : associatedName,
+ foreignKey : associatedName.toLowerCase() + "_id",
+ instanceName: associatedName + 'BelongsToInstance',
+ associationKey: associatedName.toLowerCase()
+ });
+
+ ownerProto[getterName] = me.createGetter();
+ ownerProto[setterName] = me.createSetter();
+ },
+
+ /**
+ * @private
+ * Returns a setter function to be placed on the owner model's prototype
+ * @return {Function} The setter function
+ */
+ createSetter: function() {
+ var me = this,
+ ownerModel = me.ownerModel,
+ associatedModel = me.associatedModel,
+ foreignKey = me.foreignKey,
+ primaryKey = me.primaryKey;
+
+ //'this' refers to the Model instance inside this function
+ return function(value, options, scope) {
+ this.set(foreignKey, value);
+
+ if (typeof options == 'function') {
+ options = {
+ callback: options,
+ scope: scope || this
+ };
+ }
+
+ if (Ext.isObject(options)) {
+ return this.save(options);
+ }
+ };
+ },
+
+ /**
+ * @private
+ * Returns a getter function to be placed on the owner model's prototype. We cache the loaded instance
+ * the first time it is loaded so that subsequent calls to the getter always receive the same reference.
+ * @return {Function} The getter function
+ */
+ createGetter: function() {
+ var me = this,
+ ownerModel = me.ownerModel,
+ associatedName = me.associatedName,
+ associatedModel = me.associatedModel,
+ foreignKey = me.foreignKey,
+ primaryKey = me.primaryKey,
+ instanceName = me.instanceName;
+
+ //'this' refers to the Model instance inside this function
+ return function(options, scope) {
+ options = options || {};
+
+ var model = this,
+ foreignKeyId = model.get(foreignKey),
+ instance,
+ args;
+
+ if (model[instanceName] === undefined) {
+ instance = Ext.ModelManager.create({}, associatedName);
+ instance.set(primaryKey, foreignKeyId);
+
+ if (typeof options == 'function') {
+ options = {
+ callback: options,
+ scope: scope || model
+ };
+ }
+
+ associatedModel.load(foreignKeyId, options);
+ model[instanceName] = associatedModel;
+ return associatedModel;
+ } else {
+ instance = model[instanceName];
+ args = [instance];
+ scope = scope || model;
+
+ //TODO: We're duplicating the callback invokation code that the instance.load() call above
+ //makes here - ought to be able to normalize this - perhaps by caching at the Model.load layer
+ //instead of the association layer.
+ Ext.callback(options, scope, args);
+ Ext.callback(options.success, scope, args);
+ Ext.callback(options.failure, scope, args);
+ Ext.callback(options.callback, scope, args);
+
+ return instance;
+ }
+ };
+ },
+
+ /**
+ * Read associated data
+ * @private
+ * @param {Ext.data.Model} record The record we're writing to
+ * @param {Ext.data.reader.Reader} reader The reader for the associated model
+ * @param {Object} associationData The raw associated data
+ */
+ read: function(record, reader, associationData){
+ record[this.instanceName] = reader.read([associationData]).records[0];
+ }
+});
+
+/**
+ * @class Ext.data.BufferStore
+ * @extends Ext.data.Store
+ * @ignore
+ */
+Ext.define('Ext.data.BufferStore', {
+ extend: 'Ext.data.Store',
+ alias: 'store.buffer',
+ sortOnLoad: false,
+ filterOnLoad: false,
+
+ constructor: function() {
+ Ext.Error.raise('The BufferStore class has been deprecated. Instead, specify the buffered config option on Ext.data.Store');
+ }
+});
+/**
+ * Ext.Direct aims to streamline communication between the client and server by providing a single interface that
+ * reduces the amount of common code typically required to validate data and handle returned data packets (reading data,
+ * error conditions, etc).
+ *
+ * The Ext.direct namespace includes several classes for a closer integration with the server-side. The Ext.data
+ * namespace also includes classes for working with Ext.data.Stores which are backed by data from an Ext.Direct method.
+ *
+ * # Specification
+ *
+ * For additional information consult the [Ext.Direct Specification][1].
+ *
+ * # Providers
+ *
+ * Ext.Direct uses a provider architecture, where one or more providers are used to transport data to and from the
+ * server. There are several providers that exist in the core at the moment:
+ *
+ * - {@link Ext.direct.JsonProvider JsonProvider} for simple JSON operations
+ * - {@link Ext.direct.PollingProvider PollingProvider} for repeated requests
+ * - {@link Ext.direct.RemotingProvider RemotingProvider} exposes server side on the client.
+ *
+ * A provider does not need to be invoked directly, providers are added via {@link Ext.direct.Manager}.{@link #addProvider}.
+ *
+ * # Router
+ *
+ * Ext.Direct utilizes a "router" on the server to direct requests from the client to the appropriate server-side
+ * method. Because the Ext.Direct API is completely platform-agnostic, you could completely swap out a Java based server
+ * solution and replace it with one that uses C# without changing the client side JavaScript at all.
+ *
+ * # Server side events
+ *
+ * Custom events from the server may be handled by the client by adding listeners, for example:
+ *
+ * {"type":"event","name":"message","data":"Successfully polled at: 11:19:30 am"}
+ *
+ * // add a handler for a 'message' event sent by the server
+ * Ext.direct.Manager.on('message', function(e){
+ * out.append(String.format('<p><i>{0}</i></p>', e.data));
+ * out.el.scrollTo('t', 100000, true);
+ * });
+ *
+ * [1]: http://sencha.com/products/extjs/extdirect
+ *
+ * @singleton
+ * @alternateClassName Ext.Direct
+ */
+Ext.define('Ext.direct.Manager', {
+
+ /* Begin Definitions */
+ singleton: true,
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ requires: ['Ext.util.MixedCollection'],
+
+ statics: {
+ exceptions: {
+ TRANSPORT: 'xhr',
+ PARSE: 'parse',
+ LOGIN: 'login',
+ SERVER: 'exception'
+ }
+ },
+
+ /* End Definitions */
+
+ constructor: function(){
+ var me = this;
+
+ me.addEvents(
+ /**
+ * @event event
+ * Fires after an event.
+ * @param {Ext.direct.Event} e The Ext.direct.Event type that occurred.
+ * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
+ */
+ 'event',
+ /**
+ * @event exception
+ * Fires after an event exception.
+ * @param {Ext.direct.Event} e The event type that occurred.
+ */
+ 'exception'
+ );
+ me.transactions = Ext.create('Ext.util.MixedCollection');
+ me.providers = Ext.create('Ext.util.MixedCollection');
+
+ me.mixins.observable.constructor.call(me);
+ },
+
+ /**
+ * Adds an Ext.Direct Provider and creates the proxy or stub methods to execute server-side methods. If the provider
+ * is not already connected, it will auto-connect.
+ *
+ * var pollProv = new Ext.direct.PollingProvider({
+ * url: 'php/poll2.php'
+ * });
+ *
+ * Ext.direct.Manager.addProvider({
+ * "type":"remoting", // create a {@link Ext.direct.RemotingProvider}
+ * "url":"php\/router.php", // url to connect to the Ext.Direct server-side router.
+ * "actions":{ // each property within the actions object represents a Class
+ * "TestAction":[ // array of methods within each server side Class
+ * {
+ * "name":"doEcho", // name of method
+ * "len":1
+ * },{
+ * "name":"multiply",
+ * "len":1
+ * },{
+ * "name":"doForm",
+ * "formHandler":true, // handle form on server with Ext.Direct.Transaction
+ * "len":1
+ * }]
+ * },
+ * "namespace":"myApplication",// namespace to create the Remoting Provider in
+ * },{
+ * type: 'polling', // create a {@link Ext.direct.PollingProvider}
+ * url: 'php/poll.php'
+ * }, pollProv); // reference to previously created instance
+ *
+ * @param {Ext.direct.Provider/Object...} provider
+ * Accepts any number of Provider descriptions (an instance or config object for
+ * a Provider). Each Provider description instructs Ext.Directhow to create
+ * client-side stub methods.
+ */
+ addProvider : function(provider){
+ var me = this,
+ args = arguments,
+ i = 0,
+ len;
+
+ if (args.length > 1) {
+ for (len = args.length; i < len; ++i) {
+ me.addProvider(args[i]);
+ }
+ return;
+ }
+
+ // if provider has not already been instantiated
+ if (!provider.isProvider) {
+ provider = Ext.create('direct.' + provider.type + 'provider', provider);
+ }
+ me.providers.add(provider);
+ provider.on('data', me.onProviderData, me);
+
+
+ if (!provider.isConnected()) {
+ provider.connect();
+ }
+
+ return provider;
+ },
+
+ /**
+ * Retrieves a {@link Ext.direct.Provider provider} by the **{@link Ext.direct.Provider#id id}** specified when the
+ * provider is {@link #addProvider added}.
+ * @param {String/Ext.direct.Provider} id The id of the provider, or the provider instance.
+ */
+ getProvider : function(id){
+ return id.isProvider ? id : this.providers.get(id);
+ },
+
+ /**
+ * Removes the provider.
+ * @param {String/Ext.direct.Provider} provider The provider instance or the id of the provider.
+ * @return {Ext.direct.Provider} The provider, null if not found.
+ */
+ removeProvider : function(provider){
+ var me = this,
+ providers = me.providers;
+
+ provider = provider.isProvider ? provider : providers.get(provider);
+
+ if (provider) {
+ provider.un('data', me.onProviderData, me);
+ providers.remove(provider);
+ return provider;
+ }
+ return null;
+ },
+
+ /**
+ * Adds a transaction to the manager.
+ * @private
+ * @param {Ext.direct.Transaction} transaction The transaction to add
+ * @return {Ext.direct.Transaction} transaction
+ */
+ addTransaction: function(transaction){
+ this.transactions.add(transaction);
+ return transaction;
+ },
+
+ /**
+ * Removes a transaction from the manager.
+ * @private
+ * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to remove
+ * @return {Ext.direct.Transaction} transaction
+ */
+ removeTransaction: function(transaction){
+ transaction = this.getTransaction(transaction);
+ this.transactions.remove(transaction);
+ return transaction;
+ },
+
+ /**
+ * Gets a transaction
+ * @private
+ * @param {String/Ext.direct.Transaction} transaction The transaction/id of transaction to get
+ * @return {Ext.direct.Transaction}
+ */
+ getTransaction: function(transaction){
+ return transaction.isTransaction ? transaction : this.transactions.get(transaction);
+ },
+
+ onProviderData : function(provider, event){
+ var me = this,
+ i = 0,
+ len;
+
+ if (Ext.isArray(event)) {
+ for (len = event.length; i < len; ++i) {
+ me.onProviderData(provider, event[i]);
+ }
+ return;
+ }
+ if (event.name && event.name != 'event' && event.name != 'exception') {
+ me.fireEvent(event.name, event);
+ } else if (event.status === false) {
+ me.fireEvent('exception', event);
+ }
+ me.fireEvent('event', event, provider);
+ }
+}, function(){
+ // Backwards compatibility
+ Ext.Direct = Ext.direct.Manager;
+});
+
+/**
+ * This class is used to send requests to the server using {@link Ext.direct.Manager Ext.Direct}. When a
+ * request is made, the transport mechanism is handed off to the appropriate
+ * {@link Ext.direct.RemotingProvider Provider} to complete the call.
+ *
+ * # Specifying the function
+ *
+ * This proxy expects a Direct remoting method to be passed in order to be able to complete requests.
+ * This can be done by specifying the {@link #directFn} configuration. This will use the same direct
+ * method for all requests. Alternatively, you can provide an {@link #api} configuration. This
+ * allows you to specify a different remoting method for each CRUD action.
+ *
+ * # Parameters
+ *
+ * This proxy provides options to help configure which parameters will be sent to the server.
+ * By specifying the {@link #paramsAsHash} option, it will send an object literal containing each
+ * of the passed parameters. The {@link #paramOrder} option can be used to specify the order in which
+ * the remoting method parameters are passed.
+ *
+ * # Example Usage
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['firstName', 'lastName'],
+ * proxy: {
+ * type: 'direct',
+ * directFn: MyApp.getUsers,
+ * paramOrder: 'id' // Tells the proxy to pass the id as the first parameter to the remoting method.
+ * }
+ * });
+ * User.load(1);
+ */
+Ext.define('Ext.data.proxy.Direct', {
+ /* Begin Definitions */
+
+ extend: 'Ext.data.proxy.Server',
+ alternateClassName: 'Ext.data.DirectProxy',
+
+ alias: 'proxy.direct',
+
+ requires: ['Ext.direct.Manager'],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String/String[]} paramOrder
+ * Defaults to undefined. 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 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
+ * Send parameters as a collection of named arguments.
+ * Providing a {@link #paramOrder} nullifies this configuration.
+ */
+ paramsAsHash: true,
+
+ /**
+ * @cfg {Function} directFn
+ * Function to call when executing a request. directFn is a simple alternative to defining the api configuration-parameter
+ * for Store's which will not implement a full CRUD api.
+ */
+ directFn : undefined,
+
+ /**
+ * @cfg {Object} api
+ * The same as {@link Ext.data.proxy.Server#api}, however instead of providing urls, you should provide a direct
+ * function call.
+ */
+
+ /**
+ * @cfg {Object} extraParams
+ * Extra parameters that will be included on every read request. Individual requests with params
+ * of the same name will override these params when they are in conflict.
+ */
+
+ // private
+ paramOrderRe: /[\s,|]/,
+
+ constructor: function(config){
+ var me = this;
+
+ Ext.apply(me, config);
+ if (Ext.isString(me.paramOrder)) {
+ me.paramOrder = me.paramOrder.split(me.paramOrderRe);
+ }
+ me.callParent(arguments);
+ },
+
+ doRequest: function(operation, callback, scope) {
+ var me = this,
+ writer = me.getWriter(),
+ request = me.buildRequest(operation, callback, scope),
+ fn = me.api[request.action] || me.directFn,
+ args = [],
+ params = request.params,
+ paramOrder = me.paramOrder,
+ method,
+ i = 0,
+ len;
+
+
+ if (operation.allowWrite()) {
+ request = writer.write(request);
+ }
+
+ if (operation.action == 'read') {
+ // We need to pass params
+ method = fn.directCfg.method;
+
+ if (method.ordered) {
+ if (method.len > 0) {
+ if (paramOrder) {
+ for (len = paramOrder.length; i < len; ++i) {
+ args.push(params[paramOrder[i]]);
+ }
+ } else if (me.paramsAsHash) {
+ args.push(params);
+ }
+ }
+ } else {
+ args.push(params);
+ }
+ } else {
+ args.push(request.jsonData);
+ }
+
+ Ext.apply(request, {
+ args: args,
+ directFn: fn
+ });
+ args.push(me.createRequestCallback(request, operation, callback, scope), me);
+ fn.apply(window, args);
+ },
+
+ /*
+ * Inherit docs. We don't apply any encoding here because
+ * all of the direct requests go out as jsonData
+ */
+ applyEncoding: function(value){
+ return value;
+ },
+
+ createRequestCallback: function(request, operation, callback, scope){
+ var me = this;
+
+ return function(data, event){
+ me.processResponse(event.status, operation, request, event, callback, scope);
+ };
+ },
+
+ // inherit docs
+ extractResponseData: function(response){
+ return Ext.isDefined(response.result) ? response.result : response.data;
+ },
+
+ // inherit docs
+ setException: function(operation, response) {
+ operation.setException(response.message);
+ },
+
+ // inherit docs
+ buildUrl: function(){
+ return '';
+ }
+});
+
+/**
+ * Small helper class to create an {@link Ext.data.Store} configured with an {@link Ext.data.proxy.Direct}
+ * and {@link Ext.data.reader.Json} to make interacting with an {@link Ext.direct.Manager} server-side
+ * {@link Ext.direct.Provider Provider} easier. To create a different proxy/reader combination create a basic
+ * {@link Ext.data.Store} configured as needed.
+ *
+ * **Note:** Although they are not listed, this class inherits all of the config options of:
+ *
+ * - **{@link Ext.data.Store Store}**
+ *
+ * - **{@link Ext.data.reader.Json JsonReader}**
+ *
+ * - **{@link Ext.data.reader.Json#root root}**
+ * - **{@link Ext.data.reader.Json#idProperty idProperty}**
+ * - **{@link Ext.data.reader.Json#totalProperty totalProperty}**
+ *
+ * - **{@link Ext.data.proxy.Direct DirectProxy}**
+ *
+ * - **{@link Ext.data.proxy.Direct#directFn directFn}**
+ * - **{@link Ext.data.proxy.Direct#paramOrder paramOrder}**
+ * - **{@link Ext.data.proxy.Direct#paramsAsHash paramsAsHash}**
+ *
+ */
+Ext.define('Ext.data.DirectStore', {
+ /* Begin Definitions */
+
+ extend: 'Ext.data.Store',
+
+ alias: 'store.direct',
+
+ requires: ['Ext.data.proxy.Direct'],
+
+ /* End Definitions */
+
+ constructor : function(config){
+ config = Ext.apply({}, config);
+ if (!config.proxy) {
+ var proxy = {
+ type: 'direct',
+ reader: {
+ type: 'json'
+ }
+ };
+ Ext.copyTo(proxy, config, 'paramOrder,paramsAsHash,directFn,api,simpleSortMode');
+ Ext.copyTo(proxy.reader, config, 'totalProperty,root,idProperty');
+ config.proxy = proxy;
+ }
+ this.callParent([config]);
+ }
+});
+
+/**
+ * General purpose inflector class that {@link #pluralize pluralizes}, {@link #singularize singularizes} and
+ * {@link #ordinalize ordinalizes} words. Sample usage:
+ *
+ * //turning singular words into plurals
+ * Ext.util.Inflector.pluralize('word'); //'words'
+ * Ext.util.Inflector.pluralize('person'); //'people'
+ * Ext.util.Inflector.pluralize('sheep'); //'sheep'
+ *
+ * //turning plurals into singulars
+ * Ext.util.Inflector.singularize('words'); //'word'
+ * Ext.util.Inflector.singularize('people'); //'person'
+ * Ext.util.Inflector.singularize('sheep'); //'sheep'
+ *
+ * //ordinalizing numbers
+ * Ext.util.Inflector.ordinalize(11); //"11th"
+ * Ext.util.Inflector.ordinalize(21); //"21th"
+ * Ext.util.Inflector.ordinalize(1043); //"1043rd"
+ *
+ * # Customization
+ *
+ * The Inflector comes with a default set of US English pluralization rules. These can be augmented with additional
+ * rules if the default rules do not meet your application's requirements, or swapped out entirely for other languages.
+ * Here is how we might add a rule that pluralizes "ox" to "oxen":
+ *
+ * Ext.util.Inflector.plural(/^(ox)$/i, "$1en");
+ *
+ * Each rule consists of two items - a regular expression that matches one or more rules, and a replacement string. In
+ * this case, the regular expression will only match the string "ox", and will replace that match with "oxen". Here's
+ * how we could add the inverse rule:
+ *
+ * Ext.util.Inflector.singular(/^(ox)en$/i, "$1");
+ *
+ * Note that the ox/oxen rules are present by default.
+ */
+Ext.define('Ext.util.Inflector', {
+
+ /* Begin Definitions */
+
+ singleton: true,
+
+ /* End Definitions */
+
+ /**
+ * @private
+ * The registered plural tuples. Each item in the array should contain two items - the first must be a regular
+ * expression that matchers the singular form of a word, the second must be a String that replaces the matched
+ * part of the regular expression. This is managed by the {@link #plural} method.
+ * @property {Array} plurals
+ */
+ plurals: [
+ [(/(quiz)$/i), "$1zes" ],
+ [(/^(ox)$/i), "$1en" ],
+ [(/([m|l])ouse$/i), "$1ice" ],
+ [(/(matr|vert|ind)ix|ex$/i), "$1ices" ],
+ [(/(x|ch|ss|sh)$/i), "$1es" ],
+ [(/([^aeiouy]|qu)y$/i), "$1ies" ],
+ [(/(hive)$/i), "$1s" ],
+ [(/(?:([^f])fe|([lr])f)$/i), "$1$2ves"],
+ [(/sis$/i), "ses" ],
+ [(/([ti])um$/i), "$1a" ],
+ [(/(buffal|tomat|potat)o$/i), "$1oes" ],
+ [(/(bu)s$/i), "$1ses" ],
+ [(/(alias|status|sex)$/i), "$1es" ],
+ [(/(octop|vir)us$/i), "$1i" ],
+ [(/(ax|test)is$/i), "$1es" ],
+ [(/^person$/), "people" ],
+ [(/^man$/), "men" ],
+ [(/^(child)$/), "$1ren" ],
+ [(/s$/i), "s" ],
+ [(/$/), "s" ]
+ ],
+
+ /**
+ * @private
+ * The set of registered singular matchers. Each item in the array should contain two items - the first must be a
+ * regular expression that matches the plural form of a word, the second must be a String that replaces the
+ * matched part of the regular expression. This is managed by the {@link #singular} method.
+ * @property {Array} singulars
+ */
+ singulars: [
+ [(/(quiz)zes$/i), "$1" ],
+ [(/(matr)ices$/i), "$1ix" ],
+ [(/(vert|ind)ices$/i), "$1ex" ],
+ [(/^(ox)en/i), "$1" ],
+ [(/(alias|status)es$/i), "$1" ],
+ [(/(octop|vir)i$/i), "$1us" ],
+ [(/(cris|ax|test)es$/i), "$1is" ],
+ [(/(shoe)s$/i), "$1" ],
+ [(/(o)es$/i), "$1" ],
+ [(/(bus)es$/i), "$1" ],
+ [(/([m|l])ice$/i), "$1ouse" ],
+ [(/(x|ch|ss|sh)es$/i), "$1" ],
+ [(/(m)ovies$/i), "$1ovie" ],
+ [(/(s)eries$/i), "$1eries"],
+ [(/([^aeiouy]|qu)ies$/i), "$1y" ],
+ [(/([lr])ves$/i), "$1f" ],
+ [(/(tive)s$/i), "$1" ],
+ [(/(hive)s$/i), "$1" ],
+ [(/([^f])ves$/i), "$1fe" ],
+ [(/(^analy)ses$/i), "$1sis" ],
+ [(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i), "$1$2sis"],
+ [(/([ti])a$/i), "$1um" ],
+ [(/(n)ews$/i), "$1ews" ],
+ [(/people$/i), "person" ],
+ [(/s$/i), "" ]
+ ],
+
+ /**
+ * @private
+ * The registered uncountable words
+ * @property {String[]} uncountable
+ */
+ uncountable: [
+ "sheep",
+ "fish",
+ "series",
+ "species",
+ "money",
+ "rice",
+ "information",
+ "equipment",
+ "grass",
+ "mud",
+ "offspring",
+ "deer",
+ "means"
+ ],
+
+ /**
+ * Adds a new singularization rule to the Inflector. See the intro docs for more information
+ * @param {RegExp} matcher The matcher regex
+ * @param {String} replacer The replacement string, which can reference matches from the matcher argument
+ */
+ singular: function(matcher, replacer) {
+ this.singulars.unshift([matcher, replacer]);
+ },
+
+ /**
+ * Adds a new pluralization rule to the Inflector. See the intro docs for more information
+ * @param {RegExp} matcher The matcher regex
+ * @param {String} replacer The replacement string, which can reference matches from the matcher argument
+ */
+ plural: function(matcher, replacer) {
+ this.plurals.unshift([matcher, replacer]);
+ },
+
+ /**
+ * Removes all registered singularization rules
+ */
+ clearSingulars: function() {
+ this.singulars = [];
+ },
+
+ /**
+ * Removes all registered pluralization rules
+ */
+ clearPlurals: function() {
+ this.plurals = [];
+ },
+
+ /**
+ * Returns true if the given word is transnumeral (the word is its own singular and plural form - e.g. sheep, fish)
+ * @param {String} word The word to test
+ * @return {Boolean} True if the word is transnumeral
+ */
+ isTransnumeral: function(word) {
+ return Ext.Array.indexOf(this.uncountable, word) != -1;
+ },
+
+ /**
+ * Returns the pluralized form of a word (e.g. Ext.util.Inflector.pluralize('word') returns 'words')
+ * @param {String} word The word to pluralize
+ * @return {String} The pluralized form of the word
+ */
+ pluralize: function(word) {
+ if (this.isTransnumeral(word)) {
+ return word;
+ }
+
+ var plurals = this.plurals,
+ length = plurals.length,
+ tuple, regex, i;
+
+ for (i = 0; i < length; i++) {
+ tuple = plurals[i];
+ regex = tuple[0];
+
+ if (regex == word || (regex.test && regex.test(word))) {
+ return word.replace(regex, tuple[1]);
+ }
+ }
+
+ return word;
+ },
+
+ /**
+ * Returns the singularized form of a word (e.g. Ext.util.Inflector.singularize('words') returns 'word')
+ * @param {String} word The word to singularize
+ * @return {String} The singularized form of the word
+ */
+ singularize: function(word) {
+ if (this.isTransnumeral(word)) {
+ return word;
+ }
+
+ var singulars = this.singulars,
+ length = singulars.length,
+ tuple, regex, i;
+
+ for (i = 0; i < length; i++) {
+ tuple = singulars[i];
+ regex = tuple[0];
+
+ if (regex == word || (regex.test && regex.test(word))) {
+ return word.replace(regex, tuple[1]);
+ }
+ }
+
+ return word;
+ },
+
+ /**
+ * Returns the correct {@link Ext.data.Model Model} name for a given string. Mostly used internally by the data
+ * package
+ * @param {String} word The word to classify
+ * @return {String} The classified version of the word
+ */
+ classify: function(word) {
+ return Ext.String.capitalize(this.singularize(word));
+ },
+
+ /**
+ * Ordinalizes a given number by adding a prefix such as 'st', 'nd', 'rd' or 'th' based on the last digit of the
+ * number. 21 -> 21st, 22 -> 22nd, 23 -> 23rd, 24 -> 24th etc
+ * @param {Number} number The number to ordinalize
+ * @return {String} The ordinalized number
+ */
+ ordinalize: function(number) {
+ var parsed = parseInt(number, 10),
+ mod10 = parsed % 10,
+ mod100 = parsed % 100;
+
+ //11 through 13 are a special case
+ if (11 <= mod100 && mod100 <= 13) {
+ return number + "th";
+ } else {
+ switch(mod10) {
+ case 1 : return number + "st";
+ case 2 : return number + "nd";
+ case 3 : return number + "rd";
+ default: return number + "th";
+ }
+ }
+ }
+}, function() {
+ //aside from the rules above, there are a number of words that have irregular pluralization so we add them here
+ var irregulars = {
+ alumnus: 'alumni',
+ cactus : 'cacti',
+ focus : 'foci',
+ nucleus: 'nuclei',
+ radius: 'radii',
+ stimulus: 'stimuli',
+ ellipsis: 'ellipses',
+ paralysis: 'paralyses',
+ oasis: 'oases',
+ appendix: 'appendices',
+ index: 'indexes',
+ beau: 'beaux',
+ bureau: 'bureaux',
+ tableau: 'tableaux',
+ woman: 'women',
+ child: 'children',
+ man: 'men',
+ corpus: 'corpora',
+ criterion: 'criteria',
+ curriculum: 'curricula',
+ genus: 'genera',
+ memorandum: 'memoranda',
+ phenomenon: 'phenomena',
+ foot: 'feet',
+ goose: 'geese',
+ tooth: 'teeth',
+ antenna: 'antennae',
+ formula: 'formulae',
+ nebula: 'nebulae',
+ vertebra: 'vertebrae',
+ vita: 'vitae'
+ },
+ singular;
+
+ for (singular in irregulars) {
+ this.plural(singular, irregulars[singular]);
+ this.singular(irregulars[singular], singular);
+ }
+});
+/**
+ * @author Ed Spencer
+ * @class Ext.data.HasManyAssociation
+ * @extends Ext.data.Association
+ *
+ * <p>Represents a one-to-many relationship between two models. Usually created indirectly via a model definition:</p>
+ *
+<pre><code>
+Ext.define('Product', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {name: 'id', type: 'int'},
+ {name: 'user_id', type: 'int'},
+ {name: 'name', type: 'string'}
+ ]
+});
+
+Ext.define('User', {
+ extend: 'Ext.data.Model',
+ fields: [
+ {name: 'id', type: 'int'},
+ {name: 'name', type: 'string'}
+ ],
+ // we can use the hasMany shortcut on the model to create a hasMany association
+ hasMany: {model: 'Product', name: 'products'}
+});
+</pre></code>
+*
+ * <p>Above we created Product and User models, and linked them by saying that a User hasMany Products. This gives
+ * us a new function on every User instance, in this case the function is called 'products' because that is the name
+ * we specified in the association configuration above.</p>
+ *
+ * <p>This new function returns a specialized {@link Ext.data.Store Store} which is automatically filtered to load
+ * only Products for the given model instance:</p>
+ *
+<pre><code>
+//first, we load up a User with id of 1
+var user = Ext.create('User', {id: 1, name: 'Ed'});
+
+//the user.products function was created automatically by the association and returns a {@link Ext.data.Store Store}
+//the created store is automatically scoped to the set of Products for the User with id of 1
+var products = user.products();
+
+//we still have all of the usual Store functions, for example it's easy to add a Product for this User
+products.add({
+ name: 'Another Product'
+});
+
+//saves the changes to the store - this automatically sets the new Product's user_id to 1 before saving
+products.sync();
+</code></pre>
+ *
+ * <p>The new Store is only instantiated the first time you call products() to conserve memory and processing time,
+ * though calling products() a second time returns the same store instance.</p>
+ *
+ * <p><u>Custom filtering</u></p>
+ *
+ * <p>The Store is automatically furnished with a filter - by default this filter tells the store to only return
+ * records where the associated model's foreign key matches the owner model's primary key. For example, if a User
+ * with ID = 100 hasMany Products, the filter loads only Products with user_id == 100.</p>
+ *
+ * <p>Sometimes we want to filter by another field - for example in the case of a Twitter search application we may
+ * have models for Search and Tweet:</p>
+ *
+<pre><code>
+Ext.define('Search', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'id', 'query'
+ ],
+
+ hasMany: {
+ model: 'Tweet',
+ name : 'tweets',
+ filterProperty: 'query'
+ }
+});
+
+Ext.define('Tweet', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'id', 'text', 'from_user'
+ ]
+});
+
+//returns a Store filtered by the filterProperty
+var store = new Search({query: 'Sencha Touch'}).tweets();
+</code></pre>
+ *
+ * <p>The tweets association above is filtered by the query property by setting the {@link #filterProperty}, and is
+ * equivalent to this:</p>
+ *
+<pre><code>
+var store = Ext.create('Ext.data.Store', {
+ model: 'Tweet',
+ filters: [
+ {
+ property: 'query',
+ value : 'Sencha Touch'
+ }
+ ]
+});
+</code></pre>
+ */
+Ext.define('Ext.data.HasManyAssociation', {
+ extend: 'Ext.data.Association',
+ requires: ['Ext.util.Inflector'],
+
+ alias: 'association.hasmany',
+
+ /**
+ * @cfg {String} foreignKey The name of the foreign key on the associated model that links it to the owner
+ * model. Defaults to the lowercased name of the owner model plus "_id", e.g. an association with a where a
+ * model called Group hasMany Users would create 'group_id' as the foreign key. When the remote store is loaded,
+ * the store is automatically filtered so that only records with a matching foreign key are included in the
+ * resulting child store. This can be overridden by specifying the {@link #filterProperty}.
+ * <pre><code>
+Ext.define('Group', {
+ extend: 'Ext.data.Model',
+ fields: ['id', 'name'],
+ hasMany: 'User'
+});
+
+Ext.define('User', {
+ extend: 'Ext.data.Model',
+ fields: ['id', 'name', 'group_id'], // refers to the id of the group that this user belongs to
+ belongsTo: 'Group'
+});
+ * </code></pre>
+ */
+
+ /**
+ * @cfg {String} name The name of the function to create on the owner model to retrieve the child store.
+ * If not specified, the pluralized name of the child model is used.
+ * <pre><code>
+// This will create a users() method on any Group model instance
+Ext.define('Group', {
+ extend: 'Ext.data.Model',
+ fields: ['id', 'name'],
+ hasMany: 'User'
+});
+var group = new Group();
+console.log(group.users());
+
+// The method to retrieve the users will now be getUserList
+Ext.define('Group', {
+ extend: 'Ext.data.Model',
+ fields: ['id', 'name'],
+ hasMany: {model: 'User', name: 'getUserList'}
+});
+var group = new Group();
+console.log(group.getUserList());
+ * </code></pre>
+ */
+
+ /**
+ * @cfg {Object} storeConfig Optional configuration object that will be passed to the generated Store. Defaults to
+ * undefined.
+ */
+
+ /**
+ * @cfg {String} filterProperty Optionally overrides the default filter that is set up on the associated Store. If
+ * this is not set, a filter is automatically created which filters the association based on the configured
+ * {@link #foreignKey}. See intro docs for more details. Defaults to undefined
+ */
+
+ /**
+ * @cfg {Boolean} autoLoad True to automatically load the related store from a remote source when instantiated.
+ * Defaults to <tt>false</tt>.
+ */
+
+ /**
+ * @cfg {String} type The type configuration can be used when creating associations using a configuration object.
+ * Use 'hasMany' to create a HasManyAssocation
+ * <pre><code>
+associations: [{
+ type: 'hasMany',
+ model: 'User'
+}]
+ * </code></pre>
+ */
+
+ constructor: function(config) {
+ var me = this,
+ ownerProto,
+ name;
+
+ me.callParent(arguments);
+
+ me.name = me.name || Ext.util.Inflector.pluralize(me.associatedName.toLowerCase());
+
+ ownerProto = me.ownerModel.prototype;
+ name = me.name;
+
+ Ext.applyIf(me, {
+ storeName : name + "Store",
+ foreignKey: me.ownerName.toLowerCase() + "_id"
+ });
+
+ ownerProto[name] = me.createStore();
+ },
+
+ /**
+ * @private
+ * Creates a function that returns an Ext.data.Store which is configured to load a set of data filtered
+ * by the owner model's primary key - e.g. in a hasMany association where Group hasMany Users, this function
+ * returns a Store configured to return the filtered set of a single Group's Users.
+ * @return {Function} The store-generating function
+ */
+ createStore: function() {
+ var that = this,
+ associatedModel = that.associatedModel,
+ storeName = that.storeName,
+ foreignKey = that.foreignKey,
+ primaryKey = that.primaryKey,
+ filterProperty = that.filterProperty,
+ autoLoad = that.autoLoad,
+ storeConfig = that.storeConfig || {};
+
+ return function() {
+ var me = this,
+ config, filter,
+ modelDefaults = {};
+
+ if (me[storeName] === undefined) {
+ if (filterProperty) {
+ filter = {
+ property : filterProperty,
+ value : me.get(filterProperty),
+ exactMatch: true
+ };
+ } else {
+ filter = {
+ property : foreignKey,
+ value : me.get(primaryKey),
+ exactMatch: true
+ };
+ }
+
+ modelDefaults[foreignKey] = me.get(primaryKey);
+
+ config = Ext.apply({}, storeConfig, {
+ model : associatedModel,
+ filters : [filter],
+ remoteFilter : false,
+ modelDefaults: modelDefaults
+ });
+
+ me[storeName] = Ext.create('Ext.data.Store', config);
+ if (autoLoad) {
+ me[storeName].load();
+ }
+ }
+
+ return me[storeName];
+ };
+ },
+
+ /**
+ * Read associated data
+ * @private
+ * @param {Ext.data.Model} record The record we're writing to
+ * @param {Ext.data.reader.Reader} reader The reader for the associated model
+ * @param {Object} associationData The raw associated data
+ */
+ read: function(record, reader, associationData){
+ var store = record[this.name](),
+ inverse;
+
+ store.add(reader.read(associationData).records);
+
+ //now that we've added the related records to the hasMany association, set the inverse belongsTo
+ //association on each of them if it exists
+ inverse = this.associatedModel.prototype.associations.findBy(function(assoc){
+ return assoc.type === 'belongsTo' && assoc.associatedName === record.$className;
+ });
+
+ //if the inverse association was found, set it now on each record we've just created
+ if (inverse) {
+ store.data.each(function(associatedRecord){
+ associatedRecord[inverse.instanceName] = record;
+ });
+ }
+ }
+});
+/**
+ * @class Ext.data.JsonP
+ * @singleton
+ * This class is used to create JSONP requests. JSONP is a mechanism that allows for making
+ * requests for data cross domain. More information is available <a href="http://en.wikipedia.org/wiki/JSONP">here</a>.
+ */
+Ext.define('Ext.data.JsonP', {
+
+ /* Begin Definitions */
+
+ singleton: true,
+
+ statics: {
+ requestCount: 0,
+ requests: {}
+ },
+
+ /* End Definitions */
+
+ /**
+ * @property timeout
+ * @type Number
+ * A default timeout for any JsonP requests. If the request has not completed in this time the
+ * failure callback will be fired. The timeout is in ms. Defaults to <tt>30000</tt>.
+ */
+ timeout: 30000,
+
+ /**
+ * @property disableCaching
+ * @type Boolean
+ * True to add a unique cache-buster param to requests. Defaults to <tt>true</tt>.
+ */
+ disableCaching: true,
+
+ /**
+ * @property disableCachingParam
+ * @type String
+ * Change the parameter which is sent went disabling caching through a cache buster. Defaults to <tt>'_dc'</tt>.
+ */
+ disableCachingParam: '_dc',
+
+ /**
+ * @property callbackKey
+ * @type String
+ * Specifies the GET parameter that will be sent to the server containing the function name to be executed when
+ * the request completes. Defaults to <tt>callback</tt>. Thus, a common request will be in the form of
+ * url?callback=Ext.data.JsonP.callback1
+ */
+ callbackKey: 'callback',
+
+ /**
+ * Makes a JSONP request.
+ * @param {Object} options An object which may contain the following properties. Note that options will
+ * take priority over any defaults that are specified in the class.
+ * <ul>
+ * <li><b>url</b> : String <div class="sub-desc">The URL to request.</div></li>
+ * <li><b>params</b> : Object (Optional)<div class="sub-desc">An object containing a series of
+ * key value pairs that will be sent along with the request.</div></li>
+ * <li><b>timeout</b> : Number (Optional) <div class="sub-desc">See {@link #timeout}</div></li>
+ * <li><b>callbackKey</b> : String (Optional) <div class="sub-desc">See {@link #callbackKey}</div></li>
+ * <li><b>callbackName</b> : String (Optional) <div class="sub-desc">The function name to use for this request.
+ * By default this name will be auto-generated: Ext.data.JsonP.callback1, Ext.data.JsonP.callback2, etc.
+ * Setting this option to "my_name" will force the function name to be Ext.data.JsonP.my_name.
+ * Use this if you want deterministic behavior, but be careful - the callbackName should be different
+ * in each JsonP request that you make.</div></li>
+ * <li><b>disableCaching</b> : Boolean (Optional) <div class="sub-desc">See {@link #disableCaching}</div></li>
+ * <li><b>disableCachingParam</b> : String (Optional) <div class="sub-desc">See {@link #disableCachingParam}</div></li>
+ * <li><b>success</b> : Function (Optional) <div class="sub-desc">A function to execute if the request succeeds.</div></li>
+ * <li><b>failure</b> : Function (Optional) <div class="sub-desc">A function to execute if the request fails.</div></li>
+ * <li><b>callback</b> : Function (Optional) <div class="sub-desc">A function to execute when the request
+ * completes, whether it is a success or failure.</div></li>
+ * <li><b>scope</b> : Object (Optional)<div class="sub-desc">The scope in
+ * which to execute the callbacks: The "this" object for the callback function. Defaults to the browser window.</div></li>
+ * </ul>
+ * @return {Object} request An object containing the request details.
+ */
+ request: function(options){
+ options = Ext.apply({}, options);
+
+
+ var me = this,
+ disableCaching = Ext.isDefined(options.disableCaching) ? options.disableCaching : me.disableCaching,
+ cacheParam = options.disableCachingParam || me.disableCachingParam,
+ id = ++me.statics().requestCount,
+ callbackName = options.callbackName || 'callback' + id,
+ callbackKey = options.callbackKey || me.callbackKey,
+ timeout = Ext.isDefined(options.timeout) ? options.timeout : me.timeout,
+ params = Ext.apply({}, options.params),
+ url = options.url,
+ name = Ext.isSandboxed ? Ext.getUniqueGlobalNamespace() : 'Ext',
+ request,
+ script;
+
+ params[callbackKey] = name + '.data.JsonP.' + callbackName;
+ if (disableCaching) {
+ params[cacheParam] = new Date().getTime();
+ }
+
+ script = me.createScript(url, params);
+
+ me.statics().requests[id] = request = {
+ url: url,
+ params: params,
+ script: script,
+ id: id,
+ scope: options.scope,
+ success: options.success,
+ failure: options.failure,
+ callback: options.callback,
+ callbackName: callbackName
+ };
+
+ if (timeout > 0) {
+ request.timeout = setTimeout(Ext.bind(me.handleTimeout, me, [request]), timeout);
+ }
+
+ me.setupErrorHandling(request);
+ me[callbackName] = Ext.bind(me.handleResponse, me, [request], true);
+ Ext.getHead().appendChild(script);
+ return request;
+ },
+
+ /**
+ * Abort a request. If the request parameter is not specified all open requests will
+ * be aborted.
+ * @param {Object/String} request (Optional) The request to abort
+ */
+ abort: function(request){
+ var requests = this.statics().requests,
+ key;
+
+ if (request) {
+ if (!request.id) {
+ request = requests[request];
+ }
+ this.abort(request);
+ } else {
+ for (key in requests) {
+ if (requests.hasOwnProperty(key)) {
+ this.abort(requests[key]);
+ }
+ }
+ }
+ },
+
+ /**
+ * Sets up error handling for the script
+ * @private
+ * @param {Object} request The request
+ */
+ setupErrorHandling: function(request){
+ request.script.onerror = Ext.bind(this.handleError, this, [request]);
+ },
+
+ /**
+ * Handles any aborts when loading the script
+ * @private
+ * @param {Object} request The request
+ */
+ handleAbort: function(request){
+ request.errorType = 'abort';
+ this.handleResponse(null, request);
+ },
+
+ /**
+ * Handles any script errors when loading the script
+ * @private
+ * @param {Object} request The request
+ */
+ handleError: function(request){
+ request.errorType = 'error';
+ this.handleResponse(null, request);
+ },
+
+ /**
+ * Cleans up anu script handling errors
+ * @private
+ * @param {Object} request The request
+ */
+ cleanupErrorHandling: function(request){
+ request.script.onerror = null;
+ },
+
+ /**
+ * Handle any script timeouts
+ * @private
+ * @param {Object} request The request
+ */
+ handleTimeout: function(request){
+ request.errorType = 'timeout';
+ this.handleResponse(null, request);
+ },
+
+ /**
+ * Handle a successful response
+ * @private
+ * @param {Object} result The result from the request
+ * @param {Object} request The request
+ */
+ handleResponse: function(result, request){
+
+ var success = true;
+
+ if (request.timeout) {
+ clearTimeout(request.timeout);
+ }
+ delete this[request.callbackName];
+ delete this.statics()[request.id];
+ this.cleanupErrorHandling(request);
+ Ext.fly(request.script).remove();
+
+ if (request.errorType) {
+ success = false;
+ Ext.callback(request.failure, request.scope, [request.errorType]);
+ } else {
+ Ext.callback(request.success, request.scope, [result]);
+ }
+ Ext.callback(request.callback, request.scope, [success, result, request.errorType]);
+ },
+
+ /**
+ * Create the script tag
+ * @private
+ * @param {String} url The url of the request
+ * @param {Object} params Any extra params to be sent
+ */
+ createScript: function(url, params) {
+ var script = document.createElement('script');
+ script.setAttribute("src", Ext.urlAppend(url, Ext.Object.toQueryString(params)));
+ script.setAttribute("async", true);
+ script.setAttribute("type", "text/javascript");
+ return script;
+ }
+});
+
+/**
+ * @class Ext.data.JsonPStore
+ * @extends Ext.data.Store
+ * @private
+ * <p>Small helper class to make creating {@link Ext.data.Store}s from different domain JSON data easier.
+ * A JsonPStore will be automatically configured with a {@link Ext.data.reader.Json} and a {@link Ext.data.proxy.JsonP JsonPProxy}.</p>
+ * <p>A store configuration would be something like:<pre><code>
+var store = new Ext.data.JsonPStore({
+ // store configs
+ autoDestroy: true,
+ storeId: 'myStore',
+
+ // proxy configs
+ url: 'get-images.php',
+
+ // reader configs
+ root: 'images',
+ idProperty: 'name',
+ fields: ['name', 'url', {name:'size', type: 'float'}, {name:'lastmod', type:'date'}]
+});
+ * </code></pre></p>
+ * <p>This store is configured to consume a returned object of the form:<pre><code>
+stcCallback({
+ images: [
+ {name: 'Image one', url:'/GetImage.php?id=1', size:46.5, lastmod: new Date(2007, 10, 29)},
+ {name: 'Image Two', url:'/GetImage.php?id=2', size:43.2, lastmod: new Date(2007, 10, 30)}
+ ]
+})
+ * </code></pre>
+ * <p>Where stcCallback is the callback name passed in the request to the remote domain. See {@link Ext.data.proxy.JsonP JsonPProxy}
+ * for details of how this works.</p>
+ * An object literal of this form could also be used as the {@link #data} config option.</p>
+ * <p><b>*Note:</b> Although not listed here, this class accepts all of the configuration options of
+ * <b>{@link Ext.data.reader.Json JsonReader}</b> and <b>{@link Ext.data.proxy.JsonP JsonPProxy}</b>.</p>
+ * @xtype jsonpstore
+ */
+Ext.define('Ext.data.JsonPStore', {
+ extend: 'Ext.data.Store',
+ alias : 'store.jsonp',
+
+ /**
+ * @cfg {Ext.data.DataReader} reader @hide
+ */
+ constructor: function(config) {
+ this.callParent(Ext.apply(config, {
+ reader: Ext.create('Ext.data.reader.Json', config),
+ proxy : Ext.create('Ext.data.proxy.JsonP', config)
+ }));
+ }
+});
+
+/**
+ * This class is used as a set of methods that are applied to the prototype of a
+ * Model to decorate it with a Node API. This means that models used in conjunction with a tree
+ * will have all of the tree related methods available on the model. In general this class will
+ * not be used directly by the developer. This class also creates extra fields on the model if
+ * they do not exist, to help maintain the tree state and UI. These fields are documented as
+ * config options.
+ */
+Ext.define('Ext.data.NodeInterface', {
+ requires: ['Ext.data.Field'],
+
+ /**
+ * @cfg {String} parentId
+ * ID of parent node.
+ */
+
+ /**
+ * @cfg {Number} index
+ * The position of the node inside its parent. When parent has 4 children and the node is third amongst them,
+ * index will be 2.
+ */
+
+ /**
+ * @cfg {Number} depth
+ * The number of parents this node has. A root node has depth 0, a child of it depth 1, and so on...
+ */
+
+ /**
+ * @cfg {Boolean} [expanded=false]
+ * True if the node is expanded.
+ */
+
+ /**
+ * @cfg {Boolean} [expandable=false]
+ * Set to true to allow for expanding/collapsing of this node.
+ */
+
+ /**
+ * @cfg {Boolean} [checked=null]
+ * Set to true or false to show a checkbox alongside this node.
+ */
+
+ /**
+ * @cfg {Boolean} [leaf=false]
+ * Set to true to indicate that this child can have no children. The expand icon/arrow will then not be
+ * rendered for this node.
+ */
+
+ /**
+ * @cfg {String} cls
+ * CSS class to apply for this node.
+ */
+
+ /**
+ * @cfg {String} iconCls
+ * CSS class to apply for this node's icon.
+ */
+
+ /**
+ * @cfg {String} icon
+ * URL for this node's icon.
+ */
+
+ /**
+ * @cfg {Boolean} root
+ * True if this is the root node.
+ */
+
+ /**
+ * @cfg {Boolean} isLast
+ * True if this is the last node.
+ */
+
+ /**
+ * @cfg {Boolean} isFirst
+ * True if this is the first node.
+ */
+
+ /**
+ * @cfg {Boolean} [allowDrop=true]
+ * Set to false to deny dropping on this node.
+ */
+
+ /**
+ * @cfg {Boolean} [allowDrag=true]
+ * Set to false to deny dragging of this node.
+ */
+
+ /**
+ * @cfg {Boolean} [loaded=false]
+ * True if the node has finished loading.
+ */
+
+ /**
+ * @cfg {Boolean} [loading=false]
+ * True if the node is currently loading.
+ */
+
+ /**
+ * @cfg {String} href
+ * An URL for a link that's created when this config is specified.
+ */
+
+ /**
+ * @cfg {String} hrefTarget
+ * Target for link. Only applicable when {@link #href} also specified.
+ */
+
+ /**
+ * @cfg {String} qtip
+ * Tooltip text to show on this node.
+ */
+
+ /**
+ * @cfg {String} qtitle
+ * Tooltip title.
+ */
+
+ /**
+ * @cfg {String} text
+ * The text for to show on node label.
+ */
+
+ /**
+ * @cfg {Ext.data.NodeInterface[]} children
+ * Array of child nodes.
+ */
+
+
+ /**
+ * @property nextSibling
+ * A reference to this node's next sibling node. `null` if this node does not have a next sibling.
+ */
+
+ /**
+ * @property previousSibling
+ * A reference to this node's previous sibling node. `null` if this node does not have a previous sibling.
+ */
+
+ /**
+ * @property parentNode
+ * A reference to this node's parent node. `null` if this node is the root node.
+ */
+
+ /**
+ * @property lastChild
+ * A reference to this node's last child node. `null` if this node has no children.
+ */
+
+ /**
+ * @property firstChild
+ * A reference to this node's first child node. `null` if this node has no children.
+ */
+
+ /**
+ * @property childNodes
+ * An array of this nodes children. Array will be empty if this node has no chidren.
+ */
+
+ statics: {
+ /**
+ * This method allows you to decorate a Record's prototype to implement the NodeInterface.
+ * This adds a set of methods, new events, new properties and new fields on every Record
+ * with the same Model as the passed Record.
+ * @param {Ext.data.Model} record The Record you want to decorate the prototype of.
+ * @static
+ */
+ decorate: function(record) {
+ if (!record.isNode) {
+ // Apply the methods and fields to the prototype
+ // @TODO: clean this up to use proper class system stuff
+ var mgr = Ext.ModelManager,
+ modelName = record.modelName,
+ modelClass = mgr.getModel(modelName),
+ idName = modelClass.prototype.idProperty,
+ newFields = [],
+ i, newField, len;
+
+ // Start by adding the NodeInterface methods to the Model's prototype
+ modelClass.override(this.getPrototypeBody());
+ newFields = this.applyFields(modelClass, [
+ {name: idName, type: 'string', defaultValue: null},
+ {name: 'parentId', type: 'string', defaultValue: null},
+ {name: 'index', type: 'int', defaultValue: null},
+ {name: 'depth', type: 'int', defaultValue: 0},
+ {name: 'expanded', type: 'bool', defaultValue: false, persist: false},
+ {name: 'expandable', type: 'bool', defaultValue: true, persist: false},
+ {name: 'checked', type: 'auto', defaultValue: null},
+ {name: 'leaf', type: 'bool', defaultValue: false, persist: false},
+ {name: 'cls', type: 'string', defaultValue: null, persist: false},
+ {name: 'iconCls', type: 'string', defaultValue: null, persist: false},
+ {name: 'icon', type: 'string', defaultValue: null, persist: false},
+ {name: 'root', type: 'boolean', defaultValue: false, persist: false},
+ {name: 'isLast', type: 'boolean', defaultValue: false, persist: false},
+ {name: 'isFirst', type: 'boolean', defaultValue: false, persist: false},
+ {name: 'allowDrop', type: 'boolean', defaultValue: true, persist: false},
+ {name: 'allowDrag', type: 'boolean', defaultValue: true, persist: false},
+ {name: 'loaded', type: 'boolean', defaultValue: false, persist: false},
+ {name: 'loading', type: 'boolean', defaultValue: false, persist: false},
+ {name: 'href', type: 'string', defaultValue: null, persist: false},
+ {name: 'hrefTarget', type: 'string', defaultValue: null, persist: false},
+ {name: 'qtip', type: 'string', defaultValue: null, persist: false},
+ {name: 'qtitle', type: 'string', defaultValue: null, persist: false}
+ ]);
+
+ len = newFields.length;
+ // Set default values
+ for (i = 0; i < len; ++i) {
+ newField = newFields[i];
+ if (record.get(newField.name) === undefined) {
+ record.data[newField.name] = newField.defaultValue;
+ }
+ }
+ }
+
+ Ext.applyIf(record, {
+ firstChild: null,
+ lastChild: null,
+ parentNode: null,
+ previousSibling: null,
+ nextSibling: null,
+ childNodes: []
+ });
+ // Commit any fields so the record doesn't show as dirty initially
+ record.commit(true);
+
+ record.enableBubble([
+ /**
+ * @event append
+ * Fires when a new child node is appended
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The newly appended node
+ * @param {Number} index The index of the newly appended node
+ */
+ "append",
+
+ /**
+ * @event remove
+ * Fires when a child node is removed
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The removed node
+ */
+ "remove",
+
+ /**
+ * @event move
+ * Fires when this node is moved to a new location in the tree
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} oldParent The old parent of this node
+ * @param {Ext.data.NodeInterface} newParent The new parent of this node
+ * @param {Number} index The index it was moved to
+ */
+ "move",
+
+ /**
+ * @event insert
+ * Fires when a new child node is inserted.
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The child node inserted
+ * @param {Ext.data.NodeInterface} refNode The child node the node was inserted before
+ */
+ "insert",
+
+ /**
+ * @event beforeappend
+ * Fires before a new child is appended, return false to cancel the append.
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The child node to be appended
+ */
+ "beforeappend",
+
+ /**
+ * @event beforeremove
+ * Fires before a child is removed, return false to cancel the remove.
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The child node to be removed
+ */
+ "beforeremove",
+
+ /**
+ * @event beforemove
+ * Fires before this node is moved to a new location in the tree. Return false to cancel the move.
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} oldParent The parent of this node
+ * @param {Ext.data.NodeInterface} newParent The new parent this node is moving to
+ * @param {Number} index The index it is being moved to
+ */
+ "beforemove",
+
+ /**
+ * @event beforeinsert
+ * Fires before a new child is inserted, return false to cancel the insert.
+ * @param {Ext.data.NodeInterface} this This node
+ * @param {Ext.data.NodeInterface} node The child node to be inserted
+ * @param {Ext.data.NodeInterface} refNode The child node the node is being inserted before
+ */
+ "beforeinsert",
+
+ /**
+ * @event expand
+ * Fires when this node is expanded.
+ * @param {Ext.data.NodeInterface} this The expanding node
+ */
+ "expand",
+
+ /**
+ * @event collapse
+ * Fires when this node is collapsed.
+ * @param {Ext.data.NodeInterface} this The collapsing node
+ */
+ "collapse",
+
+ /**
+ * @event beforeexpand
+ * Fires before this node is expanded.
+ * @param {Ext.data.NodeInterface} this The expanding node
+ */
+ "beforeexpand",
+
+ /**
+ * @event beforecollapse
+ * Fires before this node is collapsed.
+ * @param {Ext.data.NodeInterface} this The collapsing node
+ */
+ "beforecollapse",
+
+ /**
+ * @event sort
+ * Fires when this node's childNodes are sorted.
+ * @param {Ext.data.NodeInterface} this This node.
+ * @param {Ext.data.NodeInterface[]} childNodes The childNodes of this node.
+ */
+ "sort"
+ ]);
+
+ return record;
+ },
+
+ applyFields: function(modelClass, addFields) {
+ var modelPrototype = modelClass.prototype,
+ fields = modelPrototype.fields,
+ keys = fields.keys,
+ ln = addFields.length,
+ addField, i, name,
+ newFields = [];
+
+ for (i = 0; i < ln; i++) {
+ addField = addFields[i];
+ if (!Ext.Array.contains(keys, addField.name)) {
+ addField = Ext.create('data.field', addField);
+
+ newFields.push(addField);
+ fields.add(addField);
+ }
+ }
+
+ return newFields;
+ },
+
+ getPrototypeBody: function() {
+ return {
+ isNode: true,
+
+ /**
+ * Ensures that the passed object is an instance of a Record with the NodeInterface applied
+ * @return {Boolean}
+ */
+ createNode: function(node) {
+ if (Ext.isObject(node) && !node.isModel) {
+ node = Ext.ModelManager.create(node, this.modelName);
+ }
+ // Make sure the node implements the node interface
+ return Ext.data.NodeInterface.decorate(node);
+ },
+
+ /**
+ * Returns true if this node is a leaf
+ * @return {Boolean}
+ */
+ isLeaf : function() {
+ return this.get('leaf') === true;
+ },
+
+ /**
+ * Sets the first child of this node
+ * @private
+ * @param {Ext.data.NodeInterface} node
+ */
+ setFirstChild : function(node) {
+ this.firstChild = node;
+ },
+
+ /**
+ * Sets the last child of this node
+ * @private
+ * @param {Ext.data.NodeInterface} node
+ */
+ setLastChild : function(node) {
+ this.lastChild = node;
+ },
+
+ /**
+ * Updates general data of this node like isFirst, isLast, depth. This
+ * method is internally called after a node is moved. This shouldn't
+ * have to be called by the developer unless they are creating custom
+ * Tree plugins.
+ * @return {Boolean}
+ */
+ updateInfo: function(silent) {
+ var me = this,
+ isRoot = me.isRoot(),
+ parentNode = me.parentNode,
+ isFirst = (!parentNode ? true : parentNode.firstChild == me),
+ isLast = (!parentNode ? true : parentNode.lastChild == me),
+ depth = 0,
+ parent = me,
+ children = me.childNodes,
+ len = children.length,
+ i = 0;
+
+ while (parent.parentNode) {
+ ++depth;
+ parent = parent.parentNode;
+ }
+
+ me.beginEdit();
+ me.set({
+ isFirst: isFirst,
+ isLast: isLast,
+ depth: depth,
+ index: parentNode ? parentNode.indexOf(me) : 0,
+ parentId: parentNode ? parentNode.getId() : null
+ });
+ me.endEdit(silent);
+ if (silent) {
+ me.commit();
+ }
+
+ for (i = 0; i < len; i++) {
+ children[i].updateInfo(silent);
+ }
+ },
+
+ /**
+ * Returns true if this node is the last child of its parent
+ * @return {Boolean}
+ */
+ isLast : function() {
+ return this.get('isLast');
+ },
+
+ /**
+ * Returns true if this node is the first child of its parent
+ * @return {Boolean}
+ */
+ isFirst : function() {
+ return this.get('isFirst');
+ },
+
+ /**
+ * Returns true if this node has one or more child nodes, else false.
+ * @return {Boolean}
+ */
+ hasChildNodes : function() {
+ return !this.isLeaf() && this.childNodes.length > 0;
+ },
+
+ /**
+ * Returns true if this node has one or more child nodes, or if the <tt>expandable</tt>
+ * node attribute is explicitly specified as true, otherwise returns false.
+ * @return {Boolean}
+ */
+ isExpandable : function() {
+ var me = this;
+
+ if (me.get('expandable')) {
+ return !(me.isLeaf() || (me.isLoaded() && !me.hasChildNodes()));
+ }
+ return false;
+ },
+
+ /**
+ * Inserts node(s) as the last child node of this node.
+ *
+ * If the node was previously a child node of another parent node, it will be removed from that node first.
+ *
+ * @param {Ext.data.NodeInterface/Ext.data.NodeInterface[]} node The node or Array of nodes to append
+ * @return {Ext.data.NodeInterface} The appended node if single append, or null if an array was passed
+ */
+ appendChild : function(node, suppressEvents, suppressNodeUpdate) {
+ var me = this,
+ i, ln,
+ index,
+ oldParent,
+ ps;
+
+ // if passed an array or multiple args do them one by one
+ if (Ext.isArray(node)) {
+ for (i = 0, ln = node.length; i < ln; i++) {
+ me.appendChild(node[i]);
+ }
+ } else {
+ // Make sure it is a record
+ node = me.createNode(node);
+
+ if (suppressEvents !== true && me.fireEvent("beforeappend", me, node) === false) {
+ return false;
+ }
+
+ index = me.childNodes.length;
+ oldParent = node.parentNode;
+
+ // it's a move, make sure we move it cleanly
+ if (oldParent) {
+ if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index) === false) {
+ return false;
+ }
+ oldParent.removeChild(node, null, false, true);
+ }
+
+ index = me.childNodes.length;
+ if (index === 0) {
+ me.setFirstChild(node);
+ }
+
+ me.childNodes.push(node);
+ node.parentNode = me;
+ node.nextSibling = null;
+
+ me.setLastChild(node);
+
+ ps = me.childNodes[index - 1];
+ if (ps) {
+ node.previousSibling = ps;
+ ps.nextSibling = node;
+ ps.updateInfo(suppressNodeUpdate);
+ } else {
+ node.previousSibling = null;
+ }
+
+ node.updateInfo(suppressNodeUpdate);
+
+ // As soon as we append a child to this node, we are loaded
+ if (!me.isLoaded()) {
+ me.set('loaded', true);
+ }
+ // If this node didnt have any childnodes before, update myself
+ else if (me.childNodes.length === 1) {
+ me.set('loaded', me.isLoaded());
+ }
+
+ if (suppressEvents !== true) {
+ me.fireEvent("append", me, node, index);
+
+ if (oldParent) {
+ node.fireEvent("move", node, oldParent, me, index);
+ }
+ }
+
+ return node;
+ }
+ },
+
+ /**
+ * Returns the bubble target for this node
+ * @private
+ * @return {Object} The bubble target
+ */
+ getBubbleTarget: function() {
+ return this.parentNode;
+ },
+
+ /**
+ * Removes a child node from this node.
+ * @param {Ext.data.NodeInterface} node The node to remove
+ * @param {Boolean} [destroy=false] True to destroy the node upon removal.
+ * @return {Ext.data.NodeInterface} The removed node
+ */
+ removeChild : function(node, destroy, suppressEvents, suppressNodeUpdate) {
+ var me = this,
+ index = me.indexOf(node);
+
+ if (index == -1 || (suppressEvents !== true && me.fireEvent("beforeremove", me, node) === false)) {
+ return false;
+ }
+
+ // remove it from childNodes collection
+ Ext.Array.erase(me.childNodes, index, 1);
+
+ // update child refs
+ if (me.firstChild == node) {
+ me.setFirstChild(node.nextSibling);
+ }
+ if (me.lastChild == node) {
+ me.setLastChild(node.previousSibling);
+ }
+
+ // update siblings
+ if (node.previousSibling) {
+ node.previousSibling.nextSibling = node.nextSibling;
+ node.previousSibling.updateInfo(suppressNodeUpdate);
+ }
+ if (node.nextSibling) {
+ node.nextSibling.previousSibling = node.previousSibling;
+ node.nextSibling.updateInfo(suppressNodeUpdate);
+ }
+
+ if (suppressEvents !== true) {
+ me.fireEvent("remove", me, node);
+ }
+
+
+ // If this node suddenly doesnt have childnodes anymore, update myself
+ if (!me.childNodes.length) {
+ me.set('loaded', me.isLoaded());
+ }
+
+ if (destroy) {
+ node.destroy(true);
+ } else {
+ node.clear();
+ }
+
+ return node;
+ },
+
+ /**
+ * Creates a copy (clone) of this Node.
+ * @param {String} [id] A new id, defaults to this Node's id.
+ * @param {Boolean} [deep=false] True to recursively copy all child Nodes into the new Node.
+ * False to copy without child Nodes.
+ * @return {Ext.data.NodeInterface} A copy of this Node.
+ */
+ copy: function(newId, deep) {
+ var me = this,
+ result = me.callOverridden(arguments),
+ len = me.childNodes ? me.childNodes.length : 0,
+ i;
+
+ // Move child nodes across to the copy if required
+ if (deep) {
+ for (i = 0; i < len; i++) {
+ result.appendChild(me.childNodes[i].copy(true));
+ }
+ }
+ return result;
+ },
+
+ /**
+ * Clears the node.
+ * @private
+ * @param {Boolean} [destroy=false] True to destroy the node.
+ */
+ clear : function(destroy) {
+ var me = this;
+
+ // clear any references from the node
+ me.parentNode = me.previousSibling = me.nextSibling = null;
+ if (destroy) {
+ me.firstChild = me.lastChild = null;
+ }
+ },
+
+ /**
+ * Destroys the node.
+ */
+ destroy : function(silent) {
+ /*
+ * Silent is to be used in a number of cases
+ * 1) When setRoot is called.
+ * 2) When destroy on the tree is called
+ * 3) For destroying child nodes on a node
+ */
+ var me = this,
+ options = me.destroyOptions;
+
+ if (silent === true) {
+ me.clear(true);
+ Ext.each(me.childNodes, function(n) {
+ n.destroy(true);
+ });
+ me.childNodes = null;
+ delete me.destroyOptions;
+ me.callOverridden([options]);
+ } else {
+ me.destroyOptions = silent;
+ // overridden method will be called, since remove will end up calling destroy(true);
+ me.remove(true);
+ }
+ },
+
+ /**
+ * Inserts the first node before the second node in this nodes childNodes collection.
+ * @param {Ext.data.NodeInterface} node The node to insert
+ * @param {Ext.data.NodeInterface} refNode The node to insert before (if null the node is appended)
+ * @return {Ext.data.NodeInterface} The inserted node
+ */
+ insertBefore : function(node, refNode, suppressEvents) {
+ var me = this,
+ index = me.indexOf(refNode),
+ oldParent = node.parentNode,
+ refIndex = index,
+ ps;
+
+ if (!refNode) { // like standard Dom, refNode can be null for append
+ return me.appendChild(node);
+ }
+
+ // nothing to do
+ if (node == refNode) {
+ return false;
+ }
+
+ // Make sure it is a record with the NodeInterface
+ node = me.createNode(node);
+
+ if (suppressEvents !== true && me.fireEvent("beforeinsert", me, node, refNode) === false) {
+ return false;
+ }
+
+ // when moving internally, indexes will change after remove
+ if (oldParent == me && me.indexOf(node) < index) {
+ refIndex--;
+ }
+
+ // it's a move, make sure we move it cleanly
+ if (oldParent) {
+ if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index, refNode) === false) {
+ return false;
+ }
+ oldParent.removeChild(node);
+ }
+
+ if (refIndex === 0) {
+ me.setFirstChild(node);
+ }
+
+ Ext.Array.splice(me.childNodes, refIndex, 0, node);
+ node.parentNode = me;
+
+ node.nextSibling = refNode;
+ refNode.previousSibling = node;
+
+ ps = me.childNodes[refIndex - 1];
+ if (ps) {
+ node.previousSibling = ps;
+ ps.nextSibling = node;
+ ps.updateInfo();
+ } else {
+ node.previousSibling = null;
+ }
+
+ node.updateInfo();
+
+ if (!me.isLoaded()) {
+ me.set('loaded', true);
+ }
+ // If this node didnt have any childnodes before, update myself
+ else if (me.childNodes.length === 1) {
+ me.set('loaded', me.isLoaded());
+ }
+
+ if (suppressEvents !== true) {
+ me.fireEvent("insert", me, node, refNode);
+
+ if (oldParent) {
+ node.fireEvent("move", node, oldParent, me, refIndex, refNode);
+ }
+ }
+
+ return node;
+ },
+
+ /**
+ * Insert a node into this node
+ * @param {Number} index The zero-based index to insert the node at
+ * @param {Ext.data.Model} node The node to insert
+ * @return {Ext.data.Model} The record you just inserted
+ */
+ insertChild: function(index, node) {
+ var sibling = this.childNodes[index];
+ if (sibling) {
+ return this.insertBefore(node, sibling);
+ }
+ else {
+ return this.appendChild(node);
+ }
+ },
+
+ /**
+ * Removes this node from its parent
+ * @param {Boolean} [destroy=false] True to destroy the node upon removal.
+ * @return {Ext.data.NodeInterface} this
+ */
+ remove : function(destroy, suppressEvents) {
+ var parentNode = this.parentNode;
+
+ if (parentNode) {
+ parentNode.removeChild(this, destroy, suppressEvents, true);
+ }
+ return this;
+ },
+
+ /**
+ * Removes all child nodes from this node.
+ * @param {Boolean} [destroy=false] <True to destroy the node upon removal.
+ * @return {Ext.data.NodeInterface} this
+ */
+ removeAll : function(destroy, suppressEvents) {
+ var cn = this.childNodes,
+ n;
+
+ while ((n = cn[0])) {
+ this.removeChild(n, destroy, suppressEvents);
+ }
+ return this;
+ },
+
+ /**
+ * Returns the child node at the specified index.
+ * @param {Number} index
+ * @return {Ext.data.NodeInterface}
+ */
+ getChildAt : function(index) {
+ return this.childNodes[index];
+ },
+
+ /**
+ * Replaces one child node in this node with another.
+ * @param {Ext.data.NodeInterface} newChild The replacement node
+ * @param {Ext.data.NodeInterface} oldChild The node to replace
+ * @return {Ext.data.NodeInterface} The replaced node
+ */
+ replaceChild : function(newChild, oldChild, suppressEvents) {
+ var s = oldChild ? oldChild.nextSibling : null;
+
+ this.removeChild(oldChild, suppressEvents);
+ this.insertBefore(newChild, s, suppressEvents);
+ return oldChild;
+ },
+
+ /**
+ * Returns the index of a child node
+ * @param {Ext.data.NodeInterface} node
+ * @return {Number} The index of the node or -1 if it was not found
+ */
+ indexOf : function(child) {
+ return Ext.Array.indexOf(this.childNodes, child);
+ },
+
+ /**
+ * Gets the hierarchical path from the root of the current node.
+ * @param {String} [field] The field to construct the path from. Defaults to the model idProperty.
+ * @param {String} [separator="/"] A separator to use.
+ * @return {String} The node path
+ */
+ getPath: function(field, separator) {
+ field = field || this.idProperty;
+ separator = separator || '/';
+
+ var path = [this.get(field)],
+ parent = this.parentNode;
+
+ while (parent) {
+ path.unshift(parent.get(field));
+ parent = parent.parentNode;
+ }
+ return separator + path.join(separator);
+ },
+
+ /**
+ * Returns depth of this node (the root node has a depth of 0)
+ * @return {Number}
+ */
+ getDepth : function() {
+ return this.get('depth');
+ },
+
+ /**
+ * 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] The scope (this reference) in which the function is executed. Defaults to the current Node.
+ * @param {Array} [args] The args to call the function with. Defaults to passing the current Node.
+ */
+ bubble : function(fn, scope, args) {
+ var p = this;
+ while (p) {
+ if (fn.apply(scope || p, args || [p]) === false) {
+ break;
+ }
+ p = p.parentNode;
+ }
+ },
+
+ cascade: function() {
+ if (Ext.isDefined(Ext.global.console)) {
+ Ext.global.console.warn('Ext.data.Node: cascade has been deprecated. Please use cascadeBy instead.');
+ }
+ return this.cascadeBy.apply(this, arguments);
+ },
+
+ /**
+ * 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] The scope (this reference) in which the function is executed. Defaults to the current Node.
+ * @param {Array} [args] The args to call the function with. Defaults to passing the current Node.
+ */
+ cascadeBy : function(fn, scope, args) {
+ if (fn.apply(scope || this, args || [this]) !== false) {
+ var childNodes = this.childNodes,
+ length = childNodes.length,
+ i;
+
+ for (i = 0; i < length; i++) {
+ childNodes[i].cascadeBy(fn, scope, args);
+ }
+ }
+ },
+
+ /**
+ * 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] The scope (this reference) in which the function is executed. Defaults to the current Node in iteration.
+ * @param {Array} [args] The args to call the function with. Defaults to passing the current Node.
+ */
+ eachChild : function(fn, scope, args) {
+ var childNodes = this.childNodes,
+ length = childNodes.length,
+ i;
+
+ for (i = 0; i < length; i++) {
+ if (fn.apply(scope || this, args || [childNodes[i]]) === false) {
+ break;
+ }
+ }
+ },
+
+ /**
+ * Finds the first child that has the attribute with the specified value.
+ * @param {String} attribute The attribute name
+ * @param {Object} value The value to search for
+ * @param {Boolean} [deep=false] True to search through nodes deeper than the immediate children
+ * @return {Ext.data.NodeInterface} The found child or null if none was found
+ */
+ findChild : function(attribute, value, deep) {
+ return this.findChildBy(function() {
+ return this.get(attribute) == value;
+ }, null, deep);
+ },
+
+ /**
+ * 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] The scope (this reference) in which the function is executed. Defaults to the Node being tested.
+ * @param {Boolean} [deep=false] True to search through nodes deeper than the immediate children
+ * @return {Ext.data.NodeInterface} The found child or null if none was found
+ */
+ findChildBy : function(fn, scope, deep) {
+ var cs = this.childNodes,
+ len = cs.length,
+ i = 0, n, res;
+
+ for (; i < len; i++) {
+ n = cs[i];
+ if (fn.call(scope || n, n) === true) {
+ return n;
+ }
+ else if (deep) {
+ res = n.findChildBy(fn, scope, deep);
+ if (res !== null) {
+ return res;
+ }
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Returns true if this node is an ancestor (at any point) of the passed node.
+ * @param {Ext.data.NodeInterface} node
+ * @return {Boolean}
+ */
+ contains : function(node) {
+ return node.isAncestor(this);
+ },
+
+ /**
+ * Returns true if the passed node is an ancestor (at any point) of this node.
+ * @param {Ext.data.NodeInterface} node
+ * @return {Boolean}
+ */
+ isAncestor : function(node) {
+ var p = this.parentNode;
+ while (p) {
+ if (p == node) {
+ return true;
+ }
+ p = p.parentNode;
+ }
+ return false;
+ },
+
+ /**
+ * 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 {Boolean} [recursive=false] True to apply this sort recursively
+ * @param {Boolean} [suppressEvent=false] True to not fire a sort event.
+ */
+ sort : function(sortFn, recursive, suppressEvent) {
+ var cs = this.childNodes,
+ ln = cs.length,
+ i, n;
+
+ if (ln > 0) {
+ Ext.Array.sort(cs, sortFn);
+ for (i = 0; i < ln; i++) {
+ n = cs[i];
+ n.previousSibling = cs[i-1];
+ n.nextSibling = cs[i+1];
+
+ if (i === 0) {
+ this.setFirstChild(n);
+ n.updateInfo();
+ }
+ if (i == ln - 1) {
+ this.setLastChild(n);
+ n.updateInfo();
+ }
+ if (recursive && !n.isLeaf()) {
+ n.sort(sortFn, true, true);
+ }
+ }
+
+ if (suppressEvent !== true) {
+ this.fireEvent('sort', this, cs);
+ }
+ }
+ },
+
+ /**
+ * Returns true if this node is expaned
+ * @return {Boolean}
+ */
+ isExpanded: function() {
+ return this.get('expanded');
+ },
+
+ /**
+ * Returns true if this node is loaded
+ * @return {Boolean}
+ */
+ isLoaded: function() {
+ return this.get('loaded');
+ },
+
+ /**
+ * Returns true if this node is loading
+ * @return {Boolean}
+ */
+ isLoading: function() {
+ return this.get('loading');
+ },
+
+ /**
+ * Returns true if this node is the root node
+ * @return {Boolean}
+ */
+ isRoot: function() {
+ return !this.parentNode;
+ },
+
+ /**
+ * Returns true if this node is visible
+ * @return {Boolean}
+ */
+ isVisible: function() {
+ var parent = this.parentNode;
+ while (parent) {
+ if (!parent.isExpanded()) {
+ return false;
+ }
+ parent = parent.parentNode;
+ }
+ return true;
+ },
+
+ /**
+ * Expand this node.
+ * @param {Boolean} [recursive=false] True to recursively expand all the children
+ * @param {Function} [callback] The function to execute once the expand completes
+ * @param {Object} [scope] The scope to run the callback in
+ */
+ expand: function(recursive, callback, scope) {
+ var me = this;
+
+ // all paths must call the callback (eventually) or things like
+ // selectPath fail
+
+ // First we start by checking if this node is a parent
+ if (!me.isLeaf()) {
+ // If it's loaded, wait until it loads before proceeding
+ if (me.isLoading()) {
+ me.on('expand', function(){
+ me.expand(recursive, callback, scope);
+ }, me, {single: true});
+ } else {
+ // Now we check if this record is already expanding or expanded
+ if (!me.isExpanded()) {
+ // The TreeStore actually listens for the beforeexpand method and checks
+ // whether we have to asynchronously load the children from the server
+ // first. Thats why we pass a callback function to the event that the
+ // store can call once it has loaded and parsed all the children.
+ me.fireEvent('beforeexpand', me, function(){
+ me.set('expanded', true);
+ me.fireEvent('expand', me, me.childNodes, false);
+
+ // Call the expandChildren method if recursive was set to true
+ if (recursive) {
+ me.expandChildren(true, callback, scope);
+ } else {
+ Ext.callback(callback, scope || me, [me.childNodes]);
+ }
+ }, me);
+ } else if (recursive) {
+ // If it is is already expanded but we want to recursively expand then call expandChildren
+ me.expandChildren(true, callback, scope);
+ } else {
+ Ext.callback(callback, scope || me, [me.childNodes]);
+ }
+ }
+ } else {
+ // If it's not then we fire the callback right away
+ Ext.callback(callback, scope || me); // leaf = no childNodes
+ }
+ },
+
+ /**
+ * Expand all the children of this node.
+ * @param {Boolean} [recursive=false] True to recursively expand all the children
+ * @param {Function} [callback] The function to execute once all the children are expanded
+ * @param {Object} [scope] The scope to run the callback in
+ */
+ expandChildren: function(recursive, callback, scope) {
+ var me = this,
+ i = 0,
+ nodes = me.childNodes,
+ ln = nodes.length,
+ node,
+ expanding = 0;
+
+ for (; i < ln; ++i) {
+ node = nodes[i];
+ if (!node.isLeaf() && !node.isExpanded()) {
+ expanding++;
+ nodes[i].expand(recursive, function () {
+ expanding--;
+ if (callback && !expanding) {
+ Ext.callback(callback, scope || me, [me.childNodes]);
+ }
+ });
+ }
+ }
+
+ if (!expanding && callback) {
+ Ext.callback(callback, scope || me, [me.childNodes]); }
+ },
+
+ /**
+ * Collapse this node.
+ * @param {Boolean} [recursive=false] True to recursively collapse all the children
+ * @param {Function} [callback] The function to execute once the collapse completes
+ * @param {Object} [scope] The scope to run the callback in
+ */
+ collapse: function(recursive, callback, scope) {
+ var me = this;
+
+ // First we start by checking if this node is a parent
+ if (!me.isLeaf()) {
+ // Now we check if this record is already collapsing or collapsed
+ if (!me.collapsing && me.isExpanded()) {
+ me.fireEvent('beforecollapse', me, function() {
+ me.set('expanded', false);
+ me.fireEvent('collapse', me, me.childNodes, false);
+
+ // Call the collapseChildren method if recursive was set to true
+ if (recursive) {
+ me.collapseChildren(true, callback, scope);
+ }
+ else {
+ Ext.callback(callback, scope || me, [me.childNodes]);
+ }
+ }, me);
+ }
+ // If it is is already collapsed but we want to recursively collapse then call collapseChildren
+ else if (recursive) {
+ me.collapseChildren(true, callback, scope);
+ }
+ }
+ // If it's not then we fire the callback right away
+ else {
+ Ext.callback(callback, scope || me, [me.childNodes]);
+ }
+ },
+
+ /**
+ * Collapse all the children of this node.
+ * @param {Function} [recursive=false] True to recursively collapse all the children
+ * @param {Function} [callback] The function to execute once all the children are collapsed
+ * @param {Object} [scope] The scope to run the callback in
+ */
+ collapseChildren: function(recursive, callback, scope) {
+ var me = this,
+ i = 0,
+ nodes = me.childNodes,
+ ln = nodes.length,
+ node,
+ collapsing = 0;
+
+ for (; i < ln; ++i) {
+ node = nodes[i];
+ if (!node.isLeaf() && node.isExpanded()) {
+ collapsing++;
+ nodes[i].collapse(recursive, function () {
+ collapsing--;
+ if (callback && !collapsing) {
+ Ext.callback(callback, scope || me, [me.childNodes]);
+ }
+ });
+ }
+ }
+
+ if (!collapsing && callback) {
+ Ext.callback(callback, scope || me, [me.childNodes]);
+ }
+ }
+ };
+ }
+ }
+});
+/**
+ * @class Ext.data.NodeStore
+ * @extends Ext.data.AbstractStore
+ * Node Store
+ * @ignore
+ */
+Ext.define('Ext.data.NodeStore', {
+ extend: 'Ext.data.Store',
+ alias: 'store.node',
+ requires: ['Ext.data.NodeInterface'],
+
+ /**
+ * @cfg {Ext.data.Model} node
+ * The Record you want to bind this Store to. Note that
+ * this record will be decorated with the Ext.data.NodeInterface if this is not the
+ * case yet.
+ */
+ node: null,
+
+ /**
+ * @cfg {Boolean} recursive
+ * Set this to true if you want this NodeStore to represent
+ * all the descendents of the node in its flat data collection. This is useful for
+ * rendering a tree structure to a DataView and is being used internally by
+ * the TreeView. Any records that are moved, removed, inserted or appended to the
+ * node at any depth below the node this store is bound to will be automatically
+ * updated in this Store's internal flat data structure.
+ */
+ recursive: false,
+
+ /**
+ * @cfg {Boolean} rootVisible
+ * False to not include the root node in this Stores collection.
+ */
+ rootVisible: false,
+
+ constructor: function(config) {
+ var me = this,
+ node;
+
+ config = config || {};
+ Ext.apply(me, config);
+
+
+ config.proxy = {type: 'proxy'};
+ me.callParent([config]);
+
+ me.addEvents('expand', 'collapse', 'beforeexpand', 'beforecollapse');
+
+ node = me.node;
+ if (node) {
+ me.node = null;
+ me.setNode(node);
+ }
+ },
+
+ setNode: function(node) {
+ var me = this;
+
+ if (me.node && me.node != node) {
+ // We want to unbind our listeners on the old node
+ me.mun(me.node, {
+ expand: me.onNodeExpand,
+ collapse: me.onNodeCollapse,
+ append: me.onNodeAppend,
+ insert: me.onNodeInsert,
+ remove: me.onNodeRemove,
+ sort: me.onNodeSort,
+ scope: me
+ });
+ me.node = null;
+ }
+
+ if (node) {
+ Ext.data.NodeInterface.decorate(node);
+ me.removeAll();
+ if (me.rootVisible) {
+ me.add(node);
+ }
+ me.mon(node, {
+ expand: me.onNodeExpand,
+ collapse: me.onNodeCollapse,
+ append: me.onNodeAppend,
+ insert: me.onNodeInsert,
+ remove: me.onNodeRemove,
+ sort: me.onNodeSort,
+ scope: me
+ });
+ me.node = node;
+ if (node.isExpanded() && node.isLoaded()) {
+ me.onNodeExpand(node, node.childNodes, true);
+ }
+ }
+ },
+
+ onNodeSort: function(node, childNodes) {
+ var me = this;
+
+ if ((me.indexOf(node) !== -1 || (node === me.node && !me.rootVisible) && node.isExpanded())) {
+ me.onNodeCollapse(node, childNodes, true);
+ me.onNodeExpand(node, childNodes, true);
+ }
+ },
+
+ onNodeExpand: function(parent, records, suppressEvent) {
+ var me = this,
+ insertIndex = me.indexOf(parent) + 1,
+ ln = records ? records.length : 0,
+ i, record;
+
+ if (!me.recursive && parent !== me.node) {
+ return;
+ }
+
+ if (!me.isVisible(parent)) {
+ return;
+ }
+
+ if (!suppressEvent && me.fireEvent('beforeexpand', parent, records, insertIndex) === false) {
+ return;
+ }
+
+ if (ln) {
+ me.insert(insertIndex, records);
+ for (i = 0; i < ln; i++) {
+ record = records[i];
+ if (record.isExpanded()) {
+ if (record.isLoaded()) {
+ // Take a shortcut
+ me.onNodeExpand(record, record.childNodes, true);
+ }
+ else {
+ record.set('expanded', false);
+ record.expand();
+ }
+ }
+ }
+ }
+
+ if (!suppressEvent) {
+ me.fireEvent('expand', parent, records);
+ }
+ },
+
+ onNodeCollapse: function(parent, records, suppressEvent) {
+ var me = this,
+ ln = records.length,
+ collapseIndex = me.indexOf(parent) + 1,
+ i, record;
+
+ if (!me.recursive && parent !== me.node) {
+ return;
+ }
+
+ if (!suppressEvent && me.fireEvent('beforecollapse', parent, records, collapseIndex) === false) {
+ return;
+ }
+
+ for (i = 0; i < ln; i++) {
+ record = records[i];
+ me.remove(record);
+ if (record.isExpanded()) {
+ me.onNodeCollapse(record, record.childNodes, true);
+ }
+ }
+
+ if (!suppressEvent) {
+ me.fireEvent('collapse', parent, records, collapseIndex);
+ }
+ },
+
+ onNodeAppend: function(parent, node, index) {
+ var me = this,
+ refNode, sibling;
+
+ if (me.isVisible(node)) {
+ if (index === 0) {
+ refNode = parent;
+ } else {
+ sibling = node.previousSibling;
+ while (sibling.isExpanded() && sibling.lastChild) {
+ sibling = sibling.lastChild;
+ }
+ refNode = sibling;
+ }
+ me.insert(me.indexOf(refNode) + 1, node);
+ if (!node.isLeaf() && node.isExpanded()) {
+ if (node.isLoaded()) {
+ // Take a shortcut
+ me.onNodeExpand(node, node.childNodes, true);
+ }
+ else {
+ node.set('expanded', false);
+ node.expand();
+ }
+ }
+ }
+ },
+
+ onNodeInsert: function(parent, node, refNode) {
+ var me = this,
+ index = this.indexOf(refNode);
+
+ if (index != -1 && me.isVisible(node)) {
+ me.insert(index, node);
+ if (!node.isLeaf() && node.isExpanded()) {
+ if (node.isLoaded()) {
+ // Take a shortcut
+ me.onNodeExpand(node, node.childNodes, true);
+ }
+ else {
+ node.set('expanded', false);
+ node.expand();
+ }
+ }
+ }
+ },
+
+ onNodeRemove: function(parent, node, index) {
+ var me = this;
+ if (me.indexOf(node) != -1) {
+ if (!node.isLeaf() && node.isExpanded()) {
+ me.onNodeCollapse(node, node.childNodes, true);
+ }
+ me.remove(node);
+ }
+ },
+
+ isVisible: function(node) {
+ var parent = node.parentNode;
+ while (parent) {
+ if (parent === this.node && !this.rootVisible && parent.isExpanded()) {
+ return true;
+ }
+
+ if (this.indexOf(parent) === -1 || !parent.isExpanded()) {
+ return false;
+ }
+
+ parent = parent.parentNode;
+ }
+ return true;
+ }
+});
+/**
+ * @author Ed Spencer
+ *
+ * Simple class that represents a Request that will be made by any {@link Ext.data.proxy.Server} subclass.
+ * All this class does is standardize the representation of a Request as used by any ServerProxy subclass,
+ * it does not contain any actual logic or perform the request itself.
+ */
+Ext.define('Ext.data.Request', {
+ /**
+ * @cfg {String} action
+ * The name of the action this Request represents. Usually one of 'create', 'read', 'update' or 'destroy'.
+ */
+ action: undefined,
+
+ /**
+ * @cfg {Object} params
+ * HTTP request params. The Proxy and its Writer have access to and can modify this object.
+ */
+ params: undefined,
+
+ /**
+ * @cfg {String} method
+ * The HTTP method to use on this Request. Should be one of 'GET', 'POST', 'PUT' or 'DELETE'.
+ */
+ method: 'GET',
+
+ /**
+ * @cfg {String} url
+ * The url to access on this Request
+ */
+ url: undefined,
+
+ /**
+ * Creates the Request object.
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config) {
+ Ext.apply(this, config);
+ }
+});
+/**
+ * @author Don Griffin
+ *
+ * This class is a sequential id generator. A simple use of this class would be like so:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * idgen: 'sequential'
+ * });
+ * // assign id's of 1, 2, 3, etc.
+ *
+ * An example of a configured generator would be:
+ *
+ * Ext.define('MyApp.data.MyModel', {
+ * extend: 'Ext.data.Model',
+ * idgen: {
+ * type: 'sequential',
+ * prefix: 'ID_',
+ * seed: 1000
+ * }
+ * });
+ * // assign id's of ID_1000, ID_1001, ID_1002, etc.
+ *
+ */
+Ext.define('Ext.data.SequentialIdGenerator', {
+ extend: 'Ext.data.IdGenerator',
+ alias: 'idgen.sequential',
+
+ constructor: function() {
+ var me = this;
+
+ me.callParent(arguments);
+
+ me.parts = [ me.prefix, ''];
+ },
+
+ /**
+ * @cfg {String} prefix
+ * The string to place in front of the sequential number for each generated id. The
+ * default is blank.
+ */
+ prefix: '',
+
+ /**
+ * @cfg {Number} seed
+ * The number at which to start generating sequential id's. The default is 1.
+ */
+ seed: 1,
+
+ /**
+ * Generates and returns the next id.
+ * @return {String} The next id.
+ */
+ generate: function () {
+ var me = this,
+ parts = me.parts;
+
+ parts[1] = me.seed++;
+ return parts.join('');
+ }
+});
+
+/**
+ * @class Ext.data.Tree
+ *
+ * This class is used as a container for a series of nodes. The nodes themselves maintain
+ * the relationship between parent/child. The tree itself acts as a manager. It gives functionality
+ * to retrieve a node by its identifier: {@link #getNodeById}.
+ *
+ * The tree also relays events from any of it's child nodes, allowing them to be handled in a
+ * centralized fashion. In general this class is not used directly, rather used internally
+ * by other parts of the framework.
+ *
+ */
+Ext.define('Ext.data.Tree', {
+ alias: 'data.tree',
+
+ mixins: {
+ observable: "Ext.util.Observable"
+ },
+
+ /**
+ * @property {Ext.data.NodeInterface}
+ * The root node for this tree
+ */
+ root: null,
+
+ /**
+ * Creates new Tree object.
+ * @param {Ext.data.NodeInterface} root (optional) The root node
+ */
+ constructor: function(root) {
+ var me = this;
+
+
+
+ me.mixins.observable.constructor.call(me);
+
+ if (root) {
+ me.setRootNode(root);
+ }
+ },
+
+ /**
+ * Returns the root node for this tree.
+ * @return {Ext.data.NodeInterface}
+ */
+ getRootNode : function() {
+ return this.root;
+ },
+
+ /**
+ * Sets the root node for this tree.
+ * @param {Ext.data.NodeInterface} node
+ * @return {Ext.data.NodeInterface} The root node
+ */
+ setRootNode : function(node) {
+ var me = this;
+
+ me.root = node;
+ Ext.data.NodeInterface.decorate(node);
+
+ if (me.fireEvent('beforeappend', null, node) !== false) {
+ node.set('root', true);
+ node.updateInfo();
+
+ me.relayEvents(node, [
+ /**
+ * @event append
+ * @alias Ext.data.NodeInterface#append
+ */
+ "append",
+
+ /**
+ * @event remove
+ * @alias Ext.data.NodeInterface#remove
+ */
+ "remove",
+
+ /**
+ * @event move
+ * @alias Ext.data.NodeInterface#move
+ */
+ "move",
+
+ /**
+ * @event insert
+ * @alias Ext.data.NodeInterface#insert
+ */
+ "insert",
+
+ /**
+ * @event beforeappend
+ * @alias Ext.data.NodeInterface#beforeappend
+ */
+ "beforeappend",
+
+ /**
+ * @event beforeremove
+ * @alias Ext.data.NodeInterface#beforeremove
+ */
+ "beforeremove",
+
+ /**
+ * @event beforemove
+ * @alias Ext.data.NodeInterface#beforemove
+ */
+ "beforemove",
+
+ /**
+ * @event beforeinsert
+ * @alias Ext.data.NodeInterface#beforeinsert
+ */
+ "beforeinsert",
+
+ /**
+ * @event expand
+ * @alias Ext.data.NodeInterface#expand
+ */
+ "expand",
+
+ /**
+ * @event collapse
+ * @alias Ext.data.NodeInterface#collapse
+ */
+ "collapse",
+
+ /**
+ * @event beforeexpand
+ * @alias Ext.data.NodeInterface#beforeexpand
+ */
+ "beforeexpand",
+
+ /**
+ * @event beforecollapse
+ * @alias Ext.data.NodeInterface#beforecollapse
+ */
+ "beforecollapse" ,
+
+ /**
+ * @event rootchange
+ * Fires whenever the root node is changed in the tree.
+ * @param {Ext.data.Model} root The new root
+ */
+ "rootchange"
+ ]);
+
+ node.on({
+ scope: me,
+ insert: me.onNodeInsert,
+ append: me.onNodeAppend,
+ remove: me.onNodeRemove
+ });
+
+ me.nodeHash = {};
+ me.registerNode(node);
+ me.fireEvent('append', null, node);
+ me.fireEvent('rootchange', node);
+ }
+
+ return node;
+ },
+
+ /**
+ * Flattens all the nodes in the tree into an array.
+ * @private
+ * @return {Ext.data.NodeInterface[]} The flattened nodes.
+ */
+ flatten: function(){
+ var nodes = [],
+ hash = this.nodeHash,
+ key;
+
+ for (key in hash) {
+ if (hash.hasOwnProperty(key)) {
+ nodes.push(hash[key]);
+ }
+ }
+ return nodes;
+ },
+
+ /**
+ * Fired when a node is inserted into the root or one of it's children
+ * @private
+ * @param {Ext.data.NodeInterface} parent The parent node
+ * @param {Ext.data.NodeInterface} node The inserted node
+ */
+ onNodeInsert: function(parent, node) {
+ this.registerNode(node, true);
+ },
+
+ /**
+ * Fired when a node is appended into the root or one of it's children
+ * @private
+ * @param {Ext.data.NodeInterface} parent The parent node
+ * @param {Ext.data.NodeInterface} node The appended node
+ */
+ onNodeAppend: function(parent, node) {
+ this.registerNode(node, true);
+ },
+
+ /**
+ * Fired when a node is removed from the root or one of it's children
+ * @private
+ * @param {Ext.data.NodeInterface} parent The parent node
+ * @param {Ext.data.NodeInterface} node The removed node
+ */
+ onNodeRemove: function(parent, node) {
+ this.unregisterNode(node, true);
+ },
+
+ /**
+ * Gets a node in this tree by its id.
+ * @param {String} id
+ * @return {Ext.data.NodeInterface} The match node.
+ */
+ getNodeById : function(id) {
+ return this.nodeHash[id];
+ },
+
+ /**
+ * Registers a node with the tree
+ * @private
+ * @param {Ext.data.NodeInterface} The node to register
+ * @param {Boolean} [includeChildren] True to unregister any child nodes
+ */
+ registerNode : function(node, includeChildren) {
+ this.nodeHash[node.getId() || node.internalId] = node;
+ if (includeChildren === true) {
+ node.eachChild(function(child){
+ this.registerNode(child, true);
+ }, this);
+ }
+ },
+
+ /**
+ * Unregisters a node with the tree
+ * @private
+ * @param {Ext.data.NodeInterface} The node to unregister
+ * @param {Boolean} [includeChildren] True to unregister any child nodes
+ */
+ unregisterNode : function(node, includeChildren) {
+ delete this.nodeHash[node.getId() || node.internalId];
+ if (includeChildren === true) {
+ node.eachChild(function(child){
+ this.unregisterNode(child, true);
+ }, this);
+ }
+ },
+
+ /**
+ * Sorts this tree
+ * @private
+ * @param {Function} sorterFn The function to use for sorting
+ * @param {Boolean} recursive True to perform recursive sorting
+ */
+ sort: function(sorterFn, recursive) {
+ this.getRootNode().sort(sorterFn, recursive);
+ },
+
+ /**
+ * Filters this tree
+ * @private
+ * @param {Function} sorterFn The function to use for filtering
+ * @param {Boolean} recursive True to perform recursive filtering
+ */
+ filter: function(filters, recursive) {
+ this.getRootNode().filter(filters, recursive);
+ }
+});
+/**
+ * The TreeStore is a store implementation that is backed by by an {@link Ext.data.Tree}.
+ * It provides convenience methods for loading nodes, as well as the ability to use
+ * the hierarchical tree structure combined with a store. This class is generally used
+ * in conjunction with {@link Ext.tree.Panel}. This class also relays many events from
+ * the Tree for convenience.
+ *
+ * # Using Models
+ *
+ * If no Model is specified, an implicit model will be created that implements {@link Ext.data.NodeInterface}.
+ * The standard Tree fields will also be copied onto the Model for maintaining their state. These fields are listed
+ * in the {@link Ext.data.NodeInterface} documentation.
+ *
+ * # Reading Nested Data
+ *
+ * For the tree to read nested data, the {@link Ext.data.reader.Reader} must be configured with a root property,
+ * so the reader can find nested data for each node. If a root is not specified, it will default to
+ * 'children'.
+ */
+Ext.define('Ext.data.TreeStore', {
+ extend: 'Ext.data.AbstractStore',
+ alias: 'store.tree',
+ requires: ['Ext.data.Tree', 'Ext.data.NodeInterface', 'Ext.data.NodeStore'],
+
+ /**
+ * @cfg {Ext.data.Model/Ext.data.NodeInterface/Object} root
+ * The root node for this store. For example:
+ *
+ * root: {
+ * expanded: true,
+ * text: "My Root",
+ * children: [
+ * { text: "Child 1", leaf: true },
+ * { text: "Child 2", expanded: true, children: [
+ * { text: "GrandChild", leaf: true }
+ * ] }
+ * ]
+ * }
+ *
+ * Setting the `root` config option is the same as calling {@link #setRootNode}.
+ */
+
+ /**
+ * @cfg {Boolean} clearOnLoad
+ * Remove previously existing child nodes before loading. Default to true.
+ */
+ clearOnLoad : true,
+
+ /**
+ * @cfg {String} nodeParam
+ * The name of the parameter sent to the server which contains the identifier of the node.
+ * Defaults to 'node'.
+ */
+ nodeParam: 'node',
+
+ /**
+ * @cfg {String} defaultRootId
+ * The default root id. Defaults to 'root'
+ */
+ defaultRootId: 'root',
+
+ /**
+ * @cfg {String} defaultRootProperty
+ * The root property to specify on the reader if one is not explicitly defined.
+ */
+ defaultRootProperty: 'children',
+
+ /**
+ * @cfg {Boolean} folderSort
+ * Set to true to automatically prepend a leaf sorter. Defaults to `undefined`.
+ */
+ folderSort: false,
+
+ constructor: function(config) {
+ var me = this,
+ root,
+ fields;
+
+ config = Ext.apply({}, config);
+
+ /**
+ * If we have no fields declare for the store, add some defaults.
+ * These will be ignored if a model is explicitly specified.
+ */
+ fields = config.fields || me.fields;
+ if (!fields) {
+ config.fields = [{name: 'text', type: 'string'}];
+ }
+
+ me.callParent([config]);
+
+ // We create our data tree.
+ me.tree = Ext.create('Ext.data.Tree');
+
+ me.relayEvents(me.tree, [
+ /**
+ * @event append
+ * @alias Ext.data.Tree#append
+ */
+ "append",
+
+ /**
+ * @event remove
+ * @alias Ext.data.Tree#remove
+ */
+ "remove",
+
+ /**
+ * @event move
+ * @alias Ext.data.Tree#move
+ */
+ "move",
+
+ /**
+ * @event insert
+ * @alias Ext.data.Tree#insert
+ */
+ "insert",
+
+ /**
+ * @event beforeappend
+ * @alias Ext.data.Tree#beforeappend
+ */
+ "beforeappend",
+
+ /**
+ * @event beforeremove
+ * @alias Ext.data.Tree#beforeremove
+ */
+ "beforeremove",
+
+ /**
+ * @event beforemove
+ * @alias Ext.data.Tree#beforemove
+ */
+ "beforemove",
+
+ /**
+ * @event beforeinsert
+ * @alias Ext.data.Tree#beforeinsert
+ */
+ "beforeinsert",
+
+ /**
+ * @event expand
+ * @alias Ext.data.Tree#expand
+ */
+ "expand",
+
+ /**
+ * @event collapse
+ * @alias Ext.data.Tree#collapse
+ */
+ "collapse",
+
+ /**
+ * @event beforeexpand
+ * @alias Ext.data.Tree#beforeexpand
+ */
+ "beforeexpand",
+
+ /**
+ * @event beforecollapse
+ * @alias Ext.data.Tree#beforecollapse
+ */
+ "beforecollapse",
+
+ /**
+ * @event rootchange
+ * @alias Ext.data.Tree#rootchange
+ */
+ "rootchange"
+ ]);
+
+ me.tree.on({
+ scope: me,
+ remove: me.onNodeRemove,
+ // this event must follow the relay to beforeitemexpand to allow users to
+ // cancel the expand:
+ beforeexpand: me.onBeforeNodeExpand,
+ beforecollapse: me.onBeforeNodeCollapse,
+ append: me.onNodeAdded,
+ insert: me.onNodeAdded
+ });
+
+ me.onBeforeSort();
+
+ root = me.root;
+ if (root) {
+ delete me.root;
+ me.setRootNode(root);
+ }
+
+ me.addEvents(
+ /**
+ * @event sort
+ * Fires when this TreeStore is sorted.
+ * @param {Ext.data.NodeInterface} node The node that is sorted.
+ */
+ 'sort'
+ );
+
+ if (Ext.isDefined(me.nodeParameter)) {
+ if (Ext.isDefined(Ext.global.console)) {
+ Ext.global.console.warn('Ext.data.TreeStore: nodeParameter has been deprecated. Please use nodeParam instead.');
+ }
+ me.nodeParam = me.nodeParameter;
+ delete me.nodeParameter;
+ }
+ },
+
+ // inherit docs
+ setProxy: function(proxy) {
+ var reader,
+ needsRoot;
+
+ if (proxy instanceof Ext.data.proxy.Proxy) {
+ // proxy instance, check if a root was set
+ needsRoot = Ext.isEmpty(proxy.getReader().root);
+ } else if (Ext.isString(proxy)) {
+ // string type, means a reader can't be set
+ needsRoot = true;
+ } else {
+ // object, check if a reader and a root were specified.
+ reader = proxy.reader;
+ needsRoot = !(reader && !Ext.isEmpty(reader.root));
+ }
+ proxy = this.callParent(arguments);
+ if (needsRoot) {
+ reader = proxy.getReader();
+ reader.root = this.defaultRootProperty;
+ // force rebuild
+ reader.buildExtractors(true);
+ }
+ },
+
+ // inherit docs
+ onBeforeSort: function() {
+ if (this.folderSort) {
+ this.sort({
+ property: 'leaf',
+ direction: 'ASC'
+ }, 'prepend', false);
+ }
+ },
+
+ /**
+ * Called before a node is expanded.
+ * @private
+ * @param {Ext.data.NodeInterface} node The node being expanded.
+ * @param {Function} callback The function to run after the expand finishes
+ * @param {Object} scope The scope in which to run the callback function
+ */
+ onBeforeNodeExpand: function(node, callback, scope) {
+ if (node.isLoaded()) {
+ Ext.callback(callback, scope || node, [node.childNodes]);
+ }
+ else if (node.isLoading()) {
+ this.on('load', function() {
+ Ext.callback(callback, scope || node, [node.childNodes]);
+ }, this, {single: true});
+ }
+ else {
+ this.read({
+ node: node,
+ callback: function() {
+ Ext.callback(callback, scope || node, [node.childNodes]);
+ }
+ });
+ }
+ },
+
+ //inherit docs
+ getNewRecords: function() {
+ return Ext.Array.filter(this.tree.flatten(), this.filterNew);
+ },
+
+ //inherit docs
+ getUpdatedRecords: function() {
+ return Ext.Array.filter(this.tree.flatten(), this.filterUpdated);
+ },
+
+ /**
+ * Called before a node is collapsed.
+ * @private
+ * @param {Ext.data.NodeInterface} node The node being collapsed.
+ * @param {Function} callback The function to run after the collapse finishes
+ * @param {Object} scope The scope in which to run the callback function
+ */
+ onBeforeNodeCollapse: function(node, callback, scope) {
+ callback.call(scope || node, node.childNodes);
+ },
+
+ onNodeRemove: function(parent, node) {
+ var removed = this.removed;
+
+ if (!node.isReplace && Ext.Array.indexOf(removed, node) == -1) {
+ removed.push(node);
+ }
+ },
+
+ onNodeAdded: function(parent, node) {
+ var proxy = this.getProxy(),
+ reader = proxy.getReader(),
+ data = node.raw || node.data,
+ dataRoot, children;
+
+ Ext.Array.remove(this.removed, node);
+
+ if (!node.isLeaf() && !node.isLoaded()) {
+ dataRoot = reader.getRoot(data);
+ if (dataRoot) {
+ this.fillNode(node, reader.extractData(dataRoot));
+ delete data[reader.root];
+ }
+ }
+ },
+
+ /**
+ * Sets the root node for this store. See also the {@link #root} config option.
+ * @param {Ext.data.Model/Ext.data.NodeInterface/Object} root
+ * @return {Ext.data.NodeInterface} The new root
+ */
+ setRootNode: function(root) {
+ var me = this;
+
+ root = root || {};
+ if (!root.isNode) {
+ // create a default rootNode and create internal data struct.
+ Ext.applyIf(root, {
+ id: me.defaultRootId,
+ text: 'Root',
+ allowDrag: false
+ });
+ root = Ext.ModelManager.create(root, me.model);
+ }
+ Ext.data.NodeInterface.decorate(root);
+
+ // Because we have decorated the model with new fields,
+ // we need to build new extactor functions on the reader.
+ me.getProxy().getReader().buildExtractors(true);
+
+ // When we add the root to the tree, it will automaticaly get the NodeInterface
+ me.tree.setRootNode(root);
+
+ // If the user has set expanded: true on the root, we want to call the expand function
+ if (!root.isLoaded() && (me.autoLoad === true || root.isExpanded())) {
+ me.load({
+ node: root
+ });
+ }
+
+ return root;
+ },
+
+ /**
+ * Returns the root node for this tree.
+ * @return {Ext.data.NodeInterface}
+ */
+ getRootNode: function() {
+ return this.tree.getRootNode();
+ },
+
+ /**
+ * Returns the record node by id
+ * @return {Ext.data.NodeInterface}
+ */
+ getNodeById: function(id) {
+ return this.tree.getNodeById(id);
+ },
+
+ /**
+ * Loads the Store using its configured {@link #proxy}.
+ * @param {Object} options (Optional) config object. This is passed into the {@link Ext.data.Operation Operation}
+ * object that is created and then sent to the proxy's {@link Ext.data.proxy.Proxy#read} function.
+ * The options can also contain a node, which indicates which node is to be loaded. If not specified, it will
+ * default to the root node.
+ */
+ load: function(options) {
+ options = options || {};
+ options.params = options.params || {};
+
+ var me = this,
+ node = options.node || me.tree.getRootNode(),
+ root;
+
+ // If there is not a node it means the user hasnt defined a rootnode yet. In this case lets just
+ // create one for them.
+ if (!node) {
+ node = me.setRootNode({
+ expanded: true
+ });
+ }
+
+ if (me.clearOnLoad) {
+ node.removeAll(true);
+ }
+
+ Ext.applyIf(options, {
+ node: node
+ });
+ options.params[me.nodeParam] = node ? node.getId() : 'root';
+
+ if (node) {
+ node.set('loading', true);
+ }
+
+ return me.callParent([options]);
+ },
+
+
+ /**
+ * Fills a node with a series of child records.
+ * @private
+ * @param {Ext.data.NodeInterface} node The node to fill
+ * @param {Ext.data.Model[]} records The records to add
+ */
+ fillNode: function(node, records) {
+ var me = this,
+ ln = records ? records.length : 0,
+ i = 0, sortCollection;
+
+ if (ln && me.sortOnLoad && !me.remoteSort && me.sorters && me.sorters.items) {
+ sortCollection = Ext.create('Ext.util.MixedCollection');
+ sortCollection.addAll(records);
+ sortCollection.sort(me.sorters.items);
+ records = sortCollection.items;
+ }
+
+ node.set('loaded', true);
+ for (; i < ln; i++) {
+ node.appendChild(records[i], undefined, true);
+ }
+
+ return records;
+ },
+
+ // inherit docs
+ onProxyLoad: function(operation) {
+ var me = this,
+ successful = operation.wasSuccessful(),
+ records = operation.getRecords(),
+ node = operation.node;
+
+ me.loading = false;
+ node.set('loading', false);
+ if (successful) {
+ records = me.fillNode(node, records);
+ }
+ // The load event has an extra node parameter
+ // (differing from the load event described in AbstractStore)
+ /**
+ * @event load
+ * Fires whenever the store reads data from a remote data source.
+ * @param {Ext.data.TreeStore} this
+ * @param {Ext.data.NodeInterface} node The node that was loaded.
+ * @param {Ext.data.Model[]} records An array of records.
+ * @param {Boolean} successful True if the operation was successful.
+ */
+ // deprecate read?
+ me.fireEvent('read', me, operation.node, records, successful);
+ me.fireEvent('load', me, operation.node, records, successful);
+ //this is a callback that would have been passed to the 'read' function and is optional
+ Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
+ },
+
+ /**
+ * Creates any new records when a write is returned from the server.
+ * @private
+ * @param {Ext.data.Model[]} records The array of new records
+ * @param {Ext.data.Operation} operation The operation that just completed
+ * @param {Boolean} success True if the operation was successful
+ */
+ onCreateRecords: function(records, operation, success) {
+ if (success) {
+ var i = 0,
+ length = records.length,
+ originalRecords = operation.records,
+ parentNode,
+ record,
+ original,
+ index;
+
+ /*
+ * Loop over each record returned from the server. Assume they are
+ * returned in order of how they were sent. If we find a matching
+ * record, replace it with the newly created one.
+ */
+ for (; i < length; ++i) {
+ record = records[i];
+ original = originalRecords[i];
+ if (original) {
+ parentNode = original.parentNode;
+ if (parentNode) {
+ // prevent being added to the removed cache
+ original.isReplace = true;
+ parentNode.replaceChild(record, original);
+ delete original.isReplace;
+ }
+ record.phantom = false;
+ }
+ }
+ }
+ },
+
+ /**
+ * Updates any records when a write is returned from the server.
+ * @private
+ * @param {Ext.data.Model[]} records The array of updated records
+ * @param {Ext.data.Operation} operation The operation that just completed
+ * @param {Boolean} success True if the operation was successful
+ */
+ onUpdateRecords: function(records, operation, success){
+ if (success) {
+ var me = this,
+ i = 0,
+ length = records.length,
+ data = me.data,
+ original,
+ parentNode,
+ record;
+
+ for (; i < length; ++i) {
+ record = records[i];
+ original = me.tree.getNodeById(record.getId());
+ parentNode = original.parentNode;
+ if (parentNode) {
+ // prevent being added to the removed cache
+ original.isReplace = true;
+ parentNode.replaceChild(record, original);
+ original.isReplace = false;
+ }
+ }
+ }
+ },
+
+ /**
+ * Removes any records when a write is returned from the server.
+ * @private
+ * @param {Ext.data.Model[]} records The array of removed records
+ * @param {Ext.data.Operation} operation The operation that just completed
+ * @param {Boolean} success True if the operation was successful
+ */
+ onDestroyRecords: function(records, operation, success){
+ if (success) {
+ this.removed = [];
+ }
+ },
+
+ // inherit docs
+ removeAll: function() {
+ this.getRootNode().destroy(true);
+ this.fireEvent('clear', this);
+ },
+
+ // inherit docs
+ doSort: function(sorterFn) {
+ var me = this;
+ if (me.remoteSort) {
+ //the load function will pick up the new sorters and request the sorted data from the proxy
+ me.load();
+ } else {
+ me.tree.sort(sorterFn, true);
+ me.fireEvent('datachanged', me);
+ }
+ me.fireEvent('sort', me);
+ }
+});
+
+/**
+ * @extend Ext.data.IdGenerator
+ * @author Don Griffin
+ *
+ * This class generates UUID's according to RFC 4122. This class has a default id property.
+ * This means that a single instance is shared unless the id property is overridden. Thus,
+ * two {@link Ext.data.Model} instances configured like the following share one generator:
+ *
+ * Ext.define('MyApp.data.MyModelX', {
+ * extend: 'Ext.data.Model',
+ * idgen: 'uuid'
+ * });
+ *
+ * Ext.define('MyApp.data.MyModelY', {
+ * extend: 'Ext.data.Model',
+ * idgen: 'uuid'
+ * });
+ *
+ * This allows all models using this class to share a commonly configured instance.
+ *
+ * # Using Version 1 ("Sequential") UUID's
+ *
+ * If a server can provide a proper timestamp and a "cryptographic quality random number"
+ * (as described in RFC 4122), the shared instance can be configured as follows:
+ *
+ * Ext.data.IdGenerator.get('uuid').reconfigure({
+ * version: 1,
+ * clockSeq: clock, // 14 random bits
+ * salt: salt, // 48 secure random bits (the Node field)
+ * timestamp: ts // timestamp per Section 4.1.4
+ * });
+ *
+ * // or these values can be split into 32-bit chunks:
+ *
+ * Ext.data.IdGenerator.get('uuid').reconfigure({
+ * version: 1,
+ * clockSeq: clock,
+ * salt: { lo: saltLow32, hi: saltHigh32 },
+ * timestamp: { lo: timestampLow32, hi: timestamptHigh32 }
+ * });
+ *
+ * This approach improves the generator's uniqueness by providing a valid timestamp and
+ * higher quality random data. Version 1 UUID's should not be used unless this information
+ * can be provided by a server and care should be taken to avoid caching of this data.
+ *
+ * See http://www.ietf.org/rfc/rfc4122.txt for details.
+ */
+Ext.define('Ext.data.UuidGenerator', function () {
+ var twoPow14 = Math.pow(2, 14),
+ twoPow16 = Math.pow(2, 16),
+ twoPow28 = Math.pow(2, 28),
+ twoPow32 = Math.pow(2, 32);
+
+ function toHex (value, length) {
+ var ret = value.toString(16);
+ if (ret.length > length) {
+ ret = ret.substring(ret.length - length); // right-most digits
+ } else if (ret.length < length) {
+ ret = Ext.String.leftPad(ret, length, '0');
+ }
+ return ret;
+ }
+
+ function rand (lo, hi) {
+ var v = Math.random() * (hi - lo + 1);
+ return Math.floor(v) + lo;
+ }
+
+ function split (bignum) {
+ if (typeof(bignum) == 'number') {
+ var hi = Math.floor(bignum / twoPow32);
+ return {
+ lo: Math.floor(bignum - hi * twoPow32),
+ hi: hi
+ };
+ }
+ return bignum;
+ }
+
+ return {
+ extend: 'Ext.data.IdGenerator',
+
+ alias: 'idgen.uuid',
+
+ id: 'uuid', // shared by default
+
+ /**
+ * @property {Number/Object} salt
+ * When created, this value is a 48-bit number. For computation, this value is split
+ * into 32-bit parts and stored in an object with `hi` and `lo` properties.
+ */
+
+ /**
+ * @property {Number/Object} timestamp
+ * When created, this value is a 60-bit number. For computation, this value is split
+ * into 32-bit parts and stored in an object with `hi` and `lo` properties.
+ */
+
+ /**
+ * @cfg {Number} version
+ * The Version of UUID. Supported values are:
+ *
+ * * 1 : Time-based, "sequential" UUID.
+ * * 4 : Pseudo-random UUID.
+ *
+ * The default is 4.
+ */
+ version: 4,
+
+ constructor: function() {
+ var me = this;
+
+ me.callParent(arguments);
+
+ me.parts = [];
+ me.init();
+ },
+
+ generate: function () {
+ var me = this,
+ parts = me.parts,
+ ts = me.timestamp;
+
+ /*
+ The magic decoder ring (derived from RFC 4122 Section 4.2.2):
+
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | time_low |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | time_mid | ver | time_hi |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ |res| clock_hi | clock_low | salt 0 |M| salt 1 |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | salt (2-5) |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+ time_mid clock_hi (low 6 bits)
+ time_low | time_hi |clock_lo
+ | | | || salt[0]
+ | | | || | salt[1..5]
+ v v v vv v v
+ 0badf00d-aced-1def-b123-dfad0badbeef
+ ^ ^ ^
+ version | multicast (low bit)
+ |
+ reserved (upper 2 bits)
+ */
+ parts[0] = toHex(ts.lo, 8);
+ parts[1] = toHex(ts.hi & 0xFFFF, 4);
+ parts[2] = toHex(((ts.hi >>> 16) & 0xFFF) | (me.version << 12), 4);
+ parts[3] = toHex(0x80 | ((me.clockSeq >>> 8) & 0x3F), 2) +
+ toHex(me.clockSeq & 0xFF, 2);
+ parts[4] = toHex(me.salt.hi, 4) + toHex(me.salt.lo, 8);
+
+ if (me.version == 4) {
+ me.init(); // just regenerate all the random values...
+ } else {
+ // sequentially increment the timestamp...
+ ++ts.lo;
+ if (ts.lo >= twoPow32) { // if (overflow)
+ ts.lo = 0;
+ ++ts.hi;
+ }
+ }
+
+ return parts.join('-').toLowerCase();
+ },
+
+ getRecId: function (rec) {
+ return rec.getId();
+ },
+
+ /**
+ * @private
+ */
+ init: function () {
+ var me = this,
+ salt, time;
+
+ if (me.version == 4) {
+ // See RFC 4122 (Secion 4.4)
+ // o If the state was unavailable (e.g., non-existent or corrupted),
+ // or the saved node ID is different than the current node ID,
+ // generate a random clock sequence value.
+ me.clockSeq = rand(0, twoPow14-1);
+
+ // we run this on every id generation...
+ salt = me.salt || (me.salt = {});
+ time = me.timestamp || (me.timestamp = {});
+
+ // See RFC 4122 (Secion 4.4)
+ salt.lo = rand(0, twoPow32-1);
+ salt.hi = rand(0, twoPow16-1);
+ time.lo = rand(0, twoPow32-1);
+ time.hi = rand(0, twoPow28-1);
+ } else {
+ // this is run only once per-instance
+ me.salt = split(me.salt);
+ me.timestamp = split(me.timestamp);
+
+ // Set multicast bit: "the least significant bit of the first octet of the
+ // node ID" (nodeId = salt for this implementation):
+ me.salt.hi |= 0x100;
+ }
+ },
+
+ /**
+ * Reconfigures this generator given new config properties.
+ */
+ reconfigure: function (config) {
+ Ext.apply(this, config);
+ this.init();
+ }
+ };
+}());
+
+/**
+ * @author Ed Spencer
+ * @class Ext.data.XmlStore
+ * @extends Ext.data.Store
+ * @private
+ * @ignore
+ * <p>Small helper class to make creating {@link Ext.data.Store}s from XML data easier.
+ * A XmlStore will be automatically configured with a {@link Ext.data.reader.Xml}.</p>
+ * <p>A store configuration would be something like:<pre><code>
+var store = new Ext.data.XmlStore({
+ // store configs
+ autoDestroy: true,
+ storeId: 'myStore',
+ url: 'sheldon.xml', // automatically configures a HttpProxy
+ // reader configs
+ record: 'Item', // records will have an "Item" tag
+ idPath: 'ASIN',
+ totalRecords: '@TotalResults'
+ fields: [
+ // set up the fields mapping into the xml doc
+ // The first needs mapping, the others are very basic
+ {name: 'Author', mapping: 'ItemAttributes > Author'},
+ 'Title', 'Manufacturer', 'ProductGroup'
+ ]
+});
+ * </code></pre></p>
+ * <p>This store is configured to consume a returned object of the form:<pre><code>
+<?xml version="1.0" encoding="UTF-8"?>
+<ItemSearchResponse xmlns="http://webservices.amazon.com/AWSECommerceService/2009-05-15">
+ <Items>
+ <Request>
+ <IsValid>True</IsValid>
+ <ItemSearchRequest>
+ <Author>Sidney Sheldon</Author>
+ <SearchIndex>Books</SearchIndex>
+ </ItemSearchRequest>
+ </Request>
+ <TotalResults>203</TotalResults>
+ <TotalPages>21</TotalPages>
+ <Item>
+ <ASIN>0446355453</ASIN>
+ <DetailPageURL>
+ http://www.amazon.com/
+ </DetailPageURL>
+ <ItemAttributes>
+ <Author>Sidney Sheldon</Author>
+ <Manufacturer>Warner Books</Manufacturer>
+ <ProductGroup>Book</ProductGroup>
+ <Title>Master of the Game</Title>
+ </ItemAttributes>
+ </Item>
+ </Items>
+</ItemSearchResponse>
+ * </code></pre>
+ * An object literal of this form could also be used as the {@link #data} config option.</p>
+ * <p><b>Note:</b> This class accepts all of the configuration options of
+ * <b>{@link Ext.data.reader.Xml XmlReader}</b>.</p>
+ * @xtype xmlstore
+ */
+Ext.define('Ext.data.XmlStore', {
+ extend: 'Ext.data.Store',
+ alternateClassName: 'Ext.data.XmlStore',
+ alias: 'store.xml',
+
+ /**
+ * @cfg {Ext.data.DataReader} reader @hide
+ */
+ constructor: function(config){
+ config = config || {};
+ config = config || {};
+
+ Ext.applyIf(config, {
+ proxy: {
+ type: 'ajax',
+ reader: 'xml',
+ writer: 'xml'
+ }
+ });
+
+ this.callParent([config]);
+ }
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * Base class for any client-side storage. Used as a superclass for {@link Ext.data.proxy.Memory Memory} and
+ * {@link Ext.data.proxy.WebStorage Web Storage} proxies. Do not use directly, use one of the subclasses instead.
+ * @private
+ */
+Ext.define('Ext.data.proxy.Client', {
+ extend: 'Ext.data.proxy.Proxy',
+ alternateClassName: 'Ext.data.ClientProxy',
+
+ /**
+ * Abstract function that must be implemented by each ClientProxy subclass. This should purge all record data
+ * from the client side storage, as well as removing any supporting data (such as lists of record IDs)
+ */
+ clear: function() {
+ }
+});
+/**
+ * @author Ed Spencer
+ *
+ * The JsonP proxy is useful when you need to load data from a domain other than the one your application is running on. If
+ * your application is running on http://domainA.com it cannot use {@link Ext.data.proxy.Ajax Ajax} to load its data
+ * from http://domainB.com because cross-domain ajax requests are prohibited by the browser.
+ *
+ * We can get around this using a JsonP proxy. JsonP proxy injects a `<script>` tag into the DOM whenever an AJAX request
+ * would usually be made. Let's say we want to load data from http://domainB.com/users - the script tag that would be
+ * injected might look like this:
+ *
+ * <script src="http://domainB.com/users?callback=someCallback"></script>
+ *
+ * When we inject the tag above, the browser makes a request to that url and includes the response as if it was any
+ * other type of JavaScript include. By passing a callback in the url above, we're telling domainB's server that we want
+ * to be notified when the result comes in and that it should call our callback function with the data it sends back. So
+ * long as the server formats the response to look like this, everything will work:
+ *
+ * someCallback({
+ * users: [
+ * {
+ * id: 1,
+ * name: "Ed Spencer",
+ * email: "ed@sencha.com"
+ * }
+ * ]
+ * });
+ *
+ * As soon as the script finishes loading, the 'someCallback' function that we passed in the url is called with the JSON
+ * object that the server returned.
+ *
+ * JsonP proxy takes care of all of this automatically. It formats the url you pass, adding the callback parameter
+ * automatically. It even creates a temporary callback function, waits for it to be called and then puts the data into
+ * the Proxy making it look just like you loaded it through a normal {@link Ext.data.proxy.Ajax AjaxProxy}. Here's how
+ * we might set that up:
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', 'email']
+ * });
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'jsonp',
+ * url : 'http://domainB.com/users'
+ * }
+ * });
+ *
+ * store.load();
+ *
+ * That's all we need to do - JsonP proxy takes care of the rest. In this case the Proxy will have injected a script tag
+ * like this:
+ *
+ * <script src="http://domainB.com/users?callback=callback1"></script>
+ *
+ * # Customization
+ *
+ * This script tag can be customized using the {@link #callbackKey} configuration. For example:
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'User',
+ * proxy: {
+ * type: 'jsonp',
+ * url : 'http://domainB.com/users',
+ * callbackKey: 'theCallbackFunction'
+ * }
+ * });
+ *
+ * store.load();
+ *
+ * Would inject a script tag like this:
+ *
+ * <script src="http://domainB.com/users?theCallbackFunction=callback1"></script>
+ *
+ * # Implementing on the server side
+ *
+ * The remote server side needs to be configured to return data in this format. Here are suggestions for how you might
+ * achieve this using Java, PHP and ASP.net:
+ *
+ * Java:
+ *
+ * boolean jsonP = false;
+ * String cb = request.getParameter("callback");
+ * if (cb != null) {
+ * jsonP = true;
+ * response.setContentType("text/javascript");
+ * } else {
+ * response.setContentType("application/x-json");
+ * }
+ * Writer out = response.getWriter();
+ * if (jsonP) {
+ * out.write(cb + "(");
+ * }
+ * out.print(dataBlock.toJsonString());
+ * if (jsonP) {
+ * out.write(");");
+ * }
+ *
+ * PHP:
+ *
+ * $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);
+ * }
+ *
+ * ASP.net:
+ *
+ * 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);
+ */
+Ext.define('Ext.data.proxy.JsonP', {
+ extend: 'Ext.data.proxy.Server',
+ alternateClassName: 'Ext.data.ScriptTagProxy',
+ alias: ['proxy.jsonp', 'proxy.scripttag'],
+ requires: ['Ext.data.JsonP'],
+
+ defaultWriterType: 'base',
+
+ /**
+ * @cfg {String} callbackKey
+ * See {@link Ext.data.JsonP#callbackKey}.
+ */
+ callbackKey : 'callback',
+
+ /**
+ * @cfg {String} recordParam
+ * The param name to use when passing records to the server (e.g. 'records=someEncodedRecordString'). Defaults to
+ * 'records'
+ */
+ recordParam: 'records',
+
+ /**
+ * @cfg {Boolean} autoAppendParams
+ * True to automatically append the request's params to the generated url. Defaults to true
+ */
+ autoAppendParams: true,
+
+ constructor: function(){
+ this.addEvents(
+ /**
+ * @event
+ * Fires when the server returns an exception
+ * @param {Ext.data.proxy.Proxy} this
+ * @param {Ext.data.Request} request The request that was sent
+ * @param {Ext.data.Operation} operation The operation that triggered the request
+ */
+ 'exception'
+ );
+ this.callParent(arguments);
+ },
+
+ /**
+ * @private
+ * Performs the read request to the remote domain. JsonP proxy does not actually create an Ajax request,
+ * instead we write out a <script> tag based on the configuration of the internal Ext.data.Request object
+ * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object to execute
+ * @param {Function} callback A callback function to execute when the Operation has been completed
+ * @param {Object} scope The scope to execute the callback in
+ */
+ doRequest: function(operation, callback, scope) {
+ //generate the unique IDs for this request
+ var me = this,
+ writer = me.getWriter(),
+ request = me.buildRequest(operation),
+ params = request.params;
+
+ if (operation.allowWrite()) {
+ request = writer.write(request);
+ }
+
+ // apply JsonP proxy-specific attributes to the Request
+ Ext.apply(request, {
+ callbackKey: me.callbackKey,
+ timeout: me.timeout,
+ scope: me,
+ disableCaching: false, // handled by the proxy
+ callback: me.createRequestCallback(request, operation, callback, scope)
+ });
+
+ // prevent doubling up
+ if (me.autoAppendParams) {
+ request.params = {};
+ }
+
+ request.jsonp = Ext.data.JsonP.request(request);
+ // restore on the request
+ request.params = params;
+ operation.setStarted();
+ me.lastRequest = request;
+
+ return request;
+ },
+
+ /**
+ * @private
+ * Creates and returns the function that is called when the request has completed. The returned function
+ * should accept a Response object, which contains the response to be read by the configured Reader.
+ * The third argument is the callback that should be called after the request has been completed and the Reader has decoded
+ * the response. This callback will typically be the callback passed by a store, e.g. in proxy.read(operation, theCallback, scope)
+ * theCallback refers to the callback argument received by this function.
+ * See {@link #doRequest} for details.
+ * @param {Ext.data.Request} request The Request object
+ * @param {Ext.data.Operation} operation The Operation being executed
+ * @param {Function} callback The callback function to be called when the request completes. This is usually the callback
+ * passed to doRequest
+ * @param {Object} scope The scope in which to execute the callback function
+ * @return {Function} The callback function
+ */
+ createRequestCallback: function(request, operation, callback, scope) {
+ var me = this;
+
+ return function(success, response, errorType) {
+ delete me.lastRequest;
+ me.processResponse(success, operation, request, response, callback, scope);
+ };
+ },
+
+ // inherit docs
+ setException: function(operation, response) {
+ operation.setException(operation.request.jsonp.errorType);
+ },
+
+
+ /**
+ * Generates a url based on a given Ext.data.Request object. Adds the params and callback function name to the url
+ * @param {Ext.data.Request} request The request object
+ * @return {String} The url
+ */
+ buildUrl: function(request) {
+ var me = this,
+ url = me.callParent(arguments),
+ params = Ext.apply({}, request.params),
+ filters = params.filters,
+ records,
+ filter, i;
+
+ delete params.filters;
+
+ if (me.autoAppendParams) {
+ url = Ext.urlAppend(url, Ext.Object.toQueryString(params));
+ }
+
+ if (filters && filters.length) {
+ for (i = 0; i < filters.length; i++) {
+ filter = filters[i];
+
+ if (filter.value) {
+ url = Ext.urlAppend(url, filter.property + "=" + filter.value);
+ }
+ }
+ }
+
+ //if there are any records present, append them to the url also
+ records = request.records;
+
+ if (Ext.isArray(records) && records.length > 0) {
+ url = Ext.urlAppend(url, Ext.String.format("{0}={1}", me.recordParam, me.encodeRecords(records)));
+ }
+
+ return url;
+ },
+
+ //inherit docs
+ destroy: function() {
+ this.abort();
+ this.callParent();
+ },
+
+ /**
+ * Aborts the current server request if one is currently running
+ */
+ abort: function() {
+ var lastRequest = this.lastRequest;
+ if (lastRequest) {
+ Ext.data.JsonP.abort(lastRequest.jsonp);
+ }
+ },
+
+ /**
+ * Encodes an array of records into a string suitable to be appended to the script src url. This is broken out into
+ * its own function so that it can be easily overridden.
+ * @param {Ext.data.Model[]} records The records array
+ * @return {String} The encoded records string
+ */
+ encodeRecords: function(records) {
+ var encoded = "",
+ i = 0,
+ len = records.length;
+
+ for (; i < len; i++) {
+ encoded += Ext.Object.toQueryString(records[i].data);
+ }
+
+ return encoded;
+ }
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * WebStorageProxy is simply a superclass for the {@link Ext.data.proxy.LocalStorage LocalStorage} and {@link
+ * Ext.data.proxy.SessionStorage SessionStorage} proxies. It uses the new HTML5 key/value client-side storage objects to
+ * save {@link Ext.data.Model model instances} for offline use.
+ * @private
+ */
+Ext.define('Ext.data.proxy.WebStorage', {
+ extend: 'Ext.data.proxy.Client',
+ alternateClassName: 'Ext.data.WebStorageProxy',
+
+ /**
+ * @cfg {String} id
+ * The unique ID used as the key in which all record data are stored in the local storage object.
+ */
+ id: undefined,
+
+ /**
+ * Creates the proxy, throws an error if local storage is not supported in the current browser.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ this.callParent(arguments);
+
+ /**
+ * @property {Object} cache
+ * Cached map of records already retrieved by this Proxy. Ensures that the same instance is always retrieved.
+ */
+ this.cache = {};
+
+
+ //if an id is not given, try to use the store's id instead
+ this.id = this.id || (this.store ? this.store.storeId : undefined);
+
+
+ this.initialize();
+ },
+
+ //inherit docs
+ create: function(operation, callback, scope) {
+ var records = operation.records,
+ length = records.length,
+ ids = this.getIds(),
+ id, record, i;
+
+ operation.setStarted();
+
+ for (i = 0; i < length; i++) {
+ record = records[i];
+
+ if (record.phantom) {
+ record.phantom = false;
+ id = this.getNextId();
+ } else {
+ id = record.getId();
+ }
+
+ this.setRecord(record, id);
+ ids.push(id);
+ }
+
+ this.setIds(ids);
+
+ operation.setCompleted();
+ operation.setSuccessful();
+
+ if (typeof callback == 'function') {
+ callback.call(scope || this, operation);
+ }
+ },
+
+ //inherit docs
+ read: function(operation, callback, scope) {
+ //TODO: respect sorters, filters, start and limit options on the Operation
+
+ var records = [],
+ ids = this.getIds(),
+ length = ids.length,
+ i, recordData, record;
+
+ //read a single record
+ if (operation.id) {
+ record = this.getRecord(operation.id);
+
+ if (record) {
+ records.push(record);
+ operation.setSuccessful();
+ }
+ } else {
+ for (i = 0; i < length; i++) {
+ records.push(this.getRecord(ids[i]));
+ }
+ operation.setSuccessful();
+ }
+
+ operation.setCompleted();
+
+ operation.resultSet = Ext.create('Ext.data.ResultSet', {
+ records: records,
+ total : records.length,
+ loaded : true
+ });
+
+ if (typeof callback == 'function') {
+ callback.call(scope || this, operation);
+ }
+ },
+
+ //inherit docs
+ update: function(operation, callback, scope) {
+ var records = operation.records,
+ length = records.length,
+ ids = this.getIds(),
+ record, id, i;
+
+ operation.setStarted();
+
+ for (i = 0; i < length; i++) {
+ record = records[i];
+ this.setRecord(record);
+
+ //we need to update the set of ids here because it's possible that a non-phantom record was added
+ //to this proxy - in which case the record's id would never have been added via the normal 'create' call
+ id = record.getId();
+ if (id !== undefined && Ext.Array.indexOf(ids, id) == -1) {
+ ids.push(id);
+ }
+ }
+ this.setIds(ids);
+
+ operation.setCompleted();
+ operation.setSuccessful();
+
+ if (typeof callback == 'function') {
+ callback.call(scope || this, operation);
+ }
+ },
+
+ //inherit
+ destroy: function(operation, callback, scope) {
+ var records = operation.records,
+ length = records.length,
+ ids = this.getIds(),
+
+ //newIds is a copy of ids, from which we remove the destroyed records
+ newIds = [].concat(ids),
+ i;
+
+ for (i = 0; i < length; i++) {
+ Ext.Array.remove(newIds, records[i].getId());
+ this.removeRecord(records[i], false);
+ }
+
+ this.setIds(newIds);
+
+ operation.setCompleted();
+ operation.setSuccessful();
+
+ if (typeof callback == 'function') {
+ callback.call(scope || this, operation);
+ }
+ },
+
+ /**
+ * @private
+ * Fetches a model instance from the Proxy by ID. Runs each field's decode function (if present) to decode the data.
+ * @param {String} id The record's unique ID
+ * @return {Ext.data.Model} The model instance
+ */
+ getRecord: function(id) {
+ if (this.cache[id] === undefined) {
+ var rawData = Ext.decode(this.getStorageObject().getItem(this.getRecordKey(id))),
+ data = {},
+ Model = this.model,
+ fields = Model.prototype.fields.items,
+ length = fields.length,
+ i, field, name, record;
+
+ for (i = 0; i < length; i++) {
+ field = fields[i];
+ name = field.name;
+
+ if (typeof field.decode == 'function') {
+ data[name] = field.decode(rawData[name]);
+ } else {
+ data[name] = rawData[name];
+ }
+ }
+
+ record = new Model(data, id);
+ record.phantom = false;
+
+ this.cache[id] = record;
+ }
+
+ return this.cache[id];
+ },
+
+ /**
+ * Saves the given record in the Proxy. Runs each field's encode function (if present) to encode the data.
+ * @param {Ext.data.Model} record The model instance
+ * @param {String} [id] The id to save the record under (defaults to the value of the record's getId() function)
+ */
+ setRecord: function(record, id) {
+ if (id) {
+ record.setId(id);
+ } else {
+ id = record.getId();
+ }
+
+ var me = this,
+ rawData = record.data,
+ data = {},
+ model = me.model,
+ fields = model.prototype.fields.items,
+ length = fields.length,
+ i = 0,
+ field, name, obj, key;
+
+ for (; i < length; i++) {
+ field = fields[i];
+ name = field.name;
+
+ if (typeof field.encode == 'function') {
+ data[name] = field.encode(rawData[name], record);
+ } else {
+ data[name] = rawData[name];
+ }
+ }
+
+ obj = me.getStorageObject();
+ key = me.getRecordKey(id);
+
+ //keep the cache up to date
+ me.cache[id] = record;
+
+ //iPad bug requires that we remove the item before setting it
+ obj.removeItem(key);
+ obj.setItem(key, Ext.encode(data));
+ },
+
+ /**
+ * @private
+ * Physically removes a given record from the local storage. Used internally by {@link #destroy}, which you should
+ * use instead because it updates the list of currently-stored record ids
+ * @param {String/Number/Ext.data.Model} id The id of the record to remove, or an Ext.data.Model instance
+ */
+ removeRecord: function(id, updateIds) {
+ var me = this,
+ ids;
+
+ if (id.isModel) {
+ id = id.getId();
+ }
+
+ if (updateIds !== false) {
+ ids = me.getIds();
+ Ext.Array.remove(ids, id);
+ me.setIds(ids);
+ }
+
+ me.getStorageObject().removeItem(me.getRecordKey(id));
+ },
+
+ /**
+ * @private
+ * Given the id of a record, returns a unique string based on that id and the id of this proxy. This is used when
+ * storing data in the local storage object and should prevent naming collisions.
+ * @param {String/Number/Ext.data.Model} id The record id, or a Model instance
+ * @return {String} The unique key for this record
+ */
+ getRecordKey: function(id) {
+ if (id.isModel) {
+ id = id.getId();
+ }
+
+ return Ext.String.format("{0}-{1}", this.id, id);
+ },
+
+ /**
+ * @private
+ * Returns the unique key used to store the current record counter for this proxy. This is used internally when
+ * realizing models (creating them when they used to be phantoms), in order to give each model instance a unique id.
+ * @return {String} The counter key
+ */
+ getRecordCounterKey: function() {
+ return Ext.String.format("{0}-counter", this.id);
+ },
+
+ /**
+ * @private
+ * Returns the array of record IDs stored in this Proxy
+ * @return {Number[]} The record IDs. Each is cast as a Number
+ */
+ getIds: function() {
+ var ids = (this.getStorageObject().getItem(this.id) || "").split(","),
+ length = ids.length,
+ i;
+
+ if (length == 1 && ids[0] === "") {
+ ids = [];
+ } else {
+ for (i = 0; i < length; i++) {
+ ids[i] = parseInt(ids[i], 10);
+ }
+ }
+
+ return ids;
+ },
+
+ /**
+ * @private
+ * Saves the array of ids representing the set of all records in the Proxy
+ * @param {Number[]} ids The ids to set
+ */
+ setIds: function(ids) {
+ var obj = this.getStorageObject(),
+ str = ids.join(",");
+
+ obj.removeItem(this.id);
+
+ if (!Ext.isEmpty(str)) {
+ obj.setItem(this.id, str);
+ }
+ },
+
+ /**
+ * @private
+ * Returns the next numerical ID that can be used when realizing a model instance (see getRecordCounterKey).
+ * Increments the counter.
+ * @return {Number} The id
+ */
+ getNextId: function() {
+ var obj = this.getStorageObject(),
+ key = this.getRecordCounterKey(),
+ last = obj.getItem(key),
+ ids, id;
+
+ if (last === null) {
+ ids = this.getIds();
+ last = ids[ids.length - 1] || 0;
+ }
+
+ id = parseInt(last, 10) + 1;
+ obj.setItem(key, id);
+
+ return id;
+ },
+
+ /**
+ * @private
+ * Sets up the Proxy by claiming the key in the storage object that corresponds to the unique id of this Proxy. Called
+ * automatically by the constructor, this should not need to be called again unless {@link #clear} has been called.
+ */
+ initialize: function() {
+ var storageObject = this.getStorageObject();
+ storageObject.setItem(this.id, storageObject.getItem(this.id) || "");
+ },
+
+ /**
+ * Destroys all records stored in the proxy and removes all keys and values used to support the proxy from the
+ * storage object.
+ */
+ clear: function() {
+ var obj = this.getStorageObject(),
+ ids = this.getIds(),
+ len = ids.length,
+ i;
+
+ //remove all the records
+ for (i = 0; i < len; i++) {
+ this.removeRecord(ids[i]);
+ }
+
+ //remove the supporting objects
+ obj.removeItem(this.getRecordCounterKey());
+ obj.removeItem(this.id);
+ },
+
+ /**
+ * @private
+ * Abstract function which should return the storage object that data will be saved to. This must be implemented
+ * in each subclass.
+ * @return {Object} The storage object
+ */
+ getStorageObject: function() {
+ }
+});
+/**
+ * @author Ed Spencer
+ *
+ * The LocalStorageProxy uses the new HTML5 localStorage API to save {@link Ext.data.Model Model} data locally on the
+ * client browser. HTML5 localStorage is a key-value store (e.g. cannot save complex objects like JSON), so
+ * LocalStorageProxy automatically serializes and deserializes data when saving and retrieving it.
+ *
+ * localStorage is extremely useful for saving user-specific information without needing to build server-side
+ * infrastructure to support it. Let's imagine we're writing a Twitter search application and want to save the user's
+ * searches locally so they can easily perform a saved search again later. We'd start by creating a Search model:
+ *
+ * Ext.define('Search', {
+ * fields: ['id', 'query'],
+ * extend: 'Ext.data.Model',
+ * proxy: {
+ * type: 'localstorage',
+ * id : 'twitter-Searches'
+ * }
+ * });
+ *
+ * Our Search model contains just two fields - id and query - plus a Proxy definition. The only configuration we need to
+ * pass to the LocalStorage proxy is an {@link #id}. This is important as it separates the Model data in this Proxy from
+ * all others. The localStorage API puts all data into a single shared namespace, so by setting an id we enable
+ * LocalStorageProxy to manage the saved Search data.
+ *
+ * Saving our data into localStorage is easy and would usually be done with a {@link Ext.data.Store Store}:
+ *
+ * //our Store automatically picks up the LocalStorageProxy defined on the Search model
+ * var store = Ext.create('Ext.data.Store', {
+ * model: "Search"
+ * });
+ *
+ * //loads any existing Search data from localStorage
+ * store.load();
+ *
+ * //now add some Searches
+ * store.add({query: 'Sencha Touch'});
+ * store.add({query: 'Ext JS'});
+ *
+ * //finally, save our Search data to localStorage
+ * store.sync();
+ *
+ * The LocalStorageProxy automatically gives our new Searches an id when we call store.sync(). It encodes the Model data
+ * and places it into localStorage. We can also save directly to localStorage, bypassing the Store altogether:
+ *
+ * var search = Ext.create('Search', {query: 'Sencha Animator'});
+ *
+ * //uses the configured LocalStorageProxy to save the new Search to localStorage
+ * search.save();
+ *
+ * # Limitations
+ *
+ * If this proxy is used in a browser where local storage is not supported, the constructor will throw an error. A local
+ * storage proxy requires a unique ID which is used as a key in which all record data are stored in the local storage
+ * object.
+ *
+ * It's important to supply this unique ID as it cannot be reliably determined otherwise. If no id is provided but the
+ * attached store has a storeId, the storeId will be used. If neither option is presented the proxy will throw an error.
+ */
+Ext.define('Ext.data.proxy.LocalStorage', {
+ extend: 'Ext.data.proxy.WebStorage',
+ alias: 'proxy.localstorage',
+ alternateClassName: 'Ext.data.LocalStorageProxy',
+
+ //inherit docs
+ getStorageObject: function() {
+ return window.localStorage;
+ }
+});
+/**
+ * @author Ed Spencer
+ *
+ * In-memory proxy. This proxy simply uses a local variable for data storage/retrieval, so its contents are lost on
+ * every page refresh.
+ *
+ * Usually this Proxy isn't used directly, serving instead as a helper to a {@link Ext.data.Store Store} where a reader
+ * is required to load data. For example, say we have a Store for a User model and have some inline data we want to
+ * load, but this data isn't in quite the right format: we can use a MemoryProxy with a JsonReader to read it into our
+ * Store:
+ *
+ * //this is the model we will be using in the store
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * {name: 'id', type: 'int'},
+ * {name: 'name', type: 'string'},
+ * {name: 'phone', type: 'string', mapping: 'phoneNumber'}
+ * ]
+ * });
+ *
+ * //this data does not line up to our model fields - the phone field is called phoneNumber
+ * var data = {
+ * users: [
+ * {
+ * id: 1,
+ * name: 'Ed Spencer',
+ * phoneNumber: '555 1234'
+ * },
+ * {
+ * id: 2,
+ * name: 'Abe Elias',
+ * phoneNumber: '666 1234'
+ * }
+ * ]
+ * };
+ *
+ * //note how we set the 'root' in the reader to match the data structure above
+ * var store = Ext.create('Ext.data.Store', {
+ * autoLoad: true,
+ * model: 'User',
+ * data : data,
+ * proxy: {
+ * type: 'memory',
+ * reader: {
+ * type: 'json',
+ * root: 'users'
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.data.proxy.Memory', {
+ extend: 'Ext.data.proxy.Client',
+ alias: 'proxy.memory',
+ alternateClassName: 'Ext.data.MemoryProxy',
+
+ /**
+ * @cfg {Ext.data.Model[]} data
+ * Optional array of Records to load into the Proxy
+ */
+
+ constructor: function(config) {
+ this.callParent([config]);
+
+ //ensures that the reader has been instantiated properly
+ this.setReader(this.reader);
+ },
+
+ /**
+ * Reads data from the configured {@link #data} object. Uses the Proxy's {@link #reader}, if present.
+ * @param {Ext.data.Operation} operation The read Operation
+ * @param {Function} callback The callback to call when reading has completed
+ * @param {Object} scope The scope to call the callback function in
+ */
+ read: function(operation, callback, scope) {
+ var me = this,
+ reader = me.getReader(),
+ result = reader.read(me.data);
+
+ Ext.apply(operation, {
+ resultSet: result
+ });
+
+ operation.setCompleted();
+ operation.setSuccessful();
+ Ext.callback(callback, scope || me, [operation]);
+ },
+
+ clear: Ext.emptyFn
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * The Rest proxy is a specialization of the {@link Ext.data.proxy.Ajax AjaxProxy} which simply maps the four actions
+ * (create, read, update and destroy) to RESTful HTTP verbs. For example, let's set up a {@link Ext.data.Model Model}
+ * with an inline Rest proxy
+ *
+ * Ext.define('User', {
+ * extend: 'Ext.data.Model',
+ * fields: ['id', 'name', 'email'],
+ *
+ * proxy: {
+ * type: 'rest',
+ * url : '/users'
+ * }
+ * });
+ *
+ * Now we can create a new User instance and save it via the Rest proxy. Doing this will cause the Proxy to send a POST
+ * request to '/users':
+ *
+ * var user = Ext.create('User', {name: 'Ed Spencer', email: 'ed@sencha.com'});
+ *
+ * user.save(); //POST /users
+ *
+ * Let's expand this a little and provide a callback for the {@link Ext.data.Model#save} call to update the Model once
+ * it has been created. We'll assume the creation went successfully and that the server gave this user an ID of 123:
+ *
+ * user.save({
+ * success: function(user) {
+ * user.set('name', 'Khan Noonien Singh');
+ *
+ * user.save(); //PUT /users/123
+ * }
+ * });
+ *
+ * Now that we're no longer creating a new Model instance, the request method is changed to an HTTP PUT, targeting the
+ * relevant url for that user. Now let's delete this user, which will use the DELETE method:
+ *
+ * user.destroy(); //DELETE /users/123
+ *
+ * Finally, when we perform a load of a Model or Store, Rest proxy will use the GET method:
+ *
+ * //1. Load via Store
+ *
+ * //the Store automatically picks up the Proxy from the User model
+ * var store = Ext.create('Ext.data.Store', {
+ * model: 'User'
+ * });
+ *
+ * store.load(); //GET /users
+ *
+ * //2. Load directly from the Model
+ *
+ * //GET /users/123
+ * Ext.ModelManager.getModel('User').load(123, {
+ * success: function(user) {
+ * console.log(user.getId()); //outputs 123
+ * }
+ * });
+ *
+ * # Url generation
+ *
+ * The Rest proxy is able to automatically generate the urls above based on two configuration options - {@link #appendId} and
+ * {@link #format}. If appendId is true (it is by default) then Rest proxy will automatically append the ID of the Model
+ * instance in question to the configured url, resulting in the '/users/123' that we saw above.
+ *
+ * If the request is not for a specific Model instance (e.g. loading a Store), the url is not appended with an id.
+ * The Rest proxy will automatically insert a '/' before the ID if one is not already present.
+ *
+ * new Ext.data.proxy.Rest({
+ * url: '/users',
+ * appendId: true //default
+ * });
+ *
+ * // Collection url: /users
+ * // Instance url : /users/123
+ *
+ * The Rest proxy can also optionally append a format string to the end of any generated url:
+ *
+ * new Ext.data.proxy.Rest({
+ * url: '/users',
+ * format: 'json'
+ * });
+ *
+ * // Collection url: /users.json
+ * // Instance url : /users/123.json
+ *
+ * If further customization is needed, simply implement the {@link #buildUrl} method and add your custom generated url
+ * onto the {@link Ext.data.Request Request} object that is passed to buildUrl. See [Rest proxy's implementation][1] for
+ * an example of how to achieve this.
+ *
+ * Note that Rest proxy inherits from {@link Ext.data.proxy.Ajax AjaxProxy}, which already injects all of the sorter,
+ * filter, group and paging options into the generated url. See the {@link Ext.data.proxy.Ajax AjaxProxy docs} for more
+ * details.
+ *
+ * [1]: source/RestProxy.html#method-Ext.data.proxy.Rest-buildUrl
+ */
+Ext.define('Ext.data.proxy.Rest', {
+ extend: 'Ext.data.proxy.Ajax',
+ alternateClassName: 'Ext.data.RestProxy',
+ alias : 'proxy.rest',
+
+ /**
+ * @cfg {Boolean} appendId
+ * True to automatically append the ID of a Model instance when performing a request based on that single instance.
+ * See Rest proxy intro docs for more details. Defaults to true.
+ */
+ appendId: true,
+
+ /**
+ * @cfg {String} format
+ * Optional data format to send to the server when making any request (e.g. 'json'). See the Rest proxy intro docs
+ * for full details. Defaults to undefined.
+ */
+
+ /**
+ * @cfg {Boolean} batchActions
+ * True to batch actions of a particular type when synchronizing the store. Defaults to false.
+ */
+ batchActions: false,
+
+ /**
+ * Specialized version of buildUrl that incorporates the {@link #appendId} and {@link #format} options into the
+ * generated url. Override this to provide further customizations, but remember to call the superclass buildUrl so
+ * that additional parameters like the cache buster string are appended.
+ * @param {Object} request
+ */
+ buildUrl: function(request) {
+ var me = this,
+ operation = request.operation,
+ records = operation.records || [],
+ record = records[0],
+ format = me.format,
+ url = me.getUrl(request),
+ id = record ? record.getId() : operation.id;
+
+ if (me.appendId && id) {
+ if (!url.match(/\/$/)) {
+ url += '/';
+ }
+
+ url += id;
+ }
+
+ if (format) {
+ if (!url.match(/\.$/)) {
+ url += '.';
+ }
+
+ url += format;
+ }
+
+ request.url = url;
+
+ return me.callParent(arguments);
+ }
+}, function() {
+ Ext.apply(this.prototype, {
+ /**
+ * @property {Object} actionMethods
+ * Mapping of action name to HTTP request method. These default to RESTful conventions for the 'create', 'read',
+ * 'update' and 'destroy' actions (which map to 'POST', 'GET', 'PUT' and 'DELETE' respectively). This object
+ * should not be changed except globally via {@link Ext#override Ext.override} - the {@link #getMethod} function
+ * can be overridden instead.
+ */
+ actionMethods: {
+ create : 'POST',
+ read : 'GET',
+ update : 'PUT',
+ destroy: 'DELETE'
+ }
+ });
+});
+
+/**
+ * @author Ed Spencer
+ *
+ * Proxy which uses HTML5 session storage as its data storage/retrieval mechanism. If this proxy is used in a browser
+ * where session storage is not supported, the constructor will throw an error. A session storage proxy requires a
+ * unique ID which is used as a key in which all record data are stored in the session storage object.
+ *
+ * It's important to supply this unique ID as it cannot be reliably determined otherwise. If no id is provided but the
+ * attached store has a storeId, the storeId will be used. If neither option is presented the proxy will throw an error.
+ *
+ * Proxies are almost always used with a {@link Ext.data.Store store}:
+ *
+ * new Ext.data.Store({
+ * proxy: {
+ * type: 'sessionstorage',
+ * id : 'myProxyKey'
+ * }
+ * });
+ *
+ * Alternatively you can instantiate the Proxy directly:
+ *
+ * new Ext.data.proxy.SessionStorage({
+ * id : 'myOtherProxyKey'
+ * });
+ *
+ * Note that session storage is different to local storage (see {@link Ext.data.proxy.LocalStorage}) - if a browser
+ * session is ended (e.g. by closing the browser) then all data in a SessionStorageProxy are lost. Browser restarts
+ * don't affect the {@link Ext.data.proxy.LocalStorage} - the data are preserved.
+ */
+Ext.define('Ext.data.proxy.SessionStorage', {
+ extend: 'Ext.data.proxy.WebStorage',
+ alias: 'proxy.sessionstorage',
+ alternateClassName: 'Ext.data.SessionStorageProxy',
+
+ //inherit docs
+ getStorageObject: function() {
+ return window.sessionStorage;
+ }
+});
+
+/**
+ * @author Ed Spencer
+ * @class Ext.data.reader.Array
+ * @extends Ext.data.reader.Json
+ *
+ * <p>Data reader class to create an Array of {@link Ext.data.Model} objects from an Array.
+ * Each element of that Array represents a row of data fields. The
+ * fields are pulled into a Record object using as a subscript, the <code>mapping</code> property
+ * of the field definition if it exists, or the field's ordinal position in the definition.</p>
+ *
+ * <p><u>Example code:</u></p>
+ *
+<pre><code>
+Employee = Ext.define('Employee', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'id',
+ {name: 'name', mapping: 1}, // "mapping" only needed if an "id" field is present which
+ {name: 'occupation', mapping: 2} // precludes using the ordinal position as the index.
+ ]
+});
+
+var myReader = new Ext.data.reader.Array({
+ model: 'Employee'
+}, Employee);
+</code></pre>
+ *
+ * <p>This would consume an Array like this:</p>
+ *
+<pre><code>
+[ [1, 'Bill', 'Gardener'], [2, 'Ben', 'Horticulturalist'] ]
+</code></pre>
+ *
+ * @constructor
+ * Create a new ArrayReader
+ * @param {Object} meta Metadata configuration options.
+ */
+Ext.define('Ext.data.reader.Array', {
+ extend: 'Ext.data.reader.Json',
+ alternateClassName: 'Ext.data.ArrayReader',
+ alias : 'reader.array',
+
+ /**
+ * @private
+ * Most of the work is done for us by JsonReader, but we need to overwrite the field accessors to just
+ * reference the correct position in the array.
+ */
+ buildExtractors: function() {
+ this.callParent(arguments);
+
+ var fields = this.model.prototype.fields.items,
+ i = 0,
+ length = fields.length,
+ extractorFunctions = [],
+ map;
+
+ for (; i < length; i++) {
+ map = fields[i].mapping;
+ extractorFunctions.push(function(index) {
+ return function(data) {
+ return data[index];
+ };
+ }(map !== null ? map : i));
+ }
+
+ this.extractorFunctions = extractorFunctions;
+ }
+});
+
+/**
+ * @author Ed Spencer
+ * @class Ext.data.reader.Xml
+ * @extends Ext.data.reader.Reader
+ *
+ * <p>The XML Reader is used by a Proxy to read a server response that is sent back in XML format. This usually
+ * happens as a result of loading a Store - for example we might create something like this:</p>
+ *
+<pre><code>
+Ext.define('User', {
+ extend: 'Ext.data.Model',
+ fields: ['id', 'name', 'email']
+});
+
+var store = Ext.create('Ext.data.Store', {
+ model: 'User',
+ proxy: {
+ type: 'ajax',
+ url : 'users.xml',
+ reader: {
+ type: 'xml',
+ record: 'user'
+ }
+ }
+});
+</code></pre>
+ *
+ * <p>The example above creates a 'User' model. Models are explained in the {@link Ext.data.Model Model} docs if you're
+ * not already familiar with them.</p>
+ *
+ * <p>We created the simplest type of XML Reader possible by simply telling our {@link Ext.data.Store Store}'s
+ * {@link Ext.data.proxy.Proxy Proxy} that we want a XML Reader. The Store automatically passes the configured model to the
+ * Store, so it is as if we passed this instead:
+ *
+<pre><code>
+reader: {
+ type : 'xml',
+ model: 'User',
+ record: 'user'
+}
+</code></pre>
+ *
+ * <p>The reader we set up is ready to read data from our server - at the moment it will accept a response like this:</p>
+ *
+<pre><code>
+<?xml version="1.0" encoding="UTF-8"?>
+<user>
+ <id>1</id>
+ <name>Ed Spencer</name>
+ <email>ed@sencha.com</email>
+</user>
+<user>
+ <id>2</id>
+ <name>Abe Elias</name>
+ <email>abe@sencha.com</email>
+</user>
+</code></pre>
+ *
+ * <p>The XML Reader uses the configured {@link #record} option to pull out the data for each record - in this case we
+ * set record to 'user', so each <user> above will be converted into a User model.</p>
+ *
+ * <p><u>Reading other XML formats</u></p>
+ *
+ * <p>If you already have your XML format defined and it doesn't look quite like what we have above, you can usually
+ * pass XmlReader a couple of configuration options to make it parse your format. For example, we can use the
+ * {@link #root} configuration to parse data that comes back like this:</p>
+ *
+<pre><code>
+<?xml version="1.0" encoding="UTF-8"?>
+<users>
+ <user>
+ <id>1</id>
+ <name>Ed Spencer</name>
+ <email>ed@sencha.com</email>
+ </user>
+ <user>
+ <id>2</id>
+ <name>Abe Elias</name>
+ <email>abe@sencha.com</email>
+ </user>
+</users>
+</code></pre>
+ *
+ * <p>To parse this we just pass in a {@link #root} configuration that matches the 'users' above:</p>
+ *
+<pre><code>
+reader: {
+ type : 'xml',
+ root : 'users',
+ record: 'user'
+}
+</code></pre>
+ *
+ * <p>Note that XmlReader doesn't care whether your {@link #root} and {@link #record} elements are nested deep inside
+ * a larger structure, so a response like this will still work:
+ *
+<pre><code>
+<?xml version="1.0" encoding="UTF-8"?>
+<deeply>
+ <nested>
+ <xml>
+ <users>
+ <user>
+ <id>1</id>
+ <name>Ed Spencer</name>
+ <email>ed@sencha.com</email>
+ </user>
+ <user>
+ <id>2</id>
+ <name>Abe Elias</name>
+ <email>abe@sencha.com</email>
+ </user>
+ </users>
+ </xml>
+ </nested>
+</deeply>
+</code></pre>
+ *
+ * <p><u>Response metadata</u></p>
+ *
+ * <p>The server can return additional data in its response, such as the {@link #totalProperty total number of records}
+ * and the {@link #successProperty success status of the response}. These are typically included in the XML response
+ * like this:</p>
+ *
+<pre><code>
+<?xml version="1.0" encoding="UTF-8"?>
+<total>100</total>
+<success>true</success>
+<users>
+ <user>
+ <id>1</id>
+ <name>Ed Spencer</name>
+ <email>ed@sencha.com</email>
+ </user>
+ <user>
+ <id>2</id>
+ <name>Abe Elias</name>
+ <email>abe@sencha.com</email>
+ </user>
+</users>
+</code></pre>
+ *
+ * <p>If these properties are present in the XML response they can be parsed out by the XmlReader and used by the
+ * Store that loaded it. We can set up the names of these properties by specifying a final pair of configuration
+ * options:</p>
+ *
+<pre><code>
+reader: {
+ type: 'xml',
+ root: 'users',
+ totalProperty : 'total',
+ successProperty: 'success'
+}
+</code></pre>
+ *
+ * <p>These final options are not necessary to make the Reader work, but can be useful when the server needs to report
+ * an error or if it needs to indicate that there is a lot of data available of which only a subset is currently being
+ * returned.</p>
+ *
+ * <p><u>Response format</u></p>
+ *
+ * <p><b>Note:</b> in order for the browser to parse a returned XML document, the Content-Type header in the HTTP
+ * response must be set to "text/xml" or "application/xml". This is very important - the XmlReader will not
+ * work correctly otherwise.</p>
+ */
+Ext.define('Ext.data.reader.Xml', {
+ extend: 'Ext.data.reader.Reader',
+ alternateClassName: 'Ext.data.XmlReader',
+ alias : 'reader.xml',
+
+ /**
+ * @cfg {String} record (required)
+ * The DomQuery path to the repeated element which contains record information.
+ */
+
+ /**
+ * @private
+ * Creates a function to return some particular key of data from a response. The totalProperty and
+ * successProperty are treated as special cases for type casting, everything else is just a simple selector.
+ * @param {String} key
+ * @return {Function}
+ */
+ createAccessor: function(expr) {
+ var me = this;
+
+ if (Ext.isEmpty(expr)) {
+ return Ext.emptyFn;
+ }
+
+ if (Ext.isFunction(expr)) {
+ return expr;
+ }
+
+ return function(root) {
+ return me.getNodeValue(Ext.DomQuery.selectNode(expr, root));
+ };
+ },
+
+ getNodeValue: function(node) {
+ if (node && node.firstChild) {
+ return node.firstChild.nodeValue;
+ }
+ return undefined;
+ },
+
+ //inherit docs
+ getResponseData: function(response) {
+ var xml = response.responseXML;
+
+
+ return xml;
+ },
+
+ /**
+ * Normalizes the data object
+ * @param {Object} data The raw data object
+ * @return {Object} Returns the documentElement property of the data object if present, or the same object if not
+ */
+ getData: function(data) {
+ return data.documentElement || data;
+ },
+
+ /**
+ * @private
+ * Given an XML object, returns the Element that represents the root as configured by the Reader's meta data
+ * @param {Object} data The XML data object
+ * @return {XMLElement} The root node element
+ */
+ getRoot: function(data) {
+ var nodeName = data.nodeName,
+ root = this.root;
+
+ if (!root || (nodeName && nodeName == root)) {
+ return data;
+ } else if (Ext.DomQuery.isXml(data)) {
+ // This fix ensures we have XML data
+ // Related to TreeStore calling getRoot with the root node, which isn't XML
+ // Probably should be resolved in TreeStore at some point
+ return Ext.DomQuery.selectNode(root, data);
+ }
+ },
+
+ /**
+ * @private
+ * We're just preparing the data for the superclass by pulling out the record nodes we want
+ * @param {XMLElement} root The XML root node
+ * @return {Ext.data.Model[]} The records
+ */
+ extractData: function(root) {
+ var recordName = this.record;
+
+
+ if (recordName != root.nodeName) {
+ root = Ext.DomQuery.select(recordName, root);
+ } else {
+ root = [root];
+ }
+ return this.callParent([root]);
+ },
+
+ /**
+ * @private
+ * See Ext.data.reader.Reader's getAssociatedDataRoot docs
+ * @param {Object} data The raw data object
+ * @param {String} associationName The name of the association to get data for (uses associationKey if present)
+ * @return {XMLElement} The root
+ */
+ getAssociatedDataRoot: function(data, associationName) {
+ return Ext.DomQuery.select(associationName, data)[0];
+ },
+
+ /**
+ * Parses an XML document and returns a ResultSet containing the model instances
+ * @param {Object} doc Parsed XML document
+ * @return {Ext.data.ResultSet} The parsed result set
+ */
+ readRecords: function(doc) {
+ //it's possible that we get passed an array here by associations. Make sure we strip that out (see Ext.data.reader.Reader#readAssociated)
+ if (Ext.isArray(doc)) {
+ doc = doc[0];
+ }
+
+ /**
+ * @deprecated will be removed in Ext JS 5.0. This is just a copy of this.rawData - use that instead
+ * @property xmlData
+ * @type Object
+ */
+ this.xmlData = doc;
+ return this.callParent([doc]);
+ }
+});
+/**
+ * @author Ed Spencer
+ * @class Ext.data.writer.Xml
+ * @extends Ext.data.writer.Writer
+
+This class is used to write {@link Ext.data.Model} data to the server in an XML format.
+The {@link #documentRoot} property is used to specify the root element in the XML document.
+The {@link #record} option is used to specify the element name for each record that will make
+up the XML document.
+
+ * @markdown
+ */
+Ext.define('Ext.data.writer.Xml', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.data.writer.Writer',
+ alternateClassName: 'Ext.data.XmlWriter',
+
+ alias: 'writer.xml',
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String} documentRoot The name of the root element of the document. Defaults to <tt>'xmlData'</tt>.
+ * If there is more than 1 record and the root is not specified, the default document root will still be used
+ * to ensure a valid XML document is created.
+ */
+ documentRoot: 'xmlData',
+
+ /**
+ * @cfg {String} defaultDocumentRoot The root to be used if {@link #documentRoot} is empty and a root is required
+ * to form a valid XML document.
+ */
+ defaultDocumentRoot: 'xmlData',
+
+ /**
+ * @cfg {String} header A header to use in the XML document (such as setting the encoding or version).
+ * Defaults to <tt>''</tt>.
+ */
+ header: '',
+
+ /**
+ * @cfg {String} record The name of the node to use for each record. Defaults to <tt>'record'</tt>.
+ */
+ record: 'record',
+
+ //inherit docs
+ writeRecords: function(request, data) {
+ var me = this,
+ xml = [],
+ i = 0,
+ len = data.length,
+ root = me.documentRoot,
+ record = me.record,
+ needsRoot = data.length !== 1,
+ item,
+ key;
+
+ // may not exist
+ xml.push(me.header || '');
+
+ if (!root && needsRoot) {
+ root = me.defaultDocumentRoot;
+ }
+
+ if (root) {
+ xml.push('<', root, '>');
+ }
+
+ for (; i < len; ++i) {
+ item = data[i];
+ xml.push('<', record, '>');
+ for (key in item) {
+ if (item.hasOwnProperty(key)) {
+ xml.push('<', key, '>', item[key], '</', key, '>');
+ }
+ }
+ xml.push('</', record, '>');
+ }
+
+ if (root) {
+ xml.push('</', root, '>');
+ }
+
+ request.xmlData = xml.join('');
+ return request;
+ }
+});
+
+/**
+ * @class Ext.direct.Event
+ * A base class for all Ext.direct events. An event is
+ * created after some kind of interaction with the server.
+ * The event class is essentially just a data structure
+ * to hold a Direct response.
+ */
+Ext.define('Ext.direct.Event', {
+
+ /* Begin Definitions */
+
+ alias: 'direct.event',
+
+ requires: ['Ext.direct.Manager'],
+
+ /* End Definitions */
+
+ status: true,
+
+ /**
+ * Creates new Event.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ Ext.apply(this, config);
+ },
+
+ /**
+ * Return the raw data for this event.
+ * @return {Object} The data from the event
+ */
+ getData: function(){
+ return this.data;
+ }
+});
+
+/**
+ * @class Ext.direct.RemotingEvent
+ * @extends Ext.direct.Event
+ * An event that is fired when data is received from a
+ * {@link Ext.direct.RemotingProvider}. Contains a method to the
+ * related transaction for the direct request, see {@link #getTransaction}
+ */
+Ext.define('Ext.direct.RemotingEvent', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.direct.Event',
+
+ alias: 'direct.rpc',
+
+ /* End Definitions */
+
+ /**
+ * Get the transaction associated with this event.
+ * @return {Ext.direct.Transaction} The transaction
+ */
+ getTransaction: function(){
+ return this.transaction || Ext.direct.Manager.getTransaction(this.tid);
+ }
+});
+
+/**
+ * @class Ext.direct.ExceptionEvent
+ * @extends Ext.direct.RemotingEvent
+ * An event that is fired when an exception is received from a {@link Ext.direct.RemotingProvider}
+ */
+Ext.define('Ext.direct.ExceptionEvent', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.direct.RemotingEvent',
+
+ alias: 'direct.exception',
+
+ /* End Definitions */
+
+ status: false
+});
+
+/**
+ * @class Ext.direct.Provider
+ * <p>Ext.direct.Provider is an abstract class meant to be extended.</p>
+ *
+ * <p>For example Ext JS implements the following subclasses:</p>
+ * <pre><code>
+Provider
+|
++---{@link Ext.direct.JsonProvider JsonProvider}
+ |
+ +---{@link Ext.direct.PollingProvider PollingProvider}
+ |
+ +---{@link Ext.direct.RemotingProvider RemotingProvider}
+ * </code></pre>
+ * @abstract
+ */
+Ext.define('Ext.direct.Provider', {
+
+ /* Begin Definitions */
+
+ alias: 'direct.provider',
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String} id
+ * The unique id of the provider (defaults to an {@link Ext#id auto-assigned id}).
+ * You should assign an id if you need to be able to access the provider later and you do
+ * not have an object reference available, for example:
+ * <pre><code>
+Ext.direct.Manager.addProvider({
+ type: 'polling',
+ url: 'php/poll.php',
+ id: 'poll-provider'
+});
+var p = {@link Ext.direct.Manager}.{@link Ext.direct.Manager#getProvider getProvider}('poll-provider');
+p.disconnect();
+ * </code></pre>
+ */
+
+ constructor : function(config){
+ var me = this;
+
+ Ext.apply(me, config);
+ me.addEvents(
+ /**
+ * @event connect
+ * Fires when the Provider connects to the server-side
+ * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
+ */
+ 'connect',
+ /**
+ * @event disconnect
+ * Fires when the Provider disconnects from the server-side
+ * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
+ */
+ 'disconnect',
+ /**
+ * @event data
+ * Fires when the Provider receives data from the server-side
+ * @param {Ext.direct.Provider} provider The {@link Ext.direct.Provider Provider}.
+ * @param {Ext.direct.Event} e The Ext.direct.Event type that occurred.
+ */
+ 'data',
+ /**
+ * @event exception
+ * Fires when the Provider receives an exception from the server-side
+ */
+ 'exception'
+ );
+ me.mixins.observable.constructor.call(me, config);
+ },
+
+ /**
+ * Returns whether or not the server-side is currently connected.
+ * Abstract method for subclasses to implement.
+ */
+ isConnected: function(){
+ return false;
+ },
+
+ /**
+ * Abstract methods for subclasses to implement.
+ * @method
+ */
+ connect: Ext.emptyFn,
+
+ /**
+ * Abstract methods for subclasses to implement.
+ * @method
+ */
+ disconnect: Ext.emptyFn
+});
+
+/**
+ * @class Ext.direct.JsonProvider
+ * @extends Ext.direct.Provider
+
+A base provider for communicating using JSON. This is an abstract class
+and should not be instanced directly.
+
+ * @markdown
+ * @abstract
+ */
+
+Ext.define('Ext.direct.JsonProvider', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.direct.Provider',
+
+ alias: 'direct.jsonprovider',
+
+ uses: ['Ext.direct.ExceptionEvent'],
+
+ /* End Definitions */
+
+ /**
+ * Parse the JSON response
+ * @private
+ * @param {Object} response The XHR response object
+ * @return {Object} The data in the response.
+ */
+ parseResponse: function(response){
+ if (!Ext.isEmpty(response.responseText)) {
+ if (Ext.isObject(response.responseText)) {
+ return response.responseText;
+ }
+ return Ext.decode(response.responseText);
+ }
+ return null;
+ },
+
+ /**
+ * Creates a set of events based on the XHR response
+ * @private
+ * @param {Object} response The XHR response
+ * @return {Ext.direct.Event[]} An array of Ext.direct.Event
+ */
+ createEvents: function(response){
+ var data = null,
+ events = [],
+ event,
+ i = 0,
+ len;
+
+ try{
+ data = this.parseResponse(response);
+ } catch(e) {
+ event = Ext.create('Ext.direct.ExceptionEvent', {
+ data: e,
+ xhr: response,
+ code: Ext.direct.Manager.self.exceptions.PARSE,
+ message: 'Error parsing json response: \n\n ' + data
+ });
+ return [event];
+ }
+
+ if (Ext.isArray(data)) {
+ for (len = data.length; i < len; ++i) {
+ events.push(this.createEvent(data[i]));
+ }
+ } else {
+ events.push(this.createEvent(data));
+ }
+ return events;
+ },
+
+ /**
+ * Create an event from a response object
+ * @param {Object} response The XHR response object
+ * @return {Ext.direct.Event} The event
+ */
+ createEvent: function(response){
+ return Ext.create('direct.' + response.type, response);
+ }
+});
+/**
+ * @class Ext.direct.PollingProvider
+ * @extends Ext.direct.JsonProvider
+ *
+ * <p>Provides for repetitive polling of the server at distinct {@link #interval intervals}.
+ * The initial request for data originates from the client, and then is responded to by the
+ * server.</p>
+ *
+ * <p>All configurations for the PollingProvider should be generated by the server-side
+ * API portion of the Ext.Direct stack.</p>
+ *
+ * <p>An instance of PollingProvider may be created directly via the new keyword or by simply
+ * specifying <tt>type = 'polling'</tt>. For example:</p>
+ * <pre><code>
+var pollA = new Ext.direct.PollingProvider({
+ type:'polling',
+ url: 'php/pollA.php',
+});
+Ext.direct.Manager.addProvider(pollA);
+pollA.disconnect();
+
+Ext.direct.Manager.addProvider(
+ {
+ type:'polling',
+ url: 'php/pollB.php',
+ id: 'pollB-provider'
+ }
+);
+var pollB = Ext.direct.Manager.getProvider('pollB-provider');
+ * </code></pre>
+ */
+Ext.define('Ext.direct.PollingProvider', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.direct.JsonProvider',
+
+ alias: 'direct.pollingprovider',
+
+ uses: ['Ext.direct.ExceptionEvent'],
+
+ requires: ['Ext.Ajax', 'Ext.util.DelayedTask'],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Number} interval
+ * How often to poll the server-side in milliseconds. Defaults to every 3 seconds.
+ */
+ interval: 3000,
+
+ /**
+ * @cfg {Object} baseParams
+ * An object containing properties which are to be sent as parameters on every polling request
+ */
+
+ /**
+ * @cfg {String/Function} url
+ * The url which the PollingProvider should contact with each request. This can also be
+ * an imported Ext.Direct method which will accept the baseParams as its only argument.
+ */
+
+ // private
+ constructor : function(config){
+ this.callParent(arguments);
+ this.addEvents(
+ /**
+ * @event beforepoll
+ * Fired immediately before a poll takes place, an event handler can return false
+ * in order to cancel the poll.
+ * @param {Ext.direct.PollingProvider} this
+ */
+ 'beforepoll',
+ /**
+ * @event poll
+ * This event has not yet been implemented.
+ * @param {Ext.direct.PollingProvider} this
+ */
+ 'poll'
+ );
+ },
+
+ // inherited
+ isConnected: function(){
+ return !!this.pollTask;
+ },
+
+ /**
+ * Connect to the server-side and begin the polling process. To handle each
+ * response subscribe to the data event.
+ */
+ connect: function(){
+ var me = this, url = me.url;
+
+ if (url && !me.pollTask) {
+ me.pollTask = Ext.TaskManager.start({
+ run: function(){
+ if (me.fireEvent('beforepoll', me) !== false) {
+ if (Ext.isFunction(url)) {
+ url(me.baseParams);
+ } else {
+ Ext.Ajax.request({
+ url: url,
+ callback: me.onData,
+ scope: me,
+ params: me.baseParams
+ });
+ }
+ }
+ },
+ interval: me.interval,
+ scope: me
+ });
+ me.fireEvent('connect', me);
+ } else if (!url) {
+ }
+ },
+
+ /**
+ * Disconnect from the server-side and stop the polling process. The disconnect
+ * event will be fired on a successful disconnect.
+ */
+ disconnect: function(){
+ var me = this;
+
+ if (me.pollTask) {
+ Ext.TaskManager.stop(me.pollTask);
+ delete me.pollTask;
+ me.fireEvent('disconnect', me);
+ }
+ },
+
+ // private
+ onData: function(opt, success, response){
+ var me = this,
+ i = 0,
+ len,
+ events;
+
+ if (success) {
+ events = me.createEvents(response);
+ for (len = events.length; i < len; ++i) {
+ me.fireEvent('data', me, events[i]);
+ }
+ } else {
+ me.fireEvent('data', me, Ext.create('Ext.direct.ExceptionEvent', {
+ data: null,
+ code: Ext.direct.Manager.self.exceptions.TRANSPORT,
+ message: 'Unable to connect to the server.',
+ xhr: response
+ }));
+ }
+ }
+});
+/**
+ * Small utility class used internally to represent a Direct method.
+ * @class Ext.direct.RemotingMethod
+ * @ignore
+ */
+Ext.define('Ext.direct.RemotingMethod', {
+
+ constructor: function(config){
+ var me = this,
+ params = Ext.isDefined(config.params) ? config.params : config.len,
+ name;
+
+ me.name = config.name;
+ me.formHandler = config.formHandler;
+ if (Ext.isNumber(params)) {
+ // given only the number of parameters
+ me.len = params;
+ me.ordered = true;
+ } else {
+ /*
+ * Given an array of either
+ * a) String
+ * b) Objects with a name property. We may want to encode extra info in here later
+ */
+ me.params = [];
+ Ext.each(params, function(param){
+ name = Ext.isObject(param) ? param.name : param;
+ me.params.push(name);
+ });
+ }
+ },
+
+ /**
+ * Takes the arguments for the Direct function and splits the arguments
+ * from the scope and the callback.
+ * @param {Array} args The arguments passed to the direct call
+ * @return {Object} An object with 3 properties, args, callback & scope.
+ */
+ getCallData: function(args){
+ var me = this,
+ data = null,
+ len = me.len,
+ params = me.params,
+ callback,
+ scope,
+ name;
+
+ if (me.ordered) {
+ callback = args[len];
+ scope = args[len + 1];
+ if (len !== 0) {
+ data = args.slice(0, len);
+ }
+ } else {
+ data = Ext.apply({}, args[0]);
+ callback = args[1];
+ scope = args[2];
+
+ // filter out any non-existent properties
+ for (name in data) {
+ if (data.hasOwnProperty(name)) {
+ if (!Ext.Array.contains(params, name)) {
+ delete data[name];
+ }
+ }
+ }
+ }
+
+ return {
+ data: data,
+ callback: callback,
+ scope: scope
+ };
+ }
+});
+
+/**
+ * Supporting Class for Ext.Direct (not intended to be used directly).
+ */
+Ext.define('Ext.direct.Transaction', {
+
+ /* Begin Definitions */
+
+ alias: 'direct.transaction',
+ alternateClassName: 'Ext.Direct.Transaction',
+
+ statics: {
+ TRANSACTION_ID: 0
+ },
+
+ /* End Definitions */
+
+ /**
+ * Creates new Transaction.
+ * @param {Object} [config] Config object.
+ */
+ constructor: function(config){
+ var me = this;
+
+ Ext.apply(me, config);
+ me.id = ++me.self.TRANSACTION_ID;
+ me.retryCount = 0;
+ },
+
+ send: function(){
+ this.provider.queueTransaction(this);
+ },
+
+ retry: function(){
+ this.retryCount++;
+ this.send();
+ },
+
+ getProvider: function(){
+ return this.provider;
+ }
+});
+
+/**
+ * @class Ext.direct.RemotingProvider
+ * @extends Ext.direct.JsonProvider
+ *
+ * <p>The {@link Ext.direct.RemotingProvider RemotingProvider} exposes access to
+ * server side methods on the client (a remote procedure call (RPC) type of
+ * connection where the client can initiate a procedure on the server).</p>
+ *
+ * <p>This allows for code to be organized in a fashion that is maintainable,
+ * while providing a clear path between client and server, something that is
+ * not always apparent when using URLs.</p>
+ *
+ * <p>To accomplish this the server-side needs to describe what classes and methods
+ * are available on the client-side. This configuration will typically be
+ * outputted by the server-side Ext.Direct stack when the API description is built.</p>
+ */
+Ext.define('Ext.direct.RemotingProvider', {
+
+ /* Begin Definitions */
+
+ alias: 'direct.remotingprovider',
+
+ extend: 'Ext.direct.JsonProvider',
+
+ requires: [
+ 'Ext.util.MixedCollection',
+ 'Ext.util.DelayedTask',
+ 'Ext.direct.Transaction',
+ 'Ext.direct.RemotingMethod'
+ ],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Object} actions
+ * Object literal defining the server side actions and methods. For example, if
+ * the Provider is configured with:
+ * <pre><code>
+"actions":{ // each property within the 'actions' object represents a server side Class
+ "TestAction":[ // array of methods within each server side Class to be
+ { // stubbed out on client
+ "name":"doEcho",
+ "len":1
+ },{
+ "name":"multiply",// name of method
+ "len":2 // The number of parameters that will be used to create an
+ // array of data to send to the server side function.
+ // Ensure the server sends back a Number, not a String.
+ },{
+ "name":"doForm",
+ "formHandler":true, // direct the client to use specialized form handling method
+ "len":1
+ }]
+}
+ * </code></pre>
+ * <p>Note that a Store is not required, a server method can be called at any time.
+ * In the following example a <b>client side</b> handler is used to call the
+ * server side method "multiply" in the server-side "TestAction" Class:</p>
+ * <pre><code>
+TestAction.multiply(
+ 2, 4, // pass two arguments to server, so specify len=2
+ // callback function after the server is called
+ // result: the result returned by the server
+ // e: Ext.direct.RemotingEvent object
+ function(result, e){
+ var t = e.getTransaction();
+ var action = t.action; // server side Class called
+ var method = t.method; // server side method called
+ if(e.status){
+ var answer = Ext.encode(result); // 8
+
+ }else{
+ var msg = e.message; // failure message
+ }
+ }
+);
+ * </code></pre>
+ * In the example above, the server side "multiply" function will be passed two
+ * arguments (2 and 4). The "multiply" method should return the value 8 which will be
+ * available as the <tt>result</tt> in the example above.
+ */
+
+ /**
+ * @cfg {String/Object} namespace
+ * Namespace for the Remoting Provider (defaults to the browser global scope of <i>window</i>).
+ * Explicitly specify the namespace Object, or specify a String to have a
+ * {@link Ext#namespace namespace created} implicitly.
+ */
+
+ /**
+ * @cfg {String} url
+ * <b>Required</b>. The url to connect to the {@link Ext.direct.Manager} server-side router.
+ */
+
+ /**
+ * @cfg {String} enableUrlEncode
+ * Specify which param will hold the arguments for the method.
+ * Defaults to <tt>'data'</tt>.
+ */
+
+ /**
+ * @cfg {Number/Boolean} enableBuffer
+ * <p><tt>true</tt> or <tt>false</tt> to enable or disable combining of method
+ * calls. If a number is specified this is the amount of time in milliseconds
+ * to wait before sending a batched request.</p>
+ * <br><p>Calls which are received within the specified timeframe will be
+ * concatenated together and sent in a single request, optimizing the
+ * application by reducing the amount of round trips that have to be made
+ * to the server.</p>
+ */
+ enableBuffer: 10,
+
+ /**
+ * @cfg {Number} maxRetries
+ * Number of times to re-attempt delivery on failure of a call.
+ */
+ maxRetries: 1,
+
+ /**
+ * @cfg {Number} timeout
+ * The timeout to use for each request.
+ */
+ timeout: undefined,
+
+ constructor : function(config){
+ var me = this;
+ me.callParent(arguments);
+ me.addEvents(
+ /**
+ * @event beforecall
+ * Fires immediately before the client-side sends off the RPC call.
+ * By returning false from an event handler you can prevent the call from
+ * executing.
+ * @param {Ext.direct.RemotingProvider} provider
+ * @param {Ext.direct.Transaction} transaction
+ * @param {Object} meta The meta data
+ */
+ 'beforecall',
+ /**
+ * @event call
+ * Fires immediately after the request to the server-side is sent. This does
+ * NOT fire after the response has come back from the call.
+ * @param {Ext.direct.RemotingProvider} provider
+ * @param {Ext.direct.Transaction} transaction
+ * @param {Object} meta The meta data
+ */
+ 'call'
+ );
+ me.namespace = (Ext.isString(me.namespace)) ? Ext.ns(me.namespace) : me.namespace || window;
+ me.transactions = Ext.create('Ext.util.MixedCollection');
+ me.callBuffer = [];
+ },
+
+ /**
+ * Initialize the API
+ * @private
+ */
+ initAPI : function(){
+ var actions = this.actions,
+ namespace = this.namespace,
+ action,
+ cls,
+ methods,
+ i,
+ len,
+ method;
+
+ for (action in actions) {
+ cls = namespace[action];
+ if (!cls) {
+ cls = namespace[action] = {};
+ }
+ methods = actions[action];
+
+ for (i = 0, len = methods.length; i < len; ++i) {
+ method = Ext.create('Ext.direct.RemotingMethod', methods[i]);
+ cls[method.name] = this.createHandler(action, method);
+ }
+ }
+ },
+
+ /**
+ * Create a handler function for a direct call.
+ * @private
+ * @param {String} action The action the call is for
+ * @param {Object} method The details of the method
+ * @return {Function} A JS function that will kick off the call
+ */
+ createHandler : function(action, method){
+ var me = this,
+ handler;
+
+ if (!method.formHandler) {
+ handler = function(){
+ me.configureRequest(action, method, Array.prototype.slice.call(arguments, 0));
+ };
+ } else {
+ handler = function(form, callback, scope){
+ me.configureFormRequest(action, method, form, callback, scope);
+ };
+ }
+ handler.directCfg = {
+ action: action,
+ method: method
+ };
+ return handler;
+ },
+
+ // inherit docs
+ isConnected: function(){
+ return !!this.connected;
+ },
+
+ // inherit docs
+ connect: function(){
+ var me = this;
+
+ if (me.url) {
+ me.initAPI();
+ me.connected = true;
+ me.fireEvent('connect', me);
+ } else if(!me.url) {
+ }
+ },
+
+ // inherit docs
+ disconnect: function(){
+ var me = this;
+
+ if (me.connected) {
+ me.connected = false;
+ me.fireEvent('disconnect', me);
+ }
+ },
+
+ /**
+ * Run any callbacks related to the transaction.
+ * @private
+ * @param {Ext.direct.Transaction} transaction The transaction
+ * @param {Ext.direct.Event} event The event
+ */
+ runCallback: function(transaction, event){
+ var funcName = event.status ? 'success' : 'failure',
+ callback,
+ result;
+
+ if (transaction && transaction.callback) {
+ callback = transaction.callback;
+ result = Ext.isDefined(event.result) ? event.result : event.data;
+
+ if (Ext.isFunction(callback)) {
+ callback(result, event);
+ } else {
+ Ext.callback(callback[funcName], callback.scope, [result, event]);
+ Ext.callback(callback.callback, callback.scope, [result, event]);
+ }
+ }
+ },
+
+ /**
+ * React to the ajax request being completed
+ * @private
+ */
+ onData: function(options, success, response){
+ var me = this,
+ i = 0,
+ len,
+ events,
+ event,
+ transaction,
+ transactions;
+
+ if (success) {
+ events = me.createEvents(response);
+ for (len = events.length; i < len; ++i) {
+ event = events[i];
+ transaction = me.getTransaction(event);
+ me.fireEvent('data', me, event);
+ if (transaction) {
+ me.runCallback(transaction, event, true);
+ Ext.direct.Manager.removeTransaction(transaction);
+ }
+ }
+ } else {
+ transactions = [].concat(options.transaction);
+ for (len = transactions.length; i < len; ++i) {
+ transaction = me.getTransaction(transactions[i]);
+ if (transaction && transaction.retryCount < me.maxRetries) {
+ transaction.retry();
+ } else {
+ event = Ext.create('Ext.direct.ExceptionEvent', {
+ data: null,
+ transaction: transaction,
+ code: Ext.direct.Manager.self.exceptions.TRANSPORT,
+ message: 'Unable to connect to the server.',
+ xhr: response
+ });
+ me.fireEvent('data', me, event);
+ if (transaction) {
+ me.runCallback(transaction, event, false);
+ Ext.direct.Manager.removeTransaction(transaction);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Get transaction from XHR options
+ * @private
+ * @param {Object} options The options sent to the Ajax request
+ * @return {Ext.direct.Transaction} The transaction, null if not found
+ */
+ getTransaction: function(options){
+ return options && options.tid ? Ext.direct.Manager.getTransaction(options.tid) : null;
+ },
+
+ /**
+ * Configure a direct request
+ * @private
+ * @param {String} action The action being executed
+ * @param {Object} method The being executed
+ */
+ configureRequest: function(action, method, args){
+ var me = this,
+ callData = method.getCallData(args),
+ data = callData.data,
+ callback = callData.callback,
+ scope = callData.scope,
+ transaction;
+
+ transaction = Ext.create('Ext.direct.Transaction', {
+ provider: me,
+ args: args,
+ action: action,
+ method: method.name,
+ data: data,
+ callback: scope && Ext.isFunction(callback) ? Ext.Function.bind(callback, scope) : callback
+ });
+
+ if (me.fireEvent('beforecall', me, transaction, method) !== false) {
+ Ext.direct.Manager.addTransaction(transaction);
+ me.queueTransaction(transaction);
+ me.fireEvent('call', me, transaction, method);
+ }
+ },
+
+ /**
+ * Gets the Ajax call info for a transaction
+ * @private
+ * @param {Ext.direct.Transaction} transaction The transaction
+ * @return {Object} The call params
+ */
+ getCallData: function(transaction){
+ return {
+ action: transaction.action,
+ method: transaction.method,
+ data: transaction.data,
+ type: 'rpc',
+ tid: transaction.id
+ };
+ },
+
+ /**
+ * Sends a request to the server
+ * @private
+ * @param {Object/Array} data The data to send
+ */
+ sendRequest : function(data){
+ var me = this,
+ request = {
+ url: me.url,
+ callback: me.onData,
+ scope: me,
+ transaction: data,
+ timeout: me.timeout
+ }, callData,
+ enableUrlEncode = me.enableUrlEncode,
+ i = 0,
+ len,
+ params;
+
+
+ if (Ext.isArray(data)) {
+ callData = [];
+ for (len = data.length; i < len; ++i) {
+ callData.push(me.getCallData(data[i]));
+ }
+ } else {
+ callData = me.getCallData(data);
+ }
+
+ if (enableUrlEncode) {
+ params = {};
+ params[Ext.isString(enableUrlEncode) ? enableUrlEncode : 'data'] = Ext.encode(callData);
+ request.params = params;
+ } else {
+ request.jsonData = callData;
+ }
+ Ext.Ajax.request(request);
+ },
+
+ /**
+ * Add a new transaction to the queue
+ * @private
+ * @param {Ext.direct.Transaction} transaction The transaction
+ */
+ queueTransaction: function(transaction){
+ var me = this,
+ enableBuffer = me.enableBuffer;
+
+ if (transaction.form) {
+ me.sendFormRequest(transaction);
+ return;
+ }
+
+ me.callBuffer.push(transaction);
+ if (enableBuffer) {
+ if (!me.callTask) {
+ me.callTask = Ext.create('Ext.util.DelayedTask', me.combineAndSend, me);
+ }
+ me.callTask.delay(Ext.isNumber(enableBuffer) ? enableBuffer : 10);
+ } else {
+ me.combineAndSend();
+ }
+ },
+
+ /**
+ * Combine any buffered requests and send them off
+ * @private
+ */
+ combineAndSend : function(){
+ var buffer = this.callBuffer,
+ len = buffer.length;
+
+ if (len > 0) {
+ this.sendRequest(len == 1 ? buffer[0] : buffer);
+ this.callBuffer = [];
+ }
+ },
+
+ /**
+ * Configure a form submission request
+ * @private
+ * @param {String} action The action being executed
+ * @param {Object} method The method being executed
+ * @param {HTMLElement} form The form being submitted
+ * @param {Function} callback (optional) A callback to run after the form submits
+ * @param {Object} scope (optional) A scope to execute the callback in
+ */
+ configureFormRequest : function(action, method, form, callback, scope){
+ var me = this,
+ transaction = Ext.create('Ext.direct.Transaction', {
+ provider: me,
+ action: action,
+ method: method.name,
+ args: [form, callback, scope],
+ callback: scope && Ext.isFunction(callback) ? Ext.Function.bind(callback, scope) : callback,
+ isForm: true
+ }),
+ isUpload,
+ params;
+
+ if (me.fireEvent('beforecall', me, transaction, method) !== false) {
+ Ext.direct.Manager.addTransaction(transaction);
+ isUpload = String(form.getAttribute("enctype")).toLowerCase() == 'multipart/form-data';
+
+ params = {
+ extTID: transaction.id,
+ extAction: action,
+ extMethod: method.name,
+ extType: 'rpc',
+ extUpload: String(isUpload)
+ };
+
+ // change made from typeof callback check to callback.params
+ // to support addl param passing in DirectSubmit EAC 6/2
+ Ext.apply(transaction, {
+ form: Ext.getDom(form),
+ isUpload: isUpload,
+ params: callback && Ext.isObject(callback.params) ? Ext.apply(params, callback.params) : params
+ });
+ me.fireEvent('call', me, transaction, method);
+ me.sendFormRequest(transaction);
+ }
+ },
+
+ /**
+ * Sends a form request
+ * @private
+ * @param {Ext.direct.Transaction} transaction The transaction to send
+ */
+ sendFormRequest: function(transaction){
+ Ext.Ajax.request({
+ url: this.url,
+ params: transaction.params,
+ callback: this.onData,
+ scope: this,
+ form: transaction.form,
+ isUpload: transaction.isUpload,
+ transaction: transaction
+ });
+ }
+
+});
+
+/*
+ * @class Ext.draw.Matrix
+ * @private
+ */
+Ext.define('Ext.draw.Matrix', {
+
+ /* Begin Definitions */
+
+ requires: ['Ext.draw.Draw'],
+
+ /* End Definitions */
+
+ constructor: function(a, b, c, d, e, f) {
+ if (a != null) {
+ this.matrix = [[a, c, e], [b, d, f], [0, 0, 1]];
+ }
+ else {
+ this.matrix = [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
+ }
+ },
+
+ add: function(a, b, c, d, e, f) {
+ var me = this,
+ out = [[], [], []],
+ matrix = [[a, c, e], [b, d, f], [0, 0, 1]],
+ x,
+ y,
+ z,
+ res;
+
+ for (x = 0; x < 3; x++) {
+ for (y = 0; y < 3; y++) {
+ res = 0;
+ for (z = 0; z < 3; z++) {
+ res += me.matrix[x][z] * matrix[z][y];
+ }
+ out[x][y] = res;
+ }
+ }
+ me.matrix = out;
+ },
+
+ prepend: function(a, b, c, d, e, f) {
+ var me = this,
+ out = [[], [], []],
+ matrix = [[a, c, e], [b, d, f], [0, 0, 1]],
+ x,
+ y,
+ z,
+ res;
+
+ for (x = 0; x < 3; x++) {
+ for (y = 0; y < 3; y++) {
+ res = 0;
+ for (z = 0; z < 3; z++) {
+ res += matrix[x][z] * me.matrix[z][y];
+ }
+ out[x][y] = res;
+ }
+ }
+ me.matrix = out;
+ },
+
+ invert: function() {
+ var matrix = this.matrix,
+ a = matrix[0][0],
+ b = matrix[1][0],
+ c = matrix[0][1],
+ d = matrix[1][1],
+ e = matrix[0][2],
+ f = matrix[1][2],
+ x = a * d - b * c;
+ return new Ext.draw.Matrix(d / x, -b / x, -c / x, a / x, (c * f - d * e) / x, (b * e - a * f) / x);
+ },
+
+ clone: function() {
+ var matrix = this.matrix,
+ a = matrix[0][0],
+ b = matrix[1][0],
+ c = matrix[0][1],
+ d = matrix[1][1],
+ e = matrix[0][2],
+ f = matrix[1][2];
+ return new Ext.draw.Matrix(a, b, c, d, e, f);
+ },
+
+ translate: function(x, y) {
+ this.prepend(1, 0, 0, 1, x, y);
+ },
+
+ scale: function(x, y, cx, cy) {
+ var me = this;
+ if (y == null) {
+ y = x;
+ }
+ me.add(1, 0, 0, 1, cx, cy);
+ me.add(x, 0, 0, y, 0, 0);
+ me.add(1, 0, 0, 1, -cx, -cy);
+ },
+
+ rotate: function(a, x, y) {
+ a = Ext.draw.Draw.rad(a);
+ var me = this,
+ cos = +Math.cos(a).toFixed(9),
+ sin = +Math.sin(a).toFixed(9);
+ me.add(cos, sin, -sin, cos, x, y);
+ me.add(1, 0, 0, 1, -x, -y);
+ },
+
+ x: function(x, y) {
+ var matrix = this.matrix;
+ return x * matrix[0][0] + y * matrix[0][1] + matrix[0][2];
+ },
+
+ y: function(x, y) {
+ var matrix = this.matrix;
+ return x * matrix[1][0] + y * matrix[1][1] + matrix[1][2];
+ },
+
+ get: function(i, j) {
+ return + this.matrix[i][j].toFixed(4);
+ },
+
+ toString: function() {
+ var me = this;
+ return [me.get(0, 0), me.get(0, 1), me.get(1, 0), me.get(1, 1), 0, 0].join();
+ },
+
+ toSvg: function() {
+ var me = this;
+ return "matrix(" + [me.get(0, 0), me.get(1, 0), me.get(0, 1), me.get(1, 1), me.get(0, 2), me.get(1, 2)].join() + ")";
+ },
+
+ toFilter: function() {
+ var me = this;
+ return "progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand',FilterType=bilinear,M11=" + me.get(0, 0) +
+ ", M12=" + me.get(0, 1) + ", M21=" + me.get(1, 0) + ", M22=" + me.get(1, 1) +
+ ", Dx=" + me.get(0, 2) + ", Dy=" + me.get(1, 2) + ")";
+ },
+
+ offset: function() {
+ var matrix = this.matrix;
+ return [(matrix[0][2] || 0).toFixed(4), (matrix[1][2] || 0).toFixed(4)];
+ },
+
+ // Split matrix into Translate Scale, Shear, and Rotate
+ split: function () {
+ function norm(a) {
+ return a[0] * a[0] + a[1] * a[1];
+ }
+ function normalize(a) {
+ var mag = Math.sqrt(norm(a));
+ a[0] /= mag;
+ a[1] /= mag;
+ }
+ var matrix = this.matrix,
+ out = {
+ translateX: matrix[0][2],
+ translateY: matrix[1][2]
+ },
+ row;
+
+ // scale and shear
+ row = [[matrix[0][0], matrix[0][1]], [matrix[1][0], matrix[1][1]]];
+ out.scaleX = Math.sqrt(norm(row[0]));
+ normalize(row[0]);
+
+ out.shear = row[0][0] * row[1][0] + row[0][1] * row[1][1];
+ row[1] = [row[1][0] - row[0][0] * out.shear, row[1][1] - row[0][1] * out.shear];
+
+ out.scaleY = Math.sqrt(norm(row[1]));
+ normalize(row[1]);
+ out.shear /= out.scaleY;
+
+ // rotation
+ out.rotate = Math.asin(-row[0][1]);
+
+ out.isSimple = !+out.shear.toFixed(9) && (out.scaleX.toFixed(9) == out.scaleY.toFixed(9) || !out.rotate);
+
+ return out;
+ }
+});
+
+// private - DD implementation for Panels
+Ext.define('Ext.draw.SpriteDD', {
+ extend: 'Ext.dd.DragSource',
+
+ constructor : function(sprite, cfg){
+ var me = this,
+ el = sprite.el;
+ me.sprite = sprite;
+ me.el = el;
+ me.dragData = {el: el, sprite: sprite};
+ me.callParent([el, cfg]);
+ me.sprite.setStyle('cursor', 'move');
+ },
+
+ showFrame: Ext.emptyFn,
+ createFrame : Ext.emptyFn,
+
+ getDragEl : function(e){
+ return this.el;
+ },
+
+ getRegion: function() {
+ var me = this,
+ el = me.el,
+ pos, x1, x2, y1, y2, t, r, b, l, bbox, sprite;
+
+ sprite = me.sprite;
+ bbox = sprite.getBBox();
+
+ try {
+ pos = Ext.Element.getXY(el);
+ } catch (e) { }
+
+ if (!pos) {
+ return null;
+ }
+
+ x1 = pos[0];
+ x2 = x1 + bbox.width;
+ y1 = pos[1];
+ y2 = y1 + bbox.height;
+
+ return Ext.create('Ext.util.Region', y1, x2, y2, x1);
+ },
+
+ /*
+ TODO(nico): Cumulative translations in VML are handled
+ differently than in SVG. While in SVG we specify the translation
+ relative to the original x, y position attributes, in VML the translation
+ is a delta between the last position of the object (modified by the last
+ translation) and the new one.
+
+ In VML the translation alters the position
+ of the object, we should change that or alter the SVG impl.
+ */
+
+ startDrag: function(x, y) {
+ var me = this,
+ attr = me.sprite.attr;
+ me.prev = me.sprite.surface.transformToViewBox(x, y);
+ },
+
+ onDrag: function(e) {
+ var xy = e.getXY(),
+ me = this,
+ sprite = me.sprite,
+ attr = sprite.attr, dx, dy;
+ xy = me.sprite.surface.transformToViewBox(xy[0], xy[1]);
+ dx = xy[0] - me.prev[0];
+ dy = xy[1] - me.prev[1];
+ sprite.setAttributes({
+ translate: {
+ x: attr.translation.x + dx,
+ y: attr.translation.y + dy
+ }
+ }, true);
+ me.prev = xy;
+ },
+
+ setDragElPos: function () {
+ // Disable automatic DOM move in DD that spoils layout of VML engine.
+ return false;
+ }
+});
+/**
+ * A Sprite is an object rendered in a Drawing surface.
+ *
+ * # Translation
+ *
+ * For translate, the configuration object contains x and y attributes that indicate where to
+ * translate the object. For example:
+ *
+ * sprite.setAttributes({
+ * translate: {
+ * x: 10,
+ * y: 10
+ * }
+ * }, true);
+ *
+ *
+ * # Rotation
+ *
+ * For rotation, the configuration object contains x and y attributes for the center of the rotation (which are optional),
+ * and a `degrees` attribute that specifies the rotation in degrees. For example:
+ *
+ * sprite.setAttributes({
+ * rotate: {
+ * degrees: 90
+ * }
+ * }, true);
+ *
+ * That example will create a 90 degrees rotation using the centroid of the Sprite as center of rotation, whereas:
+ *
+ * sprite.setAttributes({
+ * rotate: {
+ * x: 0,
+ * y: 0,
+ * degrees: 90
+ * }
+ * }, true);
+ *
+ * will create a rotation around the `(0, 0)` axis.
+ *
+ *
+ * # Scaling
+ *
+ * For scaling, the configuration object contains x and y attributes for the x-axis and y-axis scaling. For example:
+ *
+ * sprite.setAttributes({
+ * scale: {
+ * x: 10,
+ * y: 3
+ * }
+ * }, true);
+ *
+ * You can also specify the center of scaling by adding `cx` and `cy` as properties:
+ *
+ * sprite.setAttributes({
+ * scale: {
+ * cx: 0,
+ * cy: 0,
+ * x: 10,
+ * y: 3
+ * }
+ * }, true);
+ *
+ * That last example will scale a sprite taking as centers of scaling the `(0, 0)` coordinate.
+ *
+ *
+ * # Creating and adding a Sprite to a Surface
+ *
+ * Sprites can be created with a reference to a {@link Ext.draw.Surface}
+ *
+ * var drawComponent = Ext.create('Ext.draw.Component', options here...);
+ *
+ * var sprite = Ext.create('Ext.draw.Sprite', {
+ * type: 'circle',
+ * fill: '#ff0',
+ * surface: drawComponent.surface,
+ * radius: 5
+ * });
+ *
+ * Sprites can also be added to the surface as a configuration object:
+ *
+ * var sprite = drawComponent.surface.add({
+ * type: 'circle',
+ * fill: '#ff0',
+ * radius: 5
+ * });
+ *
+ * In order to properly apply properties and render the sprite we have to
+ * `show` the sprite setting the option `redraw` to `true`:
+ *
+ * sprite.show(true);
+ *
+ * The constructor configuration object of the Sprite can also be used and passed into the {@link Ext.draw.Surface}
+ * add method to append a new sprite to the canvas. For example:
+ *
+ * drawComponent.surface.add({
+ * type: 'circle',
+ * fill: '#ffc',
+ * radius: 100,
+ * x: 100,
+ * y: 100
+ * });
+ */
+Ext.define('Ext.draw.Sprite', {
+
+ /* Begin Definitions */
+
+ mixins: {
+ observable: 'Ext.util.Observable',
+ animate: 'Ext.util.Animate'
+ },
+
+ requires: ['Ext.draw.SpriteDD'],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String} type The type of the sprite. Possible options are 'circle', 'path', 'rect', 'text', 'square', 'image'
+ */
+
+ /**
+ * @cfg {Number} width Used in rectangle sprites, the width of the rectangle
+ */
+
+ /**
+ * @cfg {Number} height Used in rectangle sprites, the height of the rectangle
+ */
+
+ /**
+ * @cfg {Number} size Used in square sprites, the dimension of the square
+ */
+
+ /**
+ * @cfg {Number} radius Used in circle sprites, the radius of the circle
+ */
+
+ /**
+ * @cfg {Number} x The position along the x-axis
+ */
+
+ /**
+ * @cfg {Number} y The position along the y-axis
+ */
+
+ /**
+ * @cfg {Array} path Used in path sprites, the path of the sprite written in SVG-like path syntax
+ */
+
+ /**
+ * @cfg {Number} opacity The opacity of the sprite
+ */
+
+ /**
+ * @cfg {String} fill The fill color
+ */
+
+ /**
+ * @cfg {String} stroke The stroke color
+ */
+
+ /**
+ * @cfg {Number} stroke-width The width of the stroke
+ */
+
+ /**
+ * @cfg {String} font Used with text type sprites. The full font description. Uses the same syntax as the CSS font parameter
+ */
+
+ /**
+ * @cfg {String} text Used with text type sprites. The text itself
+ */
+
+ /**
+ * @cfg {String/String[]} group The group that this sprite belongs to, or an array of groups. Only relevant when added to a
+ * {@link Ext.draw.Surface}
+ */
+
+ /**
+ * @cfg {Boolean} draggable True to make the sprite draggable.
+ */
+
+ dirty: false,
+ dirtyHidden: false,
+ dirtyTransform: false,
+ dirtyPath: true,
+ dirtyFont: true,
+ zIndexDirty: true,
+ isSprite: true,
+ zIndex: 0,
+ fontProperties: [
+ 'font',
+ 'font-size',
+ 'font-weight',
+ 'font-style',
+ 'font-family',
+ 'text-anchor',
+ 'text'
+ ],
+ pathProperties: [
+ 'x',
+ 'y',
+ 'd',
+ 'path',
+ 'height',
+ 'width',
+ 'radius',
+ 'r',
+ 'rx',
+ 'ry',
+ 'cx',
+ 'cy'
+ ],
+ constructor: function(config) {
+ var me = this;
+ config = config || {};
+ me.id = Ext.id(null, 'ext-sprite-');
+ me.transformations = [];
+ Ext.copyTo(this, config, 'surface,group,type,draggable');
+ //attribute bucket
+ me.bbox = {};
+ me.attr = {
+ zIndex: 0,
+ translation: {
+ x: null,
+ y: null
+ },
+ rotation: {
+ degrees: null,
+ x: null,
+ y: null
+ },
+ scaling: {
+ x: null,
+ y: null,
+ cx: null,
+ cy: null
+ }
+ };
+ //delete not bucket attributes
+ delete config.surface;
+ delete config.group;
+ delete config.type;
+ delete config.draggable;
+ me.setAttributes(config);
+ me.addEvents(
+ 'beforedestroy',
+ 'destroy',
+ 'render',
+ 'mousedown',
+ 'mouseup',
+ 'mouseover',
+ 'mouseout',
+ 'mousemove',
+ 'click'
+ );
+ me.mixins.observable.constructor.apply(this, arguments);
+ },
+
+ /**
+ * @property {Ext.dd.DragSource} dd
+ * If this Sprite is configured {@link #draggable}, this property will contain
+ * an instance of {@link Ext.dd.DragSource} which handles dragging the Sprite.
+ *
+ * The developer must provide implementations of the abstract methods of {@link Ext.dd.DragSource}
+ * in order to supply behaviour for each stage of the drag/drop process. See {@link #draggable}.
+ */
+
+ initDraggable: function() {
+ var me = this;
+ me.draggable = true;
+ //create element if it doesn't exist.
+ if (!me.el) {
+ me.surface.createSpriteElement(me);
+ }
+ me.dd = Ext.create('Ext.draw.SpriteDD', me, Ext.isBoolean(me.draggable) ? null : me.draggable);
+ me.on('beforedestroy', me.dd.destroy, me.dd);
+ },
+
+ /**
+ * Change the attributes of the sprite.
+ * @param {Object} attrs attributes to be changed on the sprite.
+ * @param {Boolean} redraw Flag to immediatly draw the change.
+ * @return {Ext.draw.Sprite} this
+ */
+ setAttributes: function(attrs, redraw) {
+ var me = this,
+ fontProps = me.fontProperties,
+ fontPropsLength = fontProps.length,
+ pathProps = me.pathProperties,
+ pathPropsLength = pathProps.length,
+ hasSurface = !!me.surface,
+ custom = hasSurface && me.surface.customAttributes || {},
+ spriteAttrs = me.attr,
+ attr, i, translate, translation, rotate, rotation, scale, scaling;
+
+ attrs = Ext.apply({}, attrs);
+ for (attr in custom) {
+ if (attrs.hasOwnProperty(attr) && typeof custom[attr] == "function") {
+ Ext.apply(attrs, custom[attr].apply(me, [].concat(attrs[attr])));
+ }
+ }
+
+ // Flag a change in hidden
+ if (!!attrs.hidden !== !!spriteAttrs.hidden) {
+ me.dirtyHidden = true;
+ }
+
+ // Flag path change
+ for (i = 0; i < pathPropsLength; i++) {
+ attr = pathProps[i];
+ if (attr in attrs && attrs[attr] !== spriteAttrs[attr]) {
+ me.dirtyPath = true;
+ break;
+ }
+ }
+
+ // Flag zIndex change
+ if ('zIndex' in attrs) {
+ me.zIndexDirty = true;
+ }
+
+ // Flag font/text change
+ for (i = 0; i < fontPropsLength; i++) {
+ attr = fontProps[i];
+ if (attr in attrs && attrs[attr] !== spriteAttrs[attr]) {
+ me.dirtyFont = true;
+ break;
+ }
+ }
+
+ translate = attrs.translate;
+ translation = spriteAttrs.translation;
+ if (translate) {
+ if ((translate.x && translate.x !== translation.x) ||
+ (translate.y && translate.y !== translation.y)) {
+ Ext.apply(translation, translate);
+ me.dirtyTransform = true;
+ }
+ delete attrs.translate;
+ }
+
+ rotate = attrs.rotate;
+ rotation = spriteAttrs.rotation;
+ if (rotate) {
+ if ((rotate.x && rotate.x !== rotation.x) ||
+ (rotate.y && rotate.y !== rotation.y) ||
+ (rotate.degrees && rotate.degrees !== rotation.degrees)) {
+ Ext.apply(rotation, rotate);
+ me.dirtyTransform = true;
+ }
+ delete attrs.rotate;
+ }
+
+ scale = attrs.scale;
+ scaling = spriteAttrs.scaling;
+ if (scale) {
+ if ((scale.x && scale.x !== scaling.x) ||
+ (scale.y && scale.y !== scaling.y) ||
+ (scale.cx && scale.cx !== scaling.cx) ||
+ (scale.cy && scale.cy !== scaling.cy)) {
+ Ext.apply(scaling, scale);
+ me.dirtyTransform = true;
+ }
+ delete attrs.scale;
+ }
+
+ Ext.apply(spriteAttrs, attrs);
+ me.dirty = true;
+
+ if (redraw === true && hasSurface) {
+ me.redraw();
+ }
+ return this;
+ },
+
+ /**
+ * Retrieves the bounding box of the sprite.
+ * This will be returned as an object with x, y, width, and height properties.
+ * @return {Object} bbox
+ */
+ getBBox: function() {
+ return this.surface.getBBox(this);
+ },
+
+ setText: function(text) {
+ return this.surface.setText(this, text);
+ },
+
+ /**
+ * Hides the sprite.
+ * @param {Boolean} redraw Flag to immediatly draw the change.
+ * @return {Ext.draw.Sprite} this
+ */
+ hide: function(redraw) {
+ this.setAttributes({
+ hidden: true
+ }, redraw);
+ return this;
+ },
+
+ /**
+ * Shows the sprite.
+ * @param {Boolean} redraw Flag to immediatly draw the change.
+ * @return {Ext.draw.Sprite} this
+ */
+ show: function(redraw) {
+ this.setAttributes({
+ hidden: false
+ }, redraw);
+ return this;
+ },
+
+ /**
+ * Removes the sprite.
+ */
+ remove: function() {
+ if (this.surface) {
+ this.surface.remove(this);
+ return true;
+ }
+ return false;
+ },
+
+ onRemove: function() {
+ this.surface.onRemove(this);
+ },
+
+ /**
+ * Removes the sprite and clears all listeners.
+ */
+ destroy: function() {
+ var me = this;
+ if (me.fireEvent('beforedestroy', me) !== false) {
+ me.remove();
+ me.surface.onDestroy(me);
+ me.clearListeners();
+ me.fireEvent('destroy');
+ }
+ },
+
+ /**
+ * Redraws the sprite.
+ * @return {Ext.draw.Sprite} this
+ */
+ redraw: function() {
+ this.surface.renderItem(this);
+ return this;
+ },
+
+ /**
+ * Wrapper for setting style properties, also takes single object parameter of multiple styles.
+ * @param {String/Object} property The style property to be set, or an object of multiple styles.
+ * @param {String} value (optional) The value to apply to the given property, or null if an object was passed.
+ * @return {Ext.draw.Sprite} this
+ */
+ setStyle: function() {
+ this.el.setStyle.apply(this.el, arguments);
+ return this;
+ },
+
+ /**
+ * Adds one or more CSS classes to the element. Duplicate classes are automatically filtered out. Note this method
+ * is severly limited in VML.
+ * @param {String/String[]} className The CSS class to add, or an array of classes
+ * @return {Ext.draw.Sprite} this
+ */
+ addCls: function(obj) {
+ this.surface.addCls(this, obj);
+ return this;
+ },
+
+ /**
+ * Removes one or more CSS classes from the element.
+ * @param {String/String[]} className The CSS class to remove, or an array of classes. Note this method
+ * is severly limited in VML.
+ * @return {Ext.draw.Sprite} this
+ */
+ removeCls: function(obj) {
+ this.surface.removeCls(this, obj);
+ return this;
+ }
+});
+
+/**
+ * @class Ext.draw.engine.Svg
+ * @extends Ext.draw.Surface
+ * Provides specific methods to draw with SVG.
+ */
+Ext.define('Ext.draw.engine.Svg', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.draw.Surface',
+
+ requires: ['Ext.draw.Draw', 'Ext.draw.Sprite', 'Ext.draw.Matrix', 'Ext.Element'],
+
+ /* End Definitions */
+
+ engine: 'Svg',
+
+ trimRe: /^\s+|\s+$/g,
+ spacesRe: /\s+/,
+ xlink: "http:/" + "/www.w3.org/1999/xlink",
+
+ translateAttrs: {
+ radius: "r",
+ radiusX: "rx",
+ radiusY: "ry",
+ path: "d",
+ lineWidth: "stroke-width",
+ fillOpacity: "fill-opacity",
+ strokeOpacity: "stroke-opacity",
+ strokeLinejoin: "stroke-linejoin"
+ },
+
+ parsers: {},
+
+ minDefaults: {
+ circle: {
+ cx: 0,
+ cy: 0,
+ r: 0,
+ fill: "none",
+ stroke: null,
+ "stroke-width": null,
+ opacity: null,
+ "fill-opacity": null,
+ "stroke-opacity": null
+ },
+ ellipse: {
+ cx: 0,
+ cy: 0,
+ rx: 0,
+ ry: 0,
+ fill: "none",
+ stroke: null,
+ "stroke-width": null,
+ opacity: null,
+ "fill-opacity": null,
+ "stroke-opacity": null
+ },
+ rect: {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ rx: 0,
+ ry: 0,
+ fill: "none",
+ stroke: null,
+ "stroke-width": null,
+ opacity: null,
+ "fill-opacity": null,
+ "stroke-opacity": null
+ },
+ text: {
+ x: 0,
+ y: 0,
+ "text-anchor": "start",
+ "font-family": null,
+ "font-size": null,
+ "font-weight": null,
+ "font-style": null,
+ fill: "#000",
+ stroke: null,
+ "stroke-width": null,
+ opacity: null,
+ "fill-opacity": null,
+ "stroke-opacity": null
+ },
+ path: {
+ d: "M0,0",
+ fill: "none",
+ stroke: null,
+ "stroke-width": null,
+ opacity: null,
+ "fill-opacity": null,
+ "stroke-opacity": null
+ },
+ image: {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ preserveAspectRatio: "none",
+ opacity: null
+ }
+ },
+
+ createSvgElement: function(type, attrs) {
+ var el = this.domRef.createElementNS("http:/" + "/www.w3.org/2000/svg", type),
+ key;
+ if (attrs) {
+ for (key in attrs) {
+ el.setAttribute(key, String(attrs[key]));
+ }
+ }
+ return el;
+ },
+
+ createSpriteElement: function(sprite) {
+ // Create svg element and append to the DOM.
+ var el = this.createSvgElement(sprite.type);
+ el.id = sprite.id;
+ if (el.style) {
+ el.style.webkitTapHighlightColor = "rgba(0,0,0,0)";
+ }
+ sprite.el = Ext.get(el);
+ this.applyZIndex(sprite); //performs the insertion
+ sprite.matrix = Ext.create('Ext.draw.Matrix');
+ sprite.bbox = {
+ plain: 0,
+ transform: 0
+ };
+ sprite.fireEvent("render", sprite);
+ return el;
+ },
+
+ getBBox: function (sprite, isWithoutTransform) {
+ var realPath = this["getPath" + sprite.type](sprite);
+ if (isWithoutTransform) {
+ sprite.bbox.plain = sprite.bbox.plain || Ext.draw.Draw.pathDimensions(realPath);
+ return sprite.bbox.plain;
+ }
+ sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix));
+ return sprite.bbox.transform;
+ },
+
+ getBBoxText: function (sprite) {
+ var bbox = {},
+ bb, height, width, i, ln, el;
+
+ if (sprite && sprite.el) {
+ el = sprite.el.dom;
+ try {
+ bbox = el.getBBox();
+ return bbox;
+ } catch(e) {
+ // Firefox 3.0.x plays badly here
+ }
+ bbox = {x: bbox.x, y: Infinity, width: 0, height: 0};
+ ln = el.getNumberOfChars();
+ for (i = 0; i < ln; i++) {
+ bb = el.getExtentOfChar(i);
+ bbox.y = Math.min(bb.y, bbox.y);
+ height = bb.y + bb.height - bbox.y;
+ bbox.height = Math.max(bbox.height, height);
+ width = bb.x + bb.width - bbox.x;
+ bbox.width = Math.max(bbox.width, width);
+ }
+ return bbox;
+ }
+ },
+
+ hide: function() {
+ Ext.get(this.el).hide();
+ },
+
+ show: function() {
+ Ext.get(this.el).show();
+ },
+
+ hidePrim: function(sprite) {
+ this.addCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
+ },
+
+ showPrim: function(sprite) {
+ this.removeCls(sprite, Ext.baseCSSPrefix + 'hide-visibility');
+ },
+
+ getDefs: function() {
+ return this._defs || (this._defs = this.createSvgElement("defs"));
+ },
+
+ transform: function(sprite) {
+ var me = this,
+ matrix = Ext.create('Ext.draw.Matrix'),
+ transforms = sprite.transformations,
+ transformsLength = transforms.length,
+ i = 0,
+ transform, type;
+
+ for (; i < transformsLength; i++) {
+ transform = transforms[i];
+ type = transform.type;
+ if (type == "translate") {
+ matrix.translate(transform.x, transform.y);
+ }
+ else if (type == "rotate") {
+ matrix.rotate(transform.degrees, transform.x, transform.y);
+ }
+ else if (type == "scale") {
+ matrix.scale(transform.x, transform.y, transform.centerX, transform.centerY);
+ }
+ }
+ sprite.matrix = matrix;
+ sprite.el.set({transform: matrix.toSvg()});
+ },
+
+ setSize: function(w, h) {
+ var me = this,
+ el = me.el;
+
+ w = +w || me.width;
+ h = +h || me.height;
+ me.width = w;
+ me.height = h;
+
+ el.setSize(w, h);
+ el.set({
+ width: w,
+ height: h
+ });
+ me.callParent([w, h]);
+ },
+
+ /**
+ * Get the region for the surface's canvas area
+ * @returns {Ext.util.Region}
+ */
+ getRegion: function() {
+ // Mozilla requires using the background rect because the svg element returns an
+ // incorrect region. Webkit gives no region for the rect and must use the svg element.
+ var svgXY = this.el.getXY(),
+ rectXY = this.bgRect.getXY(),
+ max = Math.max,
+ x = max(svgXY[0], rectXY[0]),
+ y = max(svgXY[1], rectXY[1]);
+ return {
+ left: x,
+ top: y,
+ right: x + this.width,
+ bottom: y + this.height
+ };
+ },
+
+ onRemove: function(sprite) {
+ if (sprite.el) {
+ sprite.el.remove();
+ delete sprite.el;
+ }
+ this.callParent(arguments);
+ },
+
+ setViewBox: function(x, y, width, height) {
+ if (isFinite(x) && isFinite(y) && isFinite(width) && isFinite(height)) {
+ this.callParent(arguments);
+ this.el.dom.setAttribute("viewBox", [x, y, width, height].join(" "));
+ }
+ },
+
+ render: function (container) {
+ var me = this;
+ if (!me.el) {
+ var width = me.width || 10,
+ height = me.height || 10,
+ el = me.createSvgElement('svg', {
+ xmlns: "http:/" + "/www.w3.org/2000/svg",
+ version: 1.1,
+ width: width,
+ height: height
+ }),
+ defs = me.getDefs(),
+
+ // Create a rect that is always the same size as the svg root; this serves 2 purposes:
+ // (1) It allows mouse events to be fired over empty areas in Webkit, and (2) we can
+ // use it rather than the svg element for retrieving the correct client rect of the
+ // surface in Mozilla (see https://bugzilla.mozilla.org/show_bug.cgi?id=530985)
+ bgRect = me.createSvgElement("rect", {
+ width: "100%",
+ height: "100%",
+ fill: "#000",
+ stroke: "none",
+ opacity: 0
+ }),
+ webkitRect;
+
+ if (Ext.isSafari3) {
+ // Rect that we will show/hide to fix old WebKit bug with rendering issues.
+ webkitRect = me.createSvgElement("rect", {
+ x: -10,
+ y: -10,
+ width: "110%",
+ height: "110%",
+ fill: "none",
+ stroke: "#000"
+ });
+ }
+ el.appendChild(defs);
+ if (Ext.isSafari3) {
+ el.appendChild(webkitRect);
+ }
+ el.appendChild(bgRect);
+ container.appendChild(el);
+ me.el = Ext.get(el);
+ me.bgRect = Ext.get(bgRect);
+ if (Ext.isSafari3) {
+ me.webkitRect = Ext.get(webkitRect);
+ me.webkitRect.hide();
+ }
+ me.el.on({
+ scope: me,
+ mouseup: me.onMouseUp,
+ mousedown: me.onMouseDown,
+ mouseover: me.onMouseOver,
+ mouseout: me.onMouseOut,
+ mousemove: me.onMouseMove,
+ mouseenter: me.onMouseEnter,
+ mouseleave: me.onMouseLeave,
+ click: me.onClick
+ });
+ }
+ me.renderAll();
+ },
+
+ // private
+ onMouseEnter: function(e) {
+ if (this.el.parent().getRegion().contains(e.getPoint())) {
+ this.fireEvent('mouseenter', e);
+ }
+ },
+
+ // private
+ onMouseLeave: function(e) {
+ if (!this.el.parent().getRegion().contains(e.getPoint())) {
+ this.fireEvent('mouseleave', e);
+ }
+ },
+ // @private - Normalize a delegated single event from the main container to each sprite and sprite group
+ processEvent: function(name, e) {
+ var target = e.getTarget(),
+ surface = this.surface,
+ sprite;
+
+ this.fireEvent(name, e);
+ // We wrap text types in a tspan, sprite is the parent.
+ if (target.nodeName == "tspan" && target.parentNode) {
+ target = target.parentNode;
+ }
+ sprite = this.items.get(target.id);
+ if (sprite) {
+ sprite.fireEvent(name, sprite, e);
+ }
+ },
+
+ /* @private - Wrap SVG text inside a tspan to allow for line wrapping. In addition this normallizes
+ * the baseline for text the vertical middle of the text to be the same as VML.
+ */
+ tuneText: function (sprite, attrs) {
+ var el = sprite.el.dom,
+ tspans = [],
+ height, tspan, text, i, ln, texts, factor;
+
+ if (attrs.hasOwnProperty("text")) {
+ tspans = this.setText(sprite, attrs.text);
+ }
+ // Normalize baseline via a DY shift of first tspan. Shift other rows by height * line height (1.2)
+ if (tspans.length) {
+ height = this.getBBoxText(sprite).height;
+ for (i = 0, ln = tspans.length; i < ln; i++) {
+ // The text baseline for FireFox 3.0 and 3.5 is different than other SVG implementations
+ // so we are going to normalize that here
+ factor = (Ext.isFF3_0 || Ext.isFF3_5) ? 2 : 4;
+ tspans[i].setAttribute("dy", i ? height * 1.2 : height / factor);
+ }
+ sprite.dirty = true;
+ }
+ },
+
+ setText: function(sprite, textString) {
+ var me = this,
+ el = sprite.el.dom,
+ x = el.getAttribute("x"),
+ tspans = [],
+ height, tspan, text, i, ln, texts;
+
+ while (el.firstChild) {
+ el.removeChild(el.firstChild);
+ }
+ // Wrap each row into tspan to emulate rows
+ texts = String(textString).split("\n");
+ for (i = 0, ln = texts.length; i < ln; i++) {
+ text = texts[i];
+ if (text) {
+ tspan = me.createSvgElement("tspan");
+ tspan.appendChild(document.createTextNode(Ext.htmlDecode(text)));
+ tspan.setAttribute("x", x);
+ el.appendChild(tspan);
+ tspans[i] = tspan;
+ }
+ }
+ return tspans;
+ },
+
+ renderAll: function() {
+ this.items.each(this.renderItem, this);
+ },
+
+ renderItem: function (sprite) {
+ if (!this.el) {
+ return;
+ }
+ if (!sprite.el) {
+ this.createSpriteElement(sprite);
+ }
+ if (sprite.zIndexDirty) {
+ this.applyZIndex(sprite);
+ }
+ if (sprite.dirty) {
+ this.applyAttrs(sprite);
+ this.applyTransformations(sprite);
+ }
+ },
+
+ redraw: function(sprite) {
+ sprite.dirty = sprite.zIndexDirty = true;
+ this.renderItem(sprite);
+ },
+
+ applyAttrs: function (sprite) {
+ var me = this,
+ el = sprite.el,
+ group = sprite.group,
+ sattr = sprite.attr,
+ parsers = me.parsers,
+ //Safari does not handle linear gradients correctly in quirksmode
+ //ref: https://bugs.webkit.org/show_bug.cgi?id=41952
+ //ref: EXTJSIV-1472
+ gradientsMap = me.gradientsMap || {},
+ safariFix = Ext.isSafari && !Ext.isStrict,
+ groups, i, ln, attrs, font, key, style, name, rect;
+
+ if (group) {
+ groups = [].concat(group);
+ ln = groups.length;
+ for (i = 0; i < ln; i++) {
+ group = groups[i];
+ me.getGroup(group).add(sprite);
+ }
+ delete sprite.group;
+ }
+ attrs = me.scrubAttrs(sprite) || {};
+
+ // if (sprite.dirtyPath) {
+ sprite.bbox.plain = 0;
+ sprite.bbox.transform = 0;
+ if (sprite.type == "circle" || sprite.type == "ellipse") {
+ attrs.cx = attrs.cx || attrs.x;
+ attrs.cy = attrs.cy || attrs.y;
+ }
+ else if (sprite.type == "rect") {
+ attrs.rx = attrs.ry = attrs.r;
+ }
+ else if (sprite.type == "path" && attrs.d) {
+ attrs.d = Ext.draw.Draw.pathToString(Ext.draw.Draw.pathToAbsolute(attrs.d));
+ }
+ sprite.dirtyPath = false;
+ // }
+ // else {
+ // delete attrs.d;
+ // }
+
+ if (attrs['clip-rect']) {
+ me.setClip(sprite, attrs);
+ delete attrs['clip-rect'];
+ }
+ if (sprite.type == 'text' && attrs.font && sprite.dirtyFont) {
+ el.set({ style: "font: " + attrs.font});
+ sprite.dirtyFont = false;
+ }
+ if (sprite.type == "image") {
+ el.dom.setAttributeNS(me.xlink, "href", attrs.src);
+ }
+ Ext.applyIf(attrs, me.minDefaults[sprite.type]);
+
+ if (sprite.dirtyHidden) {
+ (sattr.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite);
+ sprite.dirtyHidden = false;
+ }
+ for (key in attrs) {
+ if (attrs.hasOwnProperty(key) && attrs[key] != null) {
+ //Safari does not handle linear gradients correctly in quirksmode
+ //ref: https://bugs.webkit.org/show_bug.cgi?id=41952
+ //ref: EXTJSIV-1472
+ //if we're Safari in QuirksMode and we're applying some color attribute and the value of that
+ //attribute is a reference to a gradient then assign a plain color to that value instead of the gradient.
+ if (safariFix && ('color|stroke|fill'.indexOf(key) > -1) && (attrs[key] in gradientsMap)) {
+ attrs[key] = gradientsMap[attrs[key]];
+ }
+ if (key in parsers) {
+ el.dom.setAttribute(key, parsers[key](attrs[key], sprite, me));
+ } else {
+ el.dom.setAttribute(key, attrs[key]);
+ }
+ }
+ }
+
+ if (sprite.type == 'text') {
+ me.tuneText(sprite, attrs);
+ }
+
+ //set styles
+ style = sattr.style;
+ if (style) {
+ el.setStyle(style);
+ }
+
+ sprite.dirty = false;
+
+ if (Ext.isSafari3) {
+ // Refreshing the view to fix bug EXTJSIV-1: rendering issue in old Safari 3
+ me.webkitRect.show();
+ setTimeout(function () {
+ me.webkitRect.hide();
+ });
+ }
+ },
+
+ setClip: function(sprite, params) {
+ var me = this,
+ rect = params["clip-rect"],
+ clipEl, clipPath;
+ if (rect) {
+ if (sprite.clip) {
+ sprite.clip.parentNode.parentNode.removeChild(sprite.clip.parentNode);
+ }
+ clipEl = me.createSvgElement('clipPath');
+ clipPath = me.createSvgElement('rect');
+ clipEl.id = Ext.id(null, 'ext-clip-');
+ clipPath.setAttribute("x", rect.x);
+ clipPath.setAttribute("y", rect.y);
+ clipPath.setAttribute("width", rect.width);
+ clipPath.setAttribute("height", rect.height);
+ clipEl.appendChild(clipPath);
+ me.getDefs().appendChild(clipEl);
+ sprite.el.dom.setAttribute("clip-path", "url(#" + clipEl.id + ")");
+ sprite.clip = clipPath;
+ }
+ // if (!attrs[key]) {
+ // var clip = Ext.getDoc().dom.getElementById(sprite.el.getAttribute("clip-path").replace(/(^url\(#|\)$)/g, ""));
+ // clip && clip.parentNode.removeChild(clip);
+ // sprite.el.setAttribute("clip-path", "");
+ // delete attrss.clip;
+ // }
+ },
+
+ /**
+ * Insert or move a given sprite's element to the correct place in the DOM list for its zIndex
+ * @param {Ext.draw.Sprite} sprite
+ */
+ applyZIndex: function(sprite) {
+ var me = this,
+ items = me.items,
+ idx = items.indexOf(sprite),
+ el = sprite.el,
+ prevEl;
+ if (me.el.dom.childNodes[idx + 2] !== el.dom) { //shift by 2 to account for defs and bg rect
+ if (idx > 0) {
+ // Find the first previous sprite which has its DOM element created already
+ do {
+ prevEl = items.getAt(--idx).el;
+ } while (!prevEl && idx > 0);
+ }
+ el.insertAfter(prevEl || me.bgRect);
+ }
+ sprite.zIndexDirty = false;
+ },
+
+ createItem: function (config) {
+ var sprite = Ext.create('Ext.draw.Sprite', config);
+ sprite.surface = this;
+ return sprite;
+ },
+
+ addGradient: function(gradient) {
+ gradient = Ext.draw.Draw.parseGradient(gradient);
+ var me = this,
+ ln = gradient.stops.length,
+ vector = gradient.vector,
+ //Safari does not handle linear gradients correctly in quirksmode
+ //ref: https://bugs.webkit.org/show_bug.cgi?id=41952
+ //ref: EXTJSIV-1472
+ usePlain = Ext.isSafari && !Ext.isStrict,
+ gradientEl, stop, stopEl, i, gradientsMap;
+
+ gradientsMap = me.gradientsMap || {};
+
+ if (!usePlain) {
+ if (gradient.type == "linear") {
+ gradientEl = me.createSvgElement("linearGradient");
+ gradientEl.setAttribute("x1", vector[0]);
+ gradientEl.setAttribute("y1", vector[1]);
+ gradientEl.setAttribute("x2", vector[2]);
+ gradientEl.setAttribute("y2", vector[3]);
+ }
+ else {
+ gradientEl = me.createSvgElement("radialGradient");
+ gradientEl.setAttribute("cx", gradient.centerX);
+ gradientEl.setAttribute("cy", gradient.centerY);
+ gradientEl.setAttribute("r", gradient.radius);
+ if (Ext.isNumber(gradient.focalX) && Ext.isNumber(gradient.focalY)) {
+ gradientEl.setAttribute("fx", gradient.focalX);
+ gradientEl.setAttribute("fy", gradient.focalY);
+ }
+ }
+ gradientEl.id = gradient.id;
+ me.getDefs().appendChild(gradientEl);
+ for (i = 0; i < ln; i++) {
+ stop = gradient.stops[i];
+ stopEl = me.createSvgElement("stop");
+ stopEl.setAttribute("offset", stop.offset + "%");
+ stopEl.setAttribute("stop-color", stop.color);
+ stopEl.setAttribute("stop-opacity",stop.opacity);
+ gradientEl.appendChild(stopEl);
+ }
+ } else {
+ gradientsMap['url(#' + gradient.id + ')'] = gradient.stops[0].color;
+ }
+ me.gradientsMap = gradientsMap;
+ },
+
+ /**
+ * Checks if the specified CSS class exists on this element's DOM node.
+ * @param {String} className The CSS class to check for
+ * @return {Boolean} True if the class exists, else false
+ */
+ hasCls: function(sprite, className) {
+ return className && (' ' + (sprite.el.dom.getAttribute('class') || '') + ' ').indexOf(' ' + className + ' ') != -1;
+ },
+
+ addCls: function(sprite, className) {
+ var el = sprite.el,
+ i,
+ len,
+ v,
+ cls = [],
+ curCls = el.getAttribute('class') || '';
+ // Separate case is for speed
+ if (!Ext.isArray(className)) {
+ if (typeof className == 'string' && !this.hasCls(sprite, className)) {
+ el.set({ 'class': curCls + ' ' + className });
+ }
+ }
+ else {
+ for (i = 0, len = className.length; i < len; i++) {
+ v = className[i];
+ if (typeof v == 'string' && (' ' + curCls + ' ').indexOf(' ' + v + ' ') == -1) {
+ cls.push(v);
+ }
+ }
+ if (cls.length) {
+ el.set({ 'class': ' ' + cls.join(' ') });
+ }
+ }
+ },
+
+ removeCls: function(sprite, className) {
+ var me = this,
+ el = sprite.el,
+ curCls = el.getAttribute('class') || '',
+ i, idx, len, cls, elClasses;
+ if (!Ext.isArray(className)){
+ className = [className];
+ }
+ if (curCls) {
+ elClasses = curCls.replace(me.trimRe, ' ').split(me.spacesRe);
+ for (i = 0, len = className.length; i < len; i++) {
+ cls = className[i];
+ if (typeof cls == 'string') {
+ cls = cls.replace(me.trimRe, '');
+ idx = Ext.Array.indexOf(elClasses, cls);
+ if (idx != -1) {
+ Ext.Array.erase(elClasses, idx, 1);
+ }
+ }
+ }
+ el.set({ 'class': elClasses.join(' ') });
+ }
+ },
+
+ destroy: function() {
+ var me = this;
+
+ me.callParent();
+ if (me.el) {
+ me.el.remove();
+ }
+ delete me.el;
+ }
+});
+/**
+ * @class Ext.draw.engine.Vml
+ * @extends Ext.draw.Surface
+ * Provides specific methods to draw with VML.
+ */
+
+Ext.define('Ext.draw.engine.Vml', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.draw.Surface',
+
+ requires: ['Ext.draw.Draw', 'Ext.draw.Color', 'Ext.draw.Sprite', 'Ext.draw.Matrix', 'Ext.Element'],
+
+ /* End Definitions */
+
+ engine: 'Vml',
+
+ map: {M: "m", L: "l", C: "c", Z: "x", m: "t", l: "r", c: "v", z: "x"},
+ bitesRe: /([clmz]),?([^clmz]*)/gi,
+ valRe: /-?[^,\s-]+/g,
+ fillUrlRe: /^url\(\s*['"]?([^\)]+?)['"]?\s*\)$/i,
+ pathlike: /^(path|rect)$/,
+ NonVmlPathRe: /[ahqstv]/ig, // Non-VML Pathing ops
+ partialPathRe: /[clmz]/g,
+ fontFamilyRe: /^['"]+|['"]+$/g,
+ baseVmlCls: Ext.baseCSSPrefix + 'vml-base',
+ vmlGroupCls: Ext.baseCSSPrefix + 'vml-group',
+ spriteCls: Ext.baseCSSPrefix + 'vml-sprite',
+ measureSpanCls: Ext.baseCSSPrefix + 'vml-measure-span',
+ zoom: 21600,
+ coordsize: 1000,
+ coordorigin: '0 0',
+
+ // VML uses CSS z-index and therefore doesn't need sprites to be kept in zIndex order
+ orderSpritesByZIndex: false,
+
+ // @private
+ // Convert an SVG standard path into a VML path
+ path2vml: function (path) {
+ var me = this,
+ nonVML = me.NonVmlPathRe,
+ map = me.map,
+ val = me.valRe,
+ zoom = me.zoom,
+ bites = me.bitesRe,
+ command = Ext.Function.bind(Ext.draw.Draw.pathToAbsolute, Ext.draw.Draw),
+ res, pa, p, r, i, ii, j, jj;
+ if (String(path).match(nonVML)) {
+ command = Ext.Function.bind(Ext.draw.Draw.path2curve, Ext.draw.Draw);
+ } else if (!String(path).match(me.partialPathRe)) {
+ res = String(path).replace(bites, function (all, command, args) {
+ var vals = [],
+ isMove = command.toLowerCase() == "m",
+ res = map[command];
+ args.replace(val, function (value) {
+ if (isMove && vals[length] == 2) {
+ res += vals + map[command == "m" ? "l" : "L"];
+ vals = [];
+ }
+ vals.push(Math.round(value * zoom));
+ });
+ return res + vals;
+ });
+ return res;
+ }
+ pa = command(path);
+ res = [];
+ for (i = 0, ii = pa.length; i < ii; i++) {
+ p = pa[i];
+ r = pa[i][0].toLowerCase();
+ if (r == "z") {
+ r = "x";
+ }
+ for (j = 1, jj = p.length; j < jj; j++) {
+ r += Math.round(p[j] * me.zoom) + (j != jj - 1 ? "," : "");
+ }
+ res.push(r);
+ }
+ return res.join(" ");
+ },
+
+ // @private - set of attributes which need to be translated from the sprite API to the native browser API
+ translateAttrs: {
+ radius: "r",
+ radiusX: "rx",
+ radiusY: "ry",
+ lineWidth: "stroke-width",
+ fillOpacity: "fill-opacity",
+ strokeOpacity: "stroke-opacity",
+ strokeLinejoin: "stroke-linejoin"
+ },
+
+ // @private - Minimun set of defaults for different types of sprites.
+ minDefaults: {
+ circle: {
+ fill: "none",
+ stroke: null,
+ "stroke-width": null,
+ opacity: null,
+ "fill-opacity": null,
+ "stroke-opacity": null
+ },
+ ellipse: {
+ cx: 0,
+ cy: 0,
+ rx: 0,
+ ry: 0,
+ fill: "none",
+ stroke: null,
+ "stroke-width": null,
+ opacity: null,
+ "fill-opacity": null,
+ "stroke-opacity": null
+ },
+ rect: {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ rx: 0,
+ ry: 0,
+ fill: "none",
+ stroke: null,
+ "stroke-width": null,
+ opacity: null,
+ "fill-opacity": null,
+ "stroke-opacity": null
+ },
+ text: {
+ x: 0,
+ y: 0,
+ "text-anchor": "start",
+ font: '10px "Arial"',
+ fill: "#000",
+ stroke: null,
+ "stroke-width": null,
+ opacity: null,
+ "fill-opacity": null,
+ "stroke-opacity": null
+ },
+ path: {
+ d: "M0,0",
+ fill: "none",
+ stroke: null,
+ "stroke-width": null,
+ opacity: null,
+ "fill-opacity": null,
+ "stroke-opacity": null
+ },
+ image: {
+ x: 0,
+ y: 0,
+ width: 0,
+ height: 0,
+ preserveAspectRatio: "none",
+ opacity: null
+ }
+ },
+
+ // private
+ onMouseEnter: function(e) {
+ this.fireEvent("mouseenter", e);
+ },
+
+ // private
+ onMouseLeave: function(e) {
+ this.fireEvent("mouseleave", e);
+ },
+
+ // @private - Normalize a delegated single event from the main container to each sprite and sprite group
+ processEvent: function(name, e) {
+ var target = e.getTarget(),
+ surface = this.surface,
+ sprite;
+ this.fireEvent(name, e);
+ sprite = this.items.get(target.id);
+ if (sprite) {
+ sprite.fireEvent(name, sprite, e);
+ }
+ },
+
+ // Create the VML element/elements and append them to the DOM
+ createSpriteElement: function(sprite) {
+ var me = this,
+ attr = sprite.attr,
+ type = sprite.type,
+ zoom = me.zoom,
+ vml = sprite.vml || (sprite.vml = {}),
+ round = Math.round,
+ el = me.createNode('shape'),
+ path, skew, textPath;
+
+ el.coordsize = zoom + ' ' + zoom;
+ el.coordorigin = attr.coordorigin || "0 0";
+ Ext.get(el).addCls(me.spriteCls);
+ if (type == "text") {
+ vml.path = path = me.createNode("path");
+ path.textpathok = true;
+ vml.textpath = textPath = me.createNode("textpath");
+ textPath.on = true;
+ el.appendChild(textPath);
+ el.appendChild(path);
+ }
+ el.id = sprite.id;
+ sprite.el = Ext.get(el);
+ me.el.appendChild(el);
+ if (type !== 'image') {
+ skew = me.createNode("skew");
+ skew.on = true;
+ el.appendChild(skew);
+ sprite.skew = skew;
+ }
+ sprite.matrix = Ext.create('Ext.draw.Matrix');
+ sprite.bbox = {
+ plain: null,
+ transform: null
+ };
+ sprite.fireEvent("render", sprite);
+ return sprite.el;
+ },
+
+ // @private - Get bounding box for the sprite. The Sprite itself has the public method.
+ getBBox: function (sprite, isWithoutTransform) {
+ var realPath = this["getPath" + sprite.type](sprite);
+ if (isWithoutTransform) {
+ sprite.bbox.plain = sprite.bbox.plain || Ext.draw.Draw.pathDimensions(realPath);
+ return sprite.bbox.plain;
+ }
+ sprite.bbox.transform = sprite.bbox.transform || Ext.draw.Draw.pathDimensions(Ext.draw.Draw.mapPath(realPath, sprite.matrix));
+ return sprite.bbox.transform;
+ },
+
+ getBBoxText: function (sprite) {
+ var vml = sprite.vml;
+ return {
+ x: vml.X + (vml.bbx || 0) - vml.W / 2,
+ y: vml.Y - vml.H / 2,
+ width: vml.W,
+ height: vml.H
+ };
+ },
+
+ applyAttrs: function (sprite) {
+ var me = this,
+ vml = sprite.vml,
+ group = sprite.group,
+ spriteAttr = sprite.attr,
+ el = sprite.el,
+ dom = el.dom,
+ style, name, groups, i, ln, scrubbedAttrs, font, key, bbox;
+
+ if (group) {
+ groups = [].concat(group);
+ ln = groups.length;
+ for (i = 0; i < ln; i++) {
+ group = groups[i];
+ me.getGroup(group).add(sprite);
+ }
+ delete sprite.group;
+ }
+ scrubbedAttrs = me.scrubAttrs(sprite) || {};
+
+ if (sprite.zIndexDirty) {
+ me.setZIndex(sprite);
+ }
+
+ // Apply minimum default attributes
+ Ext.applyIf(scrubbedAttrs, me.minDefaults[sprite.type]);
+
+ if (dom.href) {
+ dom.href = scrubbedAttrs.href;
+ }
+ if (dom.title) {
+ dom.title = scrubbedAttrs.title;
+ }
+ if (dom.target) {
+ dom.target = scrubbedAttrs.target;
+ }
+ if (dom.cursor) {
+ dom.cursor = scrubbedAttrs.cursor;
+ }
+
+ // Change visibility
+ if (sprite.dirtyHidden) {
+ (scrubbedAttrs.hidden) ? me.hidePrim(sprite) : me.showPrim(sprite);
+ sprite.dirtyHidden = false;
+ }
+
+ // Update path
+ if (sprite.dirtyPath) {
+ if (sprite.type == "circle" || sprite.type == "ellipse") {
+ var cx = scrubbedAttrs.x,
+ cy = scrubbedAttrs.y,
+ rx = scrubbedAttrs.rx || scrubbedAttrs.r || 0,
+ ry = scrubbedAttrs.ry || scrubbedAttrs.r || 0;
+ dom.path = Ext.String.format("ar{0},{1},{2},{3},{4},{1},{4},{1}",
+ Math.round((cx - rx) * me.zoom),
+ Math.round((cy - ry) * me.zoom),
+ Math.round((cx + rx) * me.zoom),
+ Math.round((cy + ry) * me.zoom),
+ Math.round(cx * me.zoom));
+ sprite.dirtyPath = false;
+ }
+ else if (sprite.type !== "text") {
+ sprite.attr.path = scrubbedAttrs.path = me.setPaths(sprite, scrubbedAttrs) || scrubbedAttrs.path;
+ dom.path = me.path2vml(scrubbedAttrs.path);
+ sprite.dirtyPath = false;
+ }
+ }
+
+ // Apply clipping
+ if ("clip-rect" in scrubbedAttrs) {
+ me.setClip(sprite, scrubbedAttrs);
+ }
+
+ // Handle text (special handling required)
+ if (sprite.type == "text") {
+ me.setTextAttributes(sprite, scrubbedAttrs);
+ }
+
+ // Handle fill and opacity
+ if (sprite.type == 'image' || scrubbedAttrs.opacity || scrubbedAttrs['fill-opacity'] || scrubbedAttrs.fill) {
+ me.setFill(sprite, scrubbedAttrs);
+ }
+
+ // Handle stroke (all fills require a stroke element)
+ if (scrubbedAttrs.stroke || scrubbedAttrs['stroke-opacity'] || scrubbedAttrs.fill) {
+ me.setStroke(sprite, scrubbedAttrs);
+ }
+
+ //set styles
+ style = spriteAttr.style;
+ if (style) {
+ el.setStyle(style);
+ }
+
+ sprite.dirty = false;
+ },
+
+ setZIndex: function(sprite) {
+ if (sprite.el) {
+ if (sprite.attr.zIndex != undefined) {
+ sprite.el.setStyle('zIndex', sprite.attr.zIndex);
+ }
+ sprite.zIndexDirty = false;
+ }
+ },
+
+ // Normalize all virtualized types into paths.
+ setPaths: function(sprite, params) {
+ var spriteAttr = sprite.attr;
+ // Clear bbox cache
+ sprite.bbox.plain = null;
+ sprite.bbox.transform = null;
+ if (sprite.type == 'circle') {
+ spriteAttr.rx = spriteAttr.ry = params.r;
+ return Ext.draw.Draw.ellipsePath(sprite);
+ }
+ else if (sprite.type == 'ellipse') {
+ spriteAttr.rx = params.rx;
+ spriteAttr.ry = params.ry;
+ return Ext.draw.Draw.ellipsePath(sprite);
+ }
+ else if (sprite.type == 'rect' || sprite.type == 'image') {
+ spriteAttr.rx = spriteAttr.ry = params.r;
+ return Ext.draw.Draw.rectPath(sprite);
+ }
+ else if (sprite.type == 'path' && spriteAttr.path) {
+ return Ext.draw.Draw.pathToAbsolute(spriteAttr.path);
+ }
+ return false;
+ },
+
+ setFill: function(sprite, params) {
+ var me = this,
+ el = sprite.el,
+ dom = el.dom,
+ fillEl = dom.getElementsByTagName('fill')[0],
+ opacity, gradient, fillUrl, rotation, angle;
+
+ if (fillEl) {
+ dom.removeChild(fillEl);
+ } else {
+ fillEl = me.createNode('fill');
+ }
+ if (Ext.isArray(params.fill)) {
+ params.fill = params.fill[0];
+ }
+ if (sprite.type == 'image') {
+ fillEl.on = true;
+ fillEl.src = params.src;
+ fillEl.type = "tile";
+ fillEl.rotate = true;
+ } else if (params.fill == "none") {
+ fillEl.on = false;
+ } else {
+ if (typeof params.opacity == "number") {
+ fillEl.opacity = params.opacity;
+ }
+ if (typeof params["fill-opacity"] == "number") {
+ fillEl.opacity = params["fill-opacity"];
+ }
+ fillEl.on = true;
+ if (typeof params.fill == "string") {
+ fillUrl = params.fill.match(me.fillUrlRe);
+ if (fillUrl) {
+ fillUrl = fillUrl[1];
+ // If the URL matches one of the registered gradients, render that gradient
+ if (fillUrl.charAt(0) == "#") {
+ gradient = me.gradientsColl.getByKey(fillUrl.substring(1));
+ }
+ if (gradient) {
+ // VML angle is offset and inverted from standard, and must be adjusted to match rotation transform
+ rotation = params.rotation;
+ angle = -(gradient.angle + 270 + (rotation ? rotation.degrees : 0)) % 360;
+ // IE will flip the angle at 0 degrees...
+ if (angle === 0) {
+ angle = 180;
+ }
+ fillEl.angle = angle;
+ fillEl.type = "gradient";
+ fillEl.method = "sigma";
+ fillEl.colors = gradient.colors;
+ }
+ // Otherwise treat it as an image
+ else {
+ fillEl.src = fillUrl;
+ fillEl.type = "tile";
+ fillEl.rotate = true;
+ }
+ }
+ else {
+ fillEl.color = Ext.draw.Color.toHex(params.fill) || params.fill;
+ fillEl.src = "";
+ fillEl.type = "solid";
+ }
+ }
+ }
+ dom.appendChild(fillEl);
+ },
+
+ setStroke: function(sprite, params) {
+ var me = this,
+ el = sprite.el.dom,
+ strokeEl = sprite.strokeEl,
+ newStroke = false,
+ width, opacity;
+
+ if (!strokeEl) {
+ strokeEl = sprite.strokeEl = me.createNode("stroke");
+ newStroke = true;
+ }
+ if (Ext.isArray(params.stroke)) {
+ params.stroke = params.stroke[0];
+ }
+ if (!params.stroke || params.stroke == "none" || params.stroke == 0 || params["stroke-width"] == 0) {
+ strokeEl.on = false;
+ }
+ else {
+ strokeEl.on = true;
+ if (params.stroke && !params.stroke.match(me.fillUrlRe)) {
+ // VML does NOT support a gradient stroke :(
+ strokeEl.color = Ext.draw.Color.toHex(params.stroke);
+ }
+ strokeEl.joinstyle = params["stroke-linejoin"];
+ strokeEl.endcap = params["stroke-linecap"] || "round";
+ strokeEl.miterlimit = params["stroke-miterlimit"] || 8;
+ width = parseFloat(params["stroke-width"] || 1) * 0.75;
+ opacity = params["stroke-opacity"] || 1;
+ // VML Does not support stroke widths under 1, so we're going to fiddle with stroke-opacity instead.
+ if (Ext.isNumber(width) && width < 1) {
+ strokeEl.weight = 1;
+ strokeEl.opacity = opacity * width;
+ }
+ else {
+ strokeEl.weight = width;
+ strokeEl.opacity = opacity;
+ }
+ }
+ if (newStroke) {
+ el.appendChild(strokeEl);
+ }
+ },
+
+ setClip: function(sprite, params) {
+ var me = this,
+ el = sprite.el,
+ clipEl = sprite.clipEl,
+ rect = String(params["clip-rect"]).split(me.separatorRe);
+ if (!clipEl) {
+ clipEl = sprite.clipEl = me.el.insertFirst(Ext.getDoc().dom.createElement("div"));
+ clipEl.addCls(Ext.baseCSSPrefix + 'vml-sprite');
+ }
+ if (rect.length == 4) {
+ rect[2] = +rect[2] + (+rect[0]);
+ rect[3] = +rect[3] + (+rect[1]);
+ clipEl.setStyle("clip", Ext.String.format("rect({1}px {2}px {3}px {0}px)", rect[0], rect[1], rect[2], rect[3]));
+ clipEl.setSize(me.el.width, me.el.height);
+ }
+ else {
+ clipEl.setStyle("clip", "");
+ }
+ },
+
+ setTextAttributes: function(sprite, params) {
+ var me = this,
+ vml = sprite.vml,
+ textStyle = vml.textpath.style,
+ spanCacheStyle = me.span.style,
+ zoom = me.zoom,
+ round = Math.round,
+ fontObj = {
+ fontSize: "font-size",
+ fontWeight: "font-weight",
+ fontStyle: "font-style"
+ },
+ fontProp,
+ paramProp;
+ if (sprite.dirtyFont) {
+ if (params.font) {
+ textStyle.font = spanCacheStyle.font = params.font;
+ }
+ if (params["font-family"]) {
+ textStyle.fontFamily = '"' + params["font-family"].split(",")[0].replace(me.fontFamilyRe, "") + '"';
+ spanCacheStyle.fontFamily = params["font-family"];
+ }
+
+ for (fontProp in fontObj) {
+ paramProp = params[fontObj[fontProp]];
+ if (paramProp) {
+ textStyle[fontProp] = spanCacheStyle[fontProp] = paramProp;
+ }
+ }
+
+ me.setText(sprite, params.text);
+
+ if (vml.textpath.string) {
+ me.span.innerHTML = String(vml.textpath.string).replace(/</g, "<").replace(/&/g, "&").replace(/\n/g, "<br>");
+ }
+ vml.W = me.span.offsetWidth;
+ vml.H = me.span.offsetHeight + 2; // TODO handle baseline differences and offset in VML Textpath
+
+ // text-anchor emulation
+ if (params["text-anchor"] == "middle") {
+ textStyle["v-text-align"] = "center";
+ }
+ else if (params["text-anchor"] == "end") {
+ textStyle["v-text-align"] = "right";
+ vml.bbx = -Math.round(vml.W / 2);
+ }
+ else {
+ textStyle["v-text-align"] = "left";
+ vml.bbx = Math.round(vml.W / 2);
+ }
+ }
+ vml.X = params.x;
+ vml.Y = params.y;
+ vml.path.v = Ext.String.format("m{0},{1}l{2},{1}", Math.round(vml.X * zoom), Math.round(vml.Y * zoom), Math.round(vml.X * zoom) + 1);
+ // Clear bbox cache
+ sprite.bbox.plain = null;
+ sprite.bbox.transform = null;
+ sprite.dirtyFont = false;
+ },
+
+ setText: function(sprite, text) {
+ sprite.vml.textpath.string = Ext.htmlDecode(text);
+ },
+
+ hide: function() {
+ this.el.hide();
+ },
+
+ show: function() {
+ this.el.show();
+ },
+
+ hidePrim: function(sprite) {
+ sprite.el.addCls(Ext.baseCSSPrefix + 'hide-visibility');
+ },
+
+ showPrim: function(sprite) {
+ sprite.el.removeCls(Ext.baseCSSPrefix + 'hide-visibility');
+ },
+
+ setSize: function(width, height) {
+ var me = this;
+ width = width || me.width;
+ height = height || me.height;
+ me.width = width;
+ me.height = height;
+
+ if (me.el) {
+ // Size outer div
+ if (width != undefined) {
+ me.el.setWidth(width);
+ }
+ if (height != undefined) {
+ me.el.setHeight(height);
+ }
+
+ // Handle viewBox sizing
+ me.applyViewBox();
+
+ me.callParent(arguments);
+ }
+ },
+
+ setViewBox: function(x, y, width, height) {
+ this.callParent(arguments);
+ this.viewBox = {
+ x: x,
+ y: y,
+ width: width,
+ height: height
+ };
+ this.applyViewBox();
+ },
+
+ /**
+ * @private Using the current viewBox property and the surface's width and height, calculate the
+ * appropriate viewBoxShift that will be applied as a persistent transform to all sprites.
+ */
+ applyViewBox: function() {
+ var me = this,
+ viewBox = me.viewBox,
+ width = me.width,
+ height = me.height,
+ viewBoxX, viewBoxY, viewBoxWidth, viewBoxHeight,
+ relativeHeight, relativeWidth, size;
+
+ if (viewBox && (width || height)) {
+ viewBoxX = viewBox.x;
+ viewBoxY = viewBox.y;
+ viewBoxWidth = viewBox.width;
+ viewBoxHeight = viewBox.height;
+ relativeHeight = height / viewBoxHeight;
+ relativeWidth = width / viewBoxWidth;
+
+ if (viewBoxWidth * relativeHeight < width) {
+ viewBoxX -= (width - viewBoxWidth * relativeHeight) / 2 / relativeHeight;
+ }
+ if (viewBoxHeight * relativeWidth < height) {
+ viewBoxY -= (height - viewBoxHeight * relativeWidth) / 2 / relativeWidth;
+ }
+
+ size = 1 / Math.max(viewBoxWidth / width, viewBoxHeight / height);
+
+ me.viewBoxShift = {
+ dx: -viewBoxX,
+ dy: -viewBoxY,
+ scale: size
+ };
+ me.items.each(function(item) {
+ me.transform(item);
+ });
+ }
+ },
+
+ onAdd: function(item) {
+ this.callParent(arguments);
+ if (this.el) {
+ this.renderItem(item);
+ }
+ },
+
+ onRemove: function(sprite) {
+ if (sprite.el) {
+ sprite.el.remove();
+ delete sprite.el;
+ }
+ this.callParent(arguments);
+ },
+
+ // VML Node factory method (createNode)
+ createNode : (function () {
+ try {
+ var doc = Ext.getDoc().dom;
+ if (!doc.namespaces.rvml) {
+ doc.namespaces.add("rvml", "urn:schemas-microsoft-com:vml");
+ }
+ return function (tagName) {
+ return doc.createElement("<rvml:" + tagName + ' class="rvml">');
+ };
+ } catch (e) {
+ return function (tagName) {
+ return doc.createElement("<" + tagName + ' xmlns="urn:schemas-microsoft.com:vml" class="rvml">');
+ };
+ }
+ })(),
+
+ render: function (container) {
+ var me = this,
+ doc = Ext.getDoc().dom;
+
+ if (!me.el) {
+ var el = doc.createElement("div");
+ me.el = Ext.get(el);
+ me.el.addCls(me.baseVmlCls);
+
+ // Measuring span (offscrren)
+ me.span = doc.createElement("span");
+ Ext.get(me.span).addCls(me.measureSpanCls);
+ el.appendChild(me.span);
+ me.el.setSize(me.width || 10, me.height || 10);
+ container.appendChild(el);
+ me.el.on({
+ scope: me,
+ mouseup: me.onMouseUp,
+ mousedown: me.onMouseDown,
+ mouseover: me.onMouseOver,
+ mouseout: me.onMouseOut,
+ mousemove: me.onMouseMove,
+ mouseenter: me.onMouseEnter,
+ mouseleave: me.onMouseLeave,
+ click: me.onClick
+ });
+ }
+ me.renderAll();
+ },
+
+ renderAll: function() {
+ this.items.each(this.renderItem, this);
+ },
+
+ redraw: function(sprite) {
+ sprite.dirty = true;
+ this.renderItem(sprite);
+ },
+
+ renderItem: function (sprite) {
+ // Does the surface element exist?
+ if (!this.el) {
+ return;
+ }
+
+ // Create sprite element if necessary
+ if (!sprite.el) {
+ this.createSpriteElement(sprite);
+ }
+
+ if (sprite.dirty) {
+ this.applyAttrs(sprite);
+ if (sprite.dirtyTransform) {
+ this.applyTransformations(sprite);
+ }
+ }
+ },
+
+ rotationCompensation: function (deg, dx, dy) {
+ var matrix = Ext.create('Ext.draw.Matrix');
+ matrix.rotate(-deg, 0.5, 0.5);
+ return {
+ x: matrix.x(dx, dy),
+ y: matrix.y(dx, dy)
+ };
+ },
+
+ extractTransform: function (sprite) {
+ var me = this,
+ matrix = Ext.create('Ext.draw.Matrix'), scale,
+ transformstions, tranformationsLength,
+ transform, i = 0,
+ shift = me.viewBoxShift;
+
+ for(transformstions = sprite.transformations, tranformationsLength = transformstions.length;
+ i < tranformationsLength; i ++) {
+ transform = transformstions[i];
+ switch (transform.type) {
+ case 'translate' :
+ matrix.translate(transform.x, transform.y);
+ break;
+ case 'rotate':
+ matrix.rotate(transform.degrees, transform.x, transform.y);
+ break;
+ case 'scale':
+ matrix.scale(transform.x || transform.scale, transform.y || transform.scale, transform.centerX, transform.centerY);
+ break;
+ }
+ }
+
+ if (shift) {
+ matrix.add(1, 0, 0, 1, shift.dx, shift.dy);
+ matrix.prepend(shift.scale, 0, 0, shift.scale, 0, 0);
+ }
+
+ return sprite.matrix = matrix;
+ },
+
+ setSimpleCoords: function(sprite, sx, sy, dx, dy, rotate) {
+ var me = this,
+ matrix = sprite.matrix,
+ dom = sprite.el.dom,
+ style = dom.style,
+ yFlipper = 1,
+ flip = "",
+ fill = dom.getElementsByTagName('fill')[0],
+ kx = me.zoom / sx,
+ ky = me.zoom / sy,
+ rotationCompensation;
+ if (!sx || !sy) {
+ return;
+ }
+ dom.coordsize = Math.abs(kx) + ' ' + Math.abs(ky);
+ style.rotation = rotate * (sx * sy < 0 ? -1 : 1);
+ if (rotate) {
+ rotationCompensation = me.rotationCompensation(rotate, dx, dy);
+ dx = rotationCompensation.x;
+ dy = rotationCompensation.y;
+ }
+ if (sx < 0) {
+ flip += "x"
+ }
+ if (sy < 0) {
+ flip += " y";
+ yFlipper = -1;
+ }
+ style.flip = flip;
+ dom.coordorigin = (dx * -kx) + ' ' + (dy * -ky);
+ if (fill) {
+ dom.removeChild(fill);
+ rotationCompensation = me.rotationCompensation(rotate, matrix.x(sprite.x, sprite.y), matrix.y(sprite.x, sprite.y));
+ fill.position = rotationCompensation.x * yFlipper + ' ' + rotationCompensation.y * yFlipper;
+ fill.size = sprite.width * Math.abs(sx) + ' ' + sprite.height * Math.abs(sy);
+ dom.appendChild(fill);
+ }
+ },
+
+ transform : function (sprite) {
+ var me = this,
+ el = sprite.el,
+ skew = sprite.skew,
+ dom = el.dom,
+ domStyle = dom.style,
+ matrix = me.extractTransform(sprite).clone(),
+ split, zoom = me.zoom,
+ fill = dom.getElementsByTagName('fill')[0],
+ isPatt = !String(sprite.fill).indexOf("url("),
+ offset, c;
+
+
+ // Hide element while we transform
+
+ if (sprite.type != "image" && skew && !isPatt) {
+ // matrix transform via VML skew
+ skew.matrix = matrix.toString();
+ // skew.offset = '32767,1' OK
+ // skew.offset = '32768,1' Crash
+ // M$, R U kidding??
+ offset = matrix.offset();
+ if (offset[0] > 32767) {
+ offset[0] = 32767;
+ } else if (offset[0] < -32768) {
+ offset[0] = -32768
+ }
+ if (offset[1] > 32767) {
+ offset[1] = 32767;
+ } else if (offset[1] < -32768) {
+ offset[1] = -32768
+ }
+ skew.offset = offset;
+ } else {
+ if (skew) {
+ skew.matrix = "1 0 0 1";
+ skew.offset = "0 0";
+ }
+ split = matrix.split();
+ if (split.isSimple) {
+ domStyle.filter = '';
+ me.setSimpleCoords(sprite, split.scaleX, split.scaleY, split.translateX, split.translateY, split.rotate / Math.PI * 180);
+ } else {
+ domStyle.filter = matrix.toFilter();
+ var bb = me.getBBox(sprite),
+ dx = bb.x - sprite.x,
+ dy = bb.y - sprite.y;
+ dom.coordorigin = (dx * -zoom) + ' ' + (dy * -zoom);
+ if (fill) {
+ dom.removeChild(fill);
+ fill.position = dx + ' ' + dy;
+ fill.size = sprite.width * sprite.scale.x + ' ' + sprite.height * 1.1;
+ dom.appendChild(fill);
+ }
+ }
+ }
+ },
+
+ createItem: function (config) {
+ return Ext.create('Ext.draw.Sprite', config);
+ },
+
+ getRegion: function() {
+ return this.el.getRegion();
+ },
+
+ addCls: function(sprite, className) {
+ if (sprite && sprite.el) {
+ sprite.el.addCls(className);
+ }
+ },
+
+ removeCls: function(sprite, className) {
+ if (sprite && sprite.el) {
+ sprite.el.removeCls(className);
+ }
+ },
+
+ /**
+ * Adds a definition to this Surface for a linear gradient. We convert the gradient definition
+ * to its corresponding VML attributes and store it for later use by individual sprites.
+ * @param {Object} gradient
+ */
+ addGradient: function(gradient) {
+ var gradients = this.gradientsColl || (this.gradientsColl = Ext.create('Ext.util.MixedCollection')),
+ colors = [],
+ stops = Ext.create('Ext.util.MixedCollection');
+
+ // Build colors string
+ stops.addAll(gradient.stops);
+ stops.sortByKey("ASC", function(a, b) {
+ a = parseInt(a, 10);
+ b = parseInt(b, 10);
+ return a > b ? 1 : (a < b ? -1 : 0);
+ });
+ stops.eachKey(function(k, v) {
+ colors.push(k + "% " + v.color);
+ });
+
+ gradients.add(gradient.id, {
+ colors: colors.join(","),
+ angle: gradient.angle
+ });
+ },
+
+ destroy: function() {
+ var me = this;
+
+ me.callParent(arguments);
+ if (me.el) {
+ me.el.remove();
+ }
+ delete me.el;
+ }
+});
+
+/**
+ * @class Ext.fx.target.ElementCSS
+ * @extends Ext.fx.target.Element
+ *
+ * This class represents a animation target for an {@link Ext.Element} that supports CSS
+ * based animation. In general this class will not be created directly, the {@link Ext.Element}
+ * will be passed to the animation and the appropriate target will be created.
+ */
+Ext.define('Ext.fx.target.ElementCSS', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.fx.target.Element',
+
+ /* End Definitions */
+
+ setAttr: function(targetData, isFirstFrame) {
+ var cssArr = {
+ attrs: [],
+ duration: [],
+ easing: []
+ },
+ ln = targetData.length,
+ attributes,
+ attrs,
+ attr,
+ easing,
+ duration,
+ o,
+ i,
+ j,
+ ln2;
+ for (i = 0; i < ln; i++) {
+ attrs = targetData[i];
+ duration = attrs.duration;
+ easing = attrs.easing;
+ attrs = attrs.attrs;
+ for (attr in attrs) {
+ if (Ext.Array.indexOf(cssArr.attrs, attr) == -1) {
+ cssArr.attrs.push(attr.replace(/[A-Z]/g, function(v) {
+ return '-' + v.toLowerCase();
+ }));
+ cssArr.duration.push(duration + 'ms');
+ cssArr.easing.push(easing);
+ }
+ }
+ }
+ attributes = cssArr.attrs.join(',');
+ duration = cssArr.duration.join(',');
+ easing = cssArr.easing.join(', ');
+ for (i = 0; i < ln; i++) {
+ attrs = targetData[i].attrs;
+ for (attr in attrs) {
+ ln2 = attrs[attr].length;
+ for (j = 0; j < ln2; j++) {
+ o = attrs[attr][j];
+ o[0].setStyle(Ext.supports.CSS3Prefix + 'TransitionProperty', isFirstFrame ? '' : attributes);
+ o[0].setStyle(Ext.supports.CSS3Prefix + 'TransitionDuration', isFirstFrame ? '' : duration);
+ o[0].setStyle(Ext.supports.CSS3Prefix + 'TransitionTimingFunction', isFirstFrame ? '' : easing);
+ o[0].setStyle(attr, o[1]);
+
+ // Must trigger reflow to make this get used as the start point for the transition that follows
+ if (isFirstFrame) {
+ o = o[0].dom.offsetWidth;
+ }
+ else {
+ // Remove transition properties when completed.
+ o[0].on(Ext.supports.CSS3TransitionEnd, function() {
+ this.setStyle(Ext.supports.CSS3Prefix + 'TransitionProperty', null);
+ this.setStyle(Ext.supports.CSS3Prefix + 'TransitionDuration', null);
+ this.setStyle(Ext.supports.CSS3Prefix + 'TransitionTimingFunction', null);
+ }, o[0], { single: true });
+ }
+ }
+ }
+ }
+ }
+});
+/**
+ * @class Ext.fx.target.CompositeElementCSS
+ * @extends Ext.fx.target.CompositeElement
+ *
+ * This class represents a animation target for a {@link Ext.CompositeElement}, where the
+ * constituent elements support CSS based animation. It allows each {@link Ext.Element} in
+ * the group to be animated as a whole. In general this class will not be created directly,
+ * the {@link Ext.CompositeElement} will be passed to the animation and the appropriate target
+ * will be created.
+ */
+Ext.define('Ext.fx.target.CompositeElementCSS', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.fx.target.CompositeElement',
+
+ requires: ['Ext.fx.target.ElementCSS'],
+
+ /* End Definitions */
+ setAttr: function() {
+ return Ext.fx.target.ElementCSS.prototype.setAttr.apply(this, arguments);
+ }
+});
+/**
+ * @class Ext.layout.container.AbstractFit
+ * @extends Ext.layout.container.Container
+ * @private
+ */
+Ext.define('Ext.layout.container.AbstractFit', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.layout.container.Container',
+
+ /* End Definitions */
+
+ itemCls: Ext.baseCSSPrefix + 'fit-item',
+ targetCls: Ext.baseCSSPrefix + 'layout-fit',
+ type: 'fit'
+});
+/**
+ * This is a base class for layouts that contain **a single item** that automatically expands to fill the layout's
+ * container. This class is intended to be extended or created via the `layout: 'fit'`
+ * {@link Ext.container.Container#layout} config, and should generally not need to be created directly via the new keyword.
+ *
+ * Fit layout does not have any direct config options (other than inherited ones). To fit a panel to a container using
+ * Fit layout, simply set `layout: 'fit'` on the container and add a single panel to it. If the container has multiple
+ * panels, only the first one will be displayed.
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Fit Layout',
+ * width: 300,
+ * height: 150,
+ * layout:'fit',
+ * items: {
+ * title: 'Inner Panel',
+ * html: 'This is the inner panel content',
+ * bodyPadding: 20,
+ * border: false
+ * },
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.layout.container.Fit', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.layout.container.AbstractFit',
+ alias: 'layout.fit',
+ alternateClassName: 'Ext.layout.FitLayout',
+ requires: ['Ext.layout.container.Box'],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Object} defaultMargins
+ * <p>If the individual contained items do not have a <tt>margins</tt>
+ * property specified or margin specified via CSS, the default margins from this property will be
+ * applied to each item.</p>
+ * <br><p>This property may be specified as an object containing margins
+ * to apply in the format:</p><pre><code>
+{
+ top: (top margin),
+ right: (right margin),
+ bottom: (bottom margin),
+ left: (left margin)
+}</code></pre>
+ * <p>This property may also be specified as a string containing
+ * space-separated, numeric margin values. The order of the sides associated
+ * with each value matches the way CSS processes margin values:</p>
+ * <div class="mdetail-params"><ul>
+ * <li>If there is only one value, it applies to all sides.</li>
+ * <li>If there are two values, the top and bottom borders are set to the
+ * first value and the right and left are set to the second.</li>
+ * <li>If there are three values, the top is set to the first value, the left
+ * and right are set to the second, and the bottom is set to the third.</li>
+ * <li>If there are four values, they apply to the top, right, bottom, and
+ * left, respectively.</li>
+ * </ul></div>
+ * <p>Defaults to:</p><pre><code>
+ * {top:0, right:0, bottom:0, left:0}
+ * </code></pre>
+ */
+ defaultMargins: {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ },
+
+ // @private
+ onLayout : function() {
+ var me = this,
+ size,
+ item,
+ margins;
+ me.callParent();
+
+ if (me.owner.items.length) {
+ item = me.owner.items.get(0);
+ margins = item.margins || me.defaultMargins;
+ size = me.getLayoutTargetSize();
+ size.width -= margins.width;
+ size.height -= margins.height;
+ me.setItemBox(item, size);
+
+ // If any margins were configure either through the margins config, or in the CSS style,
+ // Then positioning will be used.
+ if (margins.left || margins.top) {
+ item.setPosition(margins.left, margins.top);
+ }
+ }
+ },
+
+ getTargetBox : function() {
+ return this.getLayoutTargetSize();
+ },
+
+ setItemBox : function(item, box) {
+ var me = this;
+ if (item && box.height > 0) {
+ if (!me.owner.isFixedWidth()) {
+ box.width = undefined;
+ }
+ if (!me.owner.isFixedHeight()) {
+ box.height = undefined;
+ }
+ me.setItemSize(item, box.width, box.height);
+ }
+ },
+
+ configureItem: function(item) {
+
+ // Card layout only controls dimensions which IT has controlled.
+ // That calculation has to be determined at run time by examining the ownerCt's isFixedWidth()/isFixedHeight() methods
+ item.layoutManagedHeight = 0;
+ item.layoutManagedWidth = 0;
+
+ this.callParent(arguments);
+ }
+}, function() {
+ // Use Box layout's renderItem which reads CSS margins, and adds them to any configured item margins
+ // (Defaulting to "0 0 0 0")
+ this.prototype.renderItem = Ext.layout.container.Box.prototype.renderItem;
+});
+/**
+ * Abstract base class for {@link Ext.layout.container.Card Card layout}.
+ * @private
+ */
+Ext.define('Ext.layout.container.AbstractCard', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.layout.container.Fit',
+
+ /* End Definitions */
+
+ type: 'card',
+
+ sizeAllCards: false,
+
+ hideInactive: true,
+
+ /**
+ * @cfg {Boolean} deferredRender
+ * True to render each contained item at the time it becomes active, false to render all contained items
+ * as soon as the layout is rendered. If there is a significant amount of content or
+ * a lot of heavy controls being rendered into panels that are not displayed by default, setting this to
+ * true might improve performance.
+ */
+ deferredRender : false,
+
+ beforeLayout: function() {
+ var me = this;
+ me.getActiveItem();
+ if (me.activeItem && me.deferredRender) {
+ me.renderItems([me.activeItem], me.getRenderTarget());
+ return true;
+ }
+ else {
+ return this.callParent(arguments);
+ }
+ },
+
+ renderChildren: function () {
+ if (!this.deferredRender) {
+ this.getActiveItem();
+ this.callParent();
+ }
+ },
+
+ onLayout: function() {
+ var me = this,
+ activeItem = me.activeItem,
+ items = me.getVisibleItems(),
+ ln = items.length,
+ targetBox = me.getTargetBox(),
+ i, item;
+
+ for (i = 0; i < ln; i++) {
+ item = items[i];
+ me.setItemBox(item, targetBox);
+ }
+
+ if (!me.firstActivated && activeItem) {
+ if (activeItem.fireEvent('beforeactivate', activeItem) !== false) {
+ activeItem.fireEvent('activate', activeItem);
+ }
+ me.firstActivated = true;
+ }
+ },
+
+ isValidParent : function(item, target, position) {
+ // Note: Card layout does not care about order within the target because only one is ever visible.
+ // We only care whether the item is a direct child of the target.
+ var itemEl = item.el ? item.el.dom : Ext.getDom(item);
+ return (itemEl && itemEl.parentNode === (target.dom || target)) || false;
+ },
+
+ /**
+ * Return the active (visible) component in the layout.
+ * @returns {Ext.Component}
+ */
+ getActiveItem: function() {
+ var me = this;
+ if (!me.activeItem && me.owner) {
+ me.activeItem = me.parseActiveItem(me.owner.activeItem);
+ }
+
+ if (me.activeItem && me.owner.items.indexOf(me.activeItem) != -1) {
+ return me.activeItem;
+ }
+
+ return null;
+ },
+
+ // @private
+ parseActiveItem: function(item) {
+ if (item && item.isComponent) {
+ return item;
+ }
+ else if (typeof item == 'number' || item === undefined) {
+ return this.getLayoutItems()[item || 0];
+ }
+ else {
+ return this.owner.getComponent(item);
+ }
+ },
+
+ // @private
+ configureItem: function(item, position) {
+ this.callParent([item, position]);
+ if (this.hideInactive && this.activeItem !== item) {
+ item.hide();
+ }
+ else {
+ item.show();
+ }
+ },
+
+ onRemove: function(component) {
+ if (component === this.activeItem) {
+ this.activeItem = null;
+ if (this.owner.items.getCount() === 0) {
+ this.firstActivated = false;
+ }
+ }
+ },
+
+ // @private
+ getAnimation: function(newCard, owner) {
+ var newAnim = (newCard || {}).cardSwitchAnimation;
+ if (newAnim === false) {
+ return false;
+ }
+ return newAnim || owner.cardSwitchAnimation;
+ },
+
+ /**
+ * Return the active (visible) component in the layout to the next card
+ * @returns {Ext.Component} The next component or false.
+ */
+ getNext: function() {
+ //NOTE: Removed the JSDoc for this function's arguments because it is not actually supported in 4.0. This
+ //should come back in 4.1
+ var wrap = arguments[0];
+ var items = this.getLayoutItems(),
+ index = Ext.Array.indexOf(items, this.activeItem);
+ return items[index + 1] || (wrap ? items[0] : false);
+ },
+
+ /**
+ * Sets the active (visible) component in the layout to the next card
+ * @return {Ext.Component} the activated component or false when nothing activated.
+ */
+ next: function() {
+ //NOTE: Removed the JSDoc for this function's arguments because it is not actually supported in 4.0. This
+ //should come back in 4.1
+ var anim = arguments[0], wrap = arguments[1];
+ return this.setActiveItem(this.getNext(wrap), anim);
+ },
+
+ /**
+ * Return the active (visible) component in the layout to the previous card
+ * @returns {Ext.Component} The previous component or false.
+ */
+ getPrev: function() {
+ //NOTE: Removed the JSDoc for this function's arguments because it is not actually supported in 4.0. This
+ //should come back in 4.1
+ var wrap = arguments[0];
+ var items = this.getLayoutItems(),
+ index = Ext.Array.indexOf(items, this.activeItem);
+ return items[index - 1] || (wrap ? items[items.length - 1] : false);
+ },
+
+ /**
+ * Sets the active (visible) component in the layout to the previous card
+ * @return {Ext.Component} the activated component or false when nothing activated.
+ */
+ prev: function() {
+ //NOTE: Removed the JSDoc for this function's arguments because it is not actually supported in 4.0. This
+ //should come back in 4.1
+ var anim = arguments[0], wrap = arguments[1];
+ return this.setActiveItem(this.getPrev(wrap), anim);
+ }
+});
+
+/**
+ * Tracks what records are currently selected in a databound component.
+ *
+ * This is an abstract class and is not meant to be directly used. Databound UI widgets such as
+ * {@link Ext.grid.Panel Grid} and {@link Ext.tree.Panel Tree} should subclass Ext.selection.Model
+ * and provide a way to binding to the component.
+ *
+ * The abstract methods `onSelectChange` and `onLastFocusChanged` should be implemented in these
+ * subclasses to update the UI widget.
+ */
+Ext.define('Ext.selection.Model', {
+ extend: 'Ext.util.Observable',
+ alternateClassName: 'Ext.AbstractSelectionModel',
+ requires: ['Ext.data.StoreManager'],
+ // lastSelected
+
+ /**
+ * @cfg {String} mode
+ * Mode of selection. Valid values are:
+ *
+ * - **SINGLE** - Only allows selecting one item at a time. Use {@link #allowDeselect} to allow
+ * deselecting that item. This is the default.
+ * - **SIMPLE** - Allows simple selection of multiple items one-by-one. Each click in grid will either
+ * select or deselect an item.
+ * - **MULTI** - Allows complex selection of multiple items using Ctrl and Shift keys.
+ */
+
+ /**
+ * @cfg {Boolean} allowDeselect
+ * Allow users to deselect a record in a DataView, List or Grid.
+ * Only applicable when the {@link #mode} is 'SINGLE'.
+ */
+ allowDeselect: false,
+
+ /**
+ * @property {Ext.util.MixedCollection} selected
+ * A MixedCollection that maintains all of the currently selected records. Read-only.
+ */
+ selected: null,
+
+ /**
+ * Prune records when they are removed from the store from the selection.
+ * This is a private flag. For an example of its usage, take a look at
+ * Ext.selection.TreeModel.
+ * @private
+ */
+ pruneRemoved: true,
+
+ constructor: function(cfg) {
+ var me = this;
+
+ cfg = cfg || {};
+ Ext.apply(me, cfg);
+
+ me.addEvents(
+ /**
+ * @event
+ * Fired after a selection change has occurred
+ * @param {Ext.selection.Model} this
+ * @param {Ext.data.Model[]} selected The selected records
+ */
+ 'selectionchange'
+ );
+
+ me.modes = {
+ SINGLE: true,
+ SIMPLE: true,
+ MULTI: true
+ };
+
+ // sets this.selectionMode
+ me.setSelectionMode(cfg.mode || me.mode);
+
+ // maintains the currently selected records.
+ me.selected = Ext.create('Ext.util.MixedCollection');
+
+ me.callParent(arguments);
+ },
+
+ // binds the store to the selModel.
+ bind : function(store, initial){
+ var me = this;
+
+ if(!initial && me.store){
+ if(store !== me.store && me.store.autoDestroy){
+ me.store.destroyStore();
+ }else{
+ me.store.un("add", me.onStoreAdd, me);
+ me.store.un("clear", me.onStoreClear, me);
+ me.store.un("remove", me.onStoreRemove, me);
+ me.store.un("update", me.onStoreUpdate, me);
+ }
+ }
+ if(store){
+ store = Ext.data.StoreManager.lookup(store);
+ store.on({
+ add: me.onStoreAdd,
+ clear: me.onStoreClear,
+ remove: me.onStoreRemove,
+ update: me.onStoreUpdate,
+ scope: me
+ });
+ }
+ me.store = store;
+ if(store && !initial) {
+ me.refresh();
+ }
+ },
+
+ /**
+ * Selects all records in the view.
+ * @param {Boolean} suppressEvent True to suppress any select events
+ */
+ selectAll: function(suppressEvent) {
+ var me = this,
+ selections = me.store.getRange(),
+ i = 0,
+ len = selections.length,
+ start = me.getSelection().length;
+
+ me.bulkChange = true;
+ for (; i < len; i++) {
+ me.doSelect(selections[i], true, suppressEvent);
+ }
+ delete me.bulkChange;
+ // fire selection change only if the number of selections differs
+ me.maybeFireSelectionChange(me.getSelection().length !== start);
+ },
+
+ /**
+ * Deselects all records in the view.
+ * @param {Boolean} suppressEvent True to suppress any deselect events
+ */
+ deselectAll: function(suppressEvent) {
+ var me = this,
+ selections = me.getSelection(),
+ i = 0,
+ len = selections.length,
+ start = me.getSelection().length;
+
+ me.bulkChange = true;
+ for (; i < len; i++) {
+ me.doDeselect(selections[i], suppressEvent);
+ }
+ delete me.bulkChange;
+ // fire selection change only if the number of selections differs
+ me.maybeFireSelectionChange(me.getSelection().length !== start);
+ },
+
+ // Provides differentiation of logic between MULTI, SIMPLE and SINGLE
+ // selection modes. Requires that an event be passed so that we can know
+ // if user held ctrl or shift.
+ selectWithEvent: function(record, e, keepExisting) {
+ var me = this;
+
+ switch (me.selectionMode) {
+ case 'MULTI':
+ if (e.ctrlKey && me.isSelected(record)) {
+ me.doDeselect(record, false);
+ } else if (e.shiftKey && me.lastFocused) {
+ me.selectRange(me.lastFocused, record, e.ctrlKey);
+ } else if (e.ctrlKey) {
+ me.doSelect(record, true, false);
+ } else if (me.isSelected(record) && !e.shiftKey && !e.ctrlKey && me.selected.getCount() > 1) {
+ me.doSelect(record, keepExisting, false);
+ } else {
+ me.doSelect(record, false);
+ }
+ break;
+ case 'SIMPLE':
+ if (me.isSelected(record)) {
+ me.doDeselect(record);
+ } else {
+ me.doSelect(record, true);
+ }
+ break;
+ case 'SINGLE':
+ // if allowDeselect is on and this record isSelected, deselect it
+ if (me.allowDeselect && me.isSelected(record)) {
+ me.doDeselect(record);
+ // select the record and do NOT maintain existing selections
+ } else {
+ me.doSelect(record, false);
+ }
+ break;
+ }
+ },
+
+ /**
+ * Selects a range of rows if the selection model {@link #isLocked is not locked}.
+ * All rows in between startRow and endRow are also selected.
+ * @param {Ext.data.Model/Number} startRow The record or index of the first row in the range
+ * @param {Ext.data.Model/Number} endRow The record or index of the last row in the range
+ * @param {Boolean} [keepExisting] True to retain existing selections
+ */
+ selectRange : function(startRow, endRow, keepExisting, dir){
+ var me = this,
+ store = me.store,
+ selectedCount = 0,
+ i,
+ tmp,
+ dontDeselect,
+ records = [];
+
+ if (me.isLocked()){
+ return;
+ }
+
+ if (!keepExisting) {
+ me.deselectAll(true);
+ }
+
+ if (!Ext.isNumber(startRow)) {
+ startRow = store.indexOf(startRow);
+ }
+ if (!Ext.isNumber(endRow)) {
+ endRow = store.indexOf(endRow);
+ }
+
+ // swap values
+ if (startRow > endRow){
+ tmp = endRow;
+ endRow = startRow;
+ startRow = tmp;
+ }
+
+ for (i = startRow; i <= endRow; i++) {
+ if (me.isSelected(store.getAt(i))) {
+ selectedCount++;
+ }
+ }
+
+ if (!dir) {
+ dontDeselect = -1;
+ } else {
+ dontDeselect = (dir == 'up') ? startRow : endRow;
+ }
+
+ for (i = startRow; i <= endRow; i++){
+ if (selectedCount == (endRow - startRow + 1)) {
+ if (i != dontDeselect) {
+ me.doDeselect(i, true);
+ }
+ } else {
+ records.push(store.getAt(i));
+ }
+ }
+ me.doMultiSelect(records, true);
+ },
+
+ /**
+ * Selects a record instance by record instance or index.
+ * @param {Ext.data.Model[]/Number} records An array of records or an index
+ * @param {Boolean} [keepExisting] True to retain existing selections
+ * @param {Boolean} [suppressEvent] Set to true to not fire a select event
+ */
+ select: function(records, keepExisting, suppressEvent) {
+ // Automatically selecting eg store.first() or store.last() will pass undefined, so that must just return;
+ if (Ext.isDefined(records)) {
+ this.doSelect(records, keepExisting, suppressEvent);
+ }
+ },
+
+ /**
+ * Deselects a record instance by record instance or index.
+ * @param {Ext.data.Model[]/Number} records An array of records or an index
+ * @param {Boolean} [suppressEvent] Set to true to not fire a deselect event
+ */
+ deselect: function(records, suppressEvent) {
+ this.doDeselect(records, suppressEvent);
+ },
+
+ doSelect: function(records, keepExisting, suppressEvent) {
+ var me = this,
+ record;
+
+ if (me.locked) {
+ return;
+ }
+ if (typeof records === "number") {
+ records = [me.store.getAt(records)];
+ }
+ if (me.selectionMode == "SINGLE" && records) {
+ record = records.length ? records[0] : records;
+ me.doSingleSelect(record, suppressEvent);
+ } else {
+ me.doMultiSelect(records, keepExisting, suppressEvent);
+ }
+ },
+
+ doMultiSelect: function(records, keepExisting, suppressEvent) {
+ var me = this,
+ selected = me.selected,
+ change = false,
+ i = 0,
+ len, record;
+
+ if (me.locked) {
+ return;
+ }
+
+
+ records = !Ext.isArray(records) ? [records] : records;
+ len = records.length;
+ if (!keepExisting && selected.getCount() > 0) {
+ if (me.doDeselect(me.getSelection(), suppressEvent) === false) {
+ return;
+ }
+ // TODO - coalesce the selectionchange event in deselect w/the one below...
+ }
+
+ function commit () {
+ selected.add(record);
+ change = true;
+ }
+
+ for (; i < len; i++) {
+ record = records[i];
+ if (keepExisting && me.isSelected(record)) {
+ continue;
+ }
+ me.lastSelected = record;
+
+ me.onSelectChange(record, true, suppressEvent, commit);
+ }
+ me.setLastFocused(record, suppressEvent);
+ // fire selchange if there was a change and there is no suppressEvent flag
+ me.maybeFireSelectionChange(change && !suppressEvent);
+ },
+
+ // records can be an index, a record or an array of records
+ doDeselect: function(records, suppressEvent) {
+ var me = this,
+ selected = me.selected,
+ i = 0,
+ len, record,
+ attempted = 0,
+ accepted = 0;
+
+ if (me.locked) {
+ return false;
+ }
+
+ if (typeof records === "number") {
+ records = [me.store.getAt(records)];
+ } else if (!Ext.isArray(records)) {
+ records = [records];
+ }
+
+ function commit () {
+ ++accepted;
+ selected.remove(record);
+ }
+
+ len = records.length;
+
+ for (; i < len; i++) {
+ record = records[i];
+ if (me.isSelected(record)) {
+ if (me.lastSelected == record) {
+ me.lastSelected = selected.last();
+ }
+ ++attempted;
+ me.onSelectChange(record, false, suppressEvent, commit);
+ }
+ }
+
+ // fire selchange if there was a change and there is no suppressEvent flag
+ me.maybeFireSelectionChange(accepted > 0 && !suppressEvent);
+ return accepted === attempted;
+ },
+
+ doSingleSelect: function(record, suppressEvent) {
+ var me = this,
+ changed = false,
+ selected = me.selected;
+
+ if (me.locked) {
+ return;
+ }
+ // already selected.
+ // should we also check beforeselect?
+ if (me.isSelected(record)) {
+ return;
+ }
+
+ function commit () {
+ me.bulkChange = true;
+ if (selected.getCount() > 0 && me.doDeselect(me.lastSelected, suppressEvent) === false) {
+ delete me.bulkChange;
+ return false;
+ }
+ delete me.bulkChange;
+
+ selected.add(record);
+ me.lastSelected = record;
+ changed = true;
+ }
+
+ me.onSelectChange(record, true, suppressEvent, commit);
+
+ if (changed) {
+ if (!suppressEvent) {
+ me.setLastFocused(record);
+ }
+ me.maybeFireSelectionChange(!suppressEvent);
+ }
+ },
+
+ /**
+ * Sets a record as the last focused record. This does NOT mean
+ * that the record has been selected.
+ * @param {Ext.data.Model} record
+ */
+ setLastFocused: function(record, supressFocus) {
+ var me = this,
+ recordBeforeLast = me.lastFocused;
+ me.lastFocused = record;
+ me.onLastFocusChanged(recordBeforeLast, record, supressFocus);
+ },
+
+ /**
+ * Determines if this record is currently focused.
+ * @param {Ext.data.Model} record
+ */
+ isFocused: function(record) {
+ return record === this.getLastFocused();
+ },
+
+
+ // fire selection change as long as true is not passed
+ // into maybeFireSelectionChange
+ maybeFireSelectionChange: function(fireEvent) {
+ var me = this;
+ if (fireEvent && !me.bulkChange) {
+ me.fireEvent('selectionchange', me, me.getSelection());
+ }
+ },
+
+ /**
+ * Returns the last selected record.
+ */
+ getLastSelected: function() {
+ return this.lastSelected;
+ },
+
+ getLastFocused: function() {
+ return this.lastFocused;
+ },
+
+ /**
+ * Returns an array of the currently selected records.
+ * @return {Ext.data.Model[]} The selected records
+ */
+ getSelection: function() {
+ return this.selected.getRange();
+ },
+
+ /**
+ * Returns the current selectionMode.
+ * @return {String} The selectionMode: 'SINGLE', 'MULTI' or 'SIMPLE'.
+ */
+ getSelectionMode: function() {
+ return this.selectionMode;
+ },
+
+ /**
+ * Sets the current selectionMode.
+ * @param {String} selModel 'SINGLE', 'MULTI' or 'SIMPLE'.
+ */
+ setSelectionMode: function(selMode) {
+ selMode = selMode ? selMode.toUpperCase() : 'SINGLE';
+ // set to mode specified unless it doesnt exist, in that case
+ // use single.
+ this.selectionMode = this.modes[selMode] ? selMode : 'SINGLE';
+ },
+
+ /**
+ * Returns true if the selections are locked.
+ * @return {Boolean}
+ */
+ isLocked: function() {
+ return this.locked;
+ },
+
+ /**
+ * Locks the current selection and disables any changes from happening to the selection.
+ * @param {Boolean} locked True to lock, false to unlock.
+ */
+ setLocked: function(locked) {
+ this.locked = !!locked;
+ },
+
+ /**
+ * Returns true if the specified row is selected.
+ * @param {Ext.data.Model/Number} record The record or index of the record to check
+ * @return {Boolean}
+ */
+ isSelected: function(record) {
+ record = Ext.isNumber(record) ? this.store.getAt(record) : record;
+ return this.selected.indexOf(record) !== -1;
+ },
+
+ /**
+ * Returns true if there are any a selected records.
+ * @return {Boolean}
+ */
+ hasSelection: function() {
+ return this.selected.getCount() > 0;
+ },
+
+ refresh: function() {
+ var me = this,
+ toBeSelected = [],
+ oldSelections = me.getSelection(),
+ len = oldSelections.length,
+ selection,
+ change,
+ i = 0,
+ lastFocused = this.getLastFocused();
+
+ // check to make sure that there are no records
+ // missing after the refresh was triggered, prune
+ // them from what is to be selected if so
+ for (; i < len; i++) {
+ selection = oldSelections[i];
+ if (!this.pruneRemoved || me.store.indexOf(selection) !== -1) {
+ toBeSelected.push(selection);
+ }
+ }
+
+ // there was a change from the old selected and
+ // the new selection
+ if (me.selected.getCount() != toBeSelected.length) {
+ change = true;
+ }
+
+ me.clearSelections();
+
+ if (me.store.indexOf(lastFocused) !== -1) {
+ // restore the last focus but supress restoring focus
+ this.setLastFocused(lastFocused, true);
+ }
+
+ if (toBeSelected.length) {
+ // perform the selection again
+ me.doSelect(toBeSelected, false, true);
+ }
+
+ me.maybeFireSelectionChange(change);
+ },
+
+ /**
+ * A fast reset of the selections without firing events, updating the ui, etc.
+ * For private usage only.
+ * @private
+ */
+ clearSelections: function() {
+ // reset the entire selection to nothing
+ this.selected.clear();
+ this.lastSelected = null;
+ this.setLastFocused(null);
+ },
+
+ // when a record is added to a store
+ onStoreAdd: function() {
+
+ },
+
+ // when a store is cleared remove all selections
+ // (if there were any)
+ onStoreClear: function() {
+ if (this.selected.getCount > 0) {
+ this.clearSelections();
+ this.maybeFireSelectionChange(true);
+ }
+ },
+
+ // prune records from the SelectionModel if
+ // they were selected at the time they were
+ // removed.
+ onStoreRemove: function(store, record) {
+ var me = this,
+ selected = me.selected;
+
+ if (me.locked || !me.pruneRemoved) {
+ return;
+ }
+
+ if (selected.remove(record)) {
+ if (me.lastSelected == record) {
+ me.lastSelected = null;
+ }
+ if (me.getLastFocused() == record) {
+ me.setLastFocused(null);
+ }
+ me.maybeFireSelectionChange(true);
+ }
+ },
+
+ /**
+ * Returns the count of selected records.
+ * @return {Number} The number of selected records
+ */
+ getCount: function() {
+ return this.selected.getCount();
+ },
+
+ // cleanup.
+ destroy: function() {
+
+ },
+
+ // if records are updated
+ onStoreUpdate: function() {
+
+ },
+
+ // @abstract
+ onSelectChange: function(record, isSelected, suppressEvent) {
+
+ },
+
+ // @abstract
+ onLastFocusChanged: function(oldFocused, newFocused) {
+
+ },
+
+ // @abstract
+ onEditorKey: function(field, e) {
+
+ },
+
+ // @abstract
+ bindComponent: function(cmp) {
+
+ }
+});
+/**
+ * @class Ext.selection.DataViewModel
+ * @ignore
+ */
+Ext.define('Ext.selection.DataViewModel', {
+ extend: 'Ext.selection.Model',
+
+ requires: ['Ext.util.KeyNav'],
+
+ deselectOnContainerClick: true,
+
+ /**
+ * @cfg {Boolean} enableKeyNav
+ *
+ * Turns on/off keyboard navigation within the DataView.
+ */
+ enableKeyNav: true,
+
+ constructor: function(cfg){
+ this.addEvents(
+ /**
+ * @event beforedeselect
+ * Fired before a record is deselected. If any listener returns false, the
+ * deselection is cancelled.
+ * @param {Ext.selection.DataViewModel} this
+ * @param {Ext.data.Model} record The deselected record
+ */
+ 'beforedeselect',
+
+ /**
+ * @event beforeselect
+ * Fired before a record is selected. If any listener returns false, the
+ * selection is cancelled.
+ * @param {Ext.selection.DataViewModel} this
+ * @param {Ext.data.Model} record The selected record
+ */
+ 'beforeselect',
+
+ /**
+ * @event deselect
+ * Fired after a record is deselected
+ * @param {Ext.selection.DataViewModel} this
+ * @param {Ext.data.Model} record The deselected record
+ */
+ 'deselect',
+
+ /**
+ * @event select
+ * Fired after a record is selected
+ * @param {Ext.selection.DataViewModel} this
+ * @param {Ext.data.Model} record The selected record
+ */
+ 'select'
+ );
+ this.callParent(arguments);
+ },
+
+ bindComponent: function(view) {
+ var me = this,
+ eventListeners = {
+ refresh: me.refresh,
+ scope: me
+ };
+
+ me.view = view;
+ me.bind(view.getStore());
+
+ view.on(view.triggerEvent, me.onItemClick, me);
+ view.on(view.triggerCtEvent, me.onContainerClick, me);
+
+ view.on(eventListeners);
+
+ if (me.enableKeyNav) {
+ me.initKeyNav(view);
+ }
+ },
+
+ onItemClick: function(view, record, item, index, e) {
+ this.selectWithEvent(record, e);
+ },
+
+ onContainerClick: function() {
+ if (this.deselectOnContainerClick) {
+ this.deselectAll();
+ }
+ },
+
+ initKeyNav: function(view) {
+ var me = this;
+
+ if (!view.rendered) {
+ view.on('render', Ext.Function.bind(me.initKeyNav, me, [view], 0), me, {single: true});
+ return;
+ }
+
+ view.el.set({
+ tabIndex: -1
+ });
+ me.keyNav = Ext.create('Ext.util.KeyNav', view.el, {
+ down: Ext.pass(me.onNavKey, [1], me),
+ right: Ext.pass(me.onNavKey, [1], me),
+ left: Ext.pass(me.onNavKey, [-1], me),
+ up: Ext.pass(me.onNavKey, [-1], me),
+ scope: me
+ });
+ },
+
+ onNavKey: function(step) {
+ step = step || 1;
+ var me = this,
+ view = me.view,
+ selected = me.getSelection()[0],
+ numRecords = me.view.store.getCount(),
+ idx;
+
+ if (selected) {
+ idx = view.indexOf(view.getNode(selected)) + step;
+ } else {
+ idx = 0;
+ }
+
+ if (idx < 0) {
+ idx = numRecords - 1;
+ } else if (idx >= numRecords) {
+ idx = 0;
+ }
+
+ me.select(idx);
+ },
+
+ // Allow the DataView to update the ui
+ onSelectChange: function(record, isSelected, suppressEvent, commitFn) {
+ var me = this,
+ view = me.view,
+ eventName = isSelected ? 'select' : 'deselect';
+
+ if ((suppressEvent || me.fireEvent('before' + eventName, me, record)) !== false &&
+ commitFn() !== false) {
+
+ if (isSelected) {
+ view.onItemSelect(record);
+ } else {
+ view.onItemDeselect(record);
+ }
+
+ if (!suppressEvent) {
+ me.fireEvent(eventName, me, record);
+ }
+ }
+ },
+
+ destroy: function(){
+ Ext.destroy(this.keyNav);
+ this.callParent();
+ }
+});
+
+/**
+ * A Provider implementation which saves and retrieves state via cookies. The CookieProvider supports the usual cookie
+ * options, such as:
+ *
+ * - {@link #path}
+ * - {@link #expires}
+ * - {@link #domain}
+ * - {@link #secure}
+ *
+ * Example:
+ *
+ * Ext.create('Ext.state.CookieProvider', {
+ * path: "/cgi-bin/",
+ * expires: new Date(new Date().getTime()+(1000*60*60*24*30)), //30 days
+ * domain: "sencha.com"
+ * });
+ *
+ * Ext.state.Manager.setProvider(cp);
+ *
+ * @constructor
+ * Creates a new CookieProvider.
+ * @param {Object} config (optional) Config object.
+ * @return {Object}
+ */
+Ext.define('Ext.state.CookieProvider', {
+ extend: 'Ext.state.Provider',
+
+ /**
+ * @cfg {String} path
+ * The path for which the cookie is active. Defaults to root '/' which makes it active for all pages in the site.
+ */
+
+ /**
+ * @cfg {Date} expires
+ * The cookie expiration date. Defaults to 7 days from now.
+ */
+
+ /**
+ * @cfg {String} domain
+ * The domain to save the cookie for. Note that you cannot specify a different domain than your page is on, but you can
+ * specify a sub-domain, or simply the domain itself like 'sencha.com' to include all sub-domains if you need to access
+ * cookies across different sub-domains. Defaults to null which uses the same domain the page is running on including
+ * the 'www' like 'www.sencha.com'.
+ */
+
+ /**
+ * @cfg {Boolean} [secure=false]
+ * True if the site is using SSL
+ */
+
+ /**
+ * Creates a new CookieProvider.
+ * @param {Object} [config] Config object.
+ */
+ constructor : function(config){
+ var me = this;
+ me.path = "/";
+ me.expires = new Date(new Date().getTime()+(1000*60*60*24*7)); //7 days
+ me.domain = null;
+ me.secure = false;
+ me.callParent(arguments);
+ me.state = me.readCookies();
+ },
+
+ // private
+ set : function(name, value){
+ var me = this;
+
+ if(typeof value == "undefined" || value === null){
+ me.clear(name);
+ return;
+ }
+ me.setCookie(name, value);
+ me.callParent(arguments);
+ },
+
+ // private
+ clear : function(name){
+ this.clearCookie(name);
+ this.callParent(arguments);
+ },
+
+ // private
+ readCookies : function(){
+ var cookies = {},
+ c = document.cookie + ";",
+ re = /\s?(.*?)=(.*?);/g,
+ prefix = this.prefix,
+ len = prefix.length,
+ matches,
+ name,
+ value;
+
+ while((matches = re.exec(c)) != null){
+ name = matches[1];
+ value = matches[2];
+ if (name && name.substring(0, len) == prefix){
+ cookies[name.substr(len)] = this.decodeValue(value);
+ }
+ }
+ return cookies;
+ },
+
+ // private
+ setCookie : function(name, value){
+ var me = this;
+
+ document.cookie = me.prefix + name + "=" + me.encodeValue(value) +
+ ((me.expires == null) ? "" : ("; expires=" + me.expires.toGMTString())) +
+ ((me.path == null) ? "" : ("; path=" + me.path)) +
+ ((me.domain == null) ? "" : ("; domain=" + me.domain)) +
+ ((me.secure == true) ? "; secure" : "");
+ },
+
+ // private
+ clearCookie : function(name){
+ var me = this;
+
+ document.cookie = me.prefix + name + "=null; expires=Thu, 01-Jan-70 00:00:01 GMT" +
+ ((me.path == null) ? "" : ("; path=" + me.path)) +
+ ((me.domain == null) ? "" : ("; domain=" + me.domain)) +
+ ((me.secure == true) ? "; secure" : "");
+ }
+});
+
+/**
+ * @class Ext.state.LocalStorageProvider
+ * @extends Ext.state.Provider
+ * A Provider implementation which saves and retrieves state via the HTML5 localStorage object.
+ * If the browser does not support local storage, an exception will be thrown upon instantiating
+ * this class.
+ */
+
+Ext.define('Ext.state.LocalStorageProvider', {
+ /* Begin Definitions */
+
+ extend: 'Ext.state.Provider',
+
+ alias: 'state.localstorage',
+
+ /* End Definitions */
+
+ constructor: function(){
+ var me = this;
+ me.callParent(arguments);
+ me.store = me.getStorageObject();
+ me.state = me.readLocalStorage();
+ },
+
+ readLocalStorage: function(){
+ var store = this.store,
+ i = 0,
+ len = store.length,
+ prefix = this.prefix,
+ prefixLen = prefix.length,
+ data = {},
+ key;
+
+ for (; i < len; ++i) {
+ key = store.key(i);
+ if (key.substring(0, prefixLen) == prefix) {
+ data[key.substr(prefixLen)] = this.decodeValue(store.getItem(key));
+ }
+ }
+ return data;
+ },
+
+ set : function(name, value){
+ var me = this;
+
+ me.clear(name);
+ if (typeof value == "undefined" || value === null) {
+ return;
+ }
+ me.store.setItem(me.prefix + name, me.encodeValue(value));
+ me.callParent(arguments);
+ },
+
+ // private
+ clear : function(name){
+ this.store.removeItem(this.prefix + name);
+ this.callParent(arguments);
+ },
+
+ getStorageObject: function(){
+ try {
+ var supports = 'localStorage' in window && window['localStorage'] !== null;
+ if (supports) {
+ return window.localStorage;
+ }
+ } catch (e) {
+ return false;
+ }
+ }
+});
+
+/**
+ * Represents a 2D point with x and y properties, useful for comparison and instantiation
+ * from an event:
+ *
+ * var point = Ext.util.Point.fromEvent(e);
+ *
+ */
+Ext.define('Ext.util.Point', {
+
+ /* Begin Definitions */
+ extend: 'Ext.util.Region',
+
+ statics: {
+
+ /**
+ * Returns a new instance of Ext.util.Point base on the pageX / pageY values of the given event
+ * @static
+ * @param {Event} e The event
+ * @return {Ext.util.Point}
+ */
+ fromEvent: function(e) {
+ e = (e.changedTouches && e.changedTouches.length > 0) ? e.changedTouches[0] : e;
+ return new this(e.pageX, e.pageY);
+ }
+ },
+
+ /* End Definitions */
+
+ /**
+ * Creates a point from two coordinates.
+ * @param {Number} x X coordinate.
+ * @param {Number} y Y coordinate.
+ */
+ constructor: function(x, y) {
+ this.callParent([y, x, y, x]);
+ },
+
+ /**
+ * Returns a human-eye-friendly string that represents this point,
+ * useful for debugging
+ * @return {String}
+ */
+ toString: function() {
+ return "Point[" + this.x + "," + this.y + "]";
+ },
+
+ /**
+ * Compare this point and another point
+ * @param {Ext.util.Point/Object} The point to compare with, either an instance
+ * of Ext.util.Point or an object with left and top properties
+ * @return {Boolean} Returns whether they are equivalent
+ */
+ equals: function(p) {
+ return (this.x == p.x && this.y == p.y);
+ },
+
+ /**
+ * Whether the given point is not away from this point within the given threshold amount.
+ * @param {Ext.util.Point/Object} p The point to check with, either an instance
+ * of Ext.util.Point or an object with left and top properties
+ * @param {Object/Number} threshold Can be either an object with x and y properties or a number
+ * @return {Boolean}
+ */
+ isWithin: function(p, threshold) {
+ if (!Ext.isObject(threshold)) {
+ threshold = {
+ x: threshold,
+ y: threshold
+ };
+ }
+
+ return (this.x <= p.x + threshold.x && this.x >= p.x - threshold.x &&
+ this.y <= p.y + threshold.y && this.y >= p.y - threshold.y);
+ },
+
+ /**
+ * Compare this point with another point when the x and y values of both points are rounded. E.g:
+ * [100.3,199.8] will equals to [100, 200]
+ * @param {Ext.util.Point/Object} p The point to compare with, either an instance
+ * of Ext.util.Point or an object with x and y properties
+ * @return {Boolean}
+ */
+ roundedEquals: function(p) {
+ return (Math.round(this.x) == Math.round(p.x) && Math.round(this.y) == Math.round(p.y));
+ }
+}, function() {
+ /**
+ * @method
+ * Alias for {@link #translateBy}
+ * @alias Ext.util.Region#translateBy
+ */
+ this.prototype.translate = Ext.util.Region.prototype.translateBy;
+});
+
+/**
+ * @class Ext.LoadMask
+ * <p>A modal, floating Component which may be shown above a specified {@link Ext.core.Element Element}, or a specified
+ * {@link Ext.Component Component} while loading data. When shown, the configured owning Element or Component will
+ * be covered with a modality mask, and the LoadMask's {@link #msg} will be displayed centered, accompanied by a spinner image.</p>
+ * <p>If the {@link #store} config option is specified, the masking will be automatically shown and then hidden synchronized with
+ * the Store's loading process.</p>
+ * <p>Because this is a floating Component, its z-index will be managed by the global {@link Ext.WindowManager ZIndexManager}
+ * object, and upon show, it will place itsef at the top of the hierarchy.</p>
+ * <p>Example usage:</p>
+ * <pre><code>
+// Basic mask:
+var myMask = new Ext.LoadMask(Ext.getBody(), {msg:"Please wait..."});
+myMask.show();
+</code></pre>
+
+ */
+
+Ext.define('Ext.LoadMask', {
+
+ extend: 'Ext.Component',
+
+ alias: 'widget.loadmask',
+
+ /* Begin Definitions */
+
+ mixins: {
+ floating: 'Ext.util.Floating'
+ },
+
+ uses: ['Ext.data.StoreManager'],
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Ext.data.Store} store
+ * Optional Store to which the mask is bound. The mask is displayed when a load request is issued, and
+ * hidden on either load success, or load fail.
+ */
+
+ /**
+ * @cfg {String} msg
+ * The text to display in a centered loading message box.
+ */
+ msg : 'Loading...',
+ /**
+ * @cfg {String} [msgCls="x-mask-loading"]
+ * The CSS class to apply to the loading message element.
+ */
+ msgCls : Ext.baseCSSPrefix + 'mask-loading',
+
+ /**
+ * @cfg {Boolean} useMsg
+ * Whether or not to use a loading message class or simply mask the bound element.
+ */
+ useMsg: true,
+
+ /**
+ * Read-only. True if the mask is currently disabled so that it will not be displayed
+ * @type Boolean
+ */
+ disabled: false,
+
+ baseCls: Ext.baseCSSPrefix + 'mask-msg',
+
+ renderTpl: '<div style="position:relative" class="{msgCls}"></div>',
+
+ // Private. The whole point is that there's a mask.
+ modal: true,
+
+ // Private. Obviously, it's floating.
+ floating: {
+ shadow: 'frame'
+ },
+
+ // Private. Masks are not focusable
+ focusOnToFront: false,
+
+ /**
+ * Creates new LoadMask.
+ * @param {String/HTMLElement/Ext.Element} el The element, element ID, or DOM node you wish to mask.
+ * <p>Also, may be a {@link Ext.Component Component} who's element you wish to mask. If a Component is specified, then
+ * the mask will be automatically sized upon Component resize, the message box will be kept centered,
+ * and the mask only be visible when the Component is.</p>
+ * @param {Object} [config] The config object
+ */
+ constructor : function(el, config) {
+ var me = this;
+
+ // If a Component passed, bind to it.
+ if (el.isComponent) {
+ me.ownerCt = el;
+ me.bindComponent(el);
+ }
+ // Create a dumy Component encapsulating the specified Element
+ else {
+ me.ownerCt = new Ext.Component({
+ el: Ext.get(el),
+ rendered: true,
+ componentLayoutCounter: 1
+ });
+ me.container = el;
+ }
+ me.callParent([config]);
+
+ if (me.store) {
+ me.bindStore(me.store, true);
+ }
+ me.renderData = {
+ msgCls: me.msgCls
+ };
+ me.renderSelectors = {
+ msgEl: 'div'
+ };
+ },
+
+ bindComponent: function(comp) {
+ this.mon(comp, {
+ resize: this.onComponentResize,
+ scope: this
+ });
+ },
+
+ afterRender: function() {
+ this.callParent(arguments);
+ this.container = this.floatParent.getContentTarget();
+ },
+
+ /**
+ * @private
+ * Called when this LoadMask's Component is resized. The toFront method rebases and resizes the modal mask.
+ */
+ onComponentResize: function() {
+ var me = this;
+ if (me.rendered && me.isVisible()) {
+ me.toFront();
+ me.center();
+ }
+ },
+
+ /**
+ * Changes the data store bound to this LoadMask.
+ * @param {Ext.data.Store} store The store to bind to this LoadMask
+ */
+ bindStore : function(store, initial) {
+ var me = this;
+
+ if (!initial && me.store) {
+ me.mun(me.store, {
+ scope: me,
+ beforeload: me.onBeforeLoad,
+ load: me.onLoad,
+ exception: me.onLoad
+ });
+ if (!store) {
+ me.store = null;
+ }
+ }
+ if (store) {
+ store = Ext.data.StoreManager.lookup(store);
+ me.mon(store, {
+ scope: me,
+ beforeload: me.onBeforeLoad,
+ load: me.onLoad,
+ exception: me.onLoad
+ });
+
+ }
+ me.store = store;
+ if (store && store.isLoading()) {
+ me.onBeforeLoad();
+ }
+ },
+
+ onDisable : function() {
+ this.callParent(arguments);
+ if (this.loading) {
+ this.onLoad();
+ }
+ },
+
+ // private
+ onBeforeLoad : function() {
+ var me = this,
+ owner = me.ownerCt || me.floatParent,
+ origin;
+ if (!this.disabled) {
+ // If the owning Component has not been layed out, defer so that the ZIndexManager
+ // gets to read its layed out size when sizing the modal mask
+ if (owner.componentLayoutCounter) {
+ Ext.Component.prototype.show.call(me);
+ } else {
+ // The code below is a 'run-once' interceptor.
+ origin = owner.afterComponentLayout;
+ owner.afterComponentLayout = function() {
+ owner.afterComponentLayout = origin;
+ origin.apply(owner, arguments);
+ if(me.loading) {
+ Ext.Component.prototype.show.call(me);
+ }
+ };
+ }
+ }
+ },
+
+ onHide: function(){
+ var me = this;
+ me.callParent(arguments);
+ me.showOnParentShow = true;
+ },
+
+ onShow: function() {
+ var me = this,
+ msgEl = me.msgEl;
+
+ me.callParent(arguments);
+ me.loading = true;
+ if (me.useMsg) {
+ msgEl.show().update(me.msg);
+ } else {
+ msgEl.parent().hide();
+ }
+ },
+
+ afterShow: function() {
+ this.callParent(arguments);
+ this.center();
+ },
+
+ // private
+ onLoad : function() {
+ this.loading = false;
+ Ext.Component.prototype.hide.call(this);
+ }
+});
+/**
+ * @class Ext.view.AbstractView
+ * @extends Ext.Component
+ * This is an abstract superclass and should not be used directly. Please see {@link Ext.view.View}.
+ * @private
+ */
+Ext.define('Ext.view.AbstractView', {
+ extend: 'Ext.Component',
+ alternateClassName: 'Ext.view.AbstractView',
+ requires: [
+ 'Ext.LoadMask',
+ 'Ext.data.StoreManager',
+ 'Ext.CompositeElementLite',
+ 'Ext.DomQuery',
+ 'Ext.selection.DataViewModel'
+ ],
+
+ inheritableStatics: {
+ getRecord: function(node) {
+ return this.getBoundView(node).getRecord(node);
+ },
+
+ getBoundView: function(node) {
+ return Ext.getCmp(node.boundView);
+ }
+ },
+
+ /**
+ * @cfg {String/String[]/Ext.XTemplate} tpl (required)
+ * The HTML fragment or an array of fragments that will make up the template used by this DataView. This should
+ * be specified in the same format expected by the constructor of {@link Ext.XTemplate}.
+ */
+ /**
+ * @cfg {Ext.data.Store} store (required)
+ * The {@link Ext.data.Store} to bind this DataView to.
+ */
+
+ /**
+ * @cfg {Boolean} deferInitialRefresh
+ * <p>Defaults to <code>true</code> to defer the initial refresh of the view.</p>
+ * <p>This allows the View to execute its render and initial layout more quickly because the process will not be encumbered
+ * by the expensive update of the view structure.</p>
+ * <p><b>Important: </b>Be aware that this will mean that the View's item elements will not be available immediately upon render, so
+ * <i>selection</i> may not take place at render time. To access a View's item elements as soon as possible, use the {@link #viewready} event.
+ * Or set <code>deferInitialrefresh</code> to false, but this will be at the cost of slower rendering.</p>
+ */
+ deferInitialRefresh: true,
+
+ /**
+ * @cfg {String} itemSelector (required)
+ * <b>This is a required setting</b>. A simple CSS selector (e.g. <tt>div.some-class</tt> or
+ * <tt>span:first-child</tt>) that will be used to determine what nodes this DataView will be
+ * working with. The itemSelector is used to map DOM nodes to records. As such, there should
+ * only be one root level element that matches the selector for each record.
+ */
+
+ /**
+ * @cfg {String} itemCls
+ * Specifies the class to be assigned to each element in the view when used in conjunction with the
+ * {@link #itemTpl} configuration.
+ */
+ itemCls: Ext.baseCSSPrefix + 'dataview-item',
+
+ /**
+ * @cfg {String/String[]/Ext.XTemplate} itemTpl
+ * The inner portion of the item template to be rendered. Follows an XTemplate
+ * structure and will be placed inside of a tpl.
+ */
+
+ /**
+ * @cfg {String} overItemCls
+ * A CSS class to apply to each item in the view on mouseover.
+ * Ensure {@link #trackOver} is set to `true` to make use of this.
+ */
+
+ /**
+ * @cfg {String} loadingText
+ * A string to display during data load operations. If specified, this text will be
+ * displayed in a loading div and the view's contents will be cleared while loading, otherwise the view's
+ * contents will continue to display normally until the new data is loaded and the contents are replaced.
+ */
+ loadingText: 'Loading...',
+
+ /**
+ * @cfg {Boolean/Object} loadMask
+ * False to disable a load mask from displaying will the view is loading. This can also be a
+ * {@link Ext.LoadMask} configuration object.
+ */
+ loadMask: true,
+
+ /**
+ * @cfg {String} loadingCls
+ * The CSS class to apply to the loading message element. Defaults to Ext.LoadMask.prototype.msgCls "x-mask-loading".
+ */
+
+ /**
+ * @cfg {Boolean} loadingUseMsg
+ * Whether or not to use the loading message.
+ * @private
+ */
+ loadingUseMsg: true,
+
+
+ /**
+ * @cfg {Number} loadingHeight
+ * If specified, gives an explicit height for the data view when it is showing the {@link #loadingText},
+ * if that is specified. This is useful to prevent the view's height from collapsing to zero when the
+ * loading mask is applied and there are no other contents in the data view.
+ */
+
+ /**
+ * @cfg {String} [selectedItemCls='x-view-selected']
+ * A CSS class to apply to each selected item in the view.
+ */
+ selectedItemCls: Ext.baseCSSPrefix + 'item-selected',
+
+ /**
+ * @cfg {String} emptyText
+ * The text to display in the view when there is no data to display.
+ * Note that when using local data the emptyText will not be displayed unless you set
+ * the {@link #deferEmptyText} option to false.
+ */
+ emptyText: "",
+
+ /**
+ * @cfg {Boolean} deferEmptyText
+ * True to defer emptyText being applied until the store's first load.
+ */
+ deferEmptyText: true,
+
+ /**
+ * @cfg {Boolean} trackOver
+ * True to enable mouseenter and mouseleave events
+ */
+ trackOver: false,
+
+ /**
+ * @cfg {Boolean} blockRefresh
+ * Set this to true to ignore datachanged events on the bound store. This is useful if
+ * you wish to provide custom transition animations via a plugin
+ */
+ blockRefresh: false,
+
+ /**
+ * @cfg {Boolean} disableSelection
+ * True to disable selection within the DataView. This configuration will lock the selection model
+ * that the DataView uses.
+ */
+
+
+ //private
+ last: false,
+
+ triggerEvent: 'itemclick',
+ triggerCtEvent: 'containerclick',
+
+ addCmpEvents: function() {
+
+ },
+
+ // private
+ initComponent : function(){
+ var me = this,
+ isDef = Ext.isDefined,
+ itemTpl = me.itemTpl,
+ memberFn = {};
+
+ if (itemTpl) {
+ if (Ext.isArray(itemTpl)) {
+ // string array
+ itemTpl = itemTpl.join('');
+ } else if (Ext.isObject(itemTpl)) {
+ // tpl instance
+ memberFn = Ext.apply(memberFn, itemTpl.initialConfig);
+ itemTpl = itemTpl.html;
+ }
+
+ if (!me.itemSelector) {
+ me.itemSelector = '.' + me.itemCls;
+ }
+
+ itemTpl = Ext.String.format('<tpl for="."><div class="{0}">{1}</div></tpl>', me.itemCls, itemTpl);
+ me.tpl = Ext.create('Ext.XTemplate', itemTpl, memberFn);
+ }
+
+
+ me.callParent();
+ if(Ext.isString(me.tpl) || Ext.isArray(me.tpl)){
+ me.tpl = Ext.create('Ext.XTemplate', me.tpl);
+ }
+
+
+ me.addEvents(
+ /**
+ * @event beforerefresh
+ * Fires before the view is refreshed
+ * @param {Ext.view.View} this The DataView object
+ */
+ 'beforerefresh',
+ /**
+ * @event refresh
+ * Fires when the view is refreshed
+ * @param {Ext.view.View} this The DataView object
+ */
+ 'refresh',
+ /**
+ * @event viewready
+ * Fires when the View's item elements representing Store items has been rendered. If the {@link #deferInitialRefresh} flag
+ * was set (and it is <code>true</code> by default), this will be <b>after</b> initial render, and no items will be available
+ * for selection until this event fires.
+ * @param {Ext.view.View} this
+ */
+ 'viewready',
+ /**
+ * @event itemupdate
+ * Fires when the node associated with an individual record is updated
+ * @param {Ext.data.Model} record The model instance
+ * @param {Number} index The index of the record/node
+ * @param {HTMLElement} node The node that has just been updated
+ */
+ 'itemupdate',
+ /**
+ * @event itemadd
+ * Fires when the nodes associated with an recordset have been added to the underlying store
+ * @param {Ext.data.Model[]} records The model instance
+ * @param {Number} index The index at which the set of record/nodes starts
+ * @param {HTMLElement[]} node The node that has just been updated
+ */
+ 'itemadd',
+ /**
+ * @event itemremove
+ * Fires when the node associated with an individual record is removed
+ * @param {Ext.data.Model} record The model instance
+ * @param {Number} index The index of the record/node
+ */
+ 'itemremove'
+ );
+
+ me.addCmpEvents();
+
+ // Look up the configured Store. If none configured, use the fieldless, empty Store defined in Ext.data.Store.
+ me.store = Ext.data.StoreManager.lookup(me.store || 'ext-empty-store');
+ me.all = new Ext.CompositeElementLite();
+ },
+
+ onRender: function() {
+ var me = this,
+ mask = me.loadMask,
+ cfg = {
+ msg: me.loadingText,
+ msgCls: me.loadingCls,
+ useMsg: me.loadingUseMsg
+ };
+
+ me.callParent(arguments);
+
+ if (mask) {
+ // either a config object
+ if (Ext.isObject(mask)) {
+ cfg = Ext.apply(cfg, mask);
+ }
+ // Attach the LoadMask to a *Component* so that it can be sensitive to resizing during long loads.
+ // If this DataView is floating, then mask this DataView.
+ // Otherwise, mask its owning Container (or this, if there *is* no owning Container).
+ // LoadMask captures the element upon render.
+ me.loadMask = Ext.create('Ext.LoadMask', me, cfg);
+ me.loadMask.on({
+ scope: me,
+ beforeshow: me.onMaskBeforeShow,
+ hide: me.onMaskHide
+ });
+ }
+ },
+
+ onMaskBeforeShow: function(){
+ var loadingHeight = this.loadingHeight;
+
+ this.getSelectionModel().deselectAll();
+ if (loadingHeight) {
+ this.setCalculatedSize(undefined, loadingHeight);
+ }
+ },
+
+ onMaskHide: function(){
+ var me = this;
+
+ if (!me.destroying && me.loadingHeight) {
+ me.setHeight(me.height);
+ }
+ },
+
+ afterRender: function() {
+ this.callParent(arguments);
+
+ // Init the SelectionModel after any on('render') listeners have been added.
+ // Drag plugins create a DragDrop instance in a render listener, and that needs
+ // to see an itemmousedown event first.
+ this.getSelectionModel().bindComponent(this);
+ },
+
+ /**
+ * Gets the selection model for this view.
+ * @return {Ext.selection.Model} The selection model
+ */
+ getSelectionModel: function(){
+ var me = this,
+ mode = 'SINGLE';
+
+ if (!me.selModel) {
+ me.selModel = {};
+ }
+
+ if (me.simpleSelect) {
+ mode = 'SIMPLE';
+ } else if (me.multiSelect) {
+ mode = 'MULTI';
+ }
+
+ Ext.applyIf(me.selModel, {
+ allowDeselect: me.allowDeselect,
+ mode: mode
+ });
+
+ if (!me.selModel.events) {
+ me.selModel = Ext.create('Ext.selection.DataViewModel', me.selModel);
+ }
+
+ if (!me.selModel.hasRelaySetup) {
+ me.relayEvents(me.selModel, [
+ 'selectionchange', 'beforeselect', 'beforedeselect', 'select', 'deselect'
+ ]);
+ me.selModel.hasRelaySetup = true;
+ }
+
+ // lock the selection model if user
+ // has disabled selection
+ if (me.disableSelection) {
+ me.selModel.locked = true;
+ }
+
+ return me.selModel;
+ },
+
+ /**
+ * Refreshes the view by reloading the data from the store and re-rendering the template.
+ */
+ refresh: function() {
+ var me = this,
+ el,
+ records;
+
+ if (!me.rendered || me.isDestroyed) {
+ return;
+ }
+
+ me.fireEvent('beforerefresh', me);
+ el = me.getTargetEl();
+ records = me.store.getRange();
+
+ el.update('');
+ if (records.length < 1) {
+ if (!me.deferEmptyText || me.hasSkippedEmptyText) {
+ el.update(me.emptyText);
+ }
+ me.all.clear();
+ } else {
+ me.tpl.overwrite(el, me.collectData(records, 0));
+ me.all.fill(Ext.query(me.getItemSelector(), el.dom));
+ me.updateIndexes(0);
+ }
+
+ me.selModel.refresh();
+ me.hasSkippedEmptyText = true;
+ me.fireEvent('refresh', me);
+
+ // Upon first refresh, fire the viewready event.
+ // Reconfiguring the grid "renews" this event.
+ if (!me.viewReady) {
+ // Fire an event when deferred content becomes available.
+ // This supports grid Panel's deferRowRender capability
+ me.viewReady = true;
+ me.fireEvent('viewready', me);
+ }
+ },
+
+ /**
+ * Function which can be overridden to provide custom formatting for each Record that is used by this
+ * DataView's {@link #tpl template} to render each node.
+ * @param {Object/Object[]} data The raw data object that was used to create the Record.
+ * @param {Number} recordIndex the index number of the Record being prepared for rendering.
+ * @param {Ext.data.Model} record The Record being prepared for rendering.
+ * @return {Array/Object} The formatted data in a format expected by the internal {@link #tpl template}'s overwrite() method.
+ * (either an array if your params are numeric (i.e. {0}) or an object (i.e. {foo: 'bar'}))
+ */
+ prepareData: function(data, index, record) {
+ if (record) {
+ Ext.apply(data, record.getAssociatedData());
+ }
+ return data;
+ },
+
+ /**
+ * <p>Function which can be overridden which returns the data object passed to this
+ * DataView's {@link #tpl template} to render the whole DataView.</p>
+ * <p>This is usually an Array of data objects, each element of which is processed by an
+ * {@link Ext.XTemplate XTemplate} which uses <tt>'<tpl for=".">'</tt> to iterate over its supplied
+ * data object as an Array. However, <i>named</i> properties may be placed into the data object to
+ * provide non-repeating data such as headings, totals etc.</p>
+ * @param {Ext.data.Model[]} records An Array of {@link Ext.data.Model}s to be rendered into the DataView.
+ * @param {Number} startIndex the index number of the Record being prepared for rendering.
+ * @return {Object[]} An Array of data objects to be processed by a repeating XTemplate. May also
+ * contain <i>named</i> properties.
+ */
+ collectData : function(records, startIndex){
+ var r = [],
+ i = 0,
+ len = records.length,
+ record;
+
+ for(; i < len; i++){
+ record = records[i];
+ r[r.length] = this.prepareData(record[record.persistenceProperty], startIndex + i, record);
+ }
+ return r;
+ },
+
+ // private
+ bufferRender : function(records, index){
+ var div = document.createElement('div');
+ this.tpl.overwrite(div, this.collectData(records, index));
+ return Ext.query(this.getItemSelector(), div);
+ },
+
+ // private
+ onUpdate : function(ds, record){
+ var me = this,
+ index = me.store.indexOf(record),
+ node;
+
+ if (index > -1){
+ node = me.bufferRender([record], index)[0];
+ // ensure the node actually exists in the DOM
+ if (me.getNode(record)) {
+ me.all.replaceElement(index, node, true);
+ me.updateIndexes(index, index);
+ // Maintain selection after update
+ // TODO: Move to approriate event handler.
+ me.selModel.refresh();
+ me.fireEvent('itemupdate', record, index, node);
+ }
+ }
+
+ },
+
+ // private
+ onAdd : function(ds, records, index) {
+ var me = this,
+ nodes;
+
+ if (me.all.getCount() === 0) {
+ me.refresh();
+ return;
+ }
+
+ nodes = me.bufferRender(records, index);
+ me.doAdd(nodes, records, index);
+
+ me.selModel.refresh();
+ me.updateIndexes(index);
+ me.fireEvent('itemadd', records, index, nodes);
+ },
+
+ doAdd: function(nodes, records, index) {
+ var all = this.all;
+
+ if (index < all.getCount()) {
+ all.item(index).insertSibling(nodes, 'before', true);
+ } else {
+ all.last().insertSibling(nodes, 'after', true);
+ }
+
+ Ext.Array.insert(all.elements, index, nodes);
+ },
+
+ // private
+ onRemove : function(ds, record, index) {
+ var me = this;
+
+ me.doRemove(record, index);
+ me.updateIndexes(index);
+ if (me.store.getCount() === 0){
+ me.refresh();
+ }
+ me.fireEvent('itemremove', record, index);
+ },
+
+ doRemove: function(record, index) {
+ this.all.removeElement(index, true);
+ },
+
+ /**
+ * Refreshes an individual node's data from the store.
+ * @param {Number} index The item's data index in the store
+ */
+ refreshNode : function(index){
+ this.onUpdate(this.store, this.store.getAt(index));
+ },
+
+ // private
+ updateIndexes : function(startIndex, endIndex) {
+ var ns = this.all.elements,
+ records = this.store.getRange(),
+ i;
+
+ startIndex = startIndex || 0;
+ endIndex = endIndex || ((endIndex === 0) ? 0 : (ns.length - 1));
+ for(i = startIndex; i <= endIndex; i++){
+ ns[i].viewIndex = i;
+ ns[i].viewRecordId = records[i].internalId;
+ if (!ns[i].boundView) {
+ ns[i].boundView = this.id;
+ }
+ }
+ },
+
+ /**
+ * Returns the store associated with this DataView.
+ * @return {Ext.data.Store} The store
+ */
+ getStore : function(){
+ return this.store;
+ },
+
+ /**
+ * Changes the data store bound to this view and refreshes it.
+ * @param {Ext.data.Store} store The store to bind to this view
+ */
+ bindStore : function(store, initial) {
+ var me = this,
+ maskStore;
+
+ if (!initial && me.store) {
+ if (store !== me.store && me.store.autoDestroy) {
+ me.store.destroyStore();
+ }
+ else {
+ me.mun(me.store, {
+ scope: me,
+ datachanged: me.onDataChanged,
+ add: me.onAdd,
+ remove: me.onRemove,
+ update: me.onUpdate,
+ clear: me.refresh
+ });
+ }
+ if (!store) {
+ // Ensure we have an instantiated LoadMask before we unbind it.
+ if (me.loadMask && me.loadMask.bindStore) {
+ me.loadMask.bindStore(null);
+ }
+ me.store = null;
+ }
+ }
+ if (store) {
+ store = Ext.data.StoreManager.lookup(store);
+ me.mon(store, {
+ scope: me,
+ datachanged: me.onDataChanged,
+ add: me.onAdd,
+ remove: me.onRemove,
+ update: me.onUpdate,
+ clear: me.refresh
+ });
+ // Ensure we have an instantiated LoadMask before we bind it.
+ if (me.loadMask && me.loadMask.bindStore) {
+ // View's store is a NodeStore, use owning TreePanel's Store
+ if (Ext.Array.contains(store.alias, 'store.node')) {
+ maskStore = this.ownerCt.store;
+ } else {
+ maskStore = store;
+ }
+ me.loadMask.bindStore(maskStore);
+ }
+ }
+
+ // Flag to say that initial refresh has not been performed.
+ // Set here rather than at initialization time, so that a reconfigure with a new store will refire viewready
+ me.viewReady = false;
+
+ me.store = store;
+ // Bind the store to our selection model
+ me.getSelectionModel().bind(store);
+
+ /*
+ * This code used to have checks for:
+ * if (store && (!initial || store.getCount() || me.emptyText)) {
+ * Instead, just trigger a refresh and let the view itself figure out
+ * what needs to happen. It can cause incorrect display if our store
+ * has no data.
+ */
+ if (store) {
+ if (initial && me.deferInitialRefresh) {
+ Ext.Function.defer(function () {
+ if (!me.isDestroyed) {
+ me.refresh(true);
+ }
+ }, 1);
+ } else {
+ me.refresh(true);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Calls this.refresh if this.blockRefresh is not true
+ */
+ onDataChanged: function() {
+ if (this.blockRefresh !== true) {
+ this.refresh.apply(this, arguments);
+ }
+ },
+
+ /**
+ * Returns the template node the passed child belongs to, or null if it doesn't belong to one.
+ * @param {HTMLElement} node
+ * @return {HTMLElement} The template node
+ */
+ findItemByChild: function(node){
+ return Ext.fly(node).findParent(this.getItemSelector(), this.getTargetEl());
+ },
+
+ /**
+ * Returns the template node by the Ext.EventObject or null if it is not found.
+ * @param {Ext.EventObject} e
+ */
+ findTargetByEvent: function(e) {
+ return e.getTarget(this.getItemSelector(), this.getTargetEl());
+ },
+
+
+ /**
+ * Gets the currently selected nodes.
+ * @return {HTMLElement[]} An array of HTMLElements
+ */
+ getSelectedNodes: function(){
+ var nodes = [],
+ records = this.selModel.getSelection(),
+ ln = records.length,
+ i = 0;
+
+ for (; i < ln; i++) {
+ nodes.push(this.getNode(records[i]));
+ }
+
+ return nodes;
+ },
+
+ /**
+ * Gets an array of the records from an array of nodes
+ * @param {HTMLElement[]} nodes The nodes to evaluate
+ * @return {Ext.data.Model[]} records The {@link Ext.data.Model} objects
+ */
+ getRecords: function(nodes) {
+ var records = [],
+ i = 0,
+ len = nodes.length,
+ data = this.store.data;
+
+ for (; i < len; i++) {
+ records[records.length] = data.getByKey(nodes[i].viewRecordId);
+ }
+
+ return records;
+ },
+
+ /**
+ * Gets a record from a node
+ * @param {Ext.Element/HTMLElement} node The node to evaluate
+ *
+ * @return {Ext.data.Model} record The {@link Ext.data.Model} object
+ */
+ getRecord: function(node){
+ return this.store.data.getByKey(Ext.getDom(node).viewRecordId);
+ },
+
+
+ /**
+ * Returns true if the passed node is selected, else false.
+ * @param {HTMLElement/Number/Ext.data.Model} node The node, node index or record to check
+ * @return {Boolean} True if selected, else false
+ */
+ isSelected : function(node) {
+ // TODO: El/Idx/Record
+ var r = this.getRecord(node);
+ return this.selModel.isSelected(r);
+ },
+
+ /**
+ * Selects a record instance by record instance or index.
+ * @param {Ext.data.Model[]/Number} records An array of records or an index
+ * @param {Boolean} [keepExisting] True to keep existing selections
+ * @param {Boolean} [suppressEvent] Set to true to not fire a select event
+ */
+ select: function(records, keepExisting, suppressEvent) {
+ this.selModel.select(records, keepExisting, suppressEvent);
+ },
+
+ /**
+ * Deselects a record instance by record instance or index.
+ * @param {Ext.data.Model[]/Number} records An array of records or an index
+ * @param {Boolean} [suppressEvent] Set to true to not fire a deselect event
+ */
+ deselect: function(records, suppressEvent) {
+ this.selModel.deselect(records, suppressEvent);
+ },
+
+ /**
+ * Gets a template node.
+ * @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo An HTMLElement template node, index of a template node,
+ * the id of a template node or the record associated with the node.
+ * @return {HTMLElement} The node or null if it wasn't found
+ */
+ getNode : function(nodeInfo) {
+ if (!this.rendered) {
+ return null;
+ }
+ if (Ext.isString(nodeInfo)) {
+ return document.getElementById(nodeInfo);
+ }
+ if (Ext.isNumber(nodeInfo)) {
+ return this.all.elements[nodeInfo];
+ }
+ if (nodeInfo instanceof Ext.data.Model) {
+ return this.getNodeByRecord(nodeInfo);
+ }
+ return nodeInfo; // already an HTMLElement
+ },
+
+ /**
+ * @private
+ */
+ getNodeByRecord: function(record) {
+ var ns = this.all.elements,
+ ln = ns.length,
+ i = 0;
+
+ for (; i < ln; i++) {
+ if (ns[i].viewRecordId === record.internalId) {
+ return ns[i];
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Gets a range nodes.
+ * @param {Number} start (optional) The index of the first node in the range
+ * @param {Number} end (optional) The index of the last node in the range
+ * @return {HTMLElement[]} An array of nodes
+ */
+ getNodes: function(start, end) {
+ var ns = this.all.elements,
+ nodes = [],
+ i;
+
+ start = start || 0;
+ end = !Ext.isDefined(end) ? Math.max(ns.length - 1, 0) : end;
+ if (start <= end) {
+ for (i = start; i <= end && ns[i]; i++) {
+ nodes.push(ns[i]);
+ }
+ } else {
+ for (i = start; i >= end && ns[i]; i--) {
+ nodes.push(ns[i]);
+ }
+ }
+ return nodes;
+ },
+
+ /**
+ * Finds the index of the passed node.
+ * @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo An HTMLElement template node, index of a template node, the id of a template node
+ * or a record associated with a node.
+ * @return {Number} The index of the node or -1
+ */
+ indexOf: function(node) {
+ node = this.getNode(node);
+ if (Ext.isNumber(node.viewIndex)) {
+ return node.viewIndex;
+ }
+ return this.all.indexOf(node);
+ },
+
+ onDestroy : function() {
+ var me = this;
+
+ me.all.clear();
+ me.callParent();
+ me.bindStore(null);
+ me.selModel.destroy();
+ },
+
+ // invoked by the selection model to maintain visual UI cues
+ onItemSelect: function(record) {
+ var node = this.getNode(record);
+
+ if (node) {
+ Ext.fly(node).addCls(this.selectedItemCls);
+ }
+ },
+
+ // invoked by the selection model to maintain visual UI cues
+ onItemDeselect: function(record) {
+ var node = this.getNode(record);
+
+ if (node) {
+ Ext.fly(node).removeCls(this.selectedItemCls);
+ }
+ },
+
+ getItemSelector: function() {
+ return this.itemSelector;
+ }
+}, function() {
+ // all of this information is available directly
+ // from the SelectionModel itself, the only added methods
+ // to DataView regarding selection will perform some transformation/lookup
+ // between HTMLElement/Nodes to records and vice versa.
+ Ext.deprecate('extjs', '4.0', function() {
+ Ext.view.AbstractView.override({
+ /**
+ * @cfg {Boolean} [multiSelect=false]
+ * True to allow selection of more than one item at a time, false to allow selection of only a single item
+ * at a time or no selection at all, depending on the value of {@link #singleSelect}.
+ */
+ /**
+ * @cfg {Boolean} [singleSelect=false]
+ * True to allow selection of exactly one item at a time, false to allow no selection at all.
+ * Note that if {@link #multiSelect} = true, this value will be ignored.
+ */
+ /**
+ * @cfg {Boolean} [simpleSelect=false]
+ * True to enable multiselection by clicking on multiple items without requiring the user to hold Shift or Ctrl,
+ * false to force the user to hold Ctrl or Shift to select more than on item.
+ */
+
+ /**
+ * Gets the number of selected nodes.
+ * @return {Number} The node count
+ */
+ getSelectionCount : function(){
+ if (Ext.global.console) {
+ Ext.global.console.warn("DataView: getSelectionCount will be removed, please interact with the Ext.selection.DataViewModel");
+ }
+ return this.selModel.getSelection().length;
+ },
+
+ /**
+ * Gets an array of the selected records
+ * @return {Ext.data.Model[]} An array of {@link Ext.data.Model} objects
+ */
+ getSelectedRecords : function(){
+ if (Ext.global.console) {
+ Ext.global.console.warn("DataView: getSelectedRecords will be removed, please interact with the Ext.selection.DataViewModel");
+ }
+ return this.selModel.getSelection();
+ },
+
+ select: function(records, keepExisting, supressEvents) {
+ if (Ext.global.console) {
+ Ext.global.console.warn("DataView: select will be removed, please access select through a DataView's SelectionModel, ie: view.getSelectionModel().select()");
+ }
+ var sm = this.getSelectionModel();
+ return sm.select.apply(sm, arguments);
+ },
+
+ clearSelections: function() {
+ if (Ext.global.console) {
+ Ext.global.console.warn("DataView: clearSelections will be removed, please access deselectAll through DataView's SelectionModel, ie: view.getSelectionModel().deselectAll()");
+ }
+ var sm = this.getSelectionModel();
+ return sm.deselectAll();
+ }
+ });
+ });
+});
+
+/**
+ * @class Ext.Action
+ * <p>An Action is a piece of reusable functionality that can be abstracted out of any particular component so that it
+ * can be usefully shared among multiple components. Actions let you share handlers, configuration options and UI
+ * updates across any components that support the Action interface (primarily {@link Ext.toolbar.Toolbar}, {@link Ext.button.Button}
+ * and {@link Ext.menu.Menu} components).</p>
+ * <p>Use a single Action instance as the config object for any number of UI Components which share the same configuration. The
+ * Action not only supplies the configuration, but allows all Components based upon it to have a common set of methods
+ * called at once through a single call to the Action.</p>
+ * <p>Any Component that is to be configured with an Action must also support
+ * the following methods:<ul>
+ * <li><code>setText(string)</code></li>
+ * <li><code>setIconCls(string)</code></li>
+ * <li><code>setDisabled(boolean)</code></li>
+ * <li><code>setVisible(boolean)</code></li>
+ * <li><code>setHandler(function)</code></li></ul></p>
+ * <p>This allows the Action to control its associated Components.</p>
+ * Example usage:<br>
+ * <pre><code>
+// Define the shared Action. Each Component below will have the same
+// display text and icon, and will display the same message on click.
+var action = new Ext.Action({
+ {@link #text}: 'Do something',
+ {@link #handler}: function(){
+ Ext.Msg.alert('Click', 'You did something.');
+ },
+ {@link #iconCls}: 'do-something',
+ {@link #itemId}: 'myAction'
+});
+
+var panel = new Ext.panel.Panel({
+ title: 'Actions',
+ width: 500,
+ height: 300,
+ tbar: [
+ // Add the Action directly to a toolbar as a menu button
+ action,
+ {
+ text: 'Action Menu',
+ // Add the Action to a menu as a text item
+ menu: [action]
+ }
+ ],
+ items: [
+ // Add the Action to the panel body as a standard button
+ new Ext.button.Button(action)
+ ],
+ renderTo: Ext.getBody()
+});
+
+// Change the text for all components using the Action
+action.setText('Something else');
+
+// Reference an Action through a container using the itemId
+var btn = panel.getComponent('myAction');
+var aRef = btn.baseAction;
+aRef.setText('New text');
+</code></pre>
+ */
+Ext.define('Ext.Action', {
+
+ /* Begin Definitions */
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String} [text='']
+ * The text to set for all components configured by this Action.
+ */
+ /**
+ * @cfg {String} [iconCls='']
+ * The CSS class selector that specifies a background image to be used as the header icon for
+ * all components configured by this Action.
+ * <p>An example of specifying a custom icon class would be something like:
+ * </p><pre><code>
+// specify the property in the config for the class:
+ ...
+ iconCls: 'do-something'
+
+// css class that specifies background image to be used as the icon image:
+.do-something { background-image: url(../images/my-icon.gif) 0 6px no-repeat !important; }
+</code></pre>
+ */
+ /**
+ * @cfg {Boolean} [disabled=false]
+ * True to disable all components configured by this Action, false to enable them.
+ */
+ /**
+ * @cfg {Boolean} [hidden=false]
+ * True to hide all components configured by this Action, false to show them.
+ */
+ /**
+ * @cfg {Function} handler
+ * The function that will be invoked by each component tied to this Action
+ * when the component's primary event is triggered.
+ */
+ /**
+ * @cfg {String} itemId
+ * See {@link Ext.Component}.{@link Ext.Component#itemId itemId}.
+ */
+ /**
+ * @cfg {Object} scope
+ * The scope (this reference) in which the {@link #handler} is executed.
+ * Defaults to the browser window.
+ */
+
+ /**
+ * Creates new Action.
+ * @param {Object} config Config object.
+ */
+ constructor : function(config){
+ this.initialConfig = config;
+ this.itemId = config.itemId = (config.itemId || config.id || Ext.id());
+ this.items = [];
+ },
+
+ // private
+ isAction : true,
+
+ /**
+ * Sets the text to be displayed by all components configured by this Action.
+ * @param {String} text The text to display
+ */
+ setText : function(text){
+ this.initialConfig.text = text;
+ this.callEach('setText', [text]);
+ },
+
+ /**
+ * Gets the text currently displayed by all components configured by this Action.
+ */
+ getText : function(){
+ return this.initialConfig.text;
+ },
+
+ /**
+ * Sets the icon CSS class for all components configured by this Action. The class should supply
+ * a background image that will be used as the icon image.
+ * @param {String} cls The CSS class supplying the icon image
+ */
+ setIconCls : function(cls){
+ this.initialConfig.iconCls = cls;
+ this.callEach('setIconCls', [cls]);
+ },
+
+ /**
+ * Gets the icon CSS class currently used by all components configured by this Action.
+ */
+ getIconCls : function(){
+ return this.initialConfig.iconCls;
+ },
+
+ /**
+ * Sets the disabled state of all components configured by this Action. Shortcut method
+ * for {@link #enable} and {@link #disable}.
+ * @param {Boolean} disabled True to disable the component, false to enable it
+ */
+ setDisabled : function(v){
+ this.initialConfig.disabled = v;
+ this.callEach('setDisabled', [v]);
+ },
+
+ /**
+ * Enables all components configured by this Action.
+ */
+ enable : function(){
+ this.setDisabled(false);
+ },
+
+ /**
+ * Disables all components configured by this Action.
+ */
+ disable : function(){
+ this.setDisabled(true);
+ },
+
+ /**
+ * Returns true if the components using this Action are currently disabled, else returns false.
+ */
+ isDisabled : function(){
+ return this.initialConfig.disabled;
+ },
+
+ /**
+ * Sets the hidden state of all components configured by this Action. Shortcut method
+ * for <code>{@link #hide}</code> and <code>{@link #show}</code>.
+ * @param {Boolean} hidden True to hide the component, false to show it
+ */
+ setHidden : function(v){
+ this.initialConfig.hidden = v;
+ this.callEach('setVisible', [!v]);
+ },
+
+ /**
+ * Shows all components configured by this Action.
+ */
+ show : function(){
+ this.setHidden(false);
+ },
+
+ /**
+ * Hides all components configured by this Action.
+ */
+ hide : function(){
+ this.setHidden(true);
+ },
+
+ /**
+ * Returns true if the components configured by this Action are currently hidden, else returns false.
+ */
+ isHidden : function(){
+ return this.initialConfig.hidden;
+ },
+
+ /**
+ * Sets the function that will be called by each Component using this action when its primary event is triggered.
+ * @param {Function} fn The function that will be invoked by the action's components. The function
+ * will be called with no arguments.
+ * @param {Object} scope The scope (<code>this</code> reference) in which the function is executed. Defaults to the Component firing the event.
+ */
+ setHandler : function(fn, scope){
+ this.initialConfig.handler = fn;
+ this.initialConfig.scope = scope;
+ this.callEach('setHandler', [fn, scope]);
+ },
+
+ /**
+ * Executes the specified function once for each Component currently tied to this Action. The function passed
+ * in should accept a single argument that will be an object that supports the basic Action config/method interface.
+ * @param {Function} fn The function to execute for each component
+ * @param {Object} scope The scope (<code>this</code> reference) in which the function is executed. Defaults to the Component.
+ */
+ each : function(fn, scope){
+ Ext.each(this.items, fn, scope);
+ },
+
+ // private
+ callEach : function(fnName, args){
+ var items = this.items,
+ i = 0,
+ len = items.length;
+
+ for(; i < len; i++){
+ items[i][fnName].apply(items[i], args);
+ }
+ },
+
+ // private
+ addComponent : function(comp){
+ this.items.push(comp);
+ comp.on('destroy', this.removeComponent, this);
+ },
+
+ // private
+ removeComponent : function(comp){
+ Ext.Array.remove(this.items, comp);
+ },
+
+ /**
+ * Executes this Action manually using the handler function specified in the original config object
+ * or the handler function set with <code>{@link #setHandler}</code>. Any arguments passed to this
+ * function will be passed on to the handler function.
+ * @param {Object...} args (optional) Variable number of arguments passed to the handler function
+ */
+ execute : function(){
+ this.initialConfig.handler.apply(this.initialConfig.scope || Ext.global, arguments);
+ }
+});
+
+/**
+ * Component layout for editors
+ * @class Ext.layout.component.Editor
+ * @extends Ext.layout.component.Component
+ * @private
+ */
+Ext.define('Ext.layout.component.Editor', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.editor'],
+
+ extend: 'Ext.layout.component.Component',
+
+ /* End Definitions */
+
+ onLayout: function(width, height) {
+ var me = this,
+ owner = me.owner,
+ autoSize = owner.autoSize;
+
+ if (autoSize === true) {
+ autoSize = {
+ width: 'field',
+ height: 'field'
+ };
+ }
+
+ if (autoSize) {
+ width = me.getDimension(owner, autoSize.width, 'Width', width);
+ height = me.getDimension(owner, autoSize.height, 'Height', height);
+ }
+ me.setTargetSize(width, height);
+ owner.field.setSize(width, height);
+ },
+
+ getDimension: function(owner, type, dimension, actual){
+ var method = 'get' + dimension;
+ switch (type) {
+ case 'boundEl':
+ return owner.boundEl[method]();
+ case 'field':
+ return owner.field[method]();
+ default:
+ return actual;
+ }
+ }
+});
+/**
+ * @class Ext.Editor
+ * @extends Ext.Component
+ *
+ * <p>
+ * The Editor class is used to provide inline editing for elements on the page. The editor
+ * is backed by a {@link Ext.form.field.Field} that will be displayed to edit the underlying content.
+ * The editor is a floating Component, when the editor is shown it is automatically aligned to
+ * display over the top of the bound element it is editing. The Editor contains several options
+ * for how to handle key presses:
+ * <ul>
+ * <li>{@link #completeOnEnter}</li>
+ * <li>{@link #cancelOnEsc}</li>
+ * <li>{@link #swallowKeys}</li>
+ * </ul>
+ * It also has options for how to use the value once the editor has been activated:
+ * <ul>
+ * <li>{@link #revertInvalid}</li>
+ * <li>{@link #ignoreNoChange}</li>
+ * <li>{@link #updateEl}</li>
+ * </ul>
+ * Sample usage:
+ * </p>
+ * <pre><code>
+var editor = new Ext.Editor({
+ updateEl: true, // update the innerHTML of the bound element when editing completes
+ field: {
+ xtype: 'textfield'
+ }
+});
+var el = Ext.get('my-text'); // The element to 'edit'
+editor.startEdit(el); // The value of the field will be taken as the innerHTML of the element.
+ * </code></pre>
+ * {@img Ext.Editor/Ext.Editor.png Ext.Editor component}
+ *
+ */
+Ext.define('Ext.Editor', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.Component',
+
+ alias: 'widget.editor',
+
+ requires: ['Ext.layout.component.Editor'],
+
+ /* End Definitions */
+
+ componentLayout: 'editor',
+
+ /**
+ * @cfg {Ext.form.field.Field} field
+ * The Field object (or descendant) or config object for field
+ */
+
+ /**
+ * @cfg {Boolean} allowBlur
+ * True to {@link #completeEdit complete the editing process} if in edit mode when the
+ * field is blurred.
+ */
+ allowBlur: true,
+
+ /**
+ * @cfg {Boolean/Object} autoSize
+ * True for the editor to automatically adopt the size of the underlying field. Otherwise, an object
+ * can be passed to indicate where to get each dimension. The available properties are 'boundEl' and
+ * 'field'. If a dimension is not specified, it will use the underlying height/width specified on
+ * the editor object.
+ * Examples:
+ * <pre><code>
+autoSize: true // The editor will be sized to the height/width of the field
+
+height: 21,
+autoSize: {
+ width: 'boundEl' // The width will be determined by the width of the boundEl, the height from the editor (21)
+}
+
+autoSize: {
+ width: 'field', // Width from the field
+ height: 'boundEl' // Height from the boundEl
+}
+ * </pre></code>
+ */
+
+ /**
+ * @cfg {Boolean} revertInvalid
+ * True to automatically revert the field value and cancel the edit when the user completes an edit and the field
+ * validation fails
+ */
+ revertInvalid: true,
+
+ /**
+ * @cfg {Boolean} [ignoreNoChange=false]
+ * True to skip the edit completion process (no save, no events fired) if the user completes an edit and
+ * the value has not changed. Applies only to string values - edits for other data types
+ * will never be ignored.
+ */
+
+ /**
+ * @cfg {Boolean} [hideEl=true]
+ * False to keep the bound element visible while the editor is displayed
+ */
+
+ /**
+ * @cfg {Object} value
+ * The data value of the underlying field
+ */
+ value : '',
+
+ /**
+ * @cfg {String} alignment
+ * The position to align to (see {@link Ext.Element#alignTo} for more details).
+ */
+ alignment: 'c-c?',
+
+ /**
+ * @cfg {Number[]} offsets
+ * The offsets to use when aligning (see {@link Ext.Element#alignTo} for more details.
+ */
+ offsets: [0, 0],
+
+ /**
+ * @cfg {Boolean/String} shadow
+ * "sides" for sides/bottom only, "frame" for 4-way shadow, and "drop" for bottom-right shadow.
+ */
+ shadow : 'frame',
+
+ /**
+ * @cfg {Boolean} constrain
+ * True to constrain the editor to the viewport
+ */
+ constrain : false,
+
+ /**
+ * @cfg {Boolean} swallowKeys
+ * Handle the keydown/keypress events so they don't propagate
+ */
+ swallowKeys : true,
+
+ /**
+ * @cfg {Boolean} completeOnEnter
+ * True to complete the edit when the enter key is pressed.
+ */
+ completeOnEnter : true,
+
+ /**
+ * @cfg {Boolean} cancelOnEsc
+ * True to cancel the edit when the escape key is pressed.
+ */
+ cancelOnEsc : true,
+
+ /**
+ * @cfg {Boolean} updateEl
+ * True to update the innerHTML of the bound element when the update completes
+ */
+ updateEl : false,
+
+ /**
+ * @cfg {String/HTMLElement/Ext.Element} parentEl
+ * An element to render to. Defaults to the <tt>document.body</tt>.
+ */
+
+ // private overrides
+ hidden: true,
+ baseCls: Ext.baseCSSPrefix + 'editor',
+
+ initComponent : function() {
+ var me = this,
+ field = me.field = Ext.ComponentManager.create(me.field, 'textfield');
+
+ Ext.apply(field, {
+ inEditor: true,
+ msgTarget: field.msgTarget == 'title' ? 'title' : 'qtip'
+ });
+ me.mon(field, {
+ scope: me,
+ blur: {
+ fn: me.onBlur,
+ // slight delay to avoid race condition with startEdits (e.g. grid view refresh)
+ delay: 1
+ },
+ specialkey: me.onSpecialKey
+ });
+
+ if (field.grow) {
+ me.mon(field, 'autosize', me.onAutoSize, me, {delay: 1});
+ }
+ me.floating = {
+ constrain: me.constrain
+ };
+
+ me.callParent(arguments);
+
+ me.addEvents(
+ /**
+ * @event beforestartedit
+ * Fires when editing is initiated, but before the value changes. Editing can be canceled by returning
+ * false from the handler of this event.
+ * @param {Ext.Editor} this
+ * @param {Ext.Element} boundEl The underlying element bound to this editor
+ * @param {Object} value The field value being set
+ */
+ 'beforestartedit',
+
+ /**
+ * @event startedit
+ * Fires when this editor is displayed
+ * @param {Ext.Editor} this
+ * @param {Ext.Element} boundEl The underlying element bound to this editor
+ * @param {Object} value The starting field value
+ */
+ 'startedit',
+
+ /**
+ * @event beforecomplete
+ * Fires after a change has been made to the field, but before the change is reflected in the underlying
+ * field. Saving the change to the field can be canceled by returning false from the handler of this event.
+ * Note that if the value has not changed and ignoreNoChange = true, the editing will still end but this
+ * event will not fire since no edit actually occurred.
+ * @param {Ext.Editor} this
+ * @param {Object} value The current field value
+ * @param {Object} startValue The original field value
+ */
+ 'beforecomplete',
+ /**
+ * @event complete
+ * Fires after editing is complete and any changed value has been written to the underlying field.
+ * @param {Ext.Editor} this
+ * @param {Object} value The current field value
+ * @param {Object} startValue The original field value
+ */
+ 'complete',
+ /**
+ * @event canceledit
+ * Fires after editing has been canceled and the editor's value has been reset.
+ * @param {Ext.Editor} this
+ * @param {Object} value The user-entered field value that was discarded
+ * @param {Object} startValue The original field value that was set back into the editor after cancel
+ */
+ 'canceledit',
+ /**
+ * @event specialkey
+ * Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed. You can check
+ * {@link Ext.EventObject#getKey} to determine which key was pressed.
+ * @param {Ext.Editor} this
+ * @param {Ext.form.field.Field} The field attached to this editor
+ * @param {Ext.EventObject} event The event object
+ */
+ 'specialkey'
+ );
+ },
+
+ // private
+ onAutoSize: function(){
+ this.doComponentLayout();
+ },
+
+ // private
+ onRender : function(ct, position) {
+ var me = this,
+ field = me.field,
+ inputEl = field.inputEl;
+
+ me.callParent(arguments);
+
+ field.render(me.el);
+ //field.hide();
+ // Ensure the field doesn't get submitted as part of any form
+ if (inputEl) {
+ inputEl.dom.name = '';
+ if (me.swallowKeys) {
+ inputEl.swallowEvent([
+ 'keypress', // *** Opera
+ 'keydown' // *** all other browsers
+ ]);
+ }
+ }
+ },
+
+ // private
+ onSpecialKey : function(field, event) {
+ var me = this,
+ key = event.getKey(),
+ complete = me.completeOnEnter && key == event.ENTER,
+ cancel = me.cancelOnEsc && key == event.ESC;
+
+ if (complete || cancel) {
+ event.stopEvent();
+ // Must defer this slightly to prevent exiting edit mode before the field's own
+ // key nav can handle the enter key, e.g. selecting an item in a combobox list
+ Ext.defer(function() {
+ if (complete) {
+ me.completeEdit();
+ } else {
+ me.cancelEdit();
+ }
+ if (field.triggerBlur) {
+ field.triggerBlur();
+ }
+ }, 10);
+ }
+
+ this.fireEvent('specialkey', this, field, event);
+ },
+
+ /**
+ * Starts the editing process and shows the editor.
+ * @param {String/HTMLElement/Ext.Element} el The element to edit
+ * @param {String} value (optional) A value to initialize the editor with. If a value is not provided, it defaults
+ * to the innerHTML of el.
+ */
+ startEdit : function(el, value) {
+ var me = this,
+ field = me.field;
+
+ me.completeEdit();
+ me.boundEl = Ext.get(el);
+ value = Ext.isDefined(value) ? value : me.boundEl.dom.innerHTML;
+
+ if (!me.rendered) {
+ me.render(me.parentEl || document.body);
+ }
+
+ if (me.fireEvent('beforestartedit', me, me.boundEl, value) !== false) {
+ me.startValue = value;
+ me.show();
+ field.reset();
+ field.setValue(value);
+ me.realign(true);
+ field.focus(false, 10);
+ if (field.autoSize) {
+ field.autoSize();
+ }
+ me.editing = true;
+ }
+ },
+
+ /**
+ * Realigns the editor to the bound field based on the current alignment config value.
+ * @param {Boolean} autoSize (optional) True to size the field to the dimensions of the bound element.
+ */
+ realign : function(autoSize) {
+ var me = this;
+ if (autoSize === true) {
+ me.doComponentLayout();
+ }
+ me.alignTo(me.boundEl, me.alignment, me.offsets);
+ },
+
+ /**
+ * Ends the editing process, persists the changed value to the underlying field, and hides the editor.
+ * @param {Boolean} [remainVisible=false] Override the default behavior and keep the editor visible after edit
+ */
+ completeEdit : function(remainVisible) {
+ var me = this,
+ field = me.field,
+ value;
+
+ if (!me.editing) {
+ return;
+ }
+
+ // Assert combo values first
+ if (field.assertValue) {
+ field.assertValue();
+ }
+
+ value = me.getValue();
+ if (!field.isValid()) {
+ if (me.revertInvalid !== false) {
+ me.cancelEdit(remainVisible);
+ }
+ return;
+ }
+
+ if (String(value) === String(me.startValue) && me.ignoreNoChange) {
+ me.hideEdit(remainVisible);
+ return;
+ }
+
+ if (me.fireEvent('beforecomplete', me, value, me.startValue) !== false) {
+ // Grab the value again, may have changed in beforecomplete
+ value = me.getValue();
+ if (me.updateEl && me.boundEl) {
+ me.boundEl.update(value);
+ }
+ me.hideEdit(remainVisible);
+ me.fireEvent('complete', me, value, me.startValue);
+ }
+ },
+
+ // private
+ onShow : function() {
+ var me = this;
+
+ me.callParent(arguments);
+ if (me.hideEl !== false) {
+ me.boundEl.hide();
+ }
+ me.fireEvent("startedit", me.boundEl, me.startValue);
+ },
+
+ /**
+ * Cancels the editing process and hides the editor without persisting any changes. The field value will be
+ * reverted to the original starting value.
+ * @param {Boolean} [remainVisible=false] Override the default behavior and keep the editor visible after cancel
+ */
+ cancelEdit : function(remainVisible) {
+ var me = this,
+ startValue = me.startValue,
+ value;
+
+ if (me.editing) {
+ value = me.getValue();
+ me.setValue(startValue);
+ me.hideEdit(remainVisible);
+ me.fireEvent('canceledit', me, value, startValue);
+ }
+ },
+
+ // private
+ hideEdit: function(remainVisible) {
+ if (remainVisible !== true) {
+ this.editing = false;
+ this.hide();
+ }
+ },
+
+ // private
+ onBlur : function() {
+ var me = this;
+
+ // selectSameEditor flag allows the same editor to be started without onBlur firing on itself
+ if(me.allowBlur === true && me.editing && me.selectSameEditor !== true) {
+ me.completeEdit();
+ }
+ },
+
+ // private
+ onHide : function() {
+ var me = this,
+ field = me.field;
+
+ if (me.editing) {
+ me.completeEdit();
+ return;
+ }
+ field.blur();
+ if (field.collapse) {
+ field.collapse();
+ }
+
+ //field.hide();
+ if (me.hideEl !== false) {
+ me.boundEl.show();
+ }
+ me.callParent(arguments);
+ },
+
+ /**
+ * Sets the data value of the editor
+ * @param {Object} value Any valid value supported by the underlying field
+ */
+ setValue : function(value) {
+ this.field.setValue(value);
+ },
+
+ /**
+ * Gets the data value of the editor
+ * @return {Object} The data value
+ */
+ getValue : function() {
+ return this.field.getValue();
+ },
+
+ beforeDestroy : function() {
+ var me = this;
+
+ Ext.destroy(me.field);
+ delete me.field;
+ delete me.parentEl;
+ delete me.boundEl;
+
+ me.callParent(arguments);
+ }
+});
+/**
+ * @class Ext.Img
+ * @extends Ext.Component
+ *
+ * Simple helper class for easily creating image components. This simply renders an image tag to the DOM
+ * with the configured src.
+ *
+ * {@img Ext.Img/Ext.Img.png Ext.Img component}
+ *
+ * ## Example usage:
+ *
+ * var changingImage = Ext.create('Ext.Img', {
+ * src: 'http://www.sencha.com/img/20110215-feat-html5.png',
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * // change the src of the image programmatically
+ * changingImage.setSrc('http://www.sencha.com/img/20110215-feat-perf.png');
+*/
+Ext.define('Ext.Img', {
+ extend: 'Ext.Component',
+ alias: ['widget.image', 'widget.imagecomponent'],
+ /** @cfg {String} src The image src */
+ src: '',
+
+ getElConfig: function() {
+ return {
+ tag: 'img',
+ src: this.src
+ };
+ },
+
+ // null out this function, we can't set any html inside the image
+ initRenderTpl: Ext.emptyFn,
+
+ /**
+ * Updates the {@link #src} of the image
+ */
+ setSrc: function(src) {
+ var me = this,
+ img = me.el;
+ me.src = src;
+ if (img) {
+ img.dom.src = src;
+ }
+ }
+});
+
+/**
+ * @class Ext.Layer
+ * @extends Ext.Element
+ * An extended {@link Ext.Element} object that supports a shadow and shim, constrain to viewport and
+ * automatic maintaining of shadow/shim positions.
+ *
+ * @cfg {Boolean} [shim=true]
+ * False to disable the iframe shim in browsers which need one.
+ *
+ * @cfg {String/Boolean} [shadow=false]
+ * True to automatically create an {@link Ext.Shadow}, or a string indicating the
+ * shadow's display {@link Ext.Shadow#mode}. False to disable the shadow.
+ *
+ * @cfg {Object} [dh={tag: 'div', cls: 'x-layer'}]
+ * DomHelper object config to create element with.
+ *
+ * @cfg {Boolean} [constrain=true]
+ * False to disable constrain to viewport.
+ *
+ * @cfg {String} cls
+ * CSS class to add to the element
+ *
+ * @cfg {Number} [zindex=11000]
+ * Starting z-index.
+ *
+ * @cfg {Number} [shadowOffset=4]
+ * Number of pixels to offset the shadow
+ *
+ * @cfg {Boolean} [useDisplay=false]
+ * Defaults to use css offsets to hide the Layer. Specify <tt>true</tt>
+ * to use css style <tt>'display:none;'</tt> to hide the Layer.
+ *
+ * @cfg {String} visibilityCls
+ * The CSS class name to add in order to hide this Layer if this layer
+ * is configured with <code>{@link #hideMode}: 'asclass'</code>
+ *
+ * @cfg {String} hideMode
+ * A String which specifies how this Layer will be hidden.
+ * Values may be<div class="mdetail-params"><ul>
+ * <li><code>'display'</code> : The Component will be hidden using the <code>display: none</code> style.</li>
+ * <li><code>'visibility'</code> : The Component will be hidden using the <code>visibility: hidden</code> style.</li>
+ * <li><code>'offsets'</code> : The Component will be hidden by absolutely positioning it out of the visible area of the document. This
+ * is useful when a hidden Component must maintain measurable dimensions. Hiding using <code>display</code> results
+ * in a Component having zero dimensions.</li></ul></div>
+ */
+Ext.define('Ext.Layer', {
+ uses: ['Ext.Shadow'],
+
+ // shims are shared among layer to keep from having 100 iframes
+ statics: {
+ shims: []
+ },
+
+ extend: 'Ext.Element',
+
+ /**
+ * Creates new Layer.
+ * @param {Object} config (optional) An object with config options.
+ * @param {String/HTMLElement} existingEl (optional) Uses an existing DOM element.
+ * If the element is not found it creates it.
+ */
+ constructor: function(config, existingEl) {
+ config = config || {};
+ var me = this,
+ dh = Ext.DomHelper,
+ cp = config.parentEl,
+ pel = cp ? Ext.getDom(cp) : document.body,
+ hm = config.hideMode;
+
+ if (existingEl) {
+ me.dom = Ext.getDom(existingEl);
+ }
+ if (!me.dom) {
+ me.dom = dh.append(pel, config.dh || {
+ tag: 'div',
+ cls: Ext.baseCSSPrefix + 'layer'
+ });
+ } else {
+ me.addCls(Ext.baseCSSPrefix + 'layer');
+ if (!me.dom.parentNode) {
+ pel.appendChild(me.dom);
+ }
+ }
+
+ if (config.cls) {
+ me.addCls(config.cls);
+ }
+ me.constrain = config.constrain !== false;
+
+ // Allow Components to pass their hide mode down to the Layer if they are floating.
+ // Otherwise, allow useDisplay to override the default hiding method which is visibility.
+ // TODO: Have ExtJS's Element implement visibilityMode by using classes as in Mobile.
+ if (hm) {
+ me.setVisibilityMode(Ext.Element[hm.toUpperCase()]);
+ if (me.visibilityMode == Ext.Element.ASCLASS) {
+ me.visibilityCls = config.visibilityCls;
+ }
+ } else if (config.useDisplay) {
+ me.setVisibilityMode(Ext.Element.DISPLAY);
+ } else {
+ me.setVisibilityMode(Ext.Element.VISIBILITY);
+ }
+
+ if (config.id) {
+ me.id = me.dom.id = config.id;
+ } else {
+ me.id = Ext.id(me.dom);
+ }
+ me.position('absolute');
+ if (config.shadow) {
+ me.shadowOffset = config.shadowOffset || 4;
+ me.shadow = Ext.create('Ext.Shadow', {
+ offset: me.shadowOffset,
+ mode: config.shadow
+ });
+ me.disableShadow();
+ } else {
+ me.shadowOffset = 0;
+ }
+ me.useShim = config.shim !== false && Ext.useShims;
+ if (config.hidden === true) {
+ me.hide();
+ } else {
+ me.show();
+ }
+ },
+
+ getZIndex: function() {
+ return parseInt((this.getShim() || this).getStyle('z-index'), 10);
+ },
+
+ getShim: function() {
+ var me = this,
+ shim, pn;
+
+ if (!me.useShim) {
+ return null;
+ }
+ if (!me.shim) {
+ shim = me.self.shims.shift();
+ if (!shim) {
+ shim = me.createShim();
+ shim.enableDisplayMode('block');
+ shim.hide();
+ }
+ pn = me.dom.parentNode;
+ if (shim.dom.parentNode != pn) {
+ pn.insertBefore(shim.dom, me.dom);
+ }
+ me.shim = shim;
+ }
+ return me.shim;
+ },
+
+ hideShim: function() {
+ var me = this;
+
+ if (me.shim) {
+ me.shim.setDisplayed(false);
+ me.self.shims.push(me.shim);
+ delete me.shim;
+ }
+ },
+
+ disableShadow: function() {
+ var me = this;
+
+ if (me.shadow && !me.shadowDisabled) {
+ me.shadowDisabled = true;
+ me.shadow.hide();
+ me.lastShadowOffset = me.shadowOffset;
+ me.shadowOffset = 0;
+ }
+ },
+
+ enableShadow: function(show) {
+ var me = this;
+
+ if (me.shadow && me.shadowDisabled) {
+ me.shadowDisabled = false;
+ me.shadowOffset = me.lastShadowOffset;
+ delete me.lastShadowOffset;
+ if (show) {
+ me.sync(true);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * <p>Synchronize this Layer's associated elements, the shadow, and possibly the shim.</p>
+ * <p>This code can execute repeatedly in milliseconds,
+ * eg: dragging a Component configured liveDrag: true, or which has no ghost method
+ * so code size was sacrificed for efficiency (e.g. no getBox/setBox, no XY calls)</p>
+ * @param {Boolean} doShow Pass true to ensure that the shadow is shown.
+ */
+ sync: function(doShow) {
+ var me = this,
+ shadow = me.shadow,
+ shadowPos, shimStyle, shadowSize;
+
+ if (!me.updating && me.isVisible() && (shadow || me.useShim)) {
+ var shim = me.getShim(),
+ l = me.getLeft(true),
+ t = me.getTop(true),
+ w = me.dom.offsetWidth,
+ h = me.dom.offsetHeight,
+ shimIndex;
+
+ if (shadow && !me.shadowDisabled) {
+ if (doShow && !shadow.isVisible()) {
+ shadow.show(me);
+ } else {
+ shadow.realign(l, t, w, h);
+ }
+ if (shim) {
+ // TODO: Determine how the shims zIndex is above the layer zIndex at this point
+ shimIndex = shim.getStyle('z-index');
+ if (shimIndex > me.zindex) {
+ me.shim.setStyle('z-index', me.zindex - 2);
+ }
+ shim.show();
+ // fit the shim behind the shadow, so it is shimmed too
+ if (shadow.isVisible()) {
+ shadowPos = shadow.el.getXY();
+ shimStyle = shim.dom.style;
+ shadowSize = shadow.el.getSize();
+ if (Ext.supports.CSS3BoxShadow) {
+ shadowSize.height += 6;
+ shadowSize.width += 4;
+ shadowPos[0] -= 2;
+ shadowPos[1] -= 4;
+ }
+ shimStyle.left = (shadowPos[0]) + 'px';
+ shimStyle.top = (shadowPos[1]) + 'px';
+ shimStyle.width = (shadowSize.width) + 'px';
+ shimStyle.height = (shadowSize.height) + 'px';
+ } else {
+ shim.setSize(w, h);
+ shim.setLeftTop(l, t);
+ }
+ }
+ } else if (shim) {
+ // TODO: Determine how the shims zIndex is above the layer zIndex at this point
+ shimIndex = shim.getStyle('z-index');
+ if (shimIndex > me.zindex) {
+ me.shim.setStyle('z-index', me.zindex - 2);
+ }
+ shim.show();
+ shim.setSize(w, h);
+ shim.setLeftTop(l, t);
+ }
+ }
+ return me;
+ },
+
+ remove: function() {
+ this.hideUnders();
+ this.callParent();
+ },
+
+ // private
+ beginUpdate: function() {
+ this.updating = true;
+ },
+
+ // private
+ endUpdate: function() {
+ this.updating = false;
+ this.sync(true);
+ },
+
+ // private
+ hideUnders: function() {
+ if (this.shadow) {
+ this.shadow.hide();
+ }
+ this.hideShim();
+ },
+
+ // private
+ constrainXY: function() {
+ if (this.constrain) {
+ var vw = Ext.Element.getViewWidth(),
+ vh = Ext.Element.getViewHeight(),
+ s = Ext.getDoc().getScroll(),
+ xy = this.getXY(),
+ x = xy[0],
+ y = xy[1],
+ so = this.shadowOffset,
+ w = this.dom.offsetWidth + so,
+ h = this.dom.offsetHeight + so,
+ moved = false; // only move it if it needs it
+ // first validate right/bottom
+ if ((x + w) > vw + s.left) {
+ x = vw - w - so;
+ moved = true;
+ }
+ if ((y + h) > vh + s.top) {
+ y = vh - h - so;
+ moved = true;
+ }
+ // then make sure top/left isn't negative
+ if (x < s.left) {
+ x = s.left;
+ moved = true;
+ }
+ if (y < s.top) {
+ y = s.top;
+ moved = true;
+ }
+ if (moved) {
+ Ext.Layer.superclass.setXY.call(this, [x, y]);
+ this.sync();
+ }
+ }
+ return this;
+ },
+
+ getConstrainOffset: function() {
+ return this.shadowOffset;
+ },
+
+ // overridden Element method
+ setVisible: function(visible, animate, duration, callback, easing) {
+ var me = this,
+ cb;
+
+ // post operation processing
+ cb = function() {
+ if (visible) {
+ me.sync(true);
+ }
+ if (callback) {
+ callback();
+ }
+ };
+
+ // Hide shadow and shim if hiding
+ if (!visible) {
+ me.hideUnders(true);
+ }
+ me.callParent([visible, animate, duration, callback, easing]);
+ if (!animate) {
+ cb();
+ }
+ return me;
+ },
+
+ // private
+ beforeFx: function() {
+ this.beforeAction();
+ return this.callParent(arguments);
+ },
+
+ // private
+ afterFx: function() {
+ this.callParent(arguments);
+ this.sync(this.isVisible());
+ },
+
+ // private
+ beforeAction: function() {
+ if (!this.updating && this.shadow) {
+ this.shadow.hide();
+ }
+ },
+
+ // overridden Element method
+ setLeft: function(left) {
+ this.callParent(arguments);
+ return this.sync();
+ },
+
+ setTop: function(top) {
+ this.callParent(arguments);
+ return this.sync();
+ },
+
+ setLeftTop: function(left, top) {
+ this.callParent(arguments);
+ return this.sync();
+ },
+
+ setXY: function(xy, animate, duration, callback, easing) {
+ var me = this;
+
+ // Callback will restore shadow state and call the passed callback
+ callback = me.createCB(callback);
+
+ me.fixDisplay();
+ me.beforeAction();
+ me.callParent([xy, animate, duration, callback, easing]);
+ if (!animate) {
+ callback();
+ }
+ return me;
+ },
+
+ // private
+ createCB: function(callback) {
+ var me = this,
+ showShadow = me.shadow && me.shadow.isVisible();
+
+ return function() {
+ me.constrainXY();
+ me.sync(showShadow);
+ if (callback) {
+ callback();
+ }
+ };
+ },
+
+ // overridden Element method
+ setX: function(x, animate, duration, callback, easing) {
+ this.setXY([x, this.getY()], animate, duration, callback, easing);
+ return this;
+ },
+
+ // overridden Element method
+ setY: function(y, animate, duration, callback, easing) {
+ this.setXY([this.getX(), y], animate, duration, callback, easing);
+ return this;
+ },
+
+ // overridden Element method
+ setSize: function(w, h, animate, duration, callback, easing) {
+ var me = this;
+
+ // Callback will restore shadow state and call the passed callback
+ callback = me.createCB(callback);
+
+ me.beforeAction();
+ me.callParent([w, h, animate, duration, callback, easing]);
+ if (!animate) {
+ callback();
+ }
+ return me;
+ },
+
+ // overridden Element method
+ setWidth: function(w, animate, duration, callback, easing) {
+ var me = this;
+
+ // Callback will restore shadow state and call the passed callback
+ callback = me.createCB(callback);
+
+ me.beforeAction();
+ me.callParent([w, animate, duration, callback, easing]);
+ if (!animate) {
+ callback();
+ }
+ return me;
+ },
+
+ // overridden Element method
+ setHeight: function(h, animate, duration, callback, easing) {
+ var me = this;
+
+ // Callback will restore shadow state and call the passed callback
+ callback = me.createCB(callback);
+
+ me.beforeAction();
+ me.callParent([h, animate, duration, callback, easing]);
+ if (!animate) {
+ callback();
+ }
+ return me;
+ },
+
+ // overridden Element method
+ setBounds: function(x, y, width, height, animate, duration, callback, easing) {
+ var me = this;
+
+ // Callback will restore shadow state and call the passed callback
+ callback = me.createCB(callback);
+
+ me.beforeAction();
+ if (!animate) {
+ Ext.Layer.superclass.setXY.call(me, [x, y]);
+ Ext.Layer.superclass.setSize.call(me, width, height);
+ callback();
+ } else {
+ me.callParent([x, y, width, height, animate, duration, callback, easing]);
+ }
+ return me;
+ },
+
+ /**
+ * <p>Sets the z-index of this layer and adjusts any shadow and shim z-indexes. The layer z-index is automatically
+ * incremented depending upon the presence of a shim or a shadow in so that it always shows above those two associated elements.</p>
+ * <p>Any shim, will be assigned the passed z-index. A shadow will be assigned the next highet z-index, and the Layer's
+ * element will receive the highest z-index.
+ * @param {Number} zindex The new z-index to set
+ * @return {Ext.Layer} The Layer
+ */
+ setZIndex: function(zindex) {
+ var me = this;
+
+ me.zindex = zindex;
+ if (me.getShim()) {
+ me.shim.setStyle('z-index', zindex++);
+ }
+ if (me.shadow) {
+ me.shadow.setZIndex(zindex++);
+ }
+ return me.setStyle('z-index', zindex);
+ },
+
+ setOpacity: function(opacity){
+ if (this.shadow) {
+ this.shadow.setOpacity(opacity);
+ }
+ return this.callParent(arguments);
+ }
+});
+
+/**
+ * @class Ext.layout.component.ProgressBar
+ * @extends Ext.layout.component.Component
+ * @private
+ */
+
+Ext.define('Ext.layout.component.ProgressBar', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.progressbar'],
+
+ extend: 'Ext.layout.component.Component',
+
+ /* End Definitions */
+
+ type: 'progressbar',
+
+ onLayout: function(width, height) {
+ var me = this,
+ owner = me.owner,
+ textEl = owner.textEl;
+
+ me.setElementSize(owner.el, width, height);
+ textEl.setWidth(owner.el.getWidth(true));
+
+ me.callParent([width, height]);
+
+ owner.updateProgress(owner.value);
+ }
+});
+/**
+ * An updateable progress bar component. The progress bar supports two different modes: manual and automatic.
+ *
+ * In manual mode, you are responsible for showing, updating (via {@link #updateProgress}) and clearing the progress bar
+ * as needed from your own code. This method is most appropriate when you want to show progress throughout an operation
+ * that has predictable points of interest at which you can update the control.
+ *
+ * In automatic mode, you simply call {@link #wait} and let the progress bar run indefinitely, only clearing it once the
+ * operation is complete. You can optionally have the progress bar wait for a specific amount of time and then clear
+ * itself. Automatic mode is most appropriate for timed operations or asynchronous operations in which you have no need
+ * for indicating intermediate progress.
+ *
+ * @example
+ * var p = Ext.create('Ext.ProgressBar', {
+ * renderTo: Ext.getBody(),
+ * width: 300
+ * });
+ *
+ * // Wait for 5 seconds, then update the status el (progress bar will auto-reset)
+ * p.wait({
+ * interval: 500, //bar will move fast!
+ * duration: 50000,
+ * increment: 15,
+ * text: 'Updating...',
+ * scope: this,
+ * fn: function(){
+ * p.updateText('Done!');
+ * }
+ * });
+ */
+Ext.define('Ext.ProgressBar', {
+ extend: 'Ext.Component',
+ alias: 'widget.progressbar',
+
+ requires: [
+ 'Ext.Template',
+ 'Ext.CompositeElement',
+ 'Ext.TaskManager',
+ 'Ext.layout.component.ProgressBar'
+ ],
+
+ uses: ['Ext.fx.Anim'],
+
+ /**
+ * @cfg {Number} [value=0]
+ * A floating point value between 0 and 1 (e.g., .5)
+ */
+
+ /**
+ * @cfg {String} [text='']
+ * The progress bar text (defaults to '')
+ */
+
+ /**
+ * @cfg {String/HTMLElement/Ext.Element} textEl
+ * The element to render the progress text to (defaults to the progress bar's internal text element)
+ */
+
+ /**
+ * @cfg {String} id
+ * The progress bar element's id (defaults to an auto-generated id)
+ */
+
+ /**
+ * @cfg {String} [baseCls='x-progress']
+ * The base CSS class to apply to the progress bar's wrapper element.
+ */
+ baseCls: Ext.baseCSSPrefix + 'progress',
+
+ config: {
+ /**
+ * @cfg {Boolean} animate
+ * True to animate the progress bar during transitions
+ */
+ animate: false,
+
+ /**
+ * @cfg {String} text
+ * The text shown in the progress bar
+ */
+ text: ''
+ },
+
+ // private
+ waitTimer: null,
+
+ renderTpl: [
+ '<div class="{baseCls}-text {baseCls}-text-back">',
+ '<div> </div>',
+ '</div>',
+ '<div id="{id}-bar" class="{baseCls}-bar">',
+ '<div class="{baseCls}-text">',
+ '<div> </div>',
+ '</div>',
+ '</div>'
+ ],
+
+ componentLayout: 'progressbar',
+
+ // private
+ initComponent: function() {
+ this.callParent();
+
+ this.addChildEls('bar');
+
+ this.addEvents(
+ /**
+ * @event update
+ * Fires after each update interval
+ * @param {Ext.ProgressBar} this
+ * @param {Number} value The current progress value
+ * @param {String} text The current progress text
+ */
+ "update"
+ );
+ },
+
+ afterRender : function() {
+ var me = this;
+
+ // This produces a composite w/2 el's (which is why we cannot use childEls or
+ // renderSelectors):
+ me.textEl = me.textEl ? Ext.get(me.textEl) : me.el.select('.' + me.baseCls + '-text');
+
+ me.callParent(arguments);
+
+ if (me.value) {
+ me.updateProgress(me.value, me.text);
+ }
+ else {
+ me.updateText(me.text);
+ }
+ },
+
+ /**
+ * Updates the progress bar value, and optionally its text. If the text argument is not specified, any existing text
+ * value will be unchanged. To blank out existing text, pass ''. Note that even if the progress bar value exceeds 1,
+ * it will never automatically reset -- you are responsible for determining when the progress is complete and
+ * calling {@link #reset} to clear and/or hide the control.
+ * @param {Number} [value=0] A floating point value between 0 and 1 (e.g., .5)
+ * @param {String} [text=''] The string to display in the progress text element
+ * @param {Boolean} [animate=false] Whether to animate the transition of the progress bar. If this value is not
+ * specified, the default for the class is used
+ * @return {Ext.ProgressBar} this
+ */
+ updateProgress: function(value, text, animate) {
+ var me = this,
+ newWidth;
+
+ me.value = value || 0;
+ if (text) {
+ me.updateText(text);
+ }
+ if (me.rendered && !me.isDestroyed) {
+ if (me.isVisible(true)) {
+ newWidth = Math.floor(me.value * me.el.getWidth(true));
+ if (Ext.isForcedBorderBox) {
+ newWidth += me.bar.getBorderWidth("lr");
+ }
+ if (animate === true || (animate !== false && me.animate)) {
+ me.bar.stopAnimation();
+ me.bar.animate(Ext.apply({
+ to: {
+ width: newWidth + 'px'
+ }
+ }, me.animate));
+ } else {
+ me.bar.setWidth(newWidth);
+ }
+ } else {
+ // force a layout when we're visible again
+ me.doComponentLayout();
+ }
+ }
+ me.fireEvent('update', me, me.value, text);
+ return me;
+ },
+
+ /**
+ * Updates the progress bar text. If specified, textEl will be updated, otherwise the progress bar itself will
+ * display the updated text.
+ * @param {String} [text=''] The string to display in the progress text element
+ * @return {Ext.ProgressBar} this
+ */
+ updateText: function(text) {
+ var me = this;
+
+ me.text = text;
+ if (me.rendered) {
+ me.textEl.update(me.text);
+ }
+ return me;
+ },
+
+ applyText : function(text) {
+ this.updateText(text);
+ },
+
+ /**
+ * Initiates an auto-updating progress bar. A duration can be specified, in which case the progress bar will
+ * automatically reset after a fixed amount of time and optionally call a callback function if specified. If no
+ * duration is passed in, then the progress bar will run indefinitely and must be manually cleared by calling
+ * {@link #reset}.
+ *
+ * Example usage:
+ *
+ * var p = new Ext.ProgressBar({
+ * renderTo: 'my-el'
+ * });
+ *
+ * //Wait for 5 seconds, then update the status el (progress bar will auto-reset)
+ * var p = Ext.create('Ext.ProgressBar', {
+ * renderTo: Ext.getBody(),
+ * width: 300
+ * });
+ *
+ * //Wait for 5 seconds, then update the status el (progress bar will auto-reset)
+ * p.wait({
+ * interval: 500, //bar will move fast!
+ * duration: 50000,
+ * increment: 15,
+ * text: 'Updating...',
+ * scope: this,
+ * fn: function(){
+ * p.updateText('Done!');
+ * }
+ * });
+ *
+ * //Or update indefinitely until some async action completes, then reset manually
+ * p.wait();
+ * myAction.on('complete', function(){
+ * p.reset();
+ * p.updateText('Done!');
+ * });
+ *
+ * @param {Object} config (optional) Configuration options
+ * @param {Number} config.duration The length of time in milliseconds that the progress bar should
+ * run before resetting itself (defaults to undefined, in which case it will run indefinitely
+ * until reset is called)
+ * @param {Number} config.interval The length of time in milliseconds between each progress update
+ * (defaults to 1000 ms)
+ * @param {Boolean} config.animate Whether to animate the transition of the progress bar. If this
+ * value is not specified, the default for the class is used.
+ * @param {Number} config.increment The number of progress update segments to display within the
+ * progress bar (defaults to 10). If the bar reaches the end and is still updating, it will
+ * automatically wrap back to the beginning.
+ * @param {String} config.text Optional text to display in the progress bar element (defaults to '').
+ * @param {Function} config.fn A callback function to execute after the progress bar finishes auto-
+ * updating. The function will be called with no arguments. This function will be ignored if
+ * duration is not specified since in that case the progress bar can only be stopped programmatically,
+ * so any required function should be called by the same code after it resets the progress bar.
+ * @param {Object} config.scope The scope that is passed to the callback function (only applies when
+ * duration and fn are both passed).
+ * @return {Ext.ProgressBar} this
+ */
+ wait: function(o) {
+ var me = this;
+
+ if (!me.waitTimer) {
+ scope = me;
+ o = o || {};
+ me.updateText(o.text);
+ me.waitTimer = Ext.TaskManager.start({
+ run: function(i){
+ var inc = o.increment || 10;
+ i -= 1;
+ me.updateProgress(((((i+inc)%inc)+1)*(100/inc))*0.01, null, o.animate);
+ },
+ interval: o.interval || 1000,
+ duration: o.duration,
+ onStop: function(){
+ if (o.fn) {
+ o.fn.apply(o.scope || me);
+ }
+ me.reset();
+ },
+ scope: scope
+ });
+ }
+ return me;
+ },
+
+ /**
+ * Returns true if the progress bar is currently in a {@link #wait} operation
+ * @return {Boolean} True if waiting, else false
+ */
+ isWaiting: function(){
+ return this.waitTimer !== null;
+ },
+
+ /**
+ * Resets the progress bar value to 0 and text to empty string. If hide = true, the progress bar will also be hidden
+ * (using the {@link #hideMode} property internally).
+ * @param {Boolean} [hide=false] True to hide the progress bar.
+ * @return {Ext.ProgressBar} this
+ */
+ reset: function(hide){
+ var me = this;
+
+ me.updateProgress(0);
+ me.clearTimer();
+ if (hide === true) {
+ me.hide();
+ }
+ return me;
+ },
+
+ // private
+ clearTimer: function(){
+ var me = this;
+
+ if (me.waitTimer) {
+ me.waitTimer.onStop = null; //prevent recursion
+ Ext.TaskManager.stop(me.waitTimer);
+ me.waitTimer = null;
+ }
+ },
+
+ onDestroy: function(){
+ var me = this;
+
+ me.clearTimer();
+ if (me.rendered) {
+ if (me.textEl.isComposite) {
+ me.textEl.clear();
+ }
+ Ext.destroyMembers(me, 'textEl', 'progressBar');
+ }
+ me.callParent();
+ }
+});
+
+/**
+ * Private utility class that manages the internal Shadow cache
+ * @private
+ */
+Ext.define('Ext.ShadowPool', {
+ singleton: true,
+ requires: ['Ext.DomHelper'],
+
+ markup: function() {
+ if (Ext.supports.CSS3BoxShadow) {
+ return '<div class="' + Ext.baseCSSPrefix + 'css-shadow" role="presentation"></div>';
+ } else if (Ext.isIE) {
+ return '<div class="' + Ext.baseCSSPrefix + 'ie-shadow" role="presentation"></div>';
+ } else {
+ return '<div class="' + Ext.baseCSSPrefix + 'frame-shadow" role="presentation">' +
+ '<div class="xst" role="presentation">' +
+ '<div class="xstl" role="presentation"></div>' +
+ '<div class="xstc" role="presentation"></div>' +
+ '<div class="xstr" role="presentation"></div>' +
+ '</div>' +
+ '<div class="xsc" role="presentation">' +
+ '<div class="xsml" role="presentation"></div>' +
+ '<div class="xsmc" role="presentation"></div>' +
+ '<div class="xsmr" role="presentation"></div>' +
+ '</div>' +
+ '<div class="xsb" role="presentation">' +
+ '<div class="xsbl" role="presentation"></div>' +
+ '<div class="xsbc" role="presentation"></div>' +
+ '<div class="xsbr" role="presentation"></div>' +
+ '</div>' +
+ '</div>';
+ }
+ }(),
+
+ shadows: [],
+
+ pull: function() {
+ var sh = this.shadows.shift();
+ if (!sh) {
+ sh = Ext.get(Ext.DomHelper.insertHtml("beforeBegin", document.body.firstChild, this.markup));
+ sh.autoBoxAdjust = false;
+ }
+ return sh;
+ },
+
+ push: function(sh) {
+ this.shadows.push(sh);
+ },
+
+ reset: function() {
+ Ext.Array.each(this.shadows, function(shadow) {
+ shadow.remove();
+ });
+ this.shadows = [];
+ }
+});
+/**
+ * @class Ext.Shadow
+ * Simple class that can provide a shadow effect for any element. Note that the element MUST be absolutely positioned,
+ * and the shadow does not provide any shimming. This should be used only in simple cases -- for more advanced
+ * functionality that can also provide the same shadow effect, see the {@link Ext.Layer} class.
+ */
+Ext.define('Ext.Shadow', {
+ requires: ['Ext.ShadowPool'],
+
+ /**
+ * Creates new Shadow.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ var me = this,
+ adjusts = {
+ h: 0
+ },
+ offset,
+ rad;
+
+ Ext.apply(me, config);
+ if (!Ext.isString(me.mode)) {
+ me.mode = me.defaultMode;
+ }
+ offset = me.offset;
+ rad = Math.floor(offset / 2);
+ me.opacity = 50;
+ switch (me.mode.toLowerCase()) {
+ // all this hideous nonsense calculates the various offsets for shadows
+ case "drop":
+ if (Ext.supports.CSS3BoxShadow) {
+ adjusts.w = adjusts.h = -offset;
+ adjusts.l = adjusts.t = offset;
+ } else {
+ adjusts.w = 0;
+ adjusts.l = adjusts.t = offset;
+ adjusts.t -= 1;
+ if (Ext.isIE) {
+ adjusts.l -= offset + rad;
+ adjusts.t -= offset + rad;
+ adjusts.w -= rad;
+ adjusts.h -= rad;
+ adjusts.t += 1;
+ }
+ }
+ break;
+ case "sides":
+ if (Ext.supports.CSS3BoxShadow) {
+ adjusts.h -= offset;
+ adjusts.t = offset;
+ adjusts.l = adjusts.w = 0;
+ } else {
+ adjusts.w = (offset * 2);
+ adjusts.l = -offset;
+ adjusts.t = offset - 1;
+ if (Ext.isIE) {
+ adjusts.l -= (offset - rad);
+ adjusts.t -= offset + rad;
+ adjusts.l += 1;
+ adjusts.w -= (offset - rad) * 2;
+ adjusts.w -= rad + 1;
+ adjusts.h -= 1;
+ }
+ }
+ break;
+ case "frame":
+ if (Ext.supports.CSS3BoxShadow) {
+ adjusts.l = adjusts.w = adjusts.t = 0;
+ } else {
+ adjusts.w = adjusts.h = (offset * 2);
+ adjusts.l = adjusts.t = -offset;
+ adjusts.t += 1;
+ adjusts.h -= 2;
+ if (Ext.isIE) {
+ adjusts.l -= (offset - rad);
+ adjusts.t -= (offset - rad);
+ adjusts.l += 1;
+ adjusts.w -= (offset + rad + 1);
+ adjusts.h -= (offset + rad);
+ adjusts.h += 1;
+ }
+ break;
+ }
+ }
+ me.adjusts = adjusts;
+ },
+
+ /**
+ * @cfg {String} mode
+ * The shadow display mode. Supports the following options:<div class="mdetail-params"><ul>
+ * <li><b><tt>sides</tt></b> : Shadow displays on both sides and bottom only</li>
+ * <li><b><tt>frame</tt></b> : Shadow displays equally on all four sides</li>
+ * <li><b><tt>drop</tt></b> : Traditional bottom-right drop shadow</li>
+ * </ul></div>
+ */
+ /**
+ * @cfg {Number} offset
+ * The number of pixels to offset the shadow from the element
+ */
+ offset: 4,
+
+ // private
+ defaultMode: "drop",
+
+ /**
+ * Displays the shadow under the target element
+ * @param {String/HTMLElement/Ext.Element} targetEl The id or element under which the shadow should display
+ */
+ show: function(target) {
+ var me = this,
+ index;
+
+ target = Ext.get(target);
+ if (!me.el) {
+ me.el = Ext.ShadowPool.pull();
+ if (me.el.dom.nextSibling != target.dom) {
+ me.el.insertBefore(target);
+ }
+ }
+ index = (parseInt(target.getStyle("z-index"), 10) - 1) || 0;
+ me.el.setStyle("z-index", me.zIndex || index);
+ if (Ext.isIE && !Ext.supports.CSS3BoxShadow) {
+ me.el.dom.style.filter = "progid:DXImageTransform.Microsoft.alpha(opacity=" + me.opacity + ") progid:DXImageTransform.Microsoft.Blur(pixelradius=" + (me.offset) + ")";
+ }
+ me.realign(
+ target.getLeft(true),
+ target.getTop(true),
+ target.dom.offsetWidth,
+ target.dom.offsetHeight
+ );
+ me.el.dom.style.display = "block";
+ },
+
+ /**
+ * Returns true if the shadow is visible, else false
+ */
+ isVisible: function() {
+ return this.el ? true: false;
+ },
+
+ /**
+ * Direct alignment when values are already available. Show must be called at least once before
+ * calling this method to ensure it is initialized.
+ * @param {Number} left The target element left position
+ * @param {Number} top The target element top position
+ * @param {Number} width The target element width
+ * @param {Number} height The target element height
+ */
+ realign: function(l, t, targetWidth, targetHeight) {
+ if (!this.el) {
+ return;
+ }
+ var adjusts = this.adjusts,
+ d = this.el.dom,
+ targetStyle = d.style,
+ shadowWidth,
+ shadowHeight,
+ cn,
+ sww,
+ sws,
+ shs;
+
+ targetStyle.left = (l + adjusts.l) + "px";
+ targetStyle.top = (t + adjusts.t) + "px";
+ shadowWidth = Math.max(targetWidth + adjusts.w, 0);
+ shadowHeight = Math.max(targetHeight + adjusts.h, 0);
+ sws = shadowWidth + "px";
+ shs = shadowHeight + "px";
+ if (targetStyle.width != sws || targetStyle.height != shs) {
+ targetStyle.width = sws;
+ targetStyle.height = shs;
+ if (Ext.supports.CSS3BoxShadow) {
+ targetStyle.boxShadow = '0 0 ' + this.offset + 'px 0 #888';
+ } else {
+
+ // Adjust the 9 point framed element to poke out on the required sides
+ if (!Ext.isIE) {
+ cn = d.childNodes;
+ sww = Math.max(0, (shadowWidth - 12)) + "px";
+ cn[0].childNodes[1].style.width = sww;
+ cn[1].childNodes[1].style.width = sww;
+ cn[2].childNodes[1].style.width = sww;
+ cn[1].style.height = Math.max(0, (shadowHeight - 12)) + "px";
+ }
+ }
+ }
+ },
+
+ /**
+ * Hides this shadow
+ */
+ hide: function() {
+ var me = this;
+
+ if (me.el) {
+ me.el.dom.style.display = "none";
+ Ext.ShadowPool.push(me.el);
+ delete me.el;
+ }
+ },
+
+ /**
+ * Adjust the z-index of this shadow
+ * @param {Number} zindex The new z-index
+ */
+ setZIndex: function(z) {
+ this.zIndex = z;
+ if (this.el) {
+ this.el.setStyle("z-index", z);
+ }
+ },
+
+ /**
+ * Sets the opacity of the shadow
+ * @param {Number} opacity The opacity
+ */
+ setOpacity: function(opacity){
+ if (this.el) {
+ if (Ext.isIE && !Ext.supports.CSS3BoxShadow) {
+ opacity = Math.floor(opacity * 100 / 2) / 100;
+ }
+ this.opacity = opacity;
+ this.el.setOpacity(opacity);
+ }
+ }
+});
+/**
+ * A split button that provides a built-in dropdown arrow that can fire an event separately from the default click event
+ * of the button. Typically this would be used to display a dropdown menu that provides additional options to the
+ * primary button action, but any custom handler can provide the arrowclick implementation. Example usage:
+ *
+ * @example
+ * // display a dropdown menu:
+ * Ext.create('Ext.button.Split', {
+ * renderTo: Ext.getBody(),
+ * text: 'Options',
+ * // handle a click on the button itself
+ * handler: function() {
+ * alert("The button was clicked");
+ * },
+ * menu: new Ext.menu.Menu({
+ * items: [
+ * // these will render as dropdown menu items when the arrow is clicked:
+ * {text: 'Item 1', handler: function(){ alert("Item 1 clicked"); }},
+ * {text: 'Item 2', handler: function(){ alert("Item 2 clicked"); }}
+ * ]
+ * })
+ * });
+ *
+ * Instead of showing a menu, you can provide any type of custom functionality you want when the dropdown
+ * arrow is clicked:
+ *
+ * Ext.create('Ext.button.Split', {
+ * renderTo: 'button-ct',
+ * text: 'Options',
+ * handler: optionsHandler,
+ * arrowHandler: myCustomHandler
+ * });
+ *
+ */
+Ext.define('Ext.button.Split', {
+
+ /* Begin Definitions */
+ alias: 'widget.splitbutton',
+
+ extend: 'Ext.button.Button',
+ alternateClassName: 'Ext.SplitButton',
+ /* End Definitions */
+
+ /**
+ * @cfg {Function} arrowHandler
+ * A function called when the arrow button is clicked (can be used instead of click event)
+ */
+ /**
+ * @cfg {String} arrowTooltip
+ * The title attribute of the arrow
+ */
+
+ // private
+ arrowCls : 'split',
+ split : true,
+
+ // private
+ initComponent : function(){
+ this.callParent();
+ /**
+ * @event arrowclick
+ * Fires when this button's arrow is clicked.
+ * @param {Ext.button.Split} this
+ * @param {Event} e The click event
+ */
+ this.addEvents("arrowclick");
+ },
+
+ /**
+ * Sets this button's arrow click handler.
+ * @param {Function} handler The function to call when the arrow is clicked
+ * @param {Object} scope (optional) Scope for the function passed above
+ */
+ setArrowHandler : function(handler, scope){
+ this.arrowHandler = handler;
+ this.scope = scope;
+ },
+
+ // private
+ onClick : function(e, t) {
+ var me = this;
+
+ e.preventDefault();
+ if (!me.disabled) {
+ if (me.overMenuTrigger) {
+ me.maybeShowMenu();
+ me.fireEvent("arrowclick", me, e);
+ if (me.arrowHandler) {
+ me.arrowHandler.call(me.scope || me, me, e);
+ }
+ } else {
+ me.doToggle();
+ me.fireHandler();
+ }
+ }
+ }
+});
+/**
+ * A specialized SplitButton that contains a menu of {@link Ext.menu.CheckItem} elements. The button automatically
+ * cycles through each menu item on click, raising the button's {@link #change} event (or calling the button's
+ * {@link #changeHandler} function, if supplied) for the active menu item. Clicking on the arrow section of the
+ * button displays the dropdown menu just like a normal SplitButton. Example usage:
+ *
+ * @example
+ * Ext.create('Ext.button.Cycle', {
+ * showText: true,
+ * prependText: 'View as ',
+ * renderTo: Ext.getBody(),
+ * menu: {
+ * id: 'view-type-menu',
+ * items: [{
+ * text: 'text only',
+ * iconCls: 'view-text',
+ * checked: true
+ * },{
+ * text: 'HTML',
+ * iconCls: 'view-html'
+ * }]
+ * },
+ * changeHandler: function(cycleBtn, activeItem) {
+ * Ext.Msg.alert('Change View', activeItem.text);
+ * }
+ * });
+ */
+Ext.define('Ext.button.Cycle', {
+
+ /* Begin Definitions */
+
+ alias: 'widget.cycle',
+
+ extend: 'Ext.button.Split',
+ alternateClassName: 'Ext.CycleButton',
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Object[]} items
+ * An array of {@link Ext.menu.CheckItem} **config** objects to be used when creating the button's menu items (e.g.,
+ * `{text:'Foo', iconCls:'foo-icon'}`)
+ *
+ * @deprecated 4.0 Use the {@link #menu} config instead. All menu items will be created as
+ * {@link Ext.menu.CheckItem CheckItems}.
+ */
+ /**
+ * @cfg {Boolean} [showText=false]
+ * True to display the active item's text as the button text. The Button will show its
+ * configured {@link #text} if this config is omitted.
+ */
+ /**
+ * @cfg {String} [prependText='']
+ * A static string to prepend before the active item's text when displayed as the button's text (only applies when
+ * showText = true).
+ */
+ /**
+ * @cfg {Function} changeHandler
+ * A callback function that will be invoked each time the active menu item in the button's menu has changed. If this
+ * callback is not supplied, the SplitButton will instead fire the {@link #change} event on active item change. The
+ * changeHandler function will be called with the following argument list: (SplitButton this, Ext.menu.CheckItem
+ * item)
+ */
+ /**
+ * @cfg {String} forceIcon
+ * A css class which sets an image to be used as the static icon for this button. This icon will always be displayed
+ * regardless of which item is selected in the dropdown list. This overrides the default behavior of changing the
+ * button's icon to match the selected item's icon on change.
+ */
+ /**
+ * @property {Ext.menu.Menu} menu
+ * The {@link Ext.menu.Menu Menu} object used to display the {@link Ext.menu.CheckItem CheckItems} representing the
+ * available choices.
+ */
+
+ // private
+ getButtonText: function(item) {
+ var me = this,
+ text = '';
+
+ if (item && me.showText === true) {
+ if (me.prependText) {
+ text += me.prependText;
+ }
+ text += item.text;
+ return text;
+ }
+ return me.text;
+ },
+
+ /**
+ * Sets the button's active menu item.
+ * @param {Ext.menu.CheckItem} item The item to activate
+ * @param {Boolean} [suppressEvent=false] True to prevent the button's change event from firing.
+ */
+ setActiveItem: function(item, suppressEvent) {
+ var me = this;
+
+ if (!Ext.isObject(item)) {
+ item = me.menu.getComponent(item);
+ }
+ if (item) {
+ if (!me.rendered) {
+ me.text = me.getButtonText(item);
+ me.iconCls = item.iconCls;
+ } else {
+ me.setText(me.getButtonText(item));
+ me.setIconCls(item.iconCls);
+ }
+ me.activeItem = item;
+ if (!item.checked) {
+ item.setChecked(true, false);
+ }
+ if (me.forceIcon) {
+ me.setIconCls(me.forceIcon);
+ }
+ if (!suppressEvent) {
+ me.fireEvent('change', me, item);
+ }
+ }
+ },
+
+ /**
+ * Gets the currently active menu item.
+ * @return {Ext.menu.CheckItem} The active item
+ */
+ getActiveItem: function() {
+ return this.activeItem;
+ },
+
+ // private
+ initComponent: function() {
+ var me = this,
+ checked = 0,
+ items;
+
+ me.addEvents(
+ /**
+ * @event change
+ * Fires after the button's active menu item has changed. Note that if a {@link #changeHandler} function is
+ * set on this CycleButton, it will be called instead on active item change and this change event will not
+ * be fired.
+ * @param {Ext.button.Cycle} this
+ * @param {Ext.menu.CheckItem} item The menu item that was selected
+ */
+ "change"
+ );
+
+ if (me.changeHandler) {
+ me.on('change', me.changeHandler, me.scope || me);
+ delete me.changeHandler;
+ }
+
+ // Allow them to specify a menu config which is a standard Button config.
+ // Remove direct use of "items" in 5.0.
+ items = (me.menu.items||[]).concat(me.items||[]);
+ me.menu = Ext.applyIf({
+ cls: Ext.baseCSSPrefix + 'cycle-menu',
+ items: []
+ }, me.menu);
+
+ // Convert all items to CheckItems
+ Ext.each(items, function(item, i) {
+ item = Ext.applyIf({
+ group: me.id,
+ itemIndex: i,
+ checkHandler: me.checkHandler,
+ scope: me,
+ checked: item.checked || false
+ }, item);
+ me.menu.items.push(item);
+ if (item.checked) {
+ checked = i;
+ }
+ });
+ me.itemCount = me.menu.items.length;
+ me.callParent(arguments);
+ me.on('click', me.toggleSelected, me);
+ me.setActiveItem(checked, me);
+
+ // If configured with a fixed width, the cycling will center a different child item's text each click. Prevent this.
+ if (me.width && me.showText) {
+ me.addCls(Ext.baseCSSPrefix + 'cycle-fixed-width');
+ }
+ },
+
+ // private
+ checkHandler: function(item, pressed) {
+ if (pressed) {
+ this.setActiveItem(item);
+ }
+ },
+
+ /**
+ * This is normally called internally on button click, but can be called externally to advance the button's active
+ * item programmatically to the next one in the menu. If the current item is the last one in the menu the active
+ * item will be set to the first item in the menu.
+ */
+ toggleSelected: function() {
+ var me = this,
+ m = me.menu,
+ checkItem;
+
+ checkItem = me.activeItem.next(':not([disabled])') || m.items.getAt(0);
+ checkItem.setChecked(true);
+ }
+});
+/**
+ * Provides a container for arranging a group of related Buttons in a tabular manner.
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Panel with ButtonGroup',
+ * width: 300,
+ * height:200,
+ * renderTo: document.body,
+ * bodyPadding: 10,
+ * html: 'HTML Panel Content',
+ * tbar: [{
+ * xtype: 'buttongroup',
+ * columns: 3,
+ * title: 'Clipboard',
+ * items: [{
+ * text: 'Paste',
+ * scale: 'large',
+ * rowspan: 3,
+ * iconCls: 'add',
+ * iconAlign: 'top',
+ * cls: 'btn-as-arrow'
+ * },{
+ * xtype:'splitbutton',
+ * text: 'Menu Button',
+ * scale: 'large',
+ * rowspan: 3,
+ * iconCls: 'add',
+ * iconAlign: 'top',
+ * arrowAlign:'bottom',
+ * menu: [{ text: 'Menu Item 1' }]
+ * },{
+ * xtype:'splitbutton', text: 'Cut', iconCls: 'add16', menu: [{text: 'Cut Menu Item'}]
+ * },{
+ * text: 'Copy', iconCls: 'add16'
+ * },{
+ * text: 'Format', iconCls: 'add16'
+ * }]
+ * }]
+ * });
+ *
+ */
+Ext.define('Ext.container.ButtonGroup', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.buttongroup',
+ alternateClassName: 'Ext.ButtonGroup',
+
+ /**
+ * @cfg {Number} columns The `columns` configuration property passed to the
+ * {@link #layout configured layout manager}. See {@link Ext.layout.container.Table#columns}.
+ */
+
+ /**
+ * @cfg {String} baseCls Defaults to <tt>'x-btn-group'</tt>. See {@link Ext.panel.Panel#baseCls}.
+ */
+ baseCls: Ext.baseCSSPrefix + 'btn-group',
+
+ /**
+ * @cfg {Object} layout Defaults to <tt>'table'</tt>. See {@link Ext.container.Container#layout}.
+ */
+ layout: {
+ type: 'table'
+ },
+
+ defaultType: 'button',
+
+ /**
+ * @cfg {Boolean} frame Defaults to <tt>true</tt>. See {@link Ext.panel.Panel#frame}.
+ */
+ frame: true,
+
+ frameHeader: false,
+
+ internalDefaults: {removeMode: 'container', hideParent: true},
+
+ initComponent : function(){
+ // Copy the component's columns config to the layout if specified
+ var me = this,
+ cols = me.columns;
+
+ me.noTitleCls = me.baseCls + '-notitle';
+ if (cols) {
+ me.layout = Ext.apply({}, {columns: cols}, me.layout);
+ }
+
+ if (!me.title) {
+ me.addCls(me.noTitleCls);
+ }
+ me.callParent(arguments);
+ },
+
+ afterLayout: function() {
+ var me = this;
+
+ me.callParent(arguments);
+
+ // Pugly hack for a pugly browser:
+ // If not an explicitly set width, then size the width to match the inner table
+ if (me.layout.table && (Ext.isIEQuirks || Ext.isIE6) && !me.width) {
+ var t = me.getTargetEl();
+ t.setWidth(me.layout.table.offsetWidth + t.getPadding('lr'));
+ }
+ },
+
+ afterRender: function() {
+ var me = this;
+
+ //we need to add an addition item in here so the ButtonGroup title is centered
+ if (me.header) {
+ // Header text cannot flex, but must be natural size if it's being centered
+ delete me.header.items.items[0].flex;
+
+ // For Centering, surround the text with two flex:1 spacers.
+ me.suspendLayout = true;
+ me.header.insert(1, {
+ xtype: 'component',
+ ui : me.ui,
+ flex : 1
+ });
+ me.header.insert(0, {
+ xtype: 'component',
+ ui : me.ui,
+ flex : 1
+ });
+ me.suspendLayout = false;
+ }
+
+ me.callParent(arguments);
+ },
+
+ // private
+ onBeforeAdd: function(component) {
+ if (component.is('button')) {
+ component.ui = component.ui + '-toolbar';
+ }
+ this.callParent(arguments);
+ },
+
+ //private
+ applyDefaults: function(c) {
+ if (!Ext.isString(c)) {
+ c = this.callParent(arguments);
+ var d = this.internalDefaults;
+ if (c.events) {
+ Ext.applyIf(c.initialConfig, d);
+ Ext.apply(c, d);
+ } else {
+ Ext.applyIf(c, d);
+ }
+ }
+ return c;
+ }
+
+ /**
+ * @cfg {Array} tools @hide
+ */
+ /**
+ * @cfg {Boolean} collapsible @hide
+ */
+ /**
+ * @cfg {Boolean} collapseMode @hide
+ */
+ /**
+ * @cfg {Boolean} animCollapse @hide
+ */
+ /**
+ * @cfg {Boolean} closable @hide
+ */
+});
+
+/**
+ * A specialized container representing the viewable application area (the browser viewport).
+ *
+ * The Viewport renders itself to the document body, and automatically sizes itself to the size of
+ * the browser viewport and manages window resizing. There may only be one Viewport created
+ * in a page.
+ *
+ * Like any {@link Ext.container.Container Container}, a Viewport will only perform sizing and positioning
+ * on its child Components if you configure it with a {@link #layout}.
+ *
+ * A Common layout used with Viewports is {@link Ext.layout.container.Border border layout}, but if the
+ * required layout is simpler, a different layout should be chosen.
+ *
+ * For example, to simply make a single child item occupy all available space, use
+ * {@link Ext.layout.container.Fit fit layout}.
+ *
+ * To display one "active" item at full size from a choice of several child items, use
+ * {@link Ext.layout.container.Card card layout}.
+ *
+ * Inner layouts are available by virtue of the fact that all {@link Ext.panel.Panel Panel}s
+ * added to the Viewport, either through its {@link #items}, or through the items, or the {@link #add}
+ * method of any of its child Panels may themselves have a layout.
+ *
+ * The Viewport does not provide scrolling, so child Panels within the Viewport should provide
+ * for scrolling if needed using the {@link #autoScroll} config.
+ *
+ * An example showing a classic application border layout:
+ *
+ * @example
+ * Ext.create('Ext.container.Viewport', {
+ * layout: 'border',
+ * items: [{
+ * region: 'north',
+ * html: '<h1 class="x-panel-header">Page Title</h1>',
+ * autoHeight: true,
+ * border: false,
+ * margins: '0 0 5 0'
+ * }, {
+ * region: 'west',
+ * collapsible: true,
+ * title: 'Navigation',
+ * width: 150
+ * // could use a TreePanel or AccordionLayout for navigational items
+ * }, {
+ * region: 'south',
+ * title: 'South Panel',
+ * collapsible: true,
+ * html: 'Information goes here',
+ * split: true,
+ * height: 100,
+ * minHeight: 100
+ * }, {
+ * region: 'east',
+ * title: 'East Panel',
+ * collapsible: true,
+ * split: true,
+ * width: 150
+ * }, {
+ * region: 'center',
+ * xtype: 'tabpanel', // TabPanel itself has no title
+ * activeTab: 0, // First tab active by default
+ * items: {
+ * title: 'Default Tab',
+ * html: 'The first tab\'s content. Others may be added dynamically'
+ * }
+ * }]
+ * });
+ */
+Ext.define('Ext.container.Viewport', {
+ extend: 'Ext.container.Container',
+ alias: 'widget.viewport',
+ requires: ['Ext.EventManager'],
+ alternateClassName: 'Ext.Viewport',
+
+ // Privatize config options which, if used, would interfere with the
+ // correct operation of the Viewport as the sole manager of the
+ // layout of the document body.
+
+ /**
+ * @cfg {String/HTMLElement/Ext.Element} applyTo
+ * Not applicable.
+ */
+
+ /**
+ * @cfg {Boolean} allowDomMove
+ * Not applicable.
+ */
+
+ /**
+ * @cfg {Boolean} hideParent
+ * Not applicable.
+ */
+
+ /**
+ * @cfg {String/HTMLElement/Ext.Element} renderTo
+ * Not applicable. Always renders to document body.
+ */
+
+ /**
+ * @cfg {Boolean} hideParent
+ * Not applicable.
+ */
+
+ /**
+ * @cfg {Number} height
+ * Not applicable. Sets itself to viewport width.
+ */
+
+ /**
+ * @cfg {Number} width
+ * Not applicable. Sets itself to viewport height.
+ */
+
+ /**
+ * @cfg {Boolean} autoHeight
+ * Not applicable.
+ */
+
+ /**
+ * @cfg {Boolean} autoWidth
+ * Not applicable.
+ */
+
+ /**
+ * @cfg {Boolean} deferHeight
+ * Not applicable.
+ */
+
+ /**
+ * @cfg {Boolean} monitorResize
+ * Not applicable.
+ */
+
+ isViewport: true,
+
+ ariaRole: 'application',
+
+ initComponent : function() {
+ var me = this,
+ html = Ext.fly(document.body.parentNode),
+ el;
+ me.callParent(arguments);
+ html.addCls(Ext.baseCSSPrefix + 'viewport');
+ if (me.autoScroll) {
+ html.setStyle('overflow', 'auto');
+ }
+ me.el = el = Ext.getBody();
+ el.setHeight = Ext.emptyFn;
+ el.setWidth = Ext.emptyFn;
+ el.setSize = Ext.emptyFn;
+ el.dom.scroll = 'no';
+ me.allowDomMove = false;
+ Ext.EventManager.onWindowResize(me.fireResize, me);
+ me.renderTo = me.el;
+ me.width = Ext.Element.getViewportWidth();
+ me.height = Ext.Element.getViewportHeight();
+ },
+
+ fireResize : function(w, h){
+ // setSize is the single entry point to layouts
+ this.setSize(w, h);
+ }
+});
+
+/*
+ * This is a derivative of the similarly named class in the YUI Library.
+ * The original license:
+ * Copyright (c) 2006, Yahoo! Inc. All rights reserved.
+ * Code licensed under the BSD License:
+ * http://developer.yahoo.net/yui/license.txt
+ */
+
+
+/**
+ * @class Ext.dd.DDTarget
+ * @extends Ext.dd.DragDrop
+ * A DragDrop implementation that does not move, but can be a drop
+ * target. You would get the same result by simply omitting implementation
+ * for the event callbacks, but this way we reduce the processing cost of the
+ * event listener and the callbacks.
+ */
+Ext.define('Ext.dd.DDTarget', {
+ extend: 'Ext.dd.DragDrop',
+
+ /**
+ * Creates new DDTarget.
+ * @param {String} id the id of the element that is a drop target
+ * @param {String} sGroup the group of related DragDrop objects
+ * @param {Object} config an object containing configurable attributes.
+ * Valid properties for DDTarget in addition to those in DragDrop: none.
+ */
+ constructor: function(id, sGroup, config) {
+ if (id) {
+ this.initTarget(id, sGroup, config);
+ }
+ },
+
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ getDragEl: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ isValidHandleChild: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ startDrag: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ endDrag: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ onDrag: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ onDragDrop: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ onDragEnter: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ onDragOut: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ onDragOver: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ onInvalidDrop: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ onMouseDown: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ onMouseUp: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ setXConstraint: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ setYConstraint: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ resetConstraints: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ clearConstraints: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ clearTicks: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ setInitPosition: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ setDragElId: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ setHandleElId: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ setOuterHandleElId: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ addInvalidHandleClass: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ addInvalidHandleId: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ addInvalidHandleType: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ removeInvalidHandleClass: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ removeInvalidHandleId: Ext.emptyFn,
+ /**
+ * @hide
+ * Overridden and disabled. A DDTarget does not support being dragged.
+ * @method
+ */
+ removeInvalidHandleType: Ext.emptyFn,
+
+ toString: function() {
+ return ("DDTarget " + this.id);
+ }
+});
+/**
+ * @class Ext.dd.DragTracker
+ * A DragTracker listens for drag events on an Element and fires events at the start and end of the drag,
+ * as well as during the drag. This is useful for components such as {@link Ext.slider.Multi}, where there is
+ * an element that can be dragged around to change the Slider's value.
+ * DragTracker provides a series of template methods that should be overridden to provide functionality
+ * in response to detected drag operations. These are onBeforeStart, onStart, onDrag and onEnd.
+ * See {@link Ext.slider.Multi}'s initEvents function for an example implementation.
+ */
+Ext.define('Ext.dd.DragTracker', {
+
+ uses: ['Ext.util.Region'],
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ /**
+ * @property {Boolean} active
+ * Read-only property indicated whether the user is currently dragging this
+ * tracker.
+ */
+ active: false,
+
+ /**
+ * @property {HTMLElement} dragTarget
+ * <p><b>Only valid during drag operations. Read-only.</b></p>
+ * <p>The element being dragged.</p>
+ * <p>If the {@link #delegate} option is used, this will be the delegate element which was mousedowned.</p>
+ */
+
+ /**
+ * @cfg {Boolean} trackOver
+ * <p>Defaults to <code>false</code>. Set to true to fire mouseover and mouseout events when the mouse enters or leaves the target element.</p>
+ * <p>This is implicitly set when an {@link #overCls} is specified.</p>
+ * <b>If the {@link #delegate} option is used, these events fire only when a delegate element is entered of left.</b>.
+ */
+ trackOver: false,
+
+ /**
+ * @cfg {String} overCls
+ * <p>A CSS class to add to the DragTracker's target element when the element (or, if the {@link #delegate} option is used,
+ * when a delegate element) is mouseovered.</p>
+ * <b>If the {@link #delegate} option is used, these events fire only when a delegate element is entered of left.</b>.
+ */
+
+ /**
+ * @cfg {Ext.util.Region/Ext.Element} constrainTo
+ * <p>A {@link Ext.util.Region Region} (Or an element from which a Region measurement will be read) which is used to constrain
+ * the result of the {@link #getOffset} call.</p>
+ * <p>This may be set any time during the DragTracker's lifecycle to set a dynamic constraining region.</p>
+ */
+
+ /**
+ * @cfg {Number} tolerance
+ * Number of pixels the drag target must be moved before dragging is
+ * considered to have started. Defaults to <code>5</code>.
+ */
+ tolerance: 5,
+
+ /**
+ * @cfg {Boolean/Number} autoStart
+ * Defaults to <code>false</code>. Specify <code>true</code> to defer trigger start by 1000 ms.
+ * Specify a Number for the number of milliseconds to defer trigger start.
+ */
+ autoStart: false,
+
+ /**
+ * @cfg {String} delegate
+ * Optional. <p>A {@link Ext.DomQuery DomQuery} selector which identifies child elements within the DragTracker's encapsulating
+ * Element which are the tracked elements. This limits tracking to only begin when the matching elements are mousedowned.</p>
+ * <p>This may also be a specific child element within the DragTracker's encapsulating element to use as the tracked element.</p>
+ */
+
+ /**
+ * @cfg {Boolean} preventDefault
+ * Specify <code>false</code> to enable default actions on onMouseDown events. Defaults to <code>true</code>.
+ */
+
+ /**
+ * @cfg {Boolean} stopEvent
+ * Specify <code>true</code> to stop the <code>mousedown</code> event from bubbling to outer listeners from the target element (or its delegates). Defaults to <code>false</code>.
+ */
+
+ constructor : function(config){
+ Ext.apply(this, config);
+ this.addEvents(
+ /**
+ * @event mouseover <p><b>Only available when {@link #trackOver} is <code>true</code></b></p>
+ * <p>Fires when the mouse enters the DragTracker's target element (or if {@link #delegate} is
+ * used, when the mouse enters a delegate element).</p>
+ * @param {Object} this
+ * @param {Object} e event object
+ * @param {HTMLElement} target The element mouseovered.
+ */
+ 'mouseover',
+
+ /**
+ * @event mouseout <p><b>Only available when {@link #trackOver} is <code>true</code></b></p>
+ * <p>Fires when the mouse exits the DragTracker's target element (or if {@link #delegate} is
+ * used, when the mouse exits a delegate element).</p>
+ * @param {Object} this
+ * @param {Object} e event object
+ */
+ 'mouseout',
+
+ /**
+ * @event mousedown <p>Fires when the mouse button is pressed down, but before a drag operation begins. The
+ * drag operation begins after either the mouse has been moved by {@link #tolerance} pixels, or after
+ * the {@link #autoStart} timer fires.</p>
+ * <p>Return false to veto the drag operation.</p>
+ * @param {Object} this
+ * @param {Object} e event object
+ */
+ 'mousedown',
+
+ /**
+ * @event mouseup
+ * @param {Object} this
+ * @param {Object} e event object
+ */
+ 'mouseup',
+
+ /**
+ * @event mousemove Fired when the mouse is moved. Returning false cancels the drag operation.
+ * @param {Object} this
+ * @param {Object} e event object
+ */
+ 'mousemove',
+
+ /**
+ * @event beforestart
+ * @param {Object} this
+ * @param {Object} e event object
+ */
+ 'beforedragstart',
+
+ /**
+ * @event dragstart
+ * @param {Object} this
+ * @param {Object} e event object
+ */
+ 'dragstart',
+
+ /**
+ * @event dragend
+ * @param {Object} this
+ * @param {Object} e event object
+ */
+ 'dragend',
+
+ /**
+ * @event drag
+ * @param {Object} this
+ * @param {Object} e event object
+ */
+ 'drag'
+ );
+
+ this.dragRegion = Ext.create('Ext.util.Region', 0,0,0,0);
+
+ if (this.el) {
+ this.initEl(this.el);
+ }
+
+ // Dont pass the config so that it is not applied to 'this' again
+ this.mixins.observable.constructor.call(this);
+ if (this.disabled) {
+ this.disable();
+ }
+
+ },
+
+ /**
+ * Initializes the DragTracker on a given element.
+ * @param {Ext.Element/HTMLElement} el The element
+ */
+ initEl: function(el) {
+ this.el = Ext.get(el);
+
+ // The delegate option may also be an element on which to listen
+ this.handle = Ext.get(this.delegate);
+
+ // If delegate specified an actual element to listen on, we do not use the delegate listener option
+ this.delegate = this.handle ? undefined : this.delegate;
+
+ if (!this.handle) {
+ this.handle = this.el;
+ }
+
+ // Add a mousedown listener which reacts only on the elements targeted by the delegate config.
+ // We process mousedown to begin tracking.
+ this.mon(this.handle, {
+ mousedown: this.onMouseDown,
+ delegate: this.delegate,
+ scope: this
+ });
+
+ // If configured to do so, track mouse entry and exit into the target (or delegate).
+ // The mouseover and mouseout CANNOT be replaced with mouseenter and mouseleave
+ // because delegate cannot work with those pseudoevents. Entry/exit checking is done in the handler.
+ if (this.trackOver || this.overCls) {
+ this.mon(this.handle, {
+ mouseover: this.onMouseOver,
+ mouseout: this.onMouseOut,
+ delegate: this.delegate,
+ scope: this
+ });
+ }
+ },
+
+ disable: function() {
+ this.disabled = true;
+ },
+
+ enable: function() {
+ this.disabled = false;
+ },
+
+ destroy : function() {
+ this.clearListeners();
+ delete this.el;
+ },
+
+ // When the pointer enters a tracking element, fire a mouseover if the mouse entered from outside.
+ // This is mouseenter functionality, but we cannot use mouseenter because we are using "delegate" to filter mouse targets
+ onMouseOver: function(e, target) {
+ var me = this;
+ if (!me.disabled) {
+ if (Ext.EventManager.contains(e) || me.delegate) {
+ me.mouseIsOut = false;
+ if (me.overCls) {
+ me.el.addCls(me.overCls);
+ }
+ me.fireEvent('mouseover', me, e, me.delegate ? e.getTarget(me.delegate, target) : me.handle);
+ }
+ }
+ },
+
+ // When the pointer exits a tracking element, fire a mouseout.
+ // This is mouseleave functionality, but we cannot use mouseleave because we are using "delegate" to filter mouse targets
+ onMouseOut: function(e) {
+ if (this.mouseIsDown) {
+ this.mouseIsOut = true;
+ } else {
+ if (this.overCls) {
+ this.el.removeCls(this.overCls);
+ }
+ this.fireEvent('mouseout', this, e);
+ }
+ },
+
+ onMouseDown: function(e, target){
+ // If this is disabled, or the mousedown has been processed by an upstream DragTracker, return
+ if (this.disabled ||e.dragTracked) {
+ return;
+ }
+
+ // This information should be available in mousedown listener and onBeforeStart implementations
+ this.dragTarget = this.delegate ? target : this.handle.dom;
+ this.startXY = this.lastXY = e.getXY();
+ this.startRegion = Ext.fly(this.dragTarget).getRegion();
+
+ if (this.fireEvent('mousedown', this, e) === false ||
+ this.fireEvent('beforedragstart', this, e) === false ||
+ this.onBeforeStart(e) === false) {
+ return;
+ }
+
+ // Track when the mouse is down so that mouseouts while the mouse is down are not processed.
+ // The onMouseOut method will only ever be called after mouseup.
+ this.mouseIsDown = true;
+
+ // Flag for downstream DragTracker instances that the mouse is being tracked.
+ e.dragTracked = true;
+
+ if (this.preventDefault !== false) {
+ e.preventDefault();
+ }
+ Ext.getDoc().on({
+ scope: this,
+ mouseup: this.onMouseUp,
+ mousemove: this.onMouseMove,
+ selectstart: this.stopSelect
+ });
+ if (this.autoStart) {
+ this.timer = Ext.defer(this.triggerStart, this.autoStart === true ? 1000 : this.autoStart, this, [e]);
+ }
+ },
+
+ onMouseMove: function(e, target){
+ // BrowserBug: IE hack to see if button was released outside of window.
+ // Needed in IE6-9 in quirks and strictmode
+ if (this.active && Ext.isIE && !e.browserEvent.button) {
+ e.preventDefault();
+ this.onMouseUp(e);
+ return;
+ }
+
+ e.preventDefault();
+ var xy = e.getXY(),
+ s = this.startXY;
+
+ this.lastXY = xy;
+ if (!this.active) {
+ if (Math.max(Math.abs(s[0]-xy[0]), Math.abs(s[1]-xy[1])) > this.tolerance) {
+ this.triggerStart(e);
+ } else {
+ return;
+ }
+ }
+
+ // Returning false from a mousemove listener deactivates
+ if (this.fireEvent('mousemove', this, e) === false) {
+ this.onMouseUp(e);
+ } else {
+ this.onDrag(e);
+ this.fireEvent('drag', this, e);
+ }
+ },
+
+ onMouseUp: function(e) {
+ // Clear the flag which ensures onMouseOut fires only after the mouse button
+ // is lifted if the mouseout happens *during* a drag.
+ this.mouseIsDown = false;
+
+ // If we mouseouted the el *during* the drag, the onMouseOut method will not have fired. Ensure that it gets processed.
+ if (this.mouseIsOut) {
+ this.mouseIsOut = false;
+ this.onMouseOut(e);
+ }
+ e.preventDefault();
+ this.fireEvent('mouseup', this, e);
+ this.endDrag(e);
+ },
+
+ /**
+ * @private
+ * Stop the drag operation, and remove active mouse listeners.
+ */
+ endDrag: function(e) {
+ var doc = Ext.getDoc(),
+ wasActive = this.active;
+
+ doc.un('mousemove', this.onMouseMove, this);
+ doc.un('mouseup', this.onMouseUp, this);
+ doc.un('selectstart', this.stopSelect, this);
+ this.clearStart();
+ this.active = false;
+ if (wasActive) {
+ this.onEnd(e);
+ this.fireEvent('dragend', this, e);
+ }
+ // Private property calculated when first required and only cached during a drag
+ delete this._constrainRegion;
+
+ // Remove flag from event singleton. Using "Ext.EventObject" here since "endDrag" is called directly in some cases without an "e" param
+ delete Ext.EventObject.dragTracked;
+ },
+
+ triggerStart: function(e) {
+ this.clearStart();
+ this.active = true;
+ this.onStart(e);
+ this.fireEvent('dragstart', this, e);
+ },
+
+ clearStart : function() {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ delete this.timer;
+ }
+ },
+
+ stopSelect : function(e) {
+ e.stopEvent();
+ return false;
+ },
+
+ /**
+ * Template method which should be overridden by each DragTracker instance. Called when the user first clicks and
+ * holds the mouse button down. Return false to disallow the drag
+ * @param {Ext.EventObject} e The event object
+ * @template
+ */
+ onBeforeStart : function(e) {
+
+ },
+
+ /**
+ * Template method which should be overridden by each DragTracker instance. Called when a drag operation starts
+ * (e.g. the user has moved the tracked element beyond the specified tolerance)
+ * @param {Ext.EventObject} e The event object
+ * @template
+ */
+ onStart : function(xy) {
+
+ },
+
+ /**
+ * Template method which should be overridden by each DragTracker instance. Called whenever a drag has been detected.
+ * @param {Ext.EventObject} e The event object
+ * @template
+ */
+ onDrag : function(e) {
+
+ },
+
+ /**
+ * Template method which should be overridden by each DragTracker instance. Called when a drag operation has been completed
+ * (e.g. the user clicked and held the mouse down, dragged the element and then released the mouse button)
+ * @param {Ext.EventObject} e The event object
+ * @template
+ */
+ onEnd : function(e) {
+
+ },
+
+ /**
+ * </p>Returns the drag target. This is usually the DragTracker's encapsulating element.</p>
+ * <p>If the {@link #delegate} option is being used, this may be a child element which matches the
+ * {@link #delegate} selector.</p>
+ * @return {Ext.Element} The element currently being tracked.
+ */
+ getDragTarget : function(){
+ return this.dragTarget;
+ },
+
+ /**
+ * @private
+ * @returns {Ext.Element} The DragTracker's encapsulating element.
+ */
+ getDragCt : function(){
+ return this.el;
+ },
+
+ /**
+ * @private
+ * Return the Region into which the drag operation is constrained.
+ * Either the XY pointer itself can be constrained, or the dragTarget element
+ * The private property _constrainRegion is cached until onMouseUp
+ */
+ getConstrainRegion: function() {
+ if (this.constrainTo) {
+ if (this.constrainTo instanceof Ext.util.Region) {
+ return this.constrainTo;
+ }
+ if (!this._constrainRegion) {
+ this._constrainRegion = Ext.fly(this.constrainTo).getViewRegion();
+ }
+ } else {
+ if (!this._constrainRegion) {
+ this._constrainRegion = this.getDragCt().getViewRegion();
+ }
+ }
+ return this._constrainRegion;
+ },
+
+ getXY : function(constrain){
+ return constrain ? this.constrainModes[constrain](this, this.lastXY) : this.lastXY;
+ },
+
+ /**
+ * Returns the X, Y offset of the current mouse position from the mousedown point.
+ *
+ * This method may optionally constrain the real offset values, and returns a point coerced in one
+ * of two modes:
+ *
+ * - `point`
+ * The current mouse position is coerced into the constrainRegion and the resulting position is returned.
+ * - `dragTarget`
+ * The new {@link Ext.util.Region Region} of the {@link #getDragTarget dragTarget} is calculated
+ * based upon the current mouse position, and then coerced into the constrainRegion. The returned
+ * mouse position is then adjusted by the same delta as was used to coerce the region.\
+ *
+ * @param constrainMode {String} (Optional) If omitted the true mouse position is returned. May be passed
+ * as `point` or `dragTarget`. See above.
+ * @returns {Number[]} The `X, Y` offset from the mousedown point, optionally constrained.
+ */
+ getOffset : function(constrain){
+ var xy = this.getXY(constrain),
+ s = this.startXY;
+
+ return [xy[0]-s[0], xy[1]-s[1]];
+ },
+
+ constrainModes: {
+ // Constrain the passed point to within the constrain region
+ point: function(me, xy) {
+ var dr = me.dragRegion,
+ constrainTo = me.getConstrainRegion();
+
+ // No constraint
+ if (!constrainTo) {
+ return xy;
+ }
+
+ dr.x = dr.left = dr[0] = dr.right = xy[0];
+ dr.y = dr.top = dr[1] = dr.bottom = xy[1];
+ dr.constrainTo(constrainTo);
+
+ return [dr.left, dr.top];
+ },
+
+ // Constrain the dragTarget to within the constrain region. Return the passed xy adjusted by the same delta.
+ dragTarget: function(me, xy) {
+ var s = me.startXY,
+ dr = me.startRegion.copy(),
+ constrainTo = me.getConstrainRegion(),
+ adjust;
+
+ // No constraint
+ if (!constrainTo) {
+ return xy;
+ }
+
+ // See where the passed XY would put the dragTarget if translated by the unconstrained offset.
+ // If it overflows, we constrain the passed XY to bring the potential
+ // region back within the boundary.
+ dr.translateBy(xy[0]-s[0], xy[1]-s[1]);
+
+ // Constrain the X coordinate by however much the dragTarget overflows
+ if (dr.right > constrainTo.right) {
+ xy[0] += adjust = (constrainTo.right - dr.right); // overflowed the right
+ dr.left += adjust;
+ }
+ if (dr.left < constrainTo.left) {
+ xy[0] += (constrainTo.left - dr.left); // overflowed the left
+ }
+
+ // Constrain the Y coordinate by however much the dragTarget overflows
+ if (dr.bottom > constrainTo.bottom) {
+ xy[1] += adjust = (constrainTo.bottom - dr.bottom); // overflowed the bottom
+ dr.top += adjust;
+ }
+ if (dr.top < constrainTo.top) {
+ xy[1] += (constrainTo.top - dr.top); // overflowed the top
+ }
+ return xy;
+ }
+ }
+});
+/**
+ * @class Ext.dd.DragZone
+ * @extends Ext.dd.DragSource
+ * <p>This class provides a container DD instance that allows dragging of multiple child source nodes.</p>
+ * <p>This class does not move the drag target nodes, but a proxy element which may contain
+ * any DOM structure you wish. The DOM element to show in the proxy is provided by either a
+ * provided implementation of {@link #getDragData}, or by registered draggables registered with {@link Ext.dd.Registry}</p>
+ * <p>If you wish to provide draggability for an arbitrary number of DOM nodes, each of which represent some
+ * application object (For example nodes in a {@link Ext.view.View DataView}) then use of this class
+ * is the most efficient way to "activate" those nodes.</p>
+ * <p>By default, this class requires that draggable child nodes are registered with {@link Ext.dd.Registry}.
+ * However a simpler way to allow a DragZone to manage any number of draggable elements is to configure
+ * the DragZone with an implementation of the {@link #getDragData} method which interrogates the passed
+ * mouse event to see if it has taken place within an element, or class of elements. This is easily done
+ * by using the event's {@link Ext.EventObject#getTarget getTarget} method to identify a node based on a
+ * {@link Ext.DomQuery} selector. For example, to make the nodes of a DataView draggable, use the following
+ * technique. Knowledge of the use of the DataView is required:</p><pre><code>
+myDataView.on('render', function(v) {
+ myDataView.dragZone = new Ext.dd.DragZone(v.getEl(), {
+
+// On receipt of a mousedown event, see if it is within a DataView node.
+// Return a drag data object if so.
+ getDragData: function(e) {
+
+// Use the DataView's own itemSelector (a mandatory property) to
+// test if the mousedown is within one of the DataView's nodes.
+ var sourceEl = e.getTarget(v.itemSelector, 10);
+
+// If the mousedown is within a DataView node, clone the node to produce
+// a ddel element for use by the drag proxy. Also add application data
+// to the returned data object.
+ if (sourceEl) {
+ d = sourceEl.cloneNode(true);
+ d.id = Ext.id();
+ return {
+ ddel: d,
+ sourceEl: sourceEl,
+ repairXY: Ext.fly(sourceEl).getXY(),
+ sourceStore: v.store,
+ draggedRecord: v.{@link Ext.view.View#getRecord getRecord}(sourceEl)
+ }
+ }
+ },
+
+// Provide coordinates for the proxy to slide back to on failed drag.
+// This is the original XY coordinates of the draggable element captured
+// in the getDragData method.
+ getRepairXY: function() {
+ return this.dragData.repairXY;
+ }
+ });
+});</code></pre>
+ * See the {@link Ext.dd.DropZone DropZone} documentation for details about building a DropZone which
+ * cooperates with this DragZone.
+ */
+Ext.define('Ext.dd.DragZone', {
+
+ extend: 'Ext.dd.DragSource',
+
+ /**
+ * Creates new DragZone.
+ * @param {String/HTMLElement/Ext.Element} el The container element or ID of it.
+ * @param {Object} config
+ */
+ constructor : function(el, config){
+ this.callParent([el, config]);
+ if (this.containerScroll) {
+ Ext.dd.ScrollManager.register(this.el);
+ }
+ },
+
+ /**
+ * This property contains the data representing the dragged object. This data is set up by the implementation
+ * of the {@link #getDragData} method. It must contain a <tt>ddel</tt> property, but can contain
+ * any other data according to the application's needs.
+ * @type Object
+ * @property dragData
+ */
+
+ /**
+ * @cfg {Boolean} containerScroll True to register this container with the Scrollmanager
+ * for auto scrolling during drag operations.
+ */
+
+ /**
+ * Called when a mousedown occurs in this container. Looks in {@link Ext.dd.Registry}
+ * for a valid target to drag based on the mouse down. Override this method
+ * to provide your own lookup logic (e.g. finding a child by class name). Make sure your returned
+ * object has a "ddel" attribute (with an HTML Element) for other functions to work.
+ * @param {Event} e The mouse down event
+ * @return {Object} The dragData
+ */
+ getDragData : function(e){
+ return Ext.dd.Registry.getHandleFromEvent(e);
+ },
+
+ /**
+ * Called once drag threshold has been reached to initialize the proxy element. By default, it clones the
+ * this.dragData.ddel
+ * @param {Number} x The x position of the click on the dragged object
+ * @param {Number} y The y position of the click on the dragged object
+ * @return {Boolean} true to continue the drag, false to cancel
+ */
+ onInitDrag : function(x, y){
+ this.proxy.update(this.dragData.ddel.cloneNode(true));
+ this.onStartDrag(x, y);
+ return true;
+ },
+
+ /**
+ * Called after a repair of an invalid drop. By default, highlights this.dragData.ddel
+ */
+ afterRepair : function(){
+ var me = this;
+ if (Ext.enableFx) {
+ Ext.fly(me.dragData.ddel).highlight(me.repairHighlightColor);
+ }
+ me.dragging = false;
+ },
+
+ /**
+ * Called before a repair of an invalid drop to get the XY to animate to. By default returns
+ * the XY of this.dragData.ddel
+ * @param {Event} e The mouse up event
+ * @return {Number[]} The xy location (e.g. [100, 200])
+ */
+ getRepairXY : function(e){
+ return Ext.Element.fly(this.dragData.ddel).getXY();
+ },
+
+ destroy : function(){
+ this.callParent();
+ if (this.containerScroll) {
+ Ext.dd.ScrollManager.unregister(this.el);
+ }
+ }
+});
+
+/**
+ * @class Ext.dd.ScrollManager
+ * <p>Provides automatic scrolling of overflow regions in the page during drag operations.</p>
+ * <p>The ScrollManager configs will be used as the defaults for any scroll container registered with it,
+ * but you can also override most of the configs per scroll container by adding a
+ * <tt>ddScrollConfig</tt> object to the target element that contains these properties: {@link #hthresh},
+ * {@link #vthresh}, {@link #increment} and {@link #frequency}. Example usage:
+ * <pre><code>
+var el = Ext.get('scroll-ct');
+el.ddScrollConfig = {
+ vthresh: 50,
+ hthresh: -1,
+ frequency: 100,
+ increment: 200
+};
+Ext.dd.ScrollManager.register(el);
+</code></pre>
+ * Note: This class is designed to be used in "Point Mode
+ * @singleton
+ */
+Ext.define('Ext.dd.ScrollManager', {
+ singleton: true,
+ requires: [
+ 'Ext.dd.DragDropManager'
+ ],
+
+ constructor: function() {
+ var ddm = Ext.dd.DragDropManager;
+ ddm.fireEvents = Ext.Function.createSequence(ddm.fireEvents, this.onFire, this);
+ ddm.stopDrag = Ext.Function.createSequence(ddm.stopDrag, this.onStop, this);
+ this.doScroll = Ext.Function.bind(this.doScroll, this);
+ this.ddmInstance = ddm;
+ this.els = {};
+ this.dragEl = null;
+ this.proc = {};
+ },
+
+ onStop: function(e){
+ var sm = Ext.dd.ScrollManager;
+ sm.dragEl = null;
+ sm.clearProc();
+ },
+
+ triggerRefresh: function() {
+ if (this.ddmInstance.dragCurrent) {
+ this.ddmInstance.refreshCache(this.ddmInstance.dragCurrent.groups);
+ }
+ },
+
+ doScroll: function() {
+ if (this.ddmInstance.dragCurrent) {
+ var proc = this.proc,
+ procEl = proc.el,
+ ddScrollConfig = proc.el.ddScrollConfig,
+ inc = ddScrollConfig ? ddScrollConfig.increment : this.increment;
+
+ if (!this.animate) {
+ if (procEl.scroll(proc.dir, inc)) {
+ this.triggerRefresh();
+ }
+ } else {
+ procEl.scroll(proc.dir, inc, true, this.animDuration, this.triggerRefresh);
+ }
+ }
+ },
+
+ clearProc: function() {
+ var proc = this.proc;
+ if (proc.id) {
+ clearInterval(proc.id);
+ }
+ proc.id = 0;
+ proc.el = null;
+ proc.dir = "";
+ },
+
+ startProc: function(el, dir) {
+ this.clearProc();
+ this.proc.el = el;
+ this.proc.dir = dir;
+ var group = el.ddScrollConfig ? el.ddScrollConfig.ddGroup : undefined,
+ freq = (el.ddScrollConfig && el.ddScrollConfig.frequency)
+ ? el.ddScrollConfig.frequency
+ : this.frequency;
+
+ if (group === undefined || this.ddmInstance.dragCurrent.ddGroup == group) {
+ this.proc.id = setInterval(this.doScroll, freq);
+ }
+ },
+
+ onFire: function(e, isDrop) {
+ if (isDrop || !this.ddmInstance.dragCurrent) {
+ return;
+ }
+ if (!this.dragEl || this.dragEl != this.ddmInstance.dragCurrent) {
+ this.dragEl = this.ddmInstance.dragCurrent;
+ // refresh regions on drag start
+ this.refreshCache();
+ }
+
+ var xy = e.getXY(),
+ pt = e.getPoint(),
+ proc = this.proc,
+ els = this.els;
+
+ for (var id in els) {
+ var el = els[id], r = el._region;
+ var c = el.ddScrollConfig ? el.ddScrollConfig : this;
+ if (r && r.contains(pt) && el.isScrollable()) {
+ if (r.bottom - pt.y <= c.vthresh) {
+ if(proc.el != el){
+ this.startProc(el, "down");
+ }
+ return;
+ }else if (r.right - pt.x <= c.hthresh) {
+ if (proc.el != el) {
+ this.startProc(el, "left");
+ }
+ return;
+ } else if(pt.y - r.top <= c.vthresh) {
+ if (proc.el != el) {
+ this.startProc(el, "up");
+ }
+ return;
+ } else if(pt.x - r.left <= c.hthresh) {
+ if (proc.el != el) {
+ this.startProc(el, "right");
+ }
+ return;
+ }
+ }
+ }
+ this.clearProc();
+ },
+
+ /**
+ * Registers new overflow element(s) to auto scroll
+ * @param {String/HTMLElement/Ext.Element/String[]/HTMLElement[]/Ext.Element[]} el
+ * The id of or the element to be scrolled or an array of either
+ */
+ register : function(el){
+ if (Ext.isArray(el)) {
+ for(var i = 0, len = el.length; i < len; i++) {
+ this.register(el[i]);
+ }
+ } else {
+ el = Ext.get(el);
+ this.els[el.id] = el;
+ }
+ },
+
+ /**
+ * Unregisters overflow element(s) so they are no longer scrolled
+ * @param {String/HTMLElement/Ext.Element/String[]/HTMLElement[]/Ext.Element[]} el
+ * The id of or the element to be removed or an array of either
+ */
+ unregister : function(el){
+ if(Ext.isArray(el)){
+ for (var i = 0, len = el.length; i < len; i++) {
+ this.unregister(el[i]);
+ }
+ }else{
+ el = Ext.get(el);
+ delete this.els[el.id];
+ }
+ },
+
+ /**
+ * The number of pixels from the top or bottom edge of a container the pointer needs to be to
+ * trigger scrolling
+ * @type Number
+ */
+ vthresh : 25,
+ /**
+ * The number of pixels from the right or left edge of a container the pointer needs to be to
+ * trigger scrolling
+ * @type Number
+ */
+ hthresh : 25,
+
+ /**
+ * The number of pixels to scroll in each scroll increment
+ * @type Number
+ */
+ increment : 100,
+
+ /**
+ * The frequency of scrolls in milliseconds
+ * @type Number
+ */
+ frequency : 500,
+
+ /**
+ * True to animate the scroll
+ * @type Boolean
+ */
+ animate: true,
+
+ /**
+ * The animation duration in seconds - MUST BE less than Ext.dd.ScrollManager.frequency!
+ * @type Number
+ */
+ animDuration: 0.4,
+
+ /**
+ * The named drag drop {@link Ext.dd.DragSource#ddGroup group} to which this container belongs.
+ * If a ddGroup is specified, then container scrolling will only occur when a dragged object is in the same ddGroup.
+ * @type String
+ */
+ ddGroup: undefined,
+
+ /**
+ * Manually trigger a cache refresh.
+ */
+ refreshCache : function(){
+ var els = this.els,
+ id;
+ for (id in els) {
+ if(typeof els[id] == 'object'){ // for people extending the object prototype
+ els[id]._region = els[id].getRegion();
+ }
+ }
+ }
+});
+
+/**
+ * @class Ext.dd.DropTarget
+ * @extends Ext.dd.DDTarget
+ * A simple class that provides the basic implementation needed to make any element a drop target that can have
+ * draggable items dropped onto it. The drop has no effect until an implementation of notifyDrop is provided.
+ */
+Ext.define('Ext.dd.DropTarget', {
+ extend: 'Ext.dd.DDTarget',
+ requires: ['Ext.dd.ScrollManager'],
+
+ /**
+ * Creates new DropTarget.
+ * @param {String/HTMLElement/Ext.Element} el The container element or ID of it.
+ * @param {Object} config
+ */
+ constructor : function(el, config){
+ this.el = Ext.get(el);
+
+ Ext.apply(this, config);
+
+ if(this.containerScroll){
+ Ext.dd.ScrollManager.register(this.el);
+ }
+
+ this.callParent([this.el.dom, this.ddGroup || this.group,
+ {isTarget: true}]);
+ },
+
+ /**
+ * @cfg {String} ddGroup
+ * A named drag drop group to which this object belongs. If a group is specified, then this object will only
+ * interact with other drag drop objects in the same group.
+ */
+ /**
+ * @cfg {String} [overClass=""]
+ * The CSS class applied to the drop target element while the drag source is over it.
+ */
+ /**
+ * @cfg {String} [dropAllowed="x-dd-drop-ok"]
+ * The CSS class returned to the drag source when drop is allowed.
+ */
+ dropAllowed : Ext.baseCSSPrefix + 'dd-drop-ok',
+ /**
+ * @cfg {String} [dropNotAllowed="x-dd-drop-nodrop"]
+ * The CSS class returned to the drag source when drop is not allowed.
+ */
+ dropNotAllowed : Ext.baseCSSPrefix + 'dd-drop-nodrop',
+
+ // private
+ isTarget : true,
+
+ // private
+ isNotifyTarget : true,
+
+ /**
+ * The function a {@link Ext.dd.DragSource} calls once to notify this drop target that the source is now over the
+ * target. This default implementation adds the CSS class specified by overClass (if any) to the drop element
+ * and returns the dropAllowed config value. This method should be overridden if drop validation is required.
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop target
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ * @return {String} status The CSS class that communicates the drop status back to the source so that the
+ * underlying {@link Ext.dd.StatusProxy} can be updated
+ */
+ notifyEnter : function(dd, e, data){
+ if(this.overClass){
+ this.el.addCls(this.overClass);
+ }
+ return this.dropAllowed;
+ },
+
+ /**
+ * The function a {@link Ext.dd.DragSource} calls continuously while it is being dragged over the target.
+ * This method will be called on every mouse movement while the drag source is over the drop target.
+ * This default implementation simply returns the dropAllowed config value.
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop target
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ * @return {String} status The CSS class that communicates the drop status back to the source so that the
+ * underlying {@link Ext.dd.StatusProxy} can be updated
+ */
+ notifyOver : function(dd, e, data){
+ return this.dropAllowed;
+ },
+
+ /**
+ * The function a {@link Ext.dd.DragSource} calls once to notify this drop target that the source has been dragged
+ * out of the target without dropping. This default implementation simply removes the CSS class specified by
+ * overClass (if any) from the drop element.
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop target
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ */
+ notifyOut : function(dd, e, data){
+ if(this.overClass){
+ this.el.removeCls(this.overClass);
+ }
+ },
+
+ /**
+ * The function a {@link Ext.dd.DragSource} calls once to notify this drop target that the dragged item has
+ * been dropped on it. This method has no default implementation and returns false, so you must provide an
+ * implementation that does something to process the drop event and returns true so that the drag source's
+ * repair action does not run.
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop target
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ * @return {Boolean} False if the drop was invalid.
+ */
+ notifyDrop : function(dd, e, data){
+ return false;
+ },
+
+ destroy : function(){
+ this.callParent();
+ if(this.containerScroll){
+ Ext.dd.ScrollManager.unregister(this.el);
+ }
+ }
+});
+
+/**
+ * @class Ext.dd.Registry
+ * Provides easy access to all drag drop components that are registered on a page. Items can be retrieved either
+ * directly by DOM node id, or by passing in the drag drop event that occurred and looking up the event target.
+ * @singleton
+ */
+Ext.define('Ext.dd.Registry', {
+ singleton: true,
+ constructor: function() {
+ this.elements = {};
+ this.handles = {};
+ this.autoIdSeed = 0;
+ },
+
+ getId: function(el, autogen){
+ if(typeof el == "string"){
+ return el;
+ }
+ var id = el.id;
+ if(!id && autogen !== false){
+ id = "extdd-" + (++this.autoIdSeed);
+ el.id = id;
+ }
+ return id;
+ },
+
+ /**
+ * Resgister a drag drop element
+ * @param {String/HTMLElement} element The id or DOM node to register
+ * @param {Object} data (optional) An custom data object that will be passed between the elements that are involved
+ * in drag drop operations. You can populate this object with any arbitrary properties that your own code
+ * knows how to interpret, plus there are some specific properties known to the Registry that should be
+ * populated in the data object (if applicable):
+ * <pre>
+Value Description<br />
+--------- ------------------------------------------<br />
+handles Array of DOM nodes that trigger dragging<br />
+ for the element being registered<br />
+isHandle True if the element passed in triggers<br />
+ dragging itself, else false
+</pre>
+ */
+ register : function(el, data){
+ data = data || {};
+ if (typeof el == "string") {
+ el = document.getElementById(el);
+ }
+ data.ddel = el;
+ this.elements[this.getId(el)] = data;
+ if (data.isHandle !== false) {
+ this.handles[data.ddel.id] = data;
+ }
+ if (data.handles) {
+ var hs = data.handles;
+ for (var i = 0, len = hs.length; i < len; i++) {
+ this.handles[this.getId(hs[i])] = data;
+ }
+ }
+ },
+
+ /**
+ * Unregister a drag drop element
+ * @param {String/HTMLElement} element The id or DOM node to unregister
+ */
+ unregister : function(el){
+ var id = this.getId(el, false);
+ var data = this.elements[id];
+ if(data){
+ delete this.elements[id];
+ if(data.handles){
+ var hs = data.handles;
+ for (var i = 0, len = hs.length; i < len; i++) {
+ delete this.handles[this.getId(hs[i], false)];
+ }
+ }
+ }
+ },
+
+ /**
+ * Returns the handle registered for a DOM Node by id
+ * @param {String/HTMLElement} id The DOM node or id to look up
+ * @return {Object} handle The custom handle data
+ */
+ getHandle : function(id){
+ if(typeof id != "string"){ // must be element?
+ id = id.id;
+ }
+ return this.handles[id];
+ },
+
+ /**
+ * Returns the handle that is registered for the DOM node that is the target of the event
+ * @param {Event} e The event
+ * @return {Object} handle The custom handle data
+ */
+ getHandleFromEvent : function(e){
+ var t = e.getTarget();
+ return t ? this.handles[t.id] : null;
+ },
+
+ /**
+ * Returns a custom data object that is registered for a DOM node by id
+ * @param {String/HTMLElement} id The DOM node or id to look up
+ * @return {Object} data The custom data
+ */
+ getTarget : function(id){
+ if(typeof id != "string"){ // must be element?
+ id = id.id;
+ }
+ return this.elements[id];
+ },
+
+ /**
+ * Returns a custom data object that is registered for the DOM node that is the target of the event
+ * @param {Event} e The event
+ * @return {Object} data The custom data
+ */
+ getTargetFromEvent : function(e){
+ var t = e.getTarget();
+ return t ? this.elements[t.id] || this.handles[t.id] : null;
+ }
+});
+/**
+ * @class Ext.dd.DropZone
+ * @extends Ext.dd.DropTarget
+
+This class provides a container DD instance that allows dropping on multiple child target nodes.
+
+By default, this class requires that child nodes accepting drop are registered with {@link Ext.dd.Registry}.
+However a simpler way to allow a DropZone to manage any number of target elements is to configure the
+DropZone with an implementation of {@link #getTargetFromEvent} which interrogates the passed
+mouse event to see if it has taken place within an element, or class of elements. This is easily done
+by using the event's {@link Ext.EventObject#getTarget getTarget} method to identify a node based on a
+{@link Ext.DomQuery} selector.
+
+Once the DropZone has detected through calling getTargetFromEvent, that the mouse is over
+a drop target, that target is passed as the first parameter to {@link #onNodeEnter}, {@link #onNodeOver},
+{@link #onNodeOut}, {@link #onNodeDrop}. You may configure the instance of DropZone with implementations
+of these methods to provide application-specific behaviour for these events to update both
+application state, and UI state.
+
+For example to make a GridPanel a cooperating target with the example illustrated in
+{@link Ext.dd.DragZone DragZone}, the following technique might be used:
+
+ myGridPanel.on('render', function() {
+ myGridPanel.dropZone = new Ext.dd.DropZone(myGridPanel.getView().scroller, {
+
+ // If the mouse is over a grid row, return that node. This is
+ // provided as the "target" parameter in all "onNodeXXXX" node event handling functions
+ getTargetFromEvent: function(e) {
+ return e.getTarget(myGridPanel.getView().rowSelector);
+ },
+
+ // On entry into a target node, highlight that node.
+ onNodeEnter : function(target, dd, e, data){
+ Ext.fly(target).addCls('my-row-highlight-class');
+ },
+
+ // On exit from a target node, unhighlight that node.
+ onNodeOut : function(target, dd, e, data){
+ Ext.fly(target).removeCls('my-row-highlight-class');
+ },
+
+ // While over a target node, return the default drop allowed class which
+ // places a "tick" icon into the drag proxy.
+ onNodeOver : function(target, dd, e, data){
+ return Ext.dd.DropZone.prototype.dropAllowed;
+ },
+
+ // On node drop we can interrogate the target to find the underlying
+ // application object that is the real target of the dragged data.
+ // In this case, it is a Record in the GridPanel's Store.
+ // We can use the data set up by the DragZone's getDragData method to read
+ // any data we decided to attach in the DragZone's getDragData method.
+ onNodeDrop : function(target, dd, e, data){
+ var rowIndex = myGridPanel.getView().findRowIndex(target);
+ var r = myGridPanel.getStore().getAt(rowIndex);
+ Ext.Msg.alert('Drop gesture', 'Dropped Record id ' + data.draggedRecord.id +
+ ' on Record id ' + r.id);
+ return true;
+ }
+ });
+ }
+
+See the {@link Ext.dd.DragZone DragZone} documentation for details about building a DragZone which
+cooperates with this DropZone.
+
+ * @markdown
+ */
+Ext.define('Ext.dd.DropZone', {
+ extend: 'Ext.dd.DropTarget',
+ requires: ['Ext.dd.Registry'],
+
+ /**
+ * Returns a custom data object associated with the DOM node that is the target of the event. By default
+ * this looks up the event target in the {@link Ext.dd.Registry}, although you can override this method to
+ * provide your own custom lookup.
+ * @param {Event} e The event
+ * @return {Object} data The custom data
+ */
+ getTargetFromEvent : function(e){
+ return Ext.dd.Registry.getTargetFromEvent(e);
+ },
+
+ /**
+ * Called when the DropZone determines that a {@link Ext.dd.DragSource} has entered a drop node
+ * that has either been registered or detected by a configured implementation of {@link #getTargetFromEvent}.
+ * This method has no default implementation and should be overridden to provide
+ * node-specific processing if necessary.
+ * @param {Object} nodeData The custom data associated with the drop node (this is the same value returned from
+ * {@link #getTargetFromEvent} for this node)
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ */
+ onNodeEnter : function(n, dd, e, data){
+
+ },
+
+ /**
+ * Called while the DropZone determines that a {@link Ext.dd.DragSource} is over a drop node
+ * that has either been registered or detected by a configured implementation of {@link #getTargetFromEvent}.
+ * The default implementation returns this.dropNotAllowed, so it should be
+ * overridden to provide the proper feedback.
+ * @param {Object} nodeData The custom data associated with the drop node (this is the same value returned from
+ * {@link #getTargetFromEvent} for this node)
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ * @return {String} status The CSS class that communicates the drop status back to the source so that the
+ * underlying {@link Ext.dd.StatusProxy} can be updated
+ */
+ onNodeOver : function(n, dd, e, data){
+ return this.dropAllowed;
+ },
+
+ /**
+ * Called when the DropZone determines that a {@link Ext.dd.DragSource} has been dragged out of
+ * the drop node without dropping. This method has no default implementation and should be overridden to provide
+ * node-specific processing if necessary.
+ * @param {Object} nodeData The custom data associated with the drop node (this is the same value returned from
+ * {@link #getTargetFromEvent} for this node)
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ */
+ onNodeOut : function(n, dd, e, data){
+
+ },
+
+ /**
+ * Called when the DropZone determines that a {@link Ext.dd.DragSource} has been dropped onto
+ * the drop node. The default implementation returns false, so it should be overridden to provide the
+ * appropriate processing of the drop event and return true so that the drag source's repair action does not run.
+ * @param {Object} nodeData The custom data associated with the drop node (this is the same value returned from
+ * {@link #getTargetFromEvent} for this node)
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ * @return {Boolean} True if the drop was valid, else false
+ */
+ onNodeDrop : function(n, dd, e, data){
+ return false;
+ },
+
+ /**
+ * Called while the DropZone determines that a {@link Ext.dd.DragSource} is being dragged over it,
+ * but not over any of its registered drop nodes. The default implementation returns this.dropNotAllowed, so
+ * it should be overridden to provide the proper feedback if necessary.
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ * @return {String} status The CSS class that communicates the drop status back to the source so that the
+ * underlying {@link Ext.dd.StatusProxy} can be updated
+ */
+ onContainerOver : function(dd, e, data){
+ return this.dropNotAllowed;
+ },
+
+ /**
+ * Called when the DropZone determines that a {@link Ext.dd.DragSource} has been dropped on it,
+ * but not on any of its registered drop nodes. The default implementation returns false, so it should be
+ * overridden to provide the appropriate processing of the drop event if you need the drop zone itself to
+ * be able to accept drops. It should return true when valid so that the drag source's repair action does not run.
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ * @return {Boolean} True if the drop was valid, else false
+ */
+ onContainerDrop : function(dd, e, data){
+ return false;
+ },
+
+ /**
+ * The function a {@link Ext.dd.DragSource} calls once to notify this drop zone that the source is now over
+ * the zone. The default implementation returns this.dropNotAllowed and expects that only registered drop
+ * nodes can process drag drop operations, so if you need the drop zone itself to be able to process drops
+ * you should override this method and provide a custom implementation.
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ * @return {String} status The CSS class that communicates the drop status back to the source so that the
+ * underlying {@link Ext.dd.StatusProxy} can be updated
+ */
+ notifyEnter : function(dd, e, data){
+ return this.dropNotAllowed;
+ },
+
+ /**
+ * The function a {@link Ext.dd.DragSource} calls continuously while it is being dragged over the drop zone.
+ * This method will be called on every mouse movement while the drag source is over the drop zone.
+ * It will call {@link #onNodeOver} while the drag source is over a registered node, and will also automatically
+ * delegate to the appropriate node-specific methods as necessary when the drag source enters and exits
+ * registered nodes ({@link #onNodeEnter}, {@link #onNodeOut}). If the drag source is not currently over a
+ * registered node, it will call {@link #onContainerOver}.
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ * @return {String} status The CSS class that communicates the drop status back to the source so that the
+ * underlying {@link Ext.dd.StatusProxy} can be updated
+ */
+ notifyOver : function(dd, e, data){
+ var n = this.getTargetFromEvent(e);
+ if(!n) { // not over valid drop target
+ if(this.lastOverNode){
+ this.onNodeOut(this.lastOverNode, dd, e, data);
+ this.lastOverNode = null;
+ }
+ return this.onContainerOver(dd, e, data);
+ }
+ if(this.lastOverNode != n){
+ if(this.lastOverNode){
+ this.onNodeOut(this.lastOverNode, dd, e, data);
+ }
+ this.onNodeEnter(n, dd, e, data);
+ this.lastOverNode = n;
+ }
+ return this.onNodeOver(n, dd, e, data);
+ },
+
+ /**
+ * The function a {@link Ext.dd.DragSource} calls once to notify this drop zone that the source has been dragged
+ * out of the zone without dropping. If the drag source is currently over a registered node, the notification
+ * will be delegated to {@link #onNodeOut} for node-specific handling, otherwise it will be ignored.
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop target
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag zone
+ */
+ notifyOut : function(dd, e, data){
+ if(this.lastOverNode){
+ this.onNodeOut(this.lastOverNode, dd, e, data);
+ this.lastOverNode = null;
+ }
+ },
+
+ /**
+ * The function a {@link Ext.dd.DragSource} calls once to notify this drop zone that the dragged item has
+ * been dropped on it. The drag zone will look up the target node based on the event passed in, and if there
+ * is a node registered for that event, it will delegate to {@link #onNodeDrop} for node-specific handling,
+ * otherwise it will call {@link #onContainerDrop}.
+ * @param {Ext.dd.DragSource} source The drag source that was dragged over this drop zone
+ * @param {Event} e The event
+ * @param {Object} data An object containing arbitrary data supplied by the drag source
+ * @return {Boolean} False if the drop was invalid.
+ */
+ notifyDrop : function(dd, e, data){
+ if(this.lastOverNode){
+ this.onNodeOut(this.lastOverNode, dd, e, data);
+ this.lastOverNode = null;
+ }
+ var n = this.getTargetFromEvent(e);
+ return n ?
+ this.onNodeDrop(n, dd, e, data) :
+ this.onContainerDrop(dd, e, data);
+ },
+
+ // private
+ triggerCacheRefresh : function() {
+ Ext.dd.DDM.refreshCache(this.groups);
+ }
+});
+/**
+ * @class Ext.flash.Component
+ * @extends Ext.Component
+ *
+ * A simple Component for displaying an Adobe Flash SWF movie. The movie will be sized and can participate
+ * in layout like any other Component.
+ *
+ * This component requires the third-party SWFObject library version 2.2 or above. It is not included within
+ * the ExtJS distribution, so you will have to include it into your page manually in order to use this component.
+ * The SWFObject library can be downloaded from the [SWFObject project page](http://code.google.com/p/swfobject)
+ * and then simply import it into the head of your HTML document:
+ *
+ * <script type="text/javascript" src="path/to/local/swfobject.js"></script>
+ *
+ * ## Configuration
+ *
+ * This component allows several options for configuring how the target Flash movie is embedded. The most
+ * important is the required {@link #url} which points to the location of the Flash movie to load. Other
+ * configurations include:
+ *
+ * - {@link #backgroundColor}
+ * - {@link #wmode}
+ * - {@link #flashVars}
+ * - {@link #flashParams}
+ * - {@link #flashAttributes}
+ *
+ * ## Example usage:
+ *
+ * var win = Ext.widget('window', {
+ * title: "It's a tiger!",
+ * layout: 'fit',
+ * width: 300,
+ * height: 300,
+ * x: 20,
+ * y: 20,
+ * resizable: true,
+ * items: {
+ * xtype: 'flash',
+ * url: 'tiger.swf'
+ * }
+ * });
+ * win.show();
+ *
+ * ## Express Install
+ *
+ * Adobe provides a tool called [Express Install](http://www.adobe.com/devnet/flashplayer/articles/express_install.html)
+ * that offers users an easy way to upgrade their Flash player. If you wish to make use of this, you should set
+ * the static EXPRESS\_INSTALL\_URL property to the location of your Express Install SWF file:
+ *
+ * Ext.flash.Component.EXPRESS_INSTALL_URL = 'path/to/local/expressInstall.swf';
+ *
+ * @docauthor Jason Johnston <jason@sencha.com>
+ */
+Ext.define('Ext.flash.Component', {
+ extend: 'Ext.Component',
+ alternateClassName: 'Ext.FlashComponent',
+ alias: 'widget.flash',
+
+ /**
+ * @cfg {String} flashVersion
+ * Indicates the version the flash content was published for. Defaults to <tt>'9.0.115'</tt>.
+ */
+ flashVersion : '9.0.115',
+
+ /**
+ * @cfg {String} backgroundColor
+ * The background color of the SWF movie. Defaults to <tt>'#ffffff'</tt>.
+ */
+ backgroundColor: '#ffffff',
+
+ /**
+ * @cfg {String} wmode
+ * The wmode of the flash object. This can be used to control layering. Defaults to <tt>'opaque'</tt>.
+ * Set to 'transparent' to ignore the {@link #backgroundColor} and make the background of the Flash
+ * movie transparent.
+ */
+ wmode: 'opaque',
+
+ /**
+ * @cfg {Object} flashVars
+ * A set of key value pairs to be passed to the flash object as flash variables. Defaults to <tt>undefined</tt>.
+ */
+
+ /**
+ * @cfg {Object} flashParams
+ * A set of key value pairs to be passed to the flash object as parameters. Possible parameters can be found here:
+ * http://kb2.adobe.com/cps/127/tn_12701.html Defaults to <tt>undefined</tt>.
+ */
+
+ /**
+ * @cfg {Object} flashAttributes
+ * A set of key value pairs to be passed to the flash object as attributes. Defaults to <tt>undefined</tt>.
+ */
+
+ /**
+ * @cfg {String} url
+ * The URL of the SWF file to include. Required.
+ */
+
+ /**
+ * @cfg {String/Number} swfWidth The width of the embedded SWF movie inside the component. Defaults to "100%"
+ * so that the movie matches the width of the component.
+ */
+ swfWidth: '100%',
+
+ /**
+ * @cfg {String/Number} swfHeight The height of the embedded SWF movie inside the component. Defaults to "100%"
+ * so that the movie matches the height of the component.
+ */
+ swfHeight: '100%',
+
+ /**
+ * @cfg {Boolean} expressInstall
+ * True to prompt the user to install flash if not installed. Note that this uses
+ * Ext.FlashComponent.EXPRESS_INSTALL_URL, which should be set to the local resource. Defaults to <tt>false</tt>.
+ */
+ expressInstall: false,
+
+ /**
+ * @property swf
+ * @type {Ext.Element}
+ * A reference to the object or embed element into which the SWF file is loaded. Only
+ * populated after the component is rendered and the SWF has been successfully embedded.
+ */
+
+ // Have to create a placeholder div with the swfId, which SWFObject will replace with the object/embed element.
+ renderTpl: ['<div id="{swfId}"></div>'],
+
+ initComponent: function() {
+
+ this.callParent();
+ this.addEvents(
+ /**
+ * @event success
+ * Fired when the Flash movie has been successfully embedded
+ * @param {Ext.flash.Component} this
+ */
+ 'success',
+
+ /**
+ * @event failure
+ * Fired when the Flash movie embedding fails
+ * @param {Ext.flash.Component} this
+ */
+ 'failure'
+ );
+ },
+
+ onRender: function() {
+ var me = this,
+ params, vars, undef,
+ swfId = me.getSwfId();
+
+ me.renderData.swfId = swfId;
+
+ me.callParent(arguments);
+
+ params = Ext.apply({
+ allowScriptAccess: 'always',
+ bgcolor: me.backgroundColor,
+ wmode: me.wmode
+ }, me.flashParams);
+
+ vars = Ext.apply({
+ allowedDomain: document.location.hostname
+ }, me.flashVars);
+
+ new swfobject.embedSWF(
+ me.url,
+ swfId,
+ me.swfWidth,
+ me.swfHeight,
+ me.flashVersion,
+ me.expressInstall ? me.statics.EXPRESS_INSTALL_URL : undef,
+ vars,
+ params,
+ me.flashAttributes,
+ Ext.bind(me.swfCallback, me)
+ );
+ },
+
+ /**
+ * @private
+ * The callback method for handling an embedding success or failure by SWFObject
+ * @param {Object} e The event object passed by SWFObject - see http://code.google.com/p/swfobject/wiki/api
+ */
+ swfCallback: function(e) {
+ var me = this;
+ if (e.success) {
+ me.swf = Ext.get(e.ref);
+ me.onSuccess();
+ me.fireEvent('success', me);
+ } else {
+ me.onFailure();
+ me.fireEvent('failure', me);
+ }
+ },
+
+ /**
+ * Retrieve the id of the SWF object/embed element
+ */
+ getSwfId: function() {
+ return this.swfId || (this.swfId = "extswf" + this.getAutoId());
+ },
+
+ onSuccess: function() {
+ // swfobject forces visiblity:visible on the swf element, which prevents it
+ // from getting hidden when an ancestor is given visibility:hidden.
+ this.swf.setStyle('visibility', 'inherit');
+ },
+
+ onFailure: Ext.emptyFn,
+
+ beforeDestroy: function() {
+ var me = this,
+ swf = me.swf;
+ if (swf) {
+ swfobject.removeSWF(me.getSwfId());
+ Ext.destroy(swf);
+ delete me.swf;
+ }
+ me.callParent();
+ },
+
+ statics: {
+ /**
+ * Sets the url for installing flash if it doesn't exist. This should be set to a local resource.
+ * See http://www.adobe.com/devnet/flashplayer/articles/express_install.html for details.
+ * @static
+ * @type String
+ */
+ EXPRESS_INSTALL_URL: 'http:/' + '/swfobject.googlecode.com/svn/trunk/swfobject/expressInstall.swf'
+ }
+});
+
+/**
+ * @class Ext.form.action.Action
+ * @extends Ext.Base
+ * <p>The subclasses of this class provide actions to perform upon {@link Ext.form.Basic Form}s.</p>
+ * <p>Instances of this class are only created by a {@link Ext.form.Basic 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.Basic#submit submit},
+ * {@link Ext.form.Basic#load load} and {@link Ext.form.Basic#doAction doAction}</p>
+ * <p>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.Basic#submit submit},
+ * {@link Ext.form.Basic#load load} and {@link Ext.form.Basic#doAction doAction}),
+ * and to the {@link Ext.form.Basic#actioncomplete actioncomplete} and
+ * {@link Ext.form.Basic#actionfailed actionfailed} event handlers.</p>
+ */
+Ext.define('Ext.form.action.Action', {
+ alternateClassName: 'Ext.form.Action',
+
+ /**
+ * @cfg {Ext.form.Basic} form The {@link Ext.form.Basic BasicForm} instance that
+ * is invoking this Action. Required.
+ */
+
+ /**
+ * @cfg {String} url The URL that the Action is to invoke. Will default to the {@link Ext.form.Basic#url url}
+ * configured on the {@link #form}.
+ */
+
+ /**
+ * @cfg {Boolean} reset When set to <tt><b>true</b></tt>, causes the Form to be
+ * {@link Ext.form.Basic#reset reset} on Action success. If specified, this happens
+ * before the {@link #success} callback is called and before the Form's
+ * {@link Ext.form.Basic#actioncomplete actioncomplete} event fires.
+ */
+
+ /**
+ * @cfg {String} method The HTTP method to use to access the requested URL. Defaults to the
+ * {@link Ext.form.Basic#method BasicForm's method}, or 'POST' if not specified.
+ */
+
+ /**
+ * @cfg {Object/String} params <p>Extra parameter values to pass. These are added to the Form's
+ * {@link Ext.form.Basic#baseParams} and passed to the specified URL along with the Form's
+ * input fields.</p>
+ * <p>Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode Ext.Object.toQueryString}.</p>
+ */
+
+ /**
+ * @cfg {Object} headers <p>Extra headers to be sent in the AJAX request for submit and load actions. See
+ * {@link Ext.data.proxy.Ajax#headers}.</p>
+ */
+
+ /**
+ * @cfg {Number} timeout The number of seconds to wait for a server response before
+ * failing with the {@link #failureType} as {@link Ext.form.action.Action#CONNECT_FAILURE}. If not specified,
+ * defaults to the configured <tt>{@link Ext.form.Basic#timeout timeout}</tt> of the
+ * {@link #form}.
+ */
+
+ /**
+ * @cfg {Function} success The function to call when a valid success return packet is received.
+ * The function is passed the following parameters:<ul class="mdetail-params">
+ * <li><b>form</b> : Ext.form.Basic<div class="sub-desc">The form that requested the action</div></li>
+ * <li><b>action</b> : Ext.form.action.Action<div class="sub-desc">The Action class. The {@link #result}
+ * property of this object may be examined to perform custom postprocessing.</div></li>
+ * </ul>
+ */
+
+ /**
+ * @cfg {Function} failure The function to call when a failure packet was received, or when an
+ * error ocurred in the Ajax communication.
+ * The function is passed the following parameters:<ul class="mdetail-params">
+ * <li><b>form</b> : Ext.form.Basic<div class="sub-desc">The form that requested the action</div></li>
+ * <li><b>action</b> : Ext.form.action.Action<div class="sub-desc">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.</div></li>
+ * </ul>
+ */
+
+ /**
+ * @cfg {Object} scope The scope in which to call the configured <tt>success</tt> and <tt>failure</tt>
+ * callback functions (the <tt>this</tt> reference for the callback functions).
+ */
+
+ /**
+ * @cfg {String} waitMsg The message to be displayed by a call to {@link Ext.window.MessageBox#wait}
+ * during the time the action is being processed.
+ */
+
+ /**
+ * @cfg {String} waitTitle The title to be displayed by a call to {@link Ext.window.MessageBox#wait}
+ * during the time the action is being processed.
+ */
+
+ /**
+ * @cfg {Boolean} submitEmptyText If set to <tt>true</tt>, the emptyText value will be sent with the form
+ * when it is submitted. Defaults to <tt>true</tt>.
+ */
+ submitEmptyText : true,
+ /**
+ * @property type
+ * The type of action this Action instance performs.
+ * Currently only "submit" and "load" are supported.
+ * @type {String}
+ */
+
+ /**
+ * The type of failure detected will be one of these: {@link Ext.form.action.Action#CLIENT_INVALID},
+ * {@link Ext.form.action.Action#SERVER_INVALID}, {@link Ext.form.action.Action#CONNECT_FAILURE}, or
+ * {@link Ext.form.action.Action#LOAD_FAILURE}. Usage:
+ * <pre><code>
+var fp = new Ext.form.Panel({
+...
+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} === {@link Ext.form.action.Action#CONNECT_FAILURE}) {
+ Ext.Msg.alert('Error',
+ 'Status:'+action.{@link #response}.status+': '+
+ action.{@link #response}.statusText);
+ }
+ if (action.failureType === {@link Ext.form.action.Action#SERVER_INVALID}){
+ // server responded with success = false
+ Ext.Msg.alert('Invalid', action.{@link #result}.errormsg);
+ }
+ }
+ });
+ }
+ }
+},{
+ text: 'Reset',
+ handler: function(){
+ fp.getForm().reset();
+ }
+}]
+ * </code></pre>
+ * @property failureType
+ * @type {String}
+ */
+
+ /**
+ * The raw XMLHttpRequest object used to perform the action.
+ * @property response
+ * @type {Object}
+ */
+
+ /**
+ * The decoded response object containing a boolean <tt>success</tt> property and
+ * other, action-specific properties.
+ * @property result
+ * @type {Object}
+ */
+
+ /**
+ * Creates new Action.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ if (config) {
+ Ext.apply(this, config);
+ }
+
+ // Normalize the params option to an Object
+ var params = config.params;
+ if (Ext.isString(params)) {
+ this.params = Ext.Object.fromQueryString(params);
+ }
+ },
+
+ /**
+ * Invokes this action using the current configuration.
+ */
+ run: Ext.emptyFn,
+
+ /**
+ * @private
+ * @method onSuccess
+ * Callback method that gets invoked when the action completes successfully. Must be implemented by subclasses.
+ * @param {Object} response
+ */
+
+ /**
+ * @private
+ * @method handleResponse
+ * Handles the raw response and builds a result object from it. Must be implemented by subclasses.
+ * @param {Object} response
+ */
+
+ /**
+ * @private
+ * Handles a failure response.
+ * @param {Object} response
+ */
+ onFailure : function(response){
+ this.response = response;
+ this.failureType = Ext.form.action.Action.CONNECT_FAILURE;
+ this.form.afterAction(this, false);
+ },
+
+ /**
+ * @private
+ * Validates that a response contains either responseText or responseXML and invokes
+ * {@link #handleResponse} to build the result object.
+ * @param {Object} response The raw response object.
+ * @return {Object/Boolean} result The result object as built by handleResponse, or <tt>true</tt> if
+ * the response had empty responseText and responseXML.
+ */
+ processResponse : function(response){
+ this.response = response;
+ if (!response.responseText && !response.responseXML) {
+ return true;
+ }
+ return (this.result = this.handleResponse(response));
+ },
+
+ /**
+ * @private
+ * Build the URL for the AJAX request. Used by the standard AJAX submit and load actions.
+ * @return {String} The URL.
+ */
+ getUrl: function() {
+ return this.url || this.form.url;
+ },
+
+ /**
+ * @private
+ * Determine the HTTP method to be used for the request.
+ * @return {String} The HTTP method
+ */
+ getMethod: function() {
+ return (this.method || this.form.method || 'POST').toUpperCase();
+ },
+
+ /**
+ * @private
+ * Get the set of parameters specified in the BasicForm's baseParams and/or the params option.
+ * Items in params override items of the same name in baseParams.
+ * @return {Object} the full set of parameters
+ */
+ getParams: function() {
+ return Ext.apply({}, this.params, this.form.baseParams);
+ },
+
+ /**
+ * @private
+ * Creates a callback object.
+ */
+ createCallback: function() {
+ var me = this,
+ undef,
+ form = me.form;
+ return {
+ success: me.onSuccess,
+ failure: me.onFailure,
+ scope: me,
+ timeout: (this.timeout * 1000) || (form.timeout * 1000),
+ upload: form.fileUpload ? me.onSuccess : undef
+ };
+ },
+
+ statics: {
+ /**
+ * @property CLIENT_INVALID
+ * Failure type returned when client side validation of the Form fails
+ * thus aborting a submit action. Client side validation is performed unless
+ * {@link Ext.form.action.Submit#clientValidation} is explicitly set to <tt>false</tt>.
+ * @type {String}
+ * @static
+ */
+ CLIENT_INVALID: 'client',
+
+ /**
+ * @property SERVER_INVALID
+ * <p>Failure type returned when server side processing fails and the {@link #result}'s
+ * <tt>success</tt> property is set to <tt>false</tt>.</p>
+ * <p>In the case of a form submission, field-specific error messages may be returned in the
+ * {@link #result}'s <tt>errors</tt> property.</p>
+ * @type {String}
+ * @static
+ */
+ SERVER_INVALID: 'server',
+
+ /**
+ * @property CONNECT_FAILURE
+ * 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
+ */
+ CONNECT_FAILURE: 'connect',
+
+ /**
+ * @property LOAD_FAILURE
+ * Failure type returned when the response's <tt>success</tt>
+ * property is set to <tt>false</tt>, or no field values are returned in the response's
+ * <tt>data</tt> property.
+ * @type {String}
+ * @static
+ */
+ LOAD_FAILURE: 'load'
+
+
+ }
+});
+
+/**
+ * @class Ext.form.action.Submit
+ * @extends Ext.form.action.Action
+ * <p>A class which handles submission of data from {@link Ext.form.Basic Form}s
+ * and processes the returned response.</p>
+ * <p>Instances of this class are only created by a {@link Ext.form.Basic Form} when
+ * {@link Ext.form.Basic#submit submit}ting.</p>
+ * <p><u><b>Response Packet Criteria</b></u></p>
+ * <p>A response packet may contain:
+ * <div class="mdetail-params"><ul>
+ * <li><b><code>success</code></b> property : Boolean
+ * <div class="sub-desc">The <code>success</code> property is required.</div></li>
+ * <li><b><code>errors</code></b> property : Object
+ * <div class="sub-desc"><div class="sub-desc">The <code>errors</code> property,
+ * which is optional, contains error messages for invalid fields.</div></li>
+ * </ul></div>
+ * <p><u><b>JSON Packets</b></u></p>
+ * <p>By default, response packets are assumed to be JSON, so a typical response
+ * packet may look like this:</p><pre><code>
+{
+ success: false,
+ errors: {
+ clientCode: "Client not found",
+ portOfLoading: "This field must not be null"
+ }
+}</code></pre>
+ * <p>Other data may be placed into the response for processing by the {@link Ext.form.Basic}'s callback
+ * or event handler methods. The object decoded from this JSON is available in the
+ * {@link Ext.form.action.Action#result result} property.</p>
+ * <p>Alternatively, if an {@link Ext.form.Basic#errorReader errorReader} is specified as an {@link Ext.data.reader.Xml XmlReader}:</p><pre><code>
+ errorReader: new Ext.data.reader.Xml({
+ record : 'field',
+ success: '@success'
+ }, [
+ 'id', 'msg'
+ ]
+ )
+</code></pre>
+ * <p>then the results may be sent back in XML format:</p><pre><code>
+<?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>
+</code></pre>
+ * <p>Other elements may be placed into the response XML for processing by the {@link Ext.form.Basic}'s callback
+ * or event handler methods. The XML document is available in the {@link Ext.form.Basic#errorReader errorReader}'s
+ * {@link Ext.data.reader.Xml#xmlData xmlData} property.</p>
+ */
+Ext.define('Ext.form.action.Submit', {
+ extend:'Ext.form.action.Action',
+ alternateClassName: 'Ext.form.Action.Submit',
+ alias: 'formaction.submit',
+
+ type: 'submit',
+
+ /**
+ * @cfg {Boolean} clientValidation Determines whether a Form's fields are validated
+ * in a final call to {@link Ext.form.Basic#isValid isValid} prior to submission.
+ * Pass <tt>false</tt> in the Form's submit options to prevent this. Defaults to true.
+ */
+
+ // inherit docs
+ run : function(){
+ var form = this.form;
+ if (this.clientValidation === false || form.isValid()) {
+ this.doSubmit();
+ } else {
+ // client validation failed
+ this.failureType = Ext.form.action.Action.CLIENT_INVALID;
+ form.afterAction(this, false);
+ }
+ },
+
+ /**
+ * @private
+ * Perform the submit of the form data.
+ */
+ doSubmit: function() {
+ var formEl,
+ ajaxOptions = Ext.apply(this.createCallback(), {
+ url: this.getUrl(),
+ method: this.getMethod(),
+ headers: this.headers
+ });
+
+ // For uploads we need to create an actual form that contains the file upload fields,
+ // and pass that to the ajax call so it can do its iframe-based submit method.
+ if (this.form.hasUpload()) {
+ formEl = ajaxOptions.form = this.buildForm();
+ ajaxOptions.isUpload = true;
+ } else {
+ ajaxOptions.params = this.getParams();
+ }
+
+ Ext.Ajax.request(ajaxOptions);
+
+ if (formEl) {
+ Ext.removeNode(formEl);
+ }
+ },
+
+ /**
+ * @private
+ * Build the full set of parameters from the field values plus any additional configured params.
+ */
+ getParams: function() {
+ var nope = false,
+ configParams = this.callParent(),
+ fieldParams = this.form.getValues(nope, nope, this.submitEmptyText !== nope);
+ return Ext.apply({}, fieldParams, configParams);
+ },
+
+ /**
+ * @private
+ * Build a form element containing fields corresponding to all the parameters to be
+ * submitted (everything returned by {@link #getParams}.
+ * NOTE: the form element is automatically added to the DOM, so any code that uses
+ * it must remove it from the DOM after finishing with it.
+ * @return HTMLFormElement
+ */
+ buildForm: function() {
+ var fieldsSpec = [],
+ formSpec,
+ formEl,
+ basicForm = this.form,
+ params = this.getParams(),
+ uploadFields = [];
+
+ basicForm.getFields().each(function(field) {
+ if (field.isFileUpload()) {
+ uploadFields.push(field);
+ }
+ });
+
+ function addField(name, val) {
+ fieldsSpec.push({
+ tag: 'input',
+ type: 'hidden',
+ name: name,
+ value: Ext.String.htmlEncode(val)
+ });
+ }
+
+ // Add the form field values
+ Ext.iterate(params, function(key, val) {
+ if (Ext.isArray(val)) {
+ Ext.each(val, function(v) {
+ addField(key, v);
+ });
+ } else {
+ addField(key, val);
+ }
+ });
+
+ formSpec = {
+ tag: 'form',
+ action: this.getUrl(),
+ method: this.getMethod(),
+ target: this.target || '_self',
+ style: 'display:none',
+ cn: fieldsSpec
+ };
+
+ // Set the proper encoding for file uploads
+ if (uploadFields.length) {
+ formSpec.encoding = formSpec.enctype = 'multipart/form-data';
+ }
+
+ // Create the form
+ formEl = Ext.DomHelper.append(Ext.getBody(), formSpec);
+
+ // Special handling for file upload fields: since browser security measures prevent setting
+ // their values programatically, and prevent carrying their selected values over when cloning,
+ // we have to move the actual field instances out of their components and into the form.
+ Ext.Array.each(uploadFields, function(field) {
+ if (field.rendered) { // can only have a selected file value after being rendered
+ formEl.appendChild(field.extractFileInput());
+ }
+ });
+
+ return formEl;
+ },
+
+
+
+ /**
+ * @private
+ */
+ onSuccess: function(response) {
+ var form = this.form,
+ success = true,
+ result = this.processResponse(response);
+ if (result !== true && !result.success) {
+ if (result.errors) {
+ form.markInvalid(result.errors);
+ }
+ this.failureType = Ext.form.action.Action.SERVER_INVALID;
+ success = false;
+ }
+ form.afterAction(this, success);
+ },
+
+ /**
+ * @private
+ */
+ handleResponse: function(response) {
+ var form = this.form,
+ errorReader = form.errorReader,
+ rs, errors, i, len, records;
+ if (errorReader) {
+ rs = errorReader.read(response);
+ records = rs.records;
+ errors = [];
+ if (records) {
+ for(i = 0, len = records.length; i < len; i++) {
+ errors[i] = records[i].data;
+ }
+ }
+ if (errors.length < 1) {
+ errors = null;
+ }
+ return {
+ success : rs.success,
+ errors : errors
+ };
+ }
+ return Ext.decode(response.responseText);
+ }
+});
+
+/**
+ * @class Ext.util.ComponentDragger
+ * @extends Ext.dd.DragTracker
+ * <p>A subclass of Ext.dd.DragTracker which handles dragging any Component.</p>
+ * <p>This is configured with a Component to be made draggable, and a config object for the
+ * {@link Ext.dd.DragTracker} class.</p>
+ * <p>A {@link #delegate} may be provided which may be either the element to use as the mousedown target
+ * or a {@link Ext.DomQuery} selector to activate multiple mousedown targets.</p>
+ */
+Ext.define('Ext.util.ComponentDragger', {
+
+ /**
+ * @cfg {Boolean} constrain
+ * Specify as <code>true</code> to constrain the Component to within the bounds of the {@link #constrainTo} region.
+ */
+
+ /**
+ * @cfg {String/Ext.Element} delegate
+ * Optional. <p>A {@link Ext.DomQuery DomQuery} selector which identifies child elements within the Component's encapsulating
+ * Element which are the drag handles. This limits dragging to only begin when the matching elements are mousedowned.</p>
+ * <p>This may also be a specific child element within the Component's encapsulating element to use as the drag handle.</p>
+ */
+
+ /**
+ * @cfg {Boolean} constrainDelegate
+ * Specify as <code>true</code> to constrain the drag handles within the {@link #constrainTo} region.
+ */
+
+ extend: 'Ext.dd.DragTracker',
+
+ autoStart: 500,
+
+ /**
+ * Creates new ComponentDragger.
+ * @param {Object} comp The Component to provide dragging for.
+ * @param {Object} config (optional) Config object
+ */
+ constructor: function(comp, config) {
+ this.comp = comp;
+ this.initialConstrainTo = config.constrainTo;
+ this.callParent([ config ]);
+ },
+
+ onStart: function(e) {
+ var me = this,
+ comp = me.comp;
+
+ // Cache the start [X, Y] array
+ this.startPosition = comp.getPosition();
+
+ // If client Component has a ghost method to show a lightweight version of itself
+ // then use that as a drag proxy unless configured to liveDrag.
+ if (comp.ghost && !comp.liveDrag) {
+ me.proxy = comp.ghost();
+ me.dragTarget = me.proxy.header.el;
+ }
+
+ // Set the constrainTo Region before we start dragging.
+ if (me.constrain || me.constrainDelegate) {
+ me.constrainTo = me.calculateConstrainRegion();
+ }
+ },
+
+ calculateConstrainRegion: function() {
+ var me = this,
+ comp = me.comp,
+ c = me.initialConstrainTo,
+ delegateRegion,
+ elRegion,
+ shadowSize = comp.el.shadow ? comp.el.shadow.offset : 0;
+
+ // The configured constrainTo might be a Region or an element
+ if (!(c instanceof Ext.util.Region)) {
+ c = Ext.fly(c).getViewRegion();
+ }
+
+ // Reduce the constrain region to allow for shadow
+ if (shadowSize) {
+ c.adjust(0, -shadowSize, -shadowSize, shadowSize);
+ }
+
+ // If they only want to constrain the *delegate* to within the constrain region,
+ // adjust the region to be larger based on the insets of the delegate from the outer
+ // edges of the Component.
+ if (!me.constrainDelegate) {
+ delegateRegion = Ext.fly(me.dragTarget).getRegion();
+ elRegion = me.proxy ? me.proxy.el.getRegion() : comp.el.getRegion();
+
+ c.adjust(
+ delegateRegion.top - elRegion.top,
+ delegateRegion.right - elRegion.right,
+ delegateRegion.bottom - elRegion.bottom,
+ delegateRegion.left - elRegion.left
+ );
+ }
+ return c;
+ },
+
+ // Move either the ghost Component or the target Component to its new position on drag
+ onDrag: function(e) {
+ var me = this,
+ comp = (me.proxy && !me.comp.liveDrag) ? me.proxy : me.comp,
+ offset = me.getOffset(me.constrain || me.constrainDelegate ? 'dragTarget' : null);
+
+ comp.setPosition(me.startPosition[0] + offset[0], me.startPosition[1] + offset[1]);
+ },
+
+ onEnd: function(e) {
+ if (this.proxy && !this.comp.liveDrag) {
+ this.comp.unghost();
+ }
+ }
+});
+/**
+ * A mixin which allows a component to be configured and decorated with a label and/or error message as is
+ * common for form fields. This is used by e.g. Ext.form.field.Base and Ext.form.FieldContainer
+ * to let them be managed by the Field layout.
+ *
+ * NOTE: This mixin is mainly for internal library use and most users should not need to use it directly. It
+ * is more likely you will want to use one of the component classes that import this mixin, such as
+ * Ext.form.field.Base or Ext.form.FieldContainer.
+ *
+ * Use of this mixin does not make a component a field in the logical sense, meaning it does not provide any
+ * logic or state related to values or validation; that is handled by the related Ext.form.field.Field
+ * mixin. These two mixins may be used separately (for example Ext.form.FieldContainer is Labelable but not a
+ * Field), or in combination (for example Ext.form.field.Base implements both and has logic for connecting the
+ * two.)
+ *
+ * Component classes which use this mixin should use the Field layout
+ * or a derivation thereof to properly size and position the label and message according to the component config.
+ * They must also call the {@link #initLabelable} method during component initialization to ensure the mixin gets
+ * set up correctly.
+ *
+ * @docauthor Jason Johnston <jason@sencha.com>
+ */
+Ext.define("Ext.form.Labelable", {
+ requires: ['Ext.XTemplate'],
+
+ /**
+ * @cfg {String/String[]/Ext.XTemplate} labelableRenderTpl
+ * The rendering template for the field decorations. Component classes using this mixin should include
+ * logic to use this as their {@link Ext.AbstractComponent#renderTpl renderTpl}, and implement the
+ * {@link #getSubTplMarkup} method to generate the field body content.
+ */
+ labelableRenderTpl: [
+ '<tpl if="!hideLabel && !(!fieldLabel && hideEmptyLabel)">',
+ '<label id="{id}-labelEl"<tpl if="inputId"> for="{inputId}"</tpl> class="{labelCls}"',
+ '<tpl if="labelStyle"> style="{labelStyle}"</tpl>>',
+ '<tpl if="fieldLabel">{fieldLabel}{labelSeparator}</tpl>',
+ '</label>',
+ '</tpl>',
+ '<div class="{baseBodyCls} {fieldBodyCls}" id="{id}-bodyEl" role="presentation">{subTplMarkup}</div>',
+ '<div id="{id}-errorEl" class="{errorMsgCls}" style="display:none"></div>',
+ '<div class="{clearCls}" role="presentation"><!-- --></div>',
+ {
+ compiled: true,
+ disableFormats: true
+ }
+ ],
+
+ /**
+ * @cfg {Ext.XTemplate} activeErrorsTpl
+ * The template used to format the Array of error messages passed to {@link #setActiveErrors}
+ * into a single HTML string. By default this renders each message as an item in an unordered list.
+ */
+ activeErrorsTpl: [
+ '<tpl if="errors && errors.length">',
+ '<ul><tpl for="errors"><li<tpl if="xindex == xcount"> class="last"</tpl>>{.}</li></tpl></ul>',
+ '</tpl>'
+ ],
+
+ /**
+ * @property isFieldLabelable
+ * @type Boolean
+ * Flag denoting that this object is labelable as a field. Always true.
+ */
+ isFieldLabelable: true,
+
+ /**
+ * @cfg {String} [formItemCls='x-form-item']
+ * A CSS class to be applied to the outermost element to denote that it is participating in the form
+ * field layout.
+ */
+ formItemCls: Ext.baseCSSPrefix + 'form-item',
+
+ /**
+ * @cfg {String} [labelCls='x-form-item-label']
+ * The CSS class to be applied to the label element.
+ * This (single) CSS class is used to formulate the renderSelector and drives the field
+ * layout where it is concatenated with a hyphen ('-') and {@link #labelAlign}. To add
+ * additional classes, use {@link #labelClsExtra}.
+ */
+ labelCls: Ext.baseCSSPrefix + 'form-item-label',
+
+ /**
+ * @cfg {String} labelClsExtra
+ * An optional string of one or more additional CSS classes to add to the label element.
+ * Defaults to empty.
+ */
+
+ /**
+ * @cfg {String} [errorMsgCls='x-form-error-msg']
+ * The CSS class to be applied to the error message element.
+ */
+ errorMsgCls: Ext.baseCSSPrefix + 'form-error-msg',
+
+ /**
+ * @cfg {String} [baseBodyCls='x-form-item-body']
+ * The CSS class to be applied to the body content element.
+ */
+ baseBodyCls: Ext.baseCSSPrefix + 'form-item-body',
+
+ /**
+ * @cfg {String} fieldBodyCls
+ * An extra CSS class to be applied to the body content element in addition to {@link #fieldBodyCls}.
+ */
+ fieldBodyCls: '',
+
+ /**
+ * @cfg {String} [clearCls='x-clear']
+ * The CSS class to be applied to the special clearing div rendered directly after the field
+ * contents wrapper to provide field clearing.
+ */
+ clearCls: Ext.baseCSSPrefix + 'clear',
+
+ /**
+ * @cfg {String} [invalidCls='x-form-invalid']
+ * The CSS class to use when marking the component invalid.
+ */
+ invalidCls : Ext.baseCSSPrefix + 'form-invalid',
+
+ /**
+ * @cfg {String} fieldLabel
+ * The label for the field. It gets appended with the {@link #labelSeparator}, and its position
+ * and sizing is determined by the {@link #labelAlign}, {@link #labelWidth}, and {@link #labelPad}
+ * configs.
+ */
+ fieldLabel: undefined,
+
+ /**
+ * @cfg {String} labelAlign
+ * <p>Controls the position and alignment of the {@link #fieldLabel}. Valid values are:</p>
+ * <ul>
+ * <li><tt>"left"</tt> (the default) - The label is positioned to the left of the field, with its text
+ * aligned to the left. Its width is determined by the {@link #labelWidth} config.</li>
+ * <li><tt>"top"</tt> - The label is positioned above the field.</li>
+ * <li><tt>"right"</tt> - The label is positioned to the left of the field, with its text aligned
+ * to the right. Its width is determined by the {@link #labelWidth} config.</li>
+ * </ul>
+ */
+ labelAlign : 'left',
+
+ /**
+ * @cfg {Number} labelWidth
+ * The width of the {@link #fieldLabel} in pixels. Only applicable if the {@link #labelAlign} is set
+ * to "left" or "right".
+ */
+ labelWidth: 100,
+
+ /**
+ * @cfg {Number} labelPad
+ * The amount of space in pixels between the {@link #fieldLabel} and the input field.
+ */
+ labelPad : 5,
+
+ /**
+ * @cfg {String} labelSeparator
+ * Character(s) to be inserted at the end of the {@link #fieldLabel label text}.
+ */
+ labelSeparator : ':',
+
+ /**
+ * @cfg {String} labelStyle
+ * A CSS style specification string to apply directly to this field's label.
+ */
+
+ /**
+ * @cfg {Boolean} hideLabel
+ * Set to true to completely hide the label element ({@link #fieldLabel} and {@link #labelSeparator}).
+ * Also see {@link #hideEmptyLabel}, which controls whether space will be reserved for an empty fieldLabel.
+ */
+ hideLabel: false,
+
+ /**
+ * @cfg {Boolean} hideEmptyLabel
+ * <p>When set to <tt>true</tt>, the label element ({@link #fieldLabel} and {@link #labelSeparator}) will be
+ * automatically hidden if the {@link #fieldLabel} is empty. Setting this to <tt>false</tt> will cause the empty
+ * label element to be rendered and space to be reserved for it; this is useful if you want a field without a label
+ * to line up with other labeled fields in the same form.</p>
+ * <p>If you wish to unconditionall hide the label even if a non-empty fieldLabel is configured, then set
+ * the {@link #hideLabel} config to <tt>true</tt>.</p>
+ */
+ hideEmptyLabel: true,
+
+ /**
+ * @cfg {Boolean} preventMark
+ * <tt>true</tt> to disable displaying any {@link #setActiveError error message} set on this object.
+ */
+ preventMark: false,
+
+ /**
+ * @cfg {Boolean} autoFitErrors
+ * Whether to adjust the component's body area to make room for 'side' or 'under'
+ * {@link #msgTarget error messages}.
+ */
+ autoFitErrors: true,
+
+ /**
+ * @cfg {String} msgTarget <p>The location where the error message text should display.
+ * Must be one of the following values:</p>
+ * <div class="mdetail-params"><ul>
+ * <li><code>qtip</code> Display a quick tip containing the message when the user hovers over the field. This is the default.
+ * <div class="subdesc"><b>{@link Ext.tip.QuickTipManager#init Ext.tip.QuickTipManager.init} must have been called for this setting to work.</b></div></li>
+ * <li><code>title</code> Display the message in a default browser title attribute popup.</li>
+ * <li><code>under</code> Add a block div beneath the field containing the error message.</li>
+ * <li><code>side</code> Add an error icon to the right of the field, displaying the message in a popup on hover.</li>
+ * <li><code>none</code> Don't display any error message. This might be useful if you are implementing custom error display.</li>
+ * <li><code>[element id]</code> Add the error message directly to the innerHTML of the specified element.</li>
+ * </ul></div>
+ */
+ msgTarget: 'qtip',
+
+ /**
+ * @cfg {String} activeError
+ * If specified, then the component will be displayed with this value as its active error when
+ * first rendered. Use {@link #setActiveError} or {@link #unsetActiveError} to
+ * change it after component creation.
+ */
+
+
+ /**
+ * Performs initialization of this mixin. Component classes using this mixin should call this method
+ * during their own initialization.
+ */
+ initLabelable: function() {
+ this.addCls(this.formItemCls);
+
+ this.addEvents(
+ /**
+ * @event errorchange
+ * Fires when the active error message is changed via {@link #setActiveError}.
+ * @param {Ext.form.Labelable} this
+ * @param {String} error The active error message
+ */
+ 'errorchange'
+ );
+ },
+
+ /**
+ * Returns the label for the field. Defaults to simply returning the {@link #fieldLabel} config. Can be
+ * overridden to provide
+ * @return {String} The configured field label, or empty string if not defined
+ */
+ getFieldLabel: function() {
+ return this.fieldLabel || '';
+ },
+
+ /**
+ * @protected
+ * Generates the arguments for the field decorations {@link #labelableRenderTpl rendering template}.
+ * @return {Object} The template arguments
+ */
+ getLabelableRenderData: function() {
+ var me = this,
+ labelAlign = me.labelAlign,
+ labelCls = me.labelCls,
+ labelClsExtra = me.labelClsExtra,
+ labelPad = me.labelPad,
+ labelStyle;
+
+ // Calculate label styles up front rather than in the Field layout for speed; this
+ // is safe because label alignment/width/pad are not expected to change.
+ if (labelAlign === 'top') {
+ labelStyle = 'margin-bottom:' + labelPad + 'px;';
+ } else {
+ labelStyle = 'margin-right:' + labelPad + 'px;';
+ // Add the width for border-box browsers; will be set by the Field layout for content-box
+ if (Ext.isBorderBox) {
+ labelStyle += 'width:' + me.labelWidth + 'px;';
+ }
+ }
+
+ return Ext.copyTo(
+ {
+ inputId: me.getInputId(),
+ fieldLabel: me.getFieldLabel(),
+ labelCls: labelClsExtra ? labelCls + ' ' + labelClsExtra : labelCls,
+ labelStyle: labelStyle + (me.labelStyle || ''),
+ subTplMarkup: me.getSubTplMarkup()
+ },
+ me,
+ 'hideLabel,hideEmptyLabel,fieldBodyCls,baseBodyCls,errorMsgCls,clearCls,labelSeparator',
+ true
+ );
+ },
+
+ onLabelableRender: function () {
+ this.addChildEls(
+ /**
+ * @property labelEl
+ * @type Ext.Element
+ * The label Element for this component. Only available after the component has been rendered.
+ */
+ 'labelEl',
+
+ /**
+ * @property bodyEl
+ * @type Ext.Element
+ * The div Element wrapping the component's contents. Only available after the component has been rendered.
+ */
+ 'bodyEl',
+
+ /**
+ * @property errorEl
+ * @type Ext.Element
+ * The div Element that will contain the component's error message(s). Note that depending on the
+ * configured {@link #msgTarget}, this element may be hidden in favor of some other form of
+ * presentation, but will always be present in the DOM for use by assistive technologies.
+ */
+ 'errorEl'
+ );
+ },
+
+ /**
+ * @protected
+ * Gets the markup to be inserted into the outer template's bodyEl. Defaults to empty string, should
+ * be implemented by classes including this mixin as needed.
+ * @return {String} The markup to be inserted
+ */
+ getSubTplMarkup: function() {
+ return '';
+ },
+
+ /**
+ * Get the input id, if any, for this component. This is used as the "for" attribute on the label element.
+ * Implementing subclasses may also use this as e.g. the id for their own <tt>input</tt> element.
+ * @return {String} The input id
+ */
+ getInputId: function() {
+ return '';
+ },
+
+ /**
+ * Gets the active error message for this component, if any. This does not trigger
+ * validation on its own, it merely returns any message that the component may already hold.
+ * @return {String} The active error message on the component; if there is no error, an empty string is returned.
+ */
+ getActiveError : function() {
+ return this.activeError || '';
+ },
+
+ /**
+ * Tells whether the field currently has an active error message. This does not trigger
+ * validation on its own, it merely looks for any message that the component may already hold.
+ * @return {Boolean}
+ */
+ hasActiveError: function() {
+ return !!this.getActiveError();
+ },
+
+ /**
+ * Sets the active error message to the given string. This replaces the entire error message
+ * contents with the given string. Also see {@link #setActiveErrors} which accepts an Array of
+ * messages and formats them according to the {@link #activeErrorsTpl}.
+ *
+ * Note that this only updates the error message element's text and attributes, you'll have
+ * to call doComponentLayout to actually update the field's layout to match. If the field extends
+ * {@link Ext.form.field.Base} you should call {@link Ext.form.field.Base#markInvalid markInvalid} instead.
+ *
+ * @param {String} msg The error message
+ */
+ setActiveError: function(msg) {
+ this.activeError = msg;
+ this.activeErrors = [msg];
+ this.renderActiveError();
+ },
+
+ /**
+ * Gets an Array of any active error messages currently applied to the field. This does not trigger
+ * validation on its own, it merely returns any messages that the component may already hold.
+ * @return {String[]} The active error messages on the component; if there are no errors, an empty Array is returned.
+ */
+ getActiveErrors: function() {
+ return this.activeErrors || [];
+ },
+
+ /**
+ * Set the active error message to an Array of error messages. The messages are formatted into
+ * a single message string using the {@link #activeErrorsTpl}. Also see {@link #setActiveError}
+ * which allows setting the entire error contents with a single string.
+ *
+ * Note that this only updates the error message element's text and attributes, you'll have
+ * to call doComponentLayout to actually update the field's layout to match. If the field extends
+ * {@link Ext.form.field.Base} you should call {@link Ext.form.field.Base#markInvalid markInvalid} instead.
+ *
+ * @param {String[]} errors The error messages
+ */
+ setActiveErrors: function(errors) {
+ this.activeErrors = errors;
+ this.activeError = this.getTpl('activeErrorsTpl').apply({errors: errors});
+ this.renderActiveError();
+ },
+
+ /**
+ * Clears the active error message(s).
+ *
+ * Note that this only clears the error message element's text and attributes, you'll have
+ * to call doComponentLayout to actually update the field's layout to match. If the field extends
+ * {@link Ext.form.field.Base} you should call {@link Ext.form.field.Base#clearInvalid clearInvalid} instead.
+ */
+ unsetActiveError: function() {
+ delete this.activeError;
+ delete this.activeErrors;
+ this.renderActiveError();
+ },
+
+ /**
+ * @private
+ * Updates the rendered DOM to match the current activeError. This only updates the content and
+ * attributes, you'll have to call doComponentLayout to actually update the display.
+ */
+ renderActiveError: function() {
+ var me = this,
+ activeError = me.getActiveError(),
+ hasError = !!activeError;
+
+ if (activeError !== me.lastActiveError) {
+ me.fireEvent('errorchange', me, activeError);
+ me.lastActiveError = activeError;
+ }
+
+ if (me.rendered && !me.isDestroyed && !me.preventMark) {
+ // Add/remove invalid class
+ me.el[hasError ? 'addCls' : 'removeCls'](me.invalidCls);
+
+ // Update the aria-invalid attribute
+ me.getActionEl().dom.setAttribute('aria-invalid', hasError);
+
+ // Update the errorEl with the error message text
+ me.errorEl.dom.innerHTML = activeError;
+ }
+ },
+
+ /**
+ * Applies a set of default configuration values to this Labelable instance. For each of the
+ * properties in the given object, check if this component hasOwnProperty that config; if not
+ * then it's inheriting a default value from its prototype and we should apply the default value.
+ * @param {Object} defaults The defaults to apply to the object.
+ */
+ setFieldDefaults: function(defaults) {
+ var me = this;
+ Ext.iterate(defaults, function(key, val) {
+ if (!me.hasOwnProperty(key)) {
+ me[key] = val;
+ }
+ });
+ },
+
+ /**
+ * @protected Calculate and return the natural width of the bodyEl. Override to provide custom logic.
+ * Note for implementors: if at all possible this method should be overridden with a custom implementation
+ * that can avoid anything that would cause the browser to reflow, e.g. querying offsetWidth.
+ */
+ getBodyNaturalWidth: function() {
+ return this.bodyEl.getWidth();
+ }
+
+});
+
+/**
+ * @docauthor Jason Johnston <jason@sencha.com>
+ *
+ * This mixin provides a common interface for the logical behavior and state of form fields, including:
+ *
+ * - Getter and setter methods for field values
+ * - Events and methods for tracking value and validity changes
+ * - Methods for triggering validation
+ *
+ * **NOTE**: When implementing custom fields, it is most likely that you will want to extend the {@link Ext.form.field.Base}
+ * component class rather than using this mixin directly, as BaseField contains additional logic for generating an
+ * actual DOM complete with {@link Ext.form.Labelable label and error message} display and a form input field,
+ * plus methods that bind the Field value getters and setters to the input field's value.
+ *
+ * If you do want to implement this mixin directly and don't want to extend {@link Ext.form.field.Base}, then
+ * you will most likely want to override the following methods with custom implementations: {@link #getValue},
+ * {@link #setValue}, and {@link #getErrors}. Other methods may be overridden as needed but their base
+ * implementations should be sufficient for common cases. You will also need to make sure that {@link #initField}
+ * is called during the component's initialization.
+ */
+Ext.define('Ext.form.field.Field', {
+ /**
+ * @property {Boolean} isFormField
+ * Flag denoting that this component is a Field. Always true.
+ */
+ isFormField : true,
+
+ /**
+ * @cfg {Object} value
+ * A value to initialize this field with.
+ */
+
+ /**
+ * @cfg {String} name
+ * The name of the field. By default this is used as the parameter name when including the
+ * {@link #getSubmitData field value} in a {@link Ext.form.Basic#submit form submit()}. To prevent the field from
+ * being included in the form submit, set {@link #submitValue} to false.
+ */
+
+ /**
+ * @cfg {Boolean} disabled
+ * True to disable the field. Disabled Fields will not be {@link Ext.form.Basic#submit submitted}.
+ */
+ disabled : false,
+
+ /**
+ * @cfg {Boolean} submitValue
+ * Setting this to false will prevent the field from being {@link Ext.form.Basic#submit submitted} even when it is
+ * not disabled.
+ */
+ submitValue: true,
+
+ /**
+ * @cfg {Boolean} validateOnChange
+ * Specifies whether this field should be validated immediately whenever a change in its value is detected.
+ * If the validation results in a change in the field's validity, a {@link #validitychange} event will be
+ * fired. This allows the field to show feedback about the validity of its contents immediately as the user is
+ * typing.
+ *
+ * When set to false, feedback will not be immediate. However the form will still be validated before submitting if
+ * the clientValidation option to {@link Ext.form.Basic#doAction} is enabled, or if the field or form are validated
+ * manually.
+ *
+ * See also {@link Ext.form.field.Base#checkChangeEvents} for controlling how changes to the field's value are
+ * detected.
+ */
+ validateOnChange: true,
+
+ /**
+ * @private
+ */
+ suspendCheckChange: 0,
+
+ /**
+ * Initializes this Field mixin on the current instance. Components using this mixin should call this method during
+ * their own initialization process.
+ */
+ initField: function() {
+ this.addEvents(
+ /**
+ * @event change
+ * Fires when a user-initiated change is detected in the value of the field.
+ * @param {Ext.form.field.Field} this
+ * @param {Object} newValue The new value
+ * @param {Object} oldValue The original value
+ */
+ 'change',
+ /**
+ * @event validitychange
+ * Fires when a change in the field's validity is detected.
+ * @param {Ext.form.field.Field} this
+ * @param {Boolean} isValid Whether or not the field is now valid
+ */
+ 'validitychange',
+ /**
+ * @event dirtychange
+ * Fires when a change in the field's {@link #isDirty} state is detected.
+ * @param {Ext.form.field.Field} this
+ * @param {Boolean} isDirty Whether or not the field is now dirty
+ */
+ 'dirtychange'
+ );
+
+ this.initValue();
+ },
+
+ /**
+ * Initializes the field's value based on the initial config.
+ */
+ initValue: function() {
+ var me = this;
+
+ /**
+ * @property {Object} originalValue
+ * The original value of the field as configured in the {@link #value} configuration, or as loaded by the last
+ * form load operation if the form's {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} setting is `true`.
+ */
+ me.originalValue = me.lastValue = me.value;
+
+ // Set the initial value - prevent validation on initial set
+ me.suspendCheckChange++;
+ me.setValue(me.value);
+ me.suspendCheckChange--;
+ },
+
+ /**
+ * Returns the {@link Ext.form.field.Field#name name} attribute of the field. This is used as the parameter name
+ * when including the field value in a {@link Ext.form.Basic#submit form submit()}.
+ * @return {String} name The field {@link Ext.form.field.Field#name name}
+ */
+ getName: function() {
+ return this.name;
+ },
+
+ /**
+ * Returns the current data value of the field. The type of value returned is particular to the type of the
+ * particular field (e.g. a Date object for {@link Ext.form.field.Date}).
+ * @return {Object} value The field value
+ */
+ getValue: function() {
+ return this.value;
+ },
+
+ /**
+ * Sets a data value into the field and runs the change detection and validation.
+ * @param {Object} value The value to set
+ * @return {Ext.form.field.Field} this
+ */
+ setValue: function(value) {
+ var me = this;
+ me.value = value;
+ me.checkChange();
+ return me;
+ },
+
+ /**
+ * Returns whether two field {@link #getValue values} are logically equal. Field implementations may override this
+ * to provide custom comparison logic appropriate for the particular field's data type.
+ * @param {Object} value1 The first value to compare
+ * @param {Object} value2 The second value to compare
+ * @return {Boolean} True if the values are equal, false if inequal.
+ */
+ isEqual: function(value1, value2) {
+ return String(value1) === String(value2);
+ },
+
+ /**
+ * Returns whether two values are logically equal.
+ * Similar to {@link #isEqual}, however null or undefined values will be treated as empty strings.
+ * @private
+ * @param {Object} value1 The first value to compare
+ * @param {Object} value2 The second value to compare
+ * @return {Boolean} True if the values are equal, false if inequal.
+ */
+ isEqualAsString: function(value1, value2){
+ return String(Ext.value(value1, '')) === String(Ext.value(value2, ''));
+ },
+
+ /**
+ * Returns the parameter(s) that would be included in a standard form submit for this field. Typically this will be
+ * an object with a single name-value pair, the name being this field's {@link #getName name} and the value being
+ * its current stringified value. More advanced field implementations may return more than one name-value pair.
+ *
+ * Note that the values returned from this method are not guaranteed to have been successfully {@link #validate
+ * validated}.
+ *
+ * @return {Object} A mapping of submit parameter names to values; each value should be a string, or an array of
+ * strings if that particular name has multiple values. It can also return null if there are no parameters to be
+ * submitted.
+ */
+ getSubmitData: function() {
+ var me = this,
+ data = null;
+ if (!me.disabled && me.submitValue && !me.isFileUpload()) {
+ data = {};
+ data[me.getName()] = '' + me.getValue();
+ }
+ return data;
+ },
+
+ /**
+ * Returns the value(s) that should be saved to the {@link Ext.data.Model} instance for this field, when {@link
+ * Ext.form.Basic#updateRecord} is called. Typically this will be an object with a single name-value pair, the name
+ * being this field's {@link #getName name} and the value being its current data value. More advanced field
+ * implementations may return more than one name-value pair. The returned values will be saved to the corresponding
+ * field names in the Model.
+ *
+ * Note that the values returned from this method are not guaranteed to have been successfully {@link #validate
+ * validated}.
+ *
+ * @return {Object} A mapping of submit parameter names to values; each value should be a string, or an array of
+ * strings if that particular name has multiple values. It can also return null if there are no parameters to be
+ * submitted.
+ */
+ getModelData: function() {
+ var me = this,
+ data = null;
+ if (!me.disabled && !me.isFileUpload()) {
+ data = {};
+ data[me.getName()] = me.getValue();
+ }
+ return data;
+ },
+
+ /**
+ * Resets the current field value to the originally loaded value and clears any validation messages. See {@link
+ * Ext.form.Basic}.{@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad}
+ */
+ reset : function(){
+ var me = this;
+
+ me.setValue(me.originalValue);
+ me.clearInvalid();
+ // delete here so we reset back to the original state
+ delete me.wasValid;
+ },
+
+ /**
+ * Resets the field's {@link #originalValue} property so it matches the current {@link #getValue value}. This is
+ * called by {@link Ext.form.Basic}.{@link Ext.form.Basic#setValues setValues} if the form's
+ * {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} property is set to true.
+ */
+ resetOriginalValue: function() {
+ this.originalValue = this.getValue();
+ this.checkDirty();
+ },
+
+ /**
+ * Checks whether the value of the field has changed since the last time it was checked.
+ * If the value has changed, it:
+ *
+ * 1. Fires the {@link #change change event},
+ * 2. Performs validation if the {@link #validateOnChange} config is enabled, firing the
+ * {@link #validitychange validitychange event} if the validity has changed, and
+ * 3. Checks the {@link #isDirty dirty state} of the field and fires the {@link #dirtychange dirtychange event}
+ * if it has changed.
+ */
+ checkChange: function() {
+ if (!this.suspendCheckChange) {
+ var me = this,
+ newVal = me.getValue(),
+ oldVal = me.lastValue;
+ if (!me.isEqual(newVal, oldVal) && !me.isDestroyed) {
+ me.lastValue = newVal;
+ me.fireEvent('change', me, newVal, oldVal);
+ me.onChange(newVal, oldVal);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Called when the field's value changes. Performs validation if the {@link #validateOnChange}
+ * config is enabled, and invokes the dirty check.
+ */
+ onChange: function(newVal, oldVal) {
+ if (this.validateOnChange) {
+ this.validate();
+ }
+ this.checkDirty();
+ },
+
+ /**
+ * Returns true if the value of this Field has been changed from its {@link #originalValue}.
+ * Will always return false if the field is disabled.
+ *
+ * Note that if the owning {@link Ext.form.Basic form} was configured with
+ * {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} then the {@link #originalValue} is updated when
+ * the values are loaded by {@link Ext.form.Basic}.{@link Ext.form.Basic#setValues setValues}.
+ * @return {Boolean} True if this field has been changed from its original value (and is not disabled),
+ * false otherwise.
+ */
+ isDirty : function() {
+ var me = this;
+ return !me.disabled && !me.isEqual(me.getValue(), me.originalValue);
+ },
+
+ /**
+ * Checks the {@link #isDirty} state of the field and if it has changed since the last time it was checked,
+ * fires the {@link #dirtychange} event.
+ */
+ checkDirty: function() {
+ var me = this,
+ isDirty = me.isDirty();
+ if (isDirty !== me.wasDirty) {
+ me.fireEvent('dirtychange', me, isDirty);
+ me.onDirtyChange(isDirty);
+ me.wasDirty = isDirty;
+ }
+ },
+
+ /**
+ * @private Called when the field's dirty state changes.
+ * @param {Boolean} isDirty
+ */
+ onDirtyChange: Ext.emptyFn,
+
+ /**
+ * Runs this field's validators and returns an array of error messages for any validation failures. This is called
+ * internally during validation and would not usually need to be used manually.
+ *
+ * Each subclass should override or augment the return value to provide their own errors.
+ *
+ * @param {Object} value The value to get errors for (defaults to the current field value)
+ * @return {String[]} All error messages for this field; an empty Array if none.
+ */
+ getErrors: function(value) {
+ return [];
+ },
+
+ /**
+ * Returns whether or not the field value is currently valid by {@link #getErrors validating} the field's current
+ * value. The {@link #validitychange} event will not be fired; use {@link #validate} instead if you want the event
+ * to fire. **Note**: {@link #disabled} fields are always treated as valid.
+ *
+ * Implementations are encouraged to ensure that this method does not have side-effects such as triggering error
+ * message display.
+ *
+ * @return {Boolean} True if the value is valid, else false
+ */
+ isValid : function() {
+ var me = this;
+ return me.disabled || Ext.isEmpty(me.getErrors());
+ },
+
+ /**
+ * Returns whether or not the field value is currently valid by {@link #getErrors validating} the field's current
+ * value, and fires the {@link #validitychange} event if the field's validity has changed since the last validation.
+ * **Note**: {@link #disabled} fields are always treated as valid.
+ *
+ * Custom implementations of this method are allowed to have side-effects such as triggering error message display.
+ * To validate without side-effects, use {@link #isValid}.
+ *
+ * @return {Boolean} True if the value is valid, else false
+ */
+ validate : function() {
+ var me = this,
+ isValid = me.isValid();
+ if (isValid !== me.wasValid) {
+ me.wasValid = isValid;
+ me.fireEvent('validitychange', me, isValid);
+ }
+ return isValid;
+ },
+
+ /**
+ * A utility for grouping a set of modifications which may trigger value changes into a single transaction, to
+ * prevent excessive firing of {@link #change} events. This is useful for instance if the field has sub-fields which
+ * are being updated as a group; you don't want the container field to check its own changed state for each subfield
+ * change.
+ * @param {Object} fn A function containing the transaction code
+ */
+ batchChanges: function(fn) {
+ try {
+ this.suspendCheckChange++;
+ fn();
+ } catch(e){
+ throw e;
+ } finally {
+ this.suspendCheckChange--;
+ }
+ this.checkChange();
+ },
+
+ /**
+ * Returns whether this Field is a file upload field; if it returns true, forms will use special techniques for
+ * {@link Ext.form.Basic#submit submitting the form} via AJAX. See {@link Ext.form.Basic#hasUpload} for details. If
+ * this returns true, the {@link #extractFileInput} method must also be implemented to return the corresponding file
+ * input element.
+ * @return {Boolean}
+ */
+ isFileUpload: function() {
+ return false;
+ },
+
+ /**
+ * Only relevant if the instance's {@link #isFileUpload} method returns true. Returns a reference to the file input
+ * DOM element holding the user's selected file. The input will be appended into the submission form and will not be
+ * returned, so this method should also create a replacement.
+ * @return {HTMLElement}
+ */
+ extractFileInput: function() {
+ return null;
+ },
+
+ /**
+ * @method markInvalid
+ * Associate one or more error messages with this field. Components using this mixin should implement this method to
+ * update the component's rendering to display the messages.
+ *
+ * **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid} methods 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.
+ *
+ * @param {String/String[]} errors The error message(s) for the field.
+ */
+ markInvalid: Ext.emptyFn,
+
+ /**
+ * @method clearInvalid
+ * Clear any invalid styles/messages for this field. Components using this mixin should implement this method to
+ * update the components rendering to clear any existing messages.
+ *
+ * **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid} methods to return `true`
+ * if the value does not _pass_ validation. So simply clearing a field's errors will not necessarily allow
+ * submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} option set.
+ */
+ clearInvalid: Ext.emptyFn
+
+});
+
+/**
+ * @class Ext.layout.component.field.Field
+ * @extends Ext.layout.component.Component
+ * Layout class for components with {@link Ext.form.Labelable field labeling}, handling the sizing and alignment of
+ * the form control, label, and error message treatment.
+ * @private
+ */
+Ext.define('Ext.layout.component.field.Field', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.field'],
+
+ extend: 'Ext.layout.component.Component',
+
+ uses: ['Ext.tip.QuickTip', 'Ext.util.TextMetrics'],
+
+ /* End Definitions */
+
+ type: 'field',
+
+ beforeLayout: function(width, height) {
+ var me = this;
+ return me.callParent(arguments) || (!me.owner.preventMark && me.activeError !== me.owner.getActiveError());
+ },
+
+ onLayout: function(width, height) {
+ var me = this,
+ owner = me.owner,
+ labelStrategy = me.getLabelStrategy(),
+ errorStrategy = me.getErrorStrategy(),
+ isDefined = Ext.isDefined,
+ isNumber = Ext.isNumber,
+ lastSize, autoWidth, autoHeight, info, undef;
+
+ lastSize = me.lastComponentSize || {};
+ if (!isDefined(width)) {
+ width = lastSize.width;
+ if (width < 0) { //first pass lastComponentSize.width is -Infinity
+ width = undef;
+ }
+ }
+ if (!isDefined(height)) {
+ height = lastSize.height;
+ if (height < 0) { //first pass lastComponentSize.height is -Infinity
+ height = undef;
+ }
+ }
+ autoWidth = !isNumber(width);
+ autoHeight = !isNumber(height);
+
+ info = {
+ autoWidth: autoWidth,
+ autoHeight: autoHeight,
+ width: autoWidth ? owner.getBodyNaturalWidth() : width, //always give a pixel width
+ height: height,
+ setOuterWidth: false, //whether the outer el width should be set to the calculated width
+
+ // insets for the bodyEl from each side of the component layout area
+ insets: {
+ top: 0,
+ right: 0,
+ bottom: 0,
+ left: 0
+ }
+ };
+
+ // NOTE the order of calculating insets and setting styles here is very important; we must first
+ // calculate and set horizontal layout alone, as the horizontal sizing of elements can have an impact
+ // on the vertical sizes due to wrapping, then calculate and set the vertical layout.
+
+ // perform preparation on the label and error (setting css classes, qtips, etc.)
+ labelStrategy.prepare(owner, info);
+ errorStrategy.prepare(owner, info);
+
+ // calculate the horizontal insets for the label and error
+ labelStrategy.adjustHorizInsets(owner, info);
+ errorStrategy.adjustHorizInsets(owner, info);
+
+ // set horizontal styles for label and error based on the current insets
+ labelStrategy.layoutHoriz(owner, info);
+ errorStrategy.layoutHoriz(owner, info);
+
+ // calculate the vertical insets for the label and error
+ labelStrategy.adjustVertInsets(owner, info);
+ errorStrategy.adjustVertInsets(owner, info);
+
+ // set vertical styles for label and error based on the current insets
+ labelStrategy.layoutVert(owner, info);
+ errorStrategy.layoutVert(owner, info);
+
+ // perform sizing of the elements based on the final dimensions and insets
+ if (autoWidth && autoHeight) {
+ // Don't use setTargetSize if auto-sized, so the calculated size is not reused next time
+ me.setElementSize(owner.el, (info.setOuterWidth ? info.width : undef), info.height);
+ } else {
+ me.setTargetSize((!autoWidth || info.setOuterWidth ? info.width : undef), info.height);
+ }
+ me.sizeBody(info);
+
+ me.activeError = owner.getActiveError();
+ },
+
+ onFocus: function(){
+ this.getErrorStrategy().onFocus(this.owner);
+ },
+
+
+ /**
+ * Perform sizing and alignment of the bodyEl (and children) to match the calculated insets.
+ */
+ sizeBody: function(info) {
+ var me = this,
+ owner = me.owner,
+ insets = info.insets,
+ totalWidth = info.width,
+ totalHeight = info.height,
+ width = Ext.isNumber(totalWidth) ? totalWidth - insets.left - insets.right : totalWidth,
+ height = Ext.isNumber(totalHeight) ? totalHeight - insets.top - insets.bottom : totalHeight;
+
+ // size the bodyEl
+ me.setElementSize(owner.bodyEl, width, height);
+
+ // size the bodyEl's inner contents if necessary
+ me.sizeBodyContents(width, height);
+ },
+
+ /**
+ * Size the contents of the field body, given the full dimensions of the bodyEl. Does nothing by
+ * default, subclasses can override to handle their specific contents.
+ * @param {Number} width The bodyEl width
+ * @param {Number} height The bodyEl height
+ */
+ sizeBodyContents: Ext.emptyFn,
+
+
+ /**
+ * Return the set of strategy functions from the {@link #labelStrategies labelStrategies collection}
+ * that is appropriate for the field's {@link Ext.form.Labelable#labelAlign labelAlign} config.
+ */
+ getLabelStrategy: function() {
+ var me = this,
+ strategies = me.labelStrategies,
+ labelAlign = me.owner.labelAlign;
+ return strategies[labelAlign] || strategies.base;
+ },
+
+ /**
+ * Return the set of strategy functions from the {@link #errorStrategies errorStrategies collection}
+ * that is appropriate for the field's {@link Ext.form.Labelable#msgTarget msgTarget} config.
+ */
+ getErrorStrategy: function() {
+ var me = this,
+ owner = me.owner,
+ strategies = me.errorStrategies,
+ msgTarget = owner.msgTarget;
+ return !owner.preventMark && Ext.isString(msgTarget) ?
+ (strategies[msgTarget] || strategies.elementId) :
+ strategies.none;
+ },
+
+
+
+ /**
+ * Collection of named strategies for laying out and adjusting labels to accommodate error messages.
+ * An appropriate one will be chosen based on the owner field's {@link Ext.form.Labelable#labelAlign} config.
+ */
+ labelStrategies: (function() {
+ var applyIf = Ext.applyIf,
+ emptyFn = Ext.emptyFn,
+ base = {
+ prepare: function(owner, info) {
+ var cls = owner.labelCls + '-' + owner.labelAlign,
+ labelEl = owner.labelEl;
+ if (labelEl && !labelEl.hasCls(cls)) {
+ labelEl.addCls(cls);
+ }
+ },
+ adjustHorizInsets: emptyFn,
+ adjustVertInsets: emptyFn,
+ layoutHoriz: emptyFn,
+ layoutVert: emptyFn
+ },
+ left = applyIf({
+ prepare: function(owner, info) {
+ base.prepare(owner, info);
+ // If auto width, add the label width to the body's natural width.
+ if (info.autoWidth) {
+ info.width += (!owner.labelEl ? 0 : owner.labelWidth + owner.labelPad);
+ }
+ // Must set outer width to prevent field from wrapping below floated label
+ info.setOuterWidth = true;
+ },
+ adjustHorizInsets: function(owner, info) {
+ if (owner.labelEl) {
+ info.insets.left += owner.labelWidth + owner.labelPad;
+ }
+ },
+ layoutHoriz: function(owner, info) {
+ // For content-box browsers we can't rely on Labelable.js#getLabelableRenderData
+ // setting the width style because it needs to account for the final calculated
+ // padding/border styles for the label. So we set the width programmatically here to
+ // normalize content-box sizing, while letting border-box browsers use the original
+ // width style.
+ var labelEl = owner.labelEl;
+ if (labelEl && !owner.isLabelSized && !Ext.isBorderBox) {
+ labelEl.setWidth(owner.labelWidth);
+ owner.isLabelSized = true;
+ }
+ }
+ }, base);
+
+
+ return {
+ base: base,
+
+ /**
+ * Label displayed above the bodyEl
+ */
+ top: applyIf({
+ adjustVertInsets: function(owner, info) {
+ var labelEl = owner.labelEl;
+ if (labelEl) {
+ info.insets.top += Ext.util.TextMetrics.measure(labelEl, owner.fieldLabel, info.width).height +
+ labelEl.getFrameWidth('tb') + owner.labelPad;
+ }
+ }
+ }, base),
+
+ /**
+ * Label displayed to the left of the bodyEl
+ */
+ left: left,
+
+ /**
+ * Same as left, only difference is text-align in CSS
+ */
+ right: left
+ };
+ })(),
+
+
+
+ /**
+ * Collection of named strategies for laying out and adjusting insets to accommodate error messages.
+ * An appropriate one will be chosen based on the owner field's {@link Ext.form.Labelable#msgTarget} config.
+ */
+ errorStrategies: (function() {
+ function setDisplayed(el, displayed) {
+ var wasDisplayed = el.getStyle('display') !== 'none';
+ if (displayed !== wasDisplayed) {
+ el.setDisplayed(displayed);
+ }
+ }
+
+ function setStyle(el, name, value) {
+ if (el.getStyle(name) !== value) {
+ el.setStyle(name, value);
+ }
+ }
+
+ function showTip(owner) {
+ var tip = Ext.layout.component.field.Field.tip,
+ target;
+
+ if (tip && tip.isVisible()) {
+ target = tip.activeTarget;
+ if (target && target.el === owner.getActionEl().dom) {
+ tip.toFront(true);
+ }
+ }
+ }
+
+ var applyIf = Ext.applyIf,
+ emptyFn = Ext.emptyFn,
+ base = {
+ prepare: function(owner) {
+ setDisplayed(owner.errorEl, false);
+ },
+ adjustHorizInsets: emptyFn,
+ adjustVertInsets: emptyFn,
+ layoutHoriz: emptyFn,
+ layoutVert: emptyFn,
+ onFocus: emptyFn
+ };
+
+ return {
+ none: base,
+
+ /**
+ * Error displayed as icon (with QuickTip on hover) to right of the bodyEl
+ */
+ side: applyIf({
+ prepare: function(owner) {
+ var errorEl = owner.errorEl;
+ errorEl.addCls(Ext.baseCSSPrefix + 'form-invalid-icon');
+ Ext.layout.component.field.Field.initTip();
+ errorEl.dom.setAttribute('data-errorqtip', owner.getActiveError() || '');
+ setDisplayed(errorEl, owner.hasActiveError());
+ },
+ adjustHorizInsets: function(owner, info) {
+ if (owner.autoFitErrors && owner.hasActiveError()) {
+ info.insets.right += owner.errorEl.getWidth();
+ }
+ },
+ layoutHoriz: function(owner, info) {
+ if (owner.hasActiveError()) {
+ setStyle(owner.errorEl, 'left', info.width - info.insets.right + 'px');
+ }
+ },
+ layoutVert: function(owner, info) {
+ if (owner.hasActiveError()) {
+ setStyle(owner.errorEl, 'top', info.insets.top + 'px');
+ }
+ },
+ onFocus: showTip
+ }, base),
+
+ /**
+ * Error message displayed underneath the bodyEl
+ */
+ under: applyIf({
+ prepare: function(owner) {
+ var errorEl = owner.errorEl,
+ cls = Ext.baseCSSPrefix + 'form-invalid-under';
+ if (!errorEl.hasCls(cls)) {
+ errorEl.addCls(cls);
+ }
+ setDisplayed(errorEl, owner.hasActiveError());
+ },
+ adjustVertInsets: function(owner, info) {
+ if (owner.autoFitErrors) {
+ info.insets.bottom += owner.errorEl.getHeight();
+ }
+ },
+ layoutHoriz: function(owner, info) {
+ var errorEl = owner.errorEl,
+ insets = info.insets;
+
+ setStyle(errorEl, 'width', info.width - insets.right - insets.left + 'px');
+ setStyle(errorEl, 'marginLeft', insets.left + 'px');
+ }
+ }, base),
+
+ /**
+ * Error displayed as QuickTip on hover of the field container
+ */
+ qtip: applyIf({
+ prepare: function(owner) {
+ setDisplayed(owner.errorEl, false);
+ Ext.layout.component.field.Field.initTip();
+ owner.getActionEl().dom.setAttribute('data-errorqtip', owner.getActiveError() || '');
+ },
+ onFocus: showTip
+ }, base),
+
+ /**
+ * Error displayed as title tip on hover of the field container
+ */
+ title: applyIf({
+ prepare: function(owner) {
+ setDisplayed(owner.errorEl, false);
+ owner.el.dom.title = owner.getActiveError() || '';
+ }
+ }, base),
+
+ /**
+ * Error message displayed as content of an element with a given id elsewhere in the app
+ */
+ elementId: applyIf({
+ prepare: function(owner) {
+ setDisplayed(owner.errorEl, false);
+ var targetEl = Ext.fly(owner.msgTarget);
+ if (targetEl) {
+ targetEl.dom.innerHTML = owner.getActiveError() || '';
+ targetEl.setDisplayed(owner.hasActiveError());
+ }
+ }
+ }, base)
+ };
+ })(),
+
+ statics: {
+ /**
+ * Use a custom QuickTip instance separate from the main QuickTips singleton, so that we
+ * can give it a custom frame style. Responds to errorqtip rather than the qtip property.
+ */
+ initTip: function() {
+ var tip = this.tip;
+ if (!tip) {
+ tip = this.tip = Ext.create('Ext.tip.QuickTip', {
+ baseCls: Ext.baseCSSPrefix + 'form-invalid-tip',
+ renderTo: Ext.getBody()
+ });
+ tip.tagConfig = Ext.apply({}, {attribute: 'errorqtip'}, tip.tagConfig);
+ }
+ },
+
+ /**
+ * Destroy the error tip instance.
+ */
+ destroyTip: function() {
+ var tip = this.tip;
+ if (tip) {
+ tip.destroy();
+ delete this.tip;
+ }
+ }
+ }
+
+});
+
+/**
+ * @singleton
+ * @alternateClassName Ext.form.VTypes
+ *
+ * This is a singleton object which contains a set of commonly used field validation functions
+ * and provides a mechanism for creating reusable custom field validations.
+ * The following field validation functions are provided out of the box:
+ *
+ * - {@link #alpha}
+ * - {@link #alphanum}
+ * - {@link #email}
+ * - {@link #url}
+ *
+ * VTypes can be applied to a {@link Ext.form.field.Text Text Field} using the `{@link Ext.form.field.Text#vtype vtype}` configuration:
+ *
+ * Ext.create('Ext.form.field.Text', {
+ * fieldLabel: 'Email Address',
+ * name: 'email',
+ * vtype: 'email' // applies email validation rules to this field
+ * });
+ *
+ * To create custom VTypes:
+ *
+ * // custom Vtype for vtype:'time'
+ * var timeTest = /^([1-9]|1[0-9]):([0-5][0-9])(\s[a|p]m)$/i;
+ * Ext.apply(Ext.form.field.VTypes, {
+ * // vtype validation function
+ * time: function(val, field) {
+ * return timeTest.test(val);
+ * },
+ * // vtype Text property: The error text to display when the validation function returns false
+ * timeText: 'Not a valid time. Must be in the format "12:34 PM".',
+ * // vtype Mask property: The keystroke filter mask
+ * timeMask: /[\d\s:amp]/i
+ * });
+ *
+ * In the above example the `time` function is the validator that will run when field validation occurs,
+ * `timeText` is the error message, and `timeMask` limits what characters can be typed into the field.
+ * Note that the `Text` and `Mask` functions must begin with the same name as the validator function.
+ *
+ * Using a custom validator is the same as using one of the build-in validators - just use the name of the validator function
+ * as the `{@link Ext.form.field.Text#vtype vtype}` configuration on a {@link Ext.form.field.Text Text Field}:
+ *
+ * Ext.create('Ext.form.field.Text', {
+ * fieldLabel: 'Departure Time',
+ * name: 'departureTime',
+ * vtype: 'time' // applies custom time validation rules to this field
+ * });
+ *
+ * Another example of a custom validator:
+ *
+ * // custom Vtype for vtype:'IPAddress'
+ * Ext.apply(Ext.form.field.VTypes, {
+ * IPAddress: function(v) {
+ * return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(v);
+ * },
+ * IPAddressText: 'Must be a numeric IP address',
+ * IPAddressMask: /[\d\.]/i
+ * });
+ *
+ * It's important to note that using {@link Ext#apply Ext.apply()} means that the custom validator function
+ * as well as `Text` and `Mask` fields are added as properties of the `Ext.form.field.VTypes` singleton.
+ */
+Ext.define('Ext.form.field.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,6}$/,
+ url = /(((^https?)|(^ftp)):\/\/([\-\w]+\.)+\w{2,3}(\/[%\-\w]+(\.\w{2,})?)*(([\w\-\.\?\\\/+@&#;`~=%!]*)(\.\w{2,})?)*\/?)/i;
+
+ // All these messages and functions are configurable
+ return {
+ singleton: true,
+ alternateClassName: 'Ext.form.VTypes',
+
+ /**
+ * The function used to validate email addresses. Note that this is a very basic validation - complete
+ * validation per the email RFC specifications is very complex and beyond the scope of this class, although this
+ * function can be overridden if a more comprehensive validation scheme is desired. See the validation section
+ * of the [Wikipedia article on email addresses][1] for additional information. This implementation is intended
+ * to validate the following emails:
+ *
+ * - `barney@example.de`
+ * - `barney.rubble@example.com`
+ * - `barney-rubble@example.coop`
+ * - `barney+rubble@example.com`
+ *
+ * [1]: http://en.wikipedia.org/wiki/E-mail_address
+ *
+ * @param {String} value The email address
+ * @return {Boolean} true if the RegExp test passed, and false if not.
+ */
+ 'email' : function(v){
+ return email.test(v);
+ },
+ /**
+ * @property {String} emailText
+ * The error text to display when the email validation function returns false.
+ * Defaults to: 'This field should be an e-mail address in the format "user@example.com"'
+ */
+ 'emailText' : 'This field should be an e-mail address in the format "user@example.com"',
+ /**
+ * @property {RegExp} emailMask
+ * The keystroke filter mask to be applied on email input. See the {@link #email} method for information about
+ * more complex email validation. Defaults to: /[a-z0-9_\.\-@]/i
+ */
+ 'emailMask' : /[a-z0-9_\.\-@\+]/i,
+
+ /**
+ * The function used to validate URLs
+ * @param {String} value The URL
+ * @return {Boolean} true if the RegExp test passed, and false if not.
+ */
+ 'url' : function(v){
+ return url.test(v);
+ },
+ /**
+ * @property {String} urlText
+ * The error text to display when the url validation function returns false.
+ * Defaults to: 'This field should be a URL in the format "http:/'+'/www.example.com"'
+ */
+ 'urlText' : 'This field should be a URL in the format "http:/'+'/www.example.com"',
+
+ /**
+ * The function used to validate alpha values
+ * @param {String} value The value
+ * @return {Boolean} true if the RegExp test passed, and false if not.
+ */
+ 'alpha' : function(v){
+ return alpha.test(v);
+ },
+ /**
+ * @property {String} alphaText
+ * The error text to display when the alpha validation function returns false.
+ * Defaults to: 'This field should only contain letters and _'
+ */
+ 'alphaText' : 'This field should only contain letters and _',
+ /**
+ * @property {RegExp} alphaMask
+ * The keystroke filter mask to be applied on alpha input. Defaults to: /[a-z_]/i
+ */
+ 'alphaMask' : /[a-z_]/i,
+
+ /**
+ * The function used to validate alphanumeric values
+ * @param {String} value The value
+ * @return {Boolean} true if the RegExp test passed, and false if not.
+ */
+ 'alphanum' : function(v){
+ return alphanum.test(v);
+ },
+ /**
+ * @property {String} alphanumText
+ * The error text to display when the alphanumeric validation function returns false.
+ * Defaults to: 'This field should only contain letters, numbers and _'
+ */
+ 'alphanumText' : 'This field should only contain letters, numbers and _',
+ /**
+ * @property {RegExp} alphanumMask
+ * The keystroke filter mask to be applied on alphanumeric input. Defaults to: /[a-z0-9_]/i
+ */
+ 'alphanumMask' : /[a-z0-9_]/i
+ };
+})());
+
+/**
+ * @private
+ * @class Ext.layout.component.field.Text
+ * @extends Ext.layout.component.field.Field
+ * Layout class for {@link Ext.form.field.Text} fields. Handles sizing the input field.
+ */
+Ext.define('Ext.layout.component.field.Text', {
+ extend: 'Ext.layout.component.field.Field',
+ alias: 'layout.textfield',
+ requires: ['Ext.util.TextMetrics'],
+
+ type: 'textfield',
+
+
+ /**
+ * Allow layout to proceed if the {@link Ext.form.field.Text#grow} config is enabled and the value has
+ * changed since the last layout.
+ */
+ beforeLayout: function(width, height) {
+ var me = this,
+ owner = me.owner,
+ lastValue = this.lastValue,
+ value = owner.getRawValue();
+ this.lastValue = value;
+ return me.callParent(arguments) || (owner.grow && value !== lastValue);
+ },
+
+
+ /**
+ * Size the field body contents given the total dimensions of the bodyEl, taking into account the optional
+ * {@link Ext.form.field.Text#grow} configurations.
+ * @param {Number} width The bodyEl width
+ * @param {Number} height The bodyEl height
+ */
+ sizeBodyContents: function(width, height) {
+ var size = this.adjustForGrow(width, height);
+ this.setElementSize(this.owner.inputEl, size[0], size[1]);
+ },
+
+
+ /**
+ * Given the target bodyEl dimensions, adjust them if necessary to return the correct final
+ * size based on the text field's {@link Ext.form.field.Text#grow grow config}.
+ * @param {Number} width The bodyEl width
+ * @param {Number} height The bodyEl height
+ * @return {Number[]} [inputElWidth, inputElHeight]
+ */
+ adjustForGrow: function(width, height) {
+ var me = this,
+ owner = me.owner,
+ inputEl, value, calcWidth,
+ result = [width, height];
+
+ if (owner.grow) {
+ inputEl = owner.inputEl;
+
+ // Find the width that contains the whole text value
+ value = (inputEl.dom.value || (owner.hasFocus ? '' : owner.emptyText) || '') + owner.growAppend;
+ calcWidth = inputEl.getTextWidth(value) + inputEl.getBorderWidth("lr") + inputEl.getPadding("lr");
+
+ // Constrain
+ result[0] = Ext.Number.constrain(calcWidth, owner.growMin,
+ Math.max(owner.growMin, Math.min(owner.growMax, Ext.isNumber(width) ? width : Infinity)));
+ }
+
+ return result;
+ }
+
+});
+
+/**
+ * @private
+ * @class Ext.layout.component.field.TextArea
+ * @extends Ext.layout.component.field.Field
+ * Layout class for {@link Ext.form.field.TextArea} fields. Handles sizing the textarea field.
+ */
+Ext.define('Ext.layout.component.field.TextArea', {
+ extend: 'Ext.layout.component.field.Text',
+ alias: 'layout.textareafield',
+
+ type: 'textareafield',
+
+
+ /**
+ * Given the target bodyEl dimensions, adjust them if necessary to return the correct final
+ * size based on the text field's {@link Ext.form.field.Text#grow grow config}. Overrides the
+ * textfield layout's implementation to handle height rather than width.
+ * @param {Number} width The bodyEl width
+ * @param {Number} height The bodyEl height
+ * @return {Number[]} [inputElWidth, inputElHeight]
+ */
+ adjustForGrow: function(width, height) {
+ var me = this,
+ owner = me.owner,
+ inputEl, value, max,
+ curWidth, curHeight, calcHeight,
+ result = [width, height];
+
+ if (owner.grow) {
+ inputEl = owner.inputEl;
+ curWidth = inputEl.getWidth(true); //subtract border/padding to get the available width for the text
+ curHeight = inputEl.getHeight();
+
+ // Get and normalize the field value for measurement
+ value = inputEl.dom.value || ' ';
+ value += owner.growAppend;
+
+ // Translate newlines to <br> tags
+ value = value.replace(/\n/g, '<br>');
+
+ // Find the height that contains the whole text value
+ calcHeight = Ext.util.TextMetrics.measure(inputEl, value, curWidth).height +
+ inputEl.getBorderWidth("tb") + inputEl.getPadding("tb");
+
+ // Constrain
+ max = owner.growMax;
+ if (Ext.isNumber(height)) {
+ max = Math.min(max, height);
+ }
+ result[1] = Ext.Number.constrain(calcHeight, owner.growMin, max);
+ }
+
+ return result;
+ }
+
+});
+/**
+ * @class Ext.layout.container.Anchor
+ * @extends Ext.layout.container.Container
+ *
+ * This is a layout that enables anchoring of contained elements relative to the container's dimensions.
+ * If the container is resized, all anchored items are automatically rerendered according to their
+ * `{@link #anchor}` rules.
+ *
+ * This class is intended to be extended or created via the {@link Ext.container.AbstractContainer#layout layout}: 'anchor'
+ * config, and should generally not need to be created directly via the new keyword.
+ *
+ * AnchorLayout does not have any direct config options (other than inherited ones). By default,
+ * AnchorLayout will calculate anchor measurements based on the size of the container itself. However, the
+ * container using the AnchorLayout can supply an anchoring-specific config property of `anchorSize`.
+ *
+ * If anchorSize is specifed, the layout will use it as a virtual container for the purposes of calculating
+ * anchor measurements based on it instead, allowing the container to be sized independently of the anchoring
+ * logic if necessary.
+ *
+ * @example
+ * Ext.create('Ext.Panel', {
+ * width: 500,
+ * height: 400,
+ * title: "AnchorLayout Panel",
+ * layout: 'anchor',
+ * renderTo: Ext.getBody(),
+ * items: [
+ * {
+ * xtype: 'panel',
+ * title: '75% Width and 20% Height',
+ * anchor: '75% 20%'
+ * },
+ * {
+ * xtype: 'panel',
+ * title: 'Offset -300 Width & -200 Height',
+ * anchor: '-300 -200'
+ * },
+ * {
+ * xtype: 'panel',
+ * title: 'Mixed Offset and Percent',
+ * anchor: '-250 20%'
+ * }
+ * ]
+ * });
+ */
+Ext.define('Ext.layout.container.Anchor', {
+
+ /* Begin Definitions */
+
+ alias: 'layout.anchor',
+ extend: 'Ext.layout.container.Container',
+ alternateClassName: 'Ext.layout.AnchorLayout',
+
+ /* End Definitions */
+
+ /**
+ * @cfg {String} anchor
+ *
+ * This configuation option is to be applied to **child `items`** of a container managed by
+ * this layout (ie. configured with `layout:'anchor'`).
+ *
+ * This value is what tells the layout how an item should be anchored to the container. `items`
+ * added to an AnchorLayout accept an anchoring-specific config property of **anchor** which is a string
+ * containing two values: the horizontal anchor value and the vertical anchor value (for example, '100% 50%').
+ * The following types of anchor values are supported:
+ *
+ * - **Percentage** : Any value between 1 and 100, expressed as a percentage.
+ *
+ * The first anchor is the percentage width that the item should take up within the container, and the
+ * second is the percentage height. For example:
+ *
+ * // two values specified
+ * anchor: '100% 50%' // render item complete width of the container and
+ * // 1/2 height of the container
+ * // one value specified
+ * anchor: '100%' // the width value; the height will default to auto
+ *
+ * - **Offsets** : Any positive or negative integer value.
+ *
+ * This is a raw adjustment where the first anchor is the offset from the right edge of the container,
+ * and the second is the offset from the bottom edge. For example:
+ *
+ * // two values specified
+ * anchor: '-50 -100' // render item the complete width of the container
+ * // minus 50 pixels and
+ * // the complete height minus 100 pixels.
+ * // one value specified
+ * anchor: '-50' // anchor value is assumed to be the right offset value
+ * // bottom offset will default to 0
+ *
+ * - **Sides** : Valid values are `right` (or `r`) and `bottom` (or `b`).
+ *
+ * Either the container must have a fixed size or an anchorSize config value defined at render time in
+ * order for these to have any effect.
+ *
+ * - **Mixed** :
+ *
+ * Anchor values can also be mixed as needed. For example, to render the width offset from the container
+ * right edge by 50 pixels and 75% of the container's height use:
+ *
+ * anchor: '-50 75%'
+ */
+ type: 'anchor',
+
+ /**
+ * @cfg {String} defaultAnchor
+ * Default anchor for all child <b>container</b> items applied if no anchor or specific width is set on the child item. Defaults to '100%'.
+ */
+ defaultAnchor: '100%',
+
+ parseAnchorRE: /^(r|right|b|bottom)$/i,
+
+ // private
+ onLayout: function() {
+ this.callParent(arguments);
+
+ var me = this,
+ size = me.getLayoutTargetSize(),
+ owner = me.owner,
+ target = me.getTarget(),
+ ownerWidth = size.width,
+ ownerHeight = size.height,
+ overflow = target.getStyle('overflow'),
+ components = me.getVisibleItems(owner),
+ len = components.length,
+ boxes = [],
+ box, newTargetSize, component, anchorSpec, calcWidth, calcHeight,
+ i, el, cleaner;
+
+ if (ownerWidth < 20 && ownerHeight < 20) {
+ return;
+ }
+
+ // Anchor layout uses natural HTML flow to arrange the child items.
+ // To ensure that all browsers (I'm looking at you IE!) add the bottom margin of the last child to the
+ // containing element height, we create a zero-sized element with style clear:both to force a "new line"
+ if (!me.clearEl) {
+ me.clearEl = target.createChild({
+ cls: Ext.baseCSSPrefix + 'clear',
+ role: 'presentation'
+ });
+ }
+
+ // Work around WebKit RightMargin bug. We're going to inline-block all the children only ONCE and remove it when we're done
+ if (!Ext.supports.RightMargin) {
+ cleaner = Ext.Element.getRightMarginFixCleaner(target);
+ target.addCls(Ext.baseCSSPrefix + 'inline-children');
+ }
+
+ for (i = 0; i < len; i++) {
+ component = components[i];
+ el = component.el;
+
+ anchorSpec = component.anchorSpec;
+ if (anchorSpec) {
+ if (anchorSpec.right) {
+ calcWidth = me.adjustWidthAnchor(anchorSpec.right(ownerWidth) - el.getMargin('lr'), component);
+ } else {
+ calcWidth = undefined;
+ }
+ if (anchorSpec.bottom) {
+ calcHeight = me.adjustHeightAnchor(anchorSpec.bottom(ownerHeight) - el.getMargin('tb'), component);
+ } else {
+ calcHeight = undefined;
+ }
+
+ boxes.push({
+ component: component,
+ anchor: true,
+ width: calcWidth || undefined,
+ height: calcHeight || undefined
+ });
+ } else {
+ boxes.push({
+ component: component,
+ anchor: false
+ });
+ }
+ }
+
+ // Work around WebKit RightMargin bug. We're going to inline-block all the children only ONCE and remove it when we're done
+ if (!Ext.supports.RightMargin) {
+ target.removeCls(Ext.baseCSSPrefix + 'inline-children');
+ cleaner();
+ }
+
+ for (i = 0; i < len; i++) {
+ box = boxes[i];
+ me.setItemSize(box.component, box.width, box.height);
+ }
+
+ if (overflow && overflow != 'hidden' && !me.adjustmentPass) {
+ newTargetSize = me.getLayoutTargetSize();
+ if (newTargetSize.width != size.width || newTargetSize.height != size.height) {
+ me.adjustmentPass = true;
+ me.onLayout();
+ }
+ }
+
+ delete me.adjustmentPass;
+ },
+
+ // private
+ parseAnchor: function(a, start, cstart) {
+ if (a && a != 'none') {
+ var ratio;
+ // standard anchor
+ if (this.parseAnchorRE.test(a)) {
+ var diff = cstart - start;
+ return function(v) {
+ return v - diff;
+ };
+ }
+ // percentage
+ else if (a.indexOf('%') != -1) {
+ ratio = parseFloat(a.replace('%', '')) * 0.01;
+ return function(v) {
+ return Math.floor(v * ratio);
+ };
+ }
+ // simple offset adjustment
+ else {
+ a = parseInt(a, 10);
+ if (!isNaN(a)) {
+ return function(v) {
+ return v + a;
+ };
+ }
+ }
+ }
+ return null;
+ },
+
+ // private
+ adjustWidthAnchor: function(value, comp) {
+ return value;
+ },
+
+ // private
+ adjustHeightAnchor: function(value, comp) {
+ return value;
+ },
+
+ configureItem: function(item) {
+ var me = this,
+ owner = me.owner,
+ anchor= item.anchor,
+ anchorsArray,
+ anchorSpec,
+ anchorWidth,
+ anchorHeight;
+
+ if (!item.anchor && item.items && !Ext.isNumber(item.width) && !(Ext.isIE6 && Ext.isStrict)) {
+ item.anchor = anchor = me.defaultAnchor;
+ }
+
+ // find the container anchoring size
+ if (owner.anchorSize) {
+ if (typeof owner.anchorSize == 'number') {
+ anchorWidth = owner.anchorSize;
+ }
+ else {
+ anchorWidth = owner.anchorSize.width;
+ anchorHeight = owner.anchorSize.height;
+ }
+ }
+ else {
+ anchorWidth = owner.initialConfig.width;
+ anchorHeight = owner.initialConfig.height;
+ }
+
+ if (anchor) {
+ // cache all anchor values
+ anchorsArray = anchor.split(' ');
+ item.anchorSpec = anchorSpec = {
+ right: me.parseAnchor(anchorsArray[0], item.initialConfig.width, anchorWidth),
+ bottom: me.parseAnchor(anchorsArray[1], item.initialConfig.height, anchorHeight)
+ };
+
+ if (anchorSpec.right) {
+ item.layoutManagedWidth = 1;
+ } else {
+ item.layoutManagedWidth = 2;
+ }
+
+ if (anchorSpec.bottom) {
+ item.layoutManagedHeight = 1;
+ } else {
+ item.layoutManagedHeight = 2;
+ }
+ } else {
+ item.layoutManagedWidth = 2;
+ item.layoutManagedHeight = 2;
+ }
+ this.callParent(arguments);
+ }
+
+});
+/**
+ * @class Ext.form.action.Load
+ * @extends Ext.form.action.Action
+ * <p>A class which handles loading of data from a server into the Fields of an {@link Ext.form.Basic}.</p>
+ * <p>Instances of this class are only created by a {@link Ext.form.Basic Form} when
+ * {@link Ext.form.Basic#load load}ing.</p>
+ * <p><u><b>Response Packet Criteria</b></u></p>
+ * <p>A response packet <b>must</b> contain:
+ * <div class="mdetail-params"><ul>
+ * <li><b><code>success</code></b> property : Boolean</li>
+ * <li><b><code>data</code></b> property : Object</li>
+ * <div class="sub-desc">The <code>data</code> 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.Field#setValue setValue} method.</div></li>
+ * </ul></div>
+ * <p><u><b>JSON Packets</b></u></p>
+ * <p>By default, response packets are assumed to be JSON, so for the following form load call:<pre><code>
+var myFormPanel = new Ext.form.Panel({
+ 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.Panel#getForm getForm}().{@link Ext.form.Basic#load load}({
+ url: '/getRoutingInfo.php',
+ params: {
+ consignmentRef: myConsignmentRef
+ },
+ failure: function(form, action) {
+ Ext.Msg.alert("Load failed", action.result.errorMessage);
+ }
+});
+</code></pre>
+ * a <b>success response</b> packet may look like this:</p><pre><code>
+{
+ success: true,
+ data: {
+ clientName: "Fred. Olsen Lines",
+ portOfLoading: "FXT",
+ portOfDischarge: "OSL"
+ }
+}</code></pre>
+ * while a <b>failure response</b> packet may look like this:</p><pre><code>
+{
+ success: false,
+ errorMessage: "Consignment reference not found"
+}</code></pre>
+ * <p>Other data may be placed into the response for processing the {@link Ext.form.Basic Form}'s
+ * callback or event handler methods. The object decoded from this JSON is available in the
+ * {@link Ext.form.action.Action#result result} property.</p>
+ */
+Ext.define('Ext.form.action.Load', {
+ extend:'Ext.form.action.Action',
+ requires: ['Ext.data.Connection'],
+ alternateClassName: 'Ext.form.Action.Load',
+ alias: 'formaction.load',
+
+ type: 'load',
+
+ /**
+ * @private
+ */
+ run: function() {
+ Ext.Ajax.request(Ext.apply(
+ this.createCallback(),
+ {
+ method: this.getMethod(),
+ url: this.getUrl(),
+ headers: this.headers,
+ params: this.getParams()
+ }
+ ));
+ },
+
+ /**
+ * @private
+ */
+ onSuccess: function(response){
+ var result = this.processResponse(response),
+ form = this.form;
+ if (result === true || !result.success || !result.data) {
+ this.failureType = Ext.form.action.Action.LOAD_FAILURE;
+ form.afterAction(this, false);
+ return;
+ }
+ form.clearInvalid();
+ form.setValues(result.data);
+ form.afterAction(this, true);
+ },
+
+ /**
+ * @private
+ */
+ handleResponse: function(response) {
+ var reader = this.form.reader,
+ rs, data;
+ if (reader) {
+ rs = reader.read(response);
+ data = rs.records && rs.records[0] ? rs.records[0].data : null;
+ return {
+ success : rs.success,
+ data : data
+ };
+ }
+ return Ext.decode(response.responseText);
+ }
+});
+
+
+/**
+ * A specialized panel intended for use as an application window. Windows are floated, {@link #resizable}, and
+ * {@link #draggable} by default. Windows can be {@link #maximizable maximized} to fill the viewport, restored to
+ * their prior size, and can be {@link #minimize}d.
+ *
+ * Windows can also be linked to a {@link Ext.ZIndexManager} or managed by the {@link Ext.WindowManager} to provide
+ * grouping, activation, to front, to back and other application-specific behavior.
+ *
+ * By default, Windows will be rendered to document.body. To {@link #constrain} a Window to another element specify
+ * {@link Ext.Component#renderTo renderTo}.
+ *
+ * **As with all {@link Ext.container.Container Container}s, it is important to consider how you want the Window to size
+ * and arrange any child Components. Choose an appropriate {@link #layout} configuration which lays out child Components
+ * in the required manner.**
+ *
+ * @example
+ * Ext.create('Ext.window.Window', {
+ * title: 'Hello',
+ * height: 200,
+ * width: 400,
+ * layout: 'fit',
+ * items: { // Let's put an empty grid in just to illustrate fit layout
+ * xtype: 'grid',
+ * border: false,
+ * columns: [{header: 'World'}], // One header just for show. There's no data,
+ * store: Ext.create('Ext.data.ArrayStore', {}) // A dummy empty data store
+ * }
+ * }).show();
+ */
+Ext.define('Ext.window.Window', {
+ extend: 'Ext.panel.Panel',
+
+ alternateClassName: 'Ext.Window',
+
+ requires: ['Ext.util.ComponentDragger', 'Ext.util.Region', 'Ext.EventManager'],
+
+ alias: 'widget.window',
+
+ /**
+ * @cfg {Number} x
+ * The X position of the left edge of the window on initial showing. Defaults to centering the Window within the
+ * width of the Window's container {@link Ext.Element Element} (The Element that the Window is rendered to).
+ */
+
+ /**
+ * @cfg {Number} y
+ * The Y position of the top edge of the window on initial showing. Defaults to centering the Window within the
+ * height of the Window's container {@link Ext.Element Element} (The Element that the Window is rendered to).
+ */
+
+ /**
+ * @cfg {Boolean} [modal=false]
+ * True to make the window modal and mask everything behind it when displayed, false to display it without
+ * restricting access to other UI elements.
+ */
+
+ /**
+ * @cfg {String/Ext.Element} [animateTarget=null]
+ * Id or element from which the window should animate while opening.
+ */
+
+ /**
+ * @cfg {String/Number/Ext.Component} defaultFocus
+ * Specifies a Component to receive focus when this Window is focused.
+ *
+ * This may be one of:
+ *
+ * - The index of a footer Button.
+ * - The id or {@link Ext.AbstractComponent#itemId} of a descendant Component.
+ * - A Component.
+ */
+
+ /**
+ * @cfg {Function} onEsc
+ * Allows override of the built-in processing for the escape key. Default action is to close the Window (performing
+ * whatever action is specified in {@link #closeAction}. To prevent the Window closing when the escape key is
+ * pressed, specify this as {@link Ext#emptyFn Ext.emptyFn}.
+ */
+
+ /**
+ * @cfg {Boolean} [collapsed=false]
+ * True to render the window collapsed, false to render it expanded. Note that if {@link #expandOnShow}
+ * is true (the default) it will override the `collapsed` config and the window will always be
+ * expanded when shown.
+ */
+
+ /**
+ * @cfg {Boolean} [maximized=false]
+ * True to initially display the window in a maximized state.
+ */
+
+ /**
+ * @cfg {String} [baseCls='x-window']
+ * The base CSS class to apply to this panel's element.
+ */
+ baseCls: Ext.baseCSSPrefix + 'window',
+
+ /**
+ * @cfg {Boolean/Object} resizable
+ * Specify as `true` to allow user resizing at each edge and corner of the window, false to disable resizing.
+ *
+ * This may also be specified as a config object to Ext.resizer.Resizer
+ */
+ resizable: true,
+
+ /**
+ * @cfg {Boolean} draggable
+ * True to allow the window to be dragged by the header bar, false to disable dragging. Note that
+ * by default the window will be centered in the viewport, so if dragging is disabled the window may need to be
+ * positioned programmatically after render (e.g., myWindow.setPosition(100, 100);).
+ */
+ draggable: true,
+
+ /**
+ * @cfg {Boolean} constrain
+ * True to constrain the window within its containing element, false to allow it to fall outside of its containing
+ * element. By default the window will be rendered to document.body. To render and constrain the window within
+ * another element specify {@link #renderTo}. Optionally the header only can be constrained
+ * using {@link #constrainHeader}.
+ */
+ constrain: false,
+
+ /**
+ * @cfg {Boolean} constrainHeader
+ * True to constrain the window header within its containing element (allowing the window body to fall outside of
+ * its containing element) or false to allow the header to fall outside its containing element.
+ * Optionally the entire window can be constrained using {@link #constrain}.
+ */
+ constrainHeader: false,
+
+ /**
+ * @cfg {Boolean} plain
+ * True to render the window body with a transparent background so that it will blend into the framing elements,
+ * false to add a lighter background color to visually highlight the body element and separate it more distinctly
+ * from the surrounding frame.
+ */
+ plain: false,
+
+ /**
+ * @cfg {Boolean} minimizable
+ * True to display the 'minimize' tool button and allow the user to minimize the window, false to hide the button
+ * and disallow minimizing the window. Note that this button provides no implementation -- the
+ * behavior of minimizing a window is implementation-specific, so the minimize event must be handled and a custom
+ * minimize behavior implemented for this option to be useful.
+ */
+ minimizable: false,
+
+ /**
+ * @cfg {Boolean} maximizable
+ * True to display the 'maximize' tool button and allow the user to maximize the window, false to hide the button
+ * and disallow maximizing the window. Note that when a window is maximized, the tool button
+ * will automatically change to a 'restore' button with the appropriate behavior already built-in that will restore
+ * the window to its previous size.
+ */
+ maximizable: false,
+
+ // inherit docs
+ minHeight: 100,
+
+ // inherit docs
+ minWidth: 200,
+
+ /**
+ * @cfg {Boolean} expandOnShow
+ * True to always expand the window when it is displayed, false to keep it in its current state (which may be
+ * {@link #collapsed}) when displayed.
+ */
+ expandOnShow: true,
+
+ // inherited docs, same default
+ collapsible: false,
+
+ /**
+ * @cfg {Boolean} closable
+ * True to display the 'close' tool button and allow the user to close the window, false to hide the button and
+ * disallow closing the window.
+ *
+ * By default, when close is requested by either clicking the close button in the header or pressing ESC when the
+ * Window has focus, the {@link #close} method will be called. This will _{@link Ext.Component#destroy destroy}_ the
+ * Window and its content meaning that it may not be reused.
+ *
+ * To make closing a Window _hide_ the Window so that it may be reused, set {@link #closeAction} to 'hide'.
+ */
+ closable: true,
+
+ /**
+ * @cfg {Boolean} hidden
+ * Render this Window hidden. If `true`, the {@link #hide} method will be called internally.
+ */
+ hidden: true,
+
+ // Inherit docs from Component. Windows render to the body on first show.
+ autoRender: true,
+
+ // Inherit docs from Component. Windows hide using visibility.
+ hideMode: 'visibility',
+
+ /** @cfg {Boolean} floating @hide Windows are always floating*/
+ floating: true,
+
+ ariaRole: 'alertdialog',
+
+ itemCls: 'x-window-item',
+
+ overlapHeader: true,
+
+ ignoreHeaderBorderManagement: true,
+
+ // private
+ initComponent: function() {
+ var me = this;
+ me.callParent();
+ me.addEvents(
+ /**
+ * @event activate
+ * Fires after the window has been visually activated via {@link #setActive}.
+ * @param {Ext.window.Window} this
+ */
+
+ /**
+ * @event deactivate
+ * Fires after the window has been visually deactivated via {@link #setActive}.
+ * @param {Ext.window.Window} this
+ */
+
+ /**
+ * @event resize
+ * Fires after the window has been resized.
+ * @param {Ext.window.Window} this
+ * @param {Number} width The window's new width
+ * @param {Number} height The window's new height
+ */
+ 'resize',
+
+ /**
+ * @event maximize
+ * Fires after the window has been maximized.
+ * @param {Ext.window.Window} this
+ */
+ 'maximize',
+
+ /**
+ * @event minimize
+ * Fires after the window has been minimized.
+ * @param {Ext.window.Window} this
+ */
+ 'minimize',
+
+ /**
+ * @event restore
+ * Fires after the window has been restored to its original size after being maximized.
+ * @param {Ext.window.Window} this
+ */
+ 'restore'
+ );
+
+ if (me.plain) {
+ me.addClsWithUI('plain');
+ }
+
+ if (me.modal) {
+ me.ariaRole = 'dialog';
+ }
+ },
+
+ // State Management
+ // private
+
+ initStateEvents: function(){
+ var events = this.stateEvents;
+ // push on stateEvents if they don't exist
+ Ext.each(['maximize', 'restore', 'resize', 'dragend'], function(event){
+ if (Ext.Array.indexOf(events, event)) {
+ events.push(event);
+ }
+ });
+ this.callParent();
+ },
+
+ getState: function() {
+ var me = this,
+ state = me.callParent() || {},
+ maximized = !!me.maximized;
+
+ state.maximized = maximized;
+ Ext.apply(state, {
+ size: maximized ? me.restoreSize : me.getSize(),
+ pos: maximized ? me.restorePos : me.getPosition()
+ });
+ return state;
+ },
+
+ applyState: function(state){
+ var me = this;
+
+ if (state) {
+ me.maximized = state.maximized;
+ if (me.maximized) {
+ me.hasSavedRestore = true;
+ me.restoreSize = state.size;
+ me.restorePos = state.pos;
+ } else {
+ Ext.apply(me, {
+ width: state.size.width,
+ height: state.size.height,
+ x: state.pos[0],
+ y: state.pos[1]
+ });
+ }
+ }
+ },
+
+ // private
+ onMouseDown: function (e) {
+ var preventFocus;
+
+ if (this.floating) {
+ if (Ext.fly(e.getTarget()).focusable()) {
+ preventFocus = true;
+ }
+ this.toFront(preventFocus);
+ }
+ },
+
+ // private
+ onRender: function(ct, position) {
+ var me = this;
+ me.callParent(arguments);
+ me.focusEl = me.el;
+
+ // Double clicking a header will toggleMaximize
+ if (me.maximizable) {
+ me.header.on({
+ dblclick: {
+ fn: me.toggleMaximize,
+ element: 'el',
+ scope: me
+ }
+ });
+ }
+ },
+
+ // private
+ afterRender: function() {
+ var me = this,
+ hidden = me.hidden,
+ keyMap;
+
+ me.hidden = false;
+ // Component's afterRender sizes and positions the Component
+ me.callParent();
+ me.hidden = hidden;
+
+ // Create the proxy after the size has been applied in Component.afterRender
+ me.proxy = me.getProxy();
+
+ // clickToRaise
+ me.mon(me.el, 'mousedown', me.onMouseDown, me);
+
+ // allow the element to be focusable
+ me.el.set({
+ tabIndex: -1
+ });
+
+ // Initialize
+ if (me.maximized) {
+ me.maximized = false;
+ me.maximize();
+ }
+
+ if (me.closable) {
+ keyMap = me.getKeyMap();
+ keyMap.on(27, me.onEsc, me);
+
+ //if (hidden) { ? would be consistent w/before/afterShow...
+ keyMap.disable();
+ //}
+ }
+
+ if (!hidden) {
+ me.syncMonitorWindowResize();
+ me.doConstrain();
+ }
+ },
+
+ /**
+ * @private
+ * @override
+ * Override Component.initDraggable.
+ * Window uses the header element as the delegate.
+ */
+ initDraggable: function() {
+ var me = this,
+ ddConfig;
+
+ if (!me.header) {
+ me.updateHeader(true);
+ }
+
+ /*
+ * Check the header here again. If for whatever reason it wasn't created in
+ * updateHeader (preventHeader) then we'll just ignore the rest since the
+ * header acts as the drag handle.
+ */
+ if (me.header) {
+ ddConfig = Ext.applyIf({
+ el: me.el,
+ delegate: '#' + me.header.id
+ }, me.draggable);
+
+ // Add extra configs if Window is specified to be constrained
+ if (me.constrain || me.constrainHeader) {
+ ddConfig.constrain = me.constrain;
+ ddConfig.constrainDelegate = me.constrainHeader;
+ ddConfig.constrainTo = me.constrainTo || me.container;
+ }
+
+ /**
+ * @property {Ext.util.ComponentDragger} dd
+ * If this Window is configured {@link #draggable}, this property will contain an instance of
+ * {@link Ext.util.ComponentDragger} (A subclass of {@link Ext.dd.DragTracker DragTracker}) which handles dragging
+ * the Window's DOM Element, and constraining according to the {@link #constrain} and {@link #constrainHeader} .
+ *
+ * This has implementations of `onBeforeStart`, `onDrag` and `onEnd` which perform the dragging action. If
+ * extra logic is needed at these points, use {@link Ext.Function#createInterceptor createInterceptor} or
+ * {@link Ext.Function#createSequence createSequence} to augment the existing implementations.
+ */
+ me.dd = Ext.create('Ext.util.ComponentDragger', this, ddConfig);
+ me.relayEvents(me.dd, ['dragstart', 'drag', 'dragend']);
+ }
+ },
+
+ // private
+ onEsc: function(k, e) {
+ e.stopEvent();
+ this[this.closeAction]();
+ },
+
+ // private
+ beforeDestroy: function() {
+ var me = this;
+ if (me.rendered) {
+ delete this.animateTarget;
+ me.hide();
+ Ext.destroy(
+ me.keyMap
+ );
+ }
+ me.callParent();
+ },
+
+ /**
+ * @private
+ * @override
+ * Contribute class-specific tools to the header.
+ * Called by Panel's initTools.
+ */
+ addTools: function() {
+ var me = this;
+
+ // Call Panel's initTools
+ me.callParent();
+
+ if (me.minimizable) {
+ me.addTool({
+ type: 'minimize',
+ handler: Ext.Function.bind(me.minimize, me, [])
+ });
+ }
+ if (me.maximizable) {
+ me.addTool({
+ type: 'maximize',
+ handler: Ext.Function.bind(me.maximize, me, [])
+ });
+ me.addTool({
+ type: 'restore',
+ handler: Ext.Function.bind(me.restore, me, []),
+ hidden: true
+ });
+ }
+ },
+
+ /**
+ * Gets the configured default focus item. If a {@link #defaultFocus} is set, it will receive focus, otherwise the
+ * Container itself will receive focus.
+ */
+ getFocusEl: function() {
+ var me = this,
+ f = me.focusEl,
+ defaultComp = me.defaultButton || me.defaultFocus,
+ t = typeof db,
+ el,
+ ct;
+
+ if (Ext.isDefined(defaultComp)) {
+ if (Ext.isNumber(defaultComp)) {
+ f = me.query('button')[defaultComp];
+ } else if (Ext.isString(defaultComp)) {
+ f = me.down('#' + defaultComp);
+ } else {
+ f = defaultComp;
+ }
+ }
+ return f || me.focusEl;
+ },
+
+ // private
+ beforeShow: function() {
+ this.callParent();
+
+ if (this.expandOnShow) {
+ this.expand(false);
+ }
+ },
+
+ // private
+ afterShow: function(animateTarget) {
+ var me = this,
+ animating = animateTarget || me.animateTarget;
+
+
+ // No constraining code needs to go here.
+ // Component.onShow constrains the Component. *If the constrain config is true*
+
+ // Perform superclass's afterShow tasks
+ // Which might include animating a proxy from an animateTarget
+ me.callParent(arguments);
+
+ if (me.maximized) {
+ me.fitContainer();
+ }
+
+ me.syncMonitorWindowResize();
+ if (!animating) {
+ me.doConstrain();
+ }
+
+ if (me.keyMap) {
+ me.keyMap.enable();
+ }
+ },
+
+ // private
+ doClose: function() {
+ var me = this;
+
+ // Being called as callback after going through the hide call below
+ if (me.hidden) {
+ me.fireEvent('close', me);
+ if (me.closeAction == 'destroy') {
+ this.destroy();
+ }
+ } else {
+ // close after hiding
+ me.hide(me.animateTarget, me.doClose, me);
+ }
+ },
+
+ // private
+ afterHide: function() {
+ var me = this;
+
+ // No longer subscribe to resizing now that we're hidden
+ me.syncMonitorWindowResize();
+
+ // Turn off keyboard handling once window is hidden
+ if (me.keyMap) {
+ me.keyMap.disable();
+ }
+
+ // Perform superclass's afterHide tasks.
+ me.callParent(arguments);
+ },
+
+ // private
+ onWindowResize: function() {
+ if (this.maximized) {
+ this.fitContainer();
+ }
+ this.doConstrain();
+ },
+
+ /**
+ * Placeholder method for minimizing the window. By default, this method simply fires the {@link #minimize} event
+ * since the behavior of minimizing a window is application-specific. To implement custom minimize behavior, either
+ * the minimize event can be handled or this method can be overridden.
+ * @return {Ext.window.Window} this
+ */
+ minimize: function() {
+ this.fireEvent('minimize', this);
+ return this;
+ },
+
+ afterCollapse: function() {
+ var me = this;
+
+ if (me.maximizable) {
+ me.tools.maximize.hide();
+ me.tools.restore.hide();
+ }
+ if (me.resizer) {
+ me.resizer.disable();
+ }
+ me.callParent(arguments);
+ },
+
+ afterExpand: function() {
+ var me = this;
+
+ if (me.maximized) {
+ me.tools.restore.show();
+ } else if (me.maximizable) {
+ me.tools.maximize.show();
+ }
+ if (me.resizer) {
+ me.resizer.enable();
+ }
+ me.callParent(arguments);
+ },
+
+ /**
+ * Fits the window within its current container and automatically replaces the {@link #maximizable 'maximize' tool
+ * button} with the 'restore' tool button. Also see {@link #toggleMaximize}.
+ * @return {Ext.window.Window} this
+ */
+ maximize: function() {
+ var me = this;
+
+ if (!me.maximized) {
+ me.expand(false);
+ if (!me.hasSavedRestore) {
+ me.restoreSize = me.getSize();
+ me.restorePos = me.getPosition(true);
+ }
+ if (me.maximizable) {
+ me.tools.maximize.hide();
+ me.tools.restore.show();
+ }
+ me.maximized = true;
+ me.el.disableShadow();
+
+ if (me.dd) {
+ me.dd.disable();
+ }
+ if (me.collapseTool) {
+ me.collapseTool.hide();
+ }
+ me.el.addCls(Ext.baseCSSPrefix + 'window-maximized');
+ me.container.addCls(Ext.baseCSSPrefix + 'window-maximized-ct');
+
+ me.syncMonitorWindowResize();
+ me.setPosition(0, 0);
+ me.fitContainer();
+ me.fireEvent('maximize', me);
+ }
+ return me;
+ },
+
+ /**
+ * Restores a {@link #maximizable maximized} window back to its original size and position prior to being maximized
+ * and also replaces the 'restore' tool button with the 'maximize' tool button. Also see {@link #toggleMaximize}.
+ * @return {Ext.window.Window} this
+ */
+ restore: function() {
+ var me = this,
+ tools = me.tools;
+
+ if (me.maximized) {
+ delete me.hasSavedRestore;
+ me.removeCls(Ext.baseCSSPrefix + 'window-maximized');
+
+ // Toggle tool visibility
+ if (tools.restore) {
+ tools.restore.hide();
+ }
+ if (tools.maximize) {
+ tools.maximize.show();
+ }
+ if (me.collapseTool) {
+ me.collapseTool.show();
+ }
+
+ // Restore the position/sizing
+ me.setPosition(me.restorePos);
+ me.setSize(me.restoreSize);
+
+ // Unset old position/sizing
+ delete me.restorePos;
+ delete me.restoreSize;
+
+ me.maximized = false;
+
+ me.el.enableShadow(true);
+
+ // Allow users to drag and drop again
+ if (me.dd) {
+ me.dd.enable();
+ }
+
+ me.container.removeCls(Ext.baseCSSPrefix + 'window-maximized-ct');
+
+ me.syncMonitorWindowResize();
+ me.doConstrain();
+ me.fireEvent('restore', me);
+ }
+ return me;
+ },
+
+ /**
+ * Synchronizes the presence of our listener for window resize events. This method
+ * should be called whenever this status might change.
+ * @private
+ */
+ syncMonitorWindowResize: function () {
+ var me = this,
+ currentlyMonitoring = me._monitoringResize,
+ // all the states where we should be listening to window resize:
+ yes = me.monitorResize || me.constrain || me.constrainHeader || me.maximized,
+ // all the states where we veto this:
+ veto = me.hidden || me.destroying || me.isDestroyed;
+
+ if (yes && !veto) {
+ // we should be listening...
+ if (!currentlyMonitoring) {
+ // but we aren't, so set it up
+ Ext.EventManager.onWindowResize(me.onWindowResize, me);
+ me._monitoringResize = true;
+ }
+ } else if (currentlyMonitoring) {
+ // we should not be listening, but we are, so tear it down
+ Ext.EventManager.removeResizeListener(me.onWindowResize, me);
+ me._monitoringResize = false;
+ }
+ },
+
+ /**
+ * A shortcut method for toggling between {@link #maximize} and {@link #restore} based on the current maximized
+ * state of the window.
+ * @return {Ext.window.Window} this
+ */
+ toggleMaximize: function() {
+ return this[this.maximized ? 'restore': 'maximize']();
+ }
+
+ /**
+ * @cfg {Boolean} autoWidth @hide
+ * Absolute positioned element and therefore cannot support autoWidth.
+ * A width is a required configuration.
+ **/
+});
+
+/**
+ * @docauthor Jason Johnston <jason@sencha.com>
+ *
+ * Base class for form fields that provides default event handling, rendering, and other common functionality
+ * needed by all form field types. Utilizes the {@link Ext.form.field.Field} mixin for value handling and validation,
+ * and the {@link Ext.form.Labelable} mixin to provide label and error message display.
+ *
+ * In most cases you will want to use a subclass, such as {@link Ext.form.field.Text} or {@link Ext.form.field.Checkbox},
+ * rather than creating instances of this class directly. However if you are implementing a custom form field,
+ * using this as the parent class is recommended.
+ *
+ * # Values and Conversions
+ *
+ * Because BaseField implements the Field mixin, it has a main value that can be initialized with the
+ * {@link #value} config and manipulated via the {@link #getValue} and {@link #setValue} methods. This main
+ * value can be one of many data types appropriate to the current field, for instance a {@link Ext.form.field.Date Date}
+ * field would use a JavaScript Date object as its value type. However, because the field is rendered as a HTML
+ * input, this value data type can not always be directly used in the rendered field.
+ *
+ * Therefore BaseField introduces the concept of a "raw value". This is the value of the rendered HTML input field,
+ * and is normally a String. The {@link #getRawValue} and {@link #setRawValue} methods can be used to directly
+ * work with the raw value, though it is recommended to use getValue and setValue in most cases.
+ *
+ * Conversion back and forth between the main value and the raw value is handled by the {@link #valueToRaw} and
+ * {@link #rawToValue} methods. If you are implementing a subclass that uses a non-String value data type, you
+ * should override these methods to handle the conversion.
+ *
+ * # Rendering
+ *
+ * The content of the field body is defined by the {@link #fieldSubTpl} XTemplate, with its argument data
+ * created by the {@link #getSubTplData} method. Override this template and/or method to create custom
+ * field renderings.
+ *
+ * # Example usage:
+ *
+ * @example
+ * // A simple subclass of BaseField that creates a HTML5 search field. Redirects to the
+ * // searchUrl when the Enter key is pressed.222
+ * Ext.define('Ext.form.SearchField', {
+ * extend: 'Ext.form.field.Base',
+ * alias: 'widget.searchfield',
+ *
+ * inputType: 'search',
+ *
+ * // Config defining the search URL
+ * searchUrl: 'http://www.google.com/search?q={0}',
+ *
+ * // Add specialkey listener
+ * initComponent: function() {
+ * this.callParent();
+ * this.on('specialkey', this.checkEnterKey, this);
+ * },
+ *
+ * // Handle enter key presses, execute the search if the field has a value
+ * checkEnterKey: function(field, e) {
+ * var value = this.getValue();
+ * if (e.getKey() === e.ENTER && !Ext.isEmpty(value)) {
+ * location.href = Ext.String.format(this.searchUrl, value);
+ * }
+ * }
+ * });
+ *
+ * Ext.create('Ext.form.Panel', {
+ * title: 'BaseField Example',
+ * bodyPadding: 5,
+ * width: 250,
+ *
+ * // Fields will be arranged vertically, stretched to full width
+ * layout: 'anchor',
+ * defaults: {
+ * anchor: '100%'
+ * },
+ * items: [{
+ * xtype: 'searchfield',
+ * fieldLabel: 'Search',
+ * name: 'query'
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.form.field.Base', {
+ extend: 'Ext.Component',
+ mixins: {
+ labelable: 'Ext.form.Labelable',
+ field: 'Ext.form.field.Field'
+ },
+ alias: 'widget.field',
+ alternateClassName: ['Ext.form.Field', 'Ext.form.BaseField'],
+ requires: ['Ext.util.DelayedTask', 'Ext.XTemplate', 'Ext.layout.component.field.Field'],
+
+ /**
+ * @cfg {Ext.XTemplate} fieldSubTpl
+ * The content of the field body is defined by this config option.
+ */
+ fieldSubTpl: [ // note: {id} here is really {inputId}, but {cmpId} is available
+ '<input id="{id}" type="{type}" ',
+ '<tpl if="name">name="{name}" </tpl>',
+ '<tpl if="size">size="{size}" </tpl>',
+ '<tpl if="tabIdx">tabIndex="{tabIdx}" </tpl>',
+ 'class="{fieldCls} {typeCls}" autocomplete="off" />',
+ {
+ compiled: true,
+ disableFormats: true
+ }
+ ],
+
+ /**
+ * @cfg {String} name
+ * The name of the field. This is used as the parameter name when including the field value
+ * in a {@link Ext.form.Basic#submit form submit()}. If no name is configured, it falls back to the {@link #inputId}.
+ * To prevent the field from being included in the form submit, set {@link #submitValue} to false.
+ */
+
+ /**
+ * @cfg {String} inputType
+ * The type attribute for input fields -- e.g. radio, text, password, file. The extended types
+ * supported by HTML5 inputs (url, email, etc.) may also be used, though using them will cause older browsers to
+ * fall back to 'text'.
+ *
+ * The type 'password' must be used to render that field type currently -- there is no separate Ext component for
+ * that. You can use {@link Ext.form.field.File} which creates a custom-rendered file upload field, but if you want
+ * a plain unstyled file input you can use a BaseField with inputType:'file'.
+ */
+ inputType: 'text',
+
+ /**
+ * @cfg {Number} tabIndex
+ * The tabIndex for this field. Note this only applies to fields that are rendered, not those which are built via
+ * applyTo
+ */
+
+ /**
+ * @cfg {String} invalidText
+ * The error text to use when marking a field invalid and no message is provided
+ */
+ invalidText : 'The value in this field is invalid',
+
+ /**
+ * @cfg {String} [fieldCls='x-form-field']
+ * The default CSS class for the field input
+ */
+ fieldCls : Ext.baseCSSPrefix + 'form-field',
+
+ /**
+ * @cfg {String} fieldStyle
+ * Optional CSS style(s) to be applied to the {@link #inputEl field input element}. Should be a valid argument to
+ * {@link Ext.Element#applyStyles}. Defaults to undefined. See also the {@link #setFieldStyle} method for changing
+ * the style after initialization.
+ */
+
+ /**
+ * @cfg {String} [focusCls='x-form-focus']
+ * The CSS class to use when the field receives focus
+ */
+ focusCls : Ext.baseCSSPrefix + 'form-focus',
+
+ /**
+ * @cfg {String} dirtyCls
+ * The CSS class to use when the field value {@link #isDirty is dirty}.
+ */
+ dirtyCls : Ext.baseCSSPrefix + 'form-dirty',
+
+ /**
+ * @cfg {String[]} checkChangeEvents
+ * A list of event names that will be listened for on the field's {@link #inputEl input element}, which will cause
+ * the field's value to be checked for changes. If a change is detected, the {@link #change change event} will be
+ * fired, followed by validation if the {@link #validateOnChange} option is enabled.
+ *
+ * Defaults to ['change', 'propertychange'] in Internet Explorer, and ['change', 'input', 'textInput', 'keyup',
+ * 'dragdrop'] in other browsers. This catches all the ways that field values can be changed in most supported
+ * browsers; the only known exceptions at the time of writing are:
+ *
+ * - Safari 3.2 and older: cut/paste in textareas via the context menu, and dragging text into textareas
+ * - Opera 10 and 11: dragging text into text fields and textareas, and cut via the context menu in text fields
+ * and textareas
+ * - Opera 9: Same as Opera 10 and 11, plus paste from context menu in text fields and textareas
+ *
+ * If you need to guarantee on-the-fly change notifications including these edge cases, you can call the
+ * {@link #checkChange} method on a repeating interval, e.g. using {@link Ext.TaskManager}, or if the field is within
+ * a {@link Ext.form.Panel}, you can use the FormPanel's {@link Ext.form.Panel#pollForChanges} configuration to set up
+ * such a task automatically.
+ */
+ checkChangeEvents: Ext.isIE && (!document.documentMode || document.documentMode < 9) ?
+ ['change', 'propertychange'] :
+ ['change', 'input', 'textInput', 'keyup', 'dragdrop'],
+
+ /**
+ * @cfg {Number} checkChangeBuffer
+ * Defines a timeout in milliseconds for buffering {@link #checkChangeEvents} that fire in rapid succession.
+ * Defaults to 50 milliseconds.
+ */
+ checkChangeBuffer: 50,
+
+ componentLayout: 'field',
+
+ /**
+ * @cfg {Boolean} readOnly
+ * true to mark the field as readOnly in HTML.
+ *
+ * **Note**: this only sets the element's readOnly DOM attribute. Setting `readOnly=true`, for example, will not
+ * disable triggering a ComboBox or Date; it gives you the option of forcing the user to choose via the trigger
+ * without typing in the text box. To hide the trigger use `{@link Ext.form.field.Trigger#hideTrigger hideTrigger}`.
+ */
+ readOnly : false,
+
+ /**
+ * @cfg {String} readOnlyCls
+ * The CSS class applied to the component's main element when it is {@link #readOnly}.
+ */
+ readOnlyCls: Ext.baseCSSPrefix + 'form-readonly',
+
+ /**
+ * @cfg {String} inputId
+ * The id that will be given to the generated input DOM element. Defaults to an automatically generated id. If you
+ * configure this manually, you must make sure it is unique in the document.
+ */
+
+ /**
+ * @cfg {Boolean} validateOnBlur
+ * Whether the field should validate when it loses focus. This will cause fields to be validated
+ * as the user steps through the fields in the form regardless of whether they are making changes to those fields
+ * along the way. See also {@link #validateOnChange}.
+ */
+ validateOnBlur: true,
+
+ // private
+ hasFocus : false,
+
+ baseCls: Ext.baseCSSPrefix + 'field',
+
+ maskOnDisable: false,
+
+ // private
+ initComponent : function() {
+ var me = this;
+
+ me.callParent();
+
+ me.subTplData = me.subTplData || {};
+
+ me.addEvents(
+ /**
+ * @event focus
+ * Fires when this field receives input focus.
+ * @param {Ext.form.field.Base} this
+ */
+ 'focus',
+ /**
+ * @event blur
+ * Fires when this field loses input focus.
+ * @param {Ext.form.field.Base} this
+ */
+ 'blur',
+ /**
+ * @event specialkey
+ * Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed. To handle other keys
+ * see {@link Ext.util.KeyMap}. You can check {@link Ext.EventObject#getKey} to determine which key was
+ * pressed. For example:
+ *
+ * var form = new Ext.form.Panel({
+ * ...
+ * items: [{
+ * fieldLabel: 'Field 1',
+ * name: 'field1',
+ * allowBlank: false
+ * },{
+ * fieldLabel: 'Field 2',
+ * name: 'field2',
+ * listeners: {
+ * specialkey: function(field, e){
+ * // e.HOME, e.END, e.PAGE_UP, e.PAGE_DOWN,
+ * // e.TAB, e.ESC, arrow keys: e.LEFT, e.RIGHT, e.UP, e.DOWN
+ * if (e.{@link Ext.EventObject#getKey getKey()} == e.ENTER) {
+ * var form = field.up('form').getForm();
+ * form.submit();
+ * }
+ * }
+ * }
+ * }
+ * ],
+ * ...
+ * });
+ *
+ * @param {Ext.form.field.Base} this
+ * @param {Ext.EventObject} e The event object
+ */
+ 'specialkey'
+ );
+
+ // Init mixins
+ me.initLabelable();
+ me.initField();
+
+ // Default name to inputId
+ if (!me.name) {
+ me.name = me.getInputId();
+ }
+ },
+
+ /**
+ * Returns the input id for this field. If none was specified via the {@link #inputId} config, then an id will be
+ * automatically generated.
+ */
+ getInputId: function() {
+ return this.inputId || (this.inputId = Ext.id());
+ },
+
+ /**
+ * Creates and returns the data object to be used when rendering the {@link #fieldSubTpl}.
+ * @return {Object} The template data
+ * @template
+ */
+ getSubTplData: function() {
+ var me = this,
+ type = me.inputType,
+ inputId = me.getInputId();
+
+ return Ext.applyIf(me.subTplData, {
+ id: inputId,
+ cmpId: me.id,
+ name: me.name || inputId,
+ type: type,
+ size: me.size || 20,
+ cls: me.cls,
+ fieldCls: me.fieldCls,
+ tabIdx: me.tabIndex,
+ typeCls: Ext.baseCSSPrefix + 'form-' + (type === 'password' ? 'text' : type)
+ });
+ },
+
+ afterRender: function() {
+ this.callParent();
+
+ if (this.inputEl) {
+ this.inputEl.selectable();
+ }
+ },
+
+ /**
+ * Gets the markup to be inserted into the outer template's bodyEl. For fields this is the actual input element.
+ */
+ getSubTplMarkup: function() {
+ return this.getTpl('fieldSubTpl').apply(this.getSubTplData());
+ },
+
+ initRenderTpl: function() {
+ var me = this;
+ if (!me.hasOwnProperty('renderTpl')) {
+ me.renderTpl = me.getTpl('labelableRenderTpl');
+ }
+ return me.callParent();
+ },
+
+ initRenderData: function() {
+ return Ext.applyIf(this.callParent(), this.getLabelableRenderData());
+ },
+
+ /**
+ * Set the {@link #fieldStyle CSS style} of the {@link #inputEl field input element}.
+ * @param {String/Object/Function} style The style(s) to apply. Should be a valid argument to {@link
+ * Ext.Element#applyStyles}.
+ */
+ setFieldStyle: function(style) {
+ var me = this,
+ inputEl = me.inputEl;
+ if (inputEl) {
+ inputEl.applyStyles(style);
+ }
+ me.fieldStyle = style;
+ },
+
+ // private
+ onRender : function() {
+ var me = this,
+ fieldStyle = me.fieldStyle;
+
+ me.onLabelableRender();
+
+ /**
+ * @property {Ext.Element} inputEl
+ * The input Element for this Field. Only available after the field has been rendered.
+ */
+ me.addChildEls({ name: 'inputEl', id: me.getInputId() });
+
+ me.callParent(arguments);
+
+ // Make the stored rawValue get set as the input element's value
+ me.setRawValue(me.rawValue);
+
+ if (me.readOnly) {
+ me.setReadOnly(true);
+ }
+ if (me.disabled) {
+ me.disable();
+ }
+ if (fieldStyle) {
+ me.setFieldStyle(fieldStyle);
+ }
+
+ me.renderActiveError();
+ },
+
+ initAria: function() {
+ var me = this;
+ me.callParent();
+
+ // Associate the field to the error message element
+ me.getActionEl().dom.setAttribute('aria-describedby', Ext.id(me.errorEl));
+ },
+
+ getFocusEl: function() {
+ return this.inputEl;
+ },
+
+ isFileUpload: function() {
+ return this.inputType === 'file';
+ },
+
+ extractFileInput: function() {
+ var me = this,
+ fileInput = me.isFileUpload() ? me.inputEl.dom : null,
+ clone;
+ if (fileInput) {
+ clone = fileInput.cloneNode(true);
+ fileInput.parentNode.replaceChild(clone, fileInput);
+ me.inputEl = Ext.get(clone);
+ }
+ return fileInput;
+ },
+
+ // private override to use getSubmitValue() as a convenience
+ getSubmitData: function() {
+ var me = this,
+ data = null,
+ val;
+ if (!me.disabled && me.submitValue && !me.isFileUpload()) {
+ val = me.getSubmitValue();
+ if (val !== null) {
+ data = {};
+ data[me.getName()] = val;
+ }
+ }
+ return data;
+ },
+
+ /**
+ * Returns the value that would be included in a standard form submit for this field. This will be combined with the
+ * field's name to form a name=value pair in the {@link #getSubmitData submitted parameters}. If an empty string is
+ * returned then just the name= will be submitted; if null is returned then nothing will be submitted.
+ *
+ * Note that the value returned will have been {@link #processRawValue processed} but may or may not have been
+ * successfully {@link #validate validated}.
+ *
+ * @return {String} The value to be submitted, or null.
+ */
+ getSubmitValue: function() {
+ return this.processRawValue(this.getRawValue());
+ },
+
+ /**
+ * Returns the raw value of the field, without performing any normalization, conversion, or validation. To get a
+ * normalized and converted value see {@link #getValue}.
+ * @return {String} value The raw String value of the field
+ */
+ getRawValue: function() {
+ var me = this,
+ v = (me.inputEl ? me.inputEl.getValue() : Ext.value(me.rawValue, ''));
+ me.rawValue = v;
+ return v;
+ },
+
+ /**
+ * Sets the field's raw value directly, bypassing {@link #valueToRaw value conversion}, change detection, and
+ * validation. To set the value with these additional inspections see {@link #setValue}.
+ * @param {Object} value The value to set
+ * @return {Object} value The field value that is set
+ */
+ setRawValue: function(value) {
+ var me = this;
+ value = Ext.value(value, '');
+ me.rawValue = value;
+
+ // Some Field subclasses may not render an inputEl
+ if (me.inputEl) {
+ me.inputEl.dom.value = value;
+ }
+ return value;
+ },
+
+ /**
+ * Converts a mixed-type value to a raw representation suitable for displaying in the field. This allows controlling
+ * how value objects passed to {@link #setValue} are shown to the user, including localization. For instance, for a
+ * {@link Ext.form.field.Date}, this would control how a Date object passed to {@link #setValue} would be converted
+ * to a String for display in the field.
+ *
+ * See {@link #rawToValue} for the opposite conversion.
+ *
+ * The base implementation simply does a standard toString conversion, and converts {@link Ext#isEmpty empty values}
+ * to an empty string.
+ *
+ * @param {Object} value The mixed-type value to convert to the raw representation.
+ * @return {Object} The converted raw value.
+ */
+ valueToRaw: function(value) {
+ return '' + Ext.value(value, '');
+ },
+
+ /**
+ * Converts a raw input field value into a mixed-type value that is suitable for this particular field type. This
+ * allows controlling the normalization and conversion of user-entered values into field-type-appropriate values,
+ * e.g. a Date object for {@link Ext.form.field.Date}, and is invoked by {@link #getValue}.
+ *
+ * It is up to individual implementations to decide how to handle raw values that cannot be successfully converted
+ * to the desired object type.
+ *
+ * See {@link #valueToRaw} for the opposite conversion.
+ *
+ * The base implementation does no conversion, returning the raw value untouched.
+ *
+ * @param {Object} rawValue
+ * @return {Object} The converted value.
+ */
+ rawToValue: function(rawValue) {
+ return rawValue;
+ },
+
+ /**
+ * Performs any necessary manipulation of a raw field value to prepare it for {@link #rawToValue conversion} and/or
+ * {@link #validate validation}, for instance stripping out ignored characters. In the base implementation it does
+ * nothing; individual subclasses may override this as needed.
+ *
+ * @param {Object} value The unprocessed string value
+ * @return {Object} The processed string value
+ */
+ processRawValue: function(value) {
+ return value;
+ },
+
+ /**
+ * Returns the current data value of the field. The type of value returned is particular to the type of the
+ * particular field (e.g. a Date object for {@link Ext.form.field.Date}), as the result of calling {@link #rawToValue} on
+ * the field's {@link #processRawValue processed} String value. To return the raw String value, see {@link #getRawValue}.
+ * @return {Object} value The field value
+ */
+ getValue: function() {
+ var me = this,
+ val = me.rawToValue(me.processRawValue(me.getRawValue()));
+ me.value = val;
+ return val;
+ },
+
+ /**
+ * Sets a data value into the field and runs the change detection and validation. To set the value directly
+ * without these inspections see {@link #setRawValue}.
+ * @param {Object} value The value to set
+ * @return {Ext.form.field.Field} this
+ */
+ setValue: function(value) {
+ var me = this;
+ me.setRawValue(me.valueToRaw(value));
+ return me.mixins.field.setValue.call(me, value);
+ },
+
+
+ //private
+ onDisable: function() {
+ var me = this,
+ inputEl = me.inputEl;
+ me.callParent();
+ if (inputEl) {
+ inputEl.dom.disabled = true;
+ }
+ },
+
+ //private
+ onEnable: function() {
+ var me = this,
+ inputEl = me.inputEl;
+ me.callParent();
+ if (inputEl) {
+ inputEl.dom.disabled = false;
+ }
+ },
+
+ /**
+ * Sets the read only state of this field.
+ * @param {Boolean} readOnly Whether the field should be read only.
+ */
+ setReadOnly: function(readOnly) {
+ var me = this,
+ inputEl = me.inputEl;
+ if (inputEl) {
+ inputEl.dom.readOnly = readOnly;
+ inputEl.dom.setAttribute('aria-readonly', readOnly);
+ }
+ me[readOnly ? 'addCls' : 'removeCls'](me.readOnlyCls);
+ me.readOnly = readOnly;
+ },
+
+ // private
+ fireKey: function(e){
+ if(e.isSpecialKey()){
+ this.fireEvent('specialkey', this, Ext.create('Ext.EventObjectImpl', e));
+ }
+ },
+
+ // private
+ initEvents : function(){
+ var me = this,
+ inputEl = me.inputEl,
+ onChangeTask,
+ onChangeEvent;
+ if (inputEl) {
+ me.mon(inputEl, Ext.EventManager.getKeyEvent(), me.fireKey, me);
+ me.mon(inputEl, 'focus', me.onFocus, me);
+
+ // standardise buffer across all browsers + OS-es for consistent event order.
+ // (the 10ms buffer for Editors fixes a weird FF/Win editor issue when changing OS window focus)
+ me.mon(inputEl, 'blur', me.onBlur, me, me.inEditor ? {buffer:10} : null);
+
+ // listen for immediate value changes
+ onChangeTask = Ext.create('Ext.util.DelayedTask', me.checkChange, me);
+ me.onChangeEvent = onChangeEvent = function() {
+ onChangeTask.delay(me.checkChangeBuffer);
+ };
+ Ext.each(me.checkChangeEvents, function(eventName) {
+ if (eventName === 'propertychange') {
+ me.usesPropertychange = true;
+ }
+ me.mon(inputEl, eventName, onChangeEvent);
+ }, me);
+ }
+ me.callParent();
+ },
+
+ doComponentLayout: function() {
+ var me = this,
+ inputEl = me.inputEl,
+ usesPropertychange = me.usesPropertychange,
+ ename = 'propertychange',
+ onChangeEvent = me.onChangeEvent;
+
+ // In IE if propertychange is one of the checkChangeEvents, we need to remove
+ // the listener prior to layout and re-add it after, to prevent it from firing
+ // needlessly for attribute and style changes applied to the inputEl.
+ if (usesPropertychange) {
+ me.mun(inputEl, ename, onChangeEvent);
+ }
+ me.callParent(arguments);
+ if (usesPropertychange) {
+ me.mon(inputEl, ename, onChangeEvent);
+ }
+ },
+
+ // private
+ preFocus: Ext.emptyFn,
+
+ // private
+ onFocus: function() {
+ var me = this,
+ focusCls = me.focusCls,
+ inputEl = me.inputEl;
+ me.preFocus();
+ if (focusCls && inputEl) {
+ inputEl.addCls(focusCls);
+ }
+ if (!me.hasFocus) {
+ me.hasFocus = true;
+ me.componentLayout.onFocus();
+ me.fireEvent('focus', me);
+ }
+ },
+
+ // private
+ beforeBlur : Ext.emptyFn,
+
+ // private
+ onBlur : function(){
+ var me = this,
+ focusCls = me.focusCls,
+ inputEl = me.inputEl;
+
+ if (me.destroying) {
+ return;
+ }
+
+ me.beforeBlur();
+ if (focusCls && inputEl) {
+ inputEl.removeCls(focusCls);
+ }
+ if (me.validateOnBlur) {
+ me.validate();
+ }
+ me.hasFocus = false;
+ me.fireEvent('blur', me);
+ me.postBlur();
+ },
+
+ // private
+ postBlur : Ext.emptyFn,
+
+
+ /**
+ * @private Called when the field's dirty state changes. Adds/removes the {@link #dirtyCls} on the main element.
+ * @param {Boolean} isDirty
+ */
+ onDirtyChange: function(isDirty) {
+ this[isDirty ? 'addCls' : 'removeCls'](this.dirtyCls);
+ },
+
+
+ /**
+ * Returns whether or not the field value is currently valid by {@link #getErrors validating} the
+ * {@link #processRawValue processed raw value} of the field. **Note**: {@link #disabled} fields are
+ * always treated as valid.
+ *
+ * @return {Boolean} True if the value is valid, else false
+ */
+ isValid : function() {
+ var me = this;
+ return me.disabled || me.validateValue(me.processRawValue(me.getRawValue()));
+ },
+
+
+ /**
+ * Uses {@link #getErrors} to build an array of validation errors. If any errors are found, they are passed to
+ * {@link #markInvalid} and false is returned, otherwise true is returned.
+ *
+ * Previously, subclasses were invited to provide an implementation of this to process validations - from 3.2
+ * onwards {@link #getErrors} should be overridden instead.
+ *
+ * @param {Object} value The value to validate
+ * @return {Boolean} True if all validations passed, false if one or more failed
+ */
+ validateValue: function(value) {
+ var me = this,
+ errors = me.getErrors(value),
+ isValid = Ext.isEmpty(errors);
+ if (!me.preventMark) {
+ if (isValid) {
+ me.clearInvalid();
+ } else {
+ me.markInvalid(errors);
+ }
+ }
+
+ return isValid;
+ },
+
+ /**
+ * Display one or more error messages associated with this field, using {@link #msgTarget} to determine how to
+ * display the messages and applying {@link #invalidCls} to the field's UI element.
+ *
+ * **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid} methods 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.
+ *
+ * @param {String/String[]} errors The validation message(s) to display.
+ */
+ markInvalid : function(errors) {
+ // Save the message and fire the 'invalid' event
+ var me = this,
+ oldMsg = me.getActiveError();
+ me.setActiveErrors(Ext.Array.from(errors));
+ if (oldMsg !== me.getActiveError()) {
+ me.doComponentLayout();
+ }
+ },
+
+ /**
+ * Clear any invalid styles/messages for this field.
+ *
+ * **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid} methods to return `true`
+ * if the value does not _pass_ validation. So simply clearing a field's errors will not necessarily allow
+ * submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} option set.
+ */
+ clearInvalid : function() {
+ // Clear the message and fire the 'valid' event
+ var me = this,
+ hadError = me.hasActiveError();
+ me.unsetActiveError();
+ if (hadError) {
+ me.doComponentLayout();
+ }
+ },
+
+ /**
+ * @private Overrides the method from the Ext.form.Labelable mixin to also add the invalidCls to the inputEl,
+ * as that is required for proper styling in IE with nested fields (due to lack of child selector)
+ */
+ renderActiveError: function() {
+ var me = this,
+ hasError = me.hasActiveError();
+ if (me.inputEl) {
+ // Add/remove invalid class
+ me.inputEl[hasError ? 'addCls' : 'removeCls'](me.invalidCls + '-field');
+ }
+ me.mixins.labelable.renderActiveError.call(me);
+ },
+
+
+ getActionEl: function() {
+ return this.inputEl || this.el;
+ }
+
+});
+
+/**
+ * @docauthor Jason Johnston <jason@sencha.com>
+ *
+ * A basic text field. Can be used as a direct replacement for traditional text inputs,
+ * or as the base class for more sophisticated input controls (like {@link Ext.form.field.TextArea}
+ * and {@link Ext.form.field.ComboBox}). Has support for empty-field placeholder values (see {@link #emptyText}).
+ *
+ * # Validation
+ *
+ * The Text field has a useful set of validations built in:
+ *
+ * - {@link #allowBlank} for making the field required
+ * - {@link #minLength} for requiring a minimum value length
+ * - {@link #maxLength} for setting a maximum value length (with {@link #enforceMaxLength} to add it
+ * as the `maxlength` attribute on the input element)
+ * - {@link #regex} to specify a custom regular expression for validation
+ *
+ * In addition, custom validations may be added:
+ *
+ * - {@link #vtype} specifies a virtual type implementation from {@link Ext.form.field.VTypes} which can contain
+ * custom validation logic
+ * - {@link #validator} allows a custom arbitrary function to be called during validation
+ *
+ * The details around how and when each of these validation options get used are described in the
+ * documentation for {@link #getErrors}.
+ *
+ * By default, the field value is checked for validity immediately while the user is typing in the
+ * field. This can be controlled with the {@link #validateOnChange}, {@link #checkChangeEvents}, and
+ * {@link #checkChangeBuffer} configurations. Also see the details on Form Validation in the
+ * {@link Ext.form.Panel} class documentation.
+ *
+ * # Masking and Character Stripping
+ *
+ * Text fields can be configured with custom regular expressions to be applied to entered values before
+ * validation: see {@link #maskRe} and {@link #stripCharsRe} for details.
+ *
+ * # Example usage
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'Contact Info',
+ * width: 300,
+ * bodyPadding: 10,
+ * renderTo: Ext.getBody(),
+ * items: [{
+ * xtype: 'textfield',
+ * name: 'name',
+ * fieldLabel: 'Name',
+ * allowBlank: false // requires a non-empty value
+ * }, {
+ * xtype: 'textfield',
+ * name: 'email',
+ * fieldLabel: 'Email Address',
+ * vtype: 'email' // requires value to be a valid email address format
+ * }]
+ * });
+ */
+Ext.define('Ext.form.field.Text', {
+ extend:'Ext.form.field.Base',
+ alias: 'widget.textfield',
+ requires: ['Ext.form.field.VTypes', 'Ext.layout.component.field.Text'],
+ alternateClassName: ['Ext.form.TextField', 'Ext.form.Text'],
+
+ /**
+ * @cfg {String} vtypeText
+ * A custom error message to display in place of the default message provided for the **`{@link #vtype}`** currently
+ * set for this field. **Note**: only applies if **`{@link #vtype}`** is set, else ignored.
+ */
+
+ /**
+ * @cfg {RegExp} stripCharsRe
+ * A JavaScript RegExp object used to strip unwanted content from the value
+ * before validation. If <tt>stripCharsRe</tt> is specified,
+ * every character matching <tt>stripCharsRe</tt> will be removed before fed to validation.
+ * This does not change the value of the field.
+ */
+
+ /**
+ * @cfg {Number} size
+ * An initial value for the 'size' attribute on the text input element. This is only used if the field has no
+ * configured {@link #width} and is not given a width by its container's layout. Defaults to 20.
+ */
+ size: 20,
+
+ /**
+ * @cfg {Boolean} [grow=false]
+ * true if this field should automatically grow and shrink to its content
+ */
+
+ /**
+ * @cfg {Number} growMin
+ * The minimum width to allow when `{@link #grow} = true`
+ */
+ growMin : 30,
+
+ /**
+ * @cfg {Number} growMax
+ * The maximum width to allow when `{@link #grow} = true`
+ */
+ growMax : 800,
+
+ /**
+ * @cfg {String} growAppend
+ * A string that will be appended to the field's current value for the purposes of calculating the target field
+ * size. Only used when the {@link #grow} config is true. Defaults to a single capital "W" (the widest character in
+ * common fonts) to leave enough space for the next typed character and avoid the field value shifting before the
+ * width is adjusted.
+ */
+ growAppend: 'W',
+
+ /**
+ * @cfg {String} vtype
+ * A validation type name as defined in {@link Ext.form.field.VTypes}
+ */
+
+ /**
+ * @cfg {RegExp} maskRe An input mask regular expression that will be used to filter keystrokes (character being
+ * typed) that do not match.
+ * Note: It dose not filter characters already in the input.
+ */
+
+ /**
+ * @cfg {Boolean} [disableKeyFilter=false]
+ * Specify true to disable input keystroke filtering
+ */
+
+ /**
+ * @cfg {Boolean} allowBlank
+ * Specify false to validate that the value's length is > 0
+ */
+ allowBlank : true,
+
+ /**
+ * @cfg {Number} minLength
+ * Minimum input field length required
+ */
+ minLength : 0,
+
+ /**
+ * @cfg {Number} maxLength
+ * Maximum input field length allowed by validation (defaults to Number.MAX_VALUE). This behavior is intended to
+ * provide instant feedback to the user by improving usability to allow pasting and editing or overtyping and back
+ * tracking. To restrict the maximum number of characters that can be entered into the field use the **{@link
+ * Ext.form.field.Text#enforceMaxLength enforceMaxLength}** option.
+ */
+ maxLength : Number.MAX_VALUE,
+
+ /**
+ * @cfg {Boolean} enforceMaxLength
+ * True to set the maxLength property on the underlying input field. Defaults to false
+ */
+
+ /**
+ * @cfg {String} minLengthText
+ * Error text to display if the **{@link #minLength minimum length}** validation fails.
+ */
+ minLengthText : 'The minimum length for this field is {0}',
+
+ /**
+ * @cfg {String} maxLengthText
+ * Error text to display if the **{@link #maxLength maximum length}** validation fails
+ */
+ maxLengthText : 'The maximum length for this field is {0}',
+
+ /**
+ * @cfg {Boolean} [selectOnFocus=false]
+ * true to automatically select any existing field text when the field receives input focus
+ */
+
+ /**
+ * @cfg {String} blankText
+ * The error text to display if the **{@link #allowBlank}** validation fails
+ */
+ blankText : 'This field is required',
+
+ /**
+ * @cfg {Function} validator
+ * A custom validation function to be called during field validation ({@link #getErrors}).
+ * If specified, this function will be called first, allowing the developer to override the default validation
+ * process.
+ *
+ * This function will be passed the following parameters:
+ *
+ * @cfg {Object} validator.value The current field value
+ * @cfg {Boolean/String} validator.return
+ *
+ * - True if the value is valid
+ * - An error message if the value is invalid
+ */
+
+ /**
+ * @cfg {RegExp} regex A JavaScript RegExp object to be tested against the field value during validation.
+ * If the test fails, the field will be marked invalid using
+ * either <b><tt>{@link #regexText}</tt></b> or <b><tt>{@link #invalidText}</tt></b>.
+ */
+
+ /**
+ * @cfg {String} regexText
+ * The error text to display if **{@link #regex}** is used and the test fails during validation
+ */
+ regexText : '',
+
+ /**
+ * @cfg {String} emptyText
+ * The default text to place into an empty field.
+ *
+ * Note that normally this value will be submitted to the server if this field is enabled; to prevent this you can
+ * set the {@link Ext.form.action.Action#submitEmptyText submitEmptyText} option of {@link Ext.form.Basic#submit} to
+ * false.
+ *
+ * Also note that if you use {@link #inputType inputType}:'file', {@link #emptyText} is not supported and should be
+ * avoided.
+ */
+
+ /**
+ * @cfg {String} [emptyCls='x-form-empty-field']
+ * The CSS class to apply to an empty field to style the **{@link #emptyText}**.
+ * This class is automatically added and removed as needed depending on the current field value.
+ */
+ emptyCls : Ext.baseCSSPrefix + 'form-empty-field',
+
+ ariaRole: 'textbox',
+
+ /**
+ * @cfg {Boolean} [enableKeyEvents=false]
+ * true to enable the proxying of key events for the HTML input field
+ */
+
+ componentLayout: 'textfield',
+
+ initComponent : function(){
+ this.callParent();
+ this.addEvents(
+ /**
+ * @event autosize
+ * Fires when the **{@link #autoSize}** function is triggered and the field is resized according to the
+ * {@link #grow}/{@link #growMin}/{@link #growMax} configs as a result. This event provides a hook for the
+ * developer to apply additional logic at runtime to resize the field if needed.
+ * @param {Ext.form.field.Text} this This text field
+ * @param {Number} width The new field width
+ */
+ 'autosize',
+
+ /**
+ * @event keydown
+ * Keydown input field event. This event only fires if **{@link #enableKeyEvents}** is set to true.
+ * @param {Ext.form.field.Text} this This text field
+ * @param {Ext.EventObject} e
+ */
+ 'keydown',
+ /**
+ * @event keyup
+ * Keyup input field event. This event only fires if **{@link #enableKeyEvents}** is set to true.
+ * @param {Ext.form.field.Text} this This text field
+ * @param {Ext.EventObject} e
+ */
+ 'keyup',
+ /**
+ * @event keypress
+ * Keypress input field event. This event only fires if **{@link #enableKeyEvents}** is set to true.
+ * @param {Ext.form.field.Text} this This text field
+ * @param {Ext.EventObject} e
+ */
+ 'keypress'
+ );
+ },
+
+ // private
+ initEvents : function(){
+ var me = this,
+ el = me.inputEl;
+
+ me.callParent();
+ if(me.selectOnFocus || me.emptyText){
+ me.mon(el, 'mousedown', me.onMouseDown, me);
+ }
+ if(me.maskRe || (me.vtype && me.disableKeyFilter !== true && (me.maskRe = Ext.form.field.VTypes[me.vtype+'Mask']))){
+ me.mon(el, 'keypress', me.filterKeys, me);
+ }
+
+ if (me.enableKeyEvents) {
+ me.mon(el, {
+ scope: me,
+ keyup: me.onKeyUp,
+ keydown: me.onKeyDown,
+ keypress: me.onKeyPress
+ });
+ }
+ },
+
+ /**
+ * @private
+ * Override. Treat undefined and null values as equal to an empty string value.
+ */
+ isEqual: function(value1, value2) {
+ return this.isEqualAsString(value1, value2);
+ },
+
+ /**
+ * @private
+ * If grow=true, invoke the autoSize method when the field's value is changed.
+ */
+ onChange: function() {
+ this.callParent();
+ this.autoSize();
+ },
+
+ afterRender: function(){
+ var me = this;
+ if (me.enforceMaxLength) {
+ me.inputEl.dom.maxLength = me.maxLength;
+ }
+ me.applyEmptyText();
+ me.autoSize();
+ me.callParent();
+ },
+
+ onMouseDown: function(e){
+ var me = this;
+ if(!me.hasFocus){
+ me.mon(me.inputEl, 'mouseup', Ext.emptyFn, me, { single: true, preventDefault: true });
+ }
+ },
+
+ /**
+ * Performs any necessary manipulation of a raw String value to prepare it for conversion and/or
+ * {@link #validate validation}. For text fields this applies the configured {@link #stripCharsRe}
+ * to the raw value.
+ * @param {String} value The unprocessed string value
+ * @return {String} The processed string value
+ */
+ processRawValue: function(value) {
+ var me = this,
+ stripRe = me.stripCharsRe,
+ newValue;
+
+ if (stripRe) {
+ newValue = value.replace(stripRe, '');
+ if (newValue !== value) {
+ me.setRawValue(newValue);
+ value = newValue;
+ }
+ }
+ return value;
+ },
+
+ //private
+ onDisable: function(){
+ this.callParent();
+ if (Ext.isIE) {
+ this.inputEl.dom.unselectable = 'on';
+ }
+ },
+
+ //private
+ onEnable: function(){
+ this.callParent();
+ if (Ext.isIE) {
+ this.inputEl.dom.unselectable = '';
+ }
+ },
+
+ onKeyDown: function(e) {
+ this.fireEvent('keydown', this, e);
+ },
+
+ onKeyUp: function(e) {
+ this.fireEvent('keyup', this, e);
+ },
+
+ onKeyPress: function(e) {
+ this.fireEvent('keypress', this, e);
+ },
+
+ /**
+ * Resets the current field value to the originally-loaded value and clears any validation messages.
+ * Also adds **{@link #emptyText}** and **{@link #emptyCls}** if the original value was blank.
+ */
+ reset : function(){
+ this.callParent();
+ this.applyEmptyText();
+ },
+
+ applyEmptyText : function(){
+ var me = this,
+ emptyText = me.emptyText,
+ isEmpty;
+
+ if (me.rendered && emptyText) {
+ isEmpty = me.getRawValue().length < 1 && !me.hasFocus;
+
+ if (Ext.supports.Placeholder) {
+ me.inputEl.dom.placeholder = emptyText;
+ } else if (isEmpty) {
+ me.setRawValue(emptyText);
+ }
+
+ //all browsers need this because of a styling issue with chrome + placeholders.
+ //the text isnt vertically aligned when empty (and using the placeholder)
+ if (isEmpty) {
+ me.inputEl.addCls(me.emptyCls);
+ }
+
+ me.autoSize();
+ }
+ },
+
+ // private
+ preFocus : function(){
+ var me = this,
+ inputEl = me.inputEl,
+ emptyText = me.emptyText,
+ isEmpty;
+
+ if (emptyText && !Ext.supports.Placeholder && inputEl.dom.value === emptyText) {
+ me.setRawValue('');
+ isEmpty = true;
+ inputEl.removeCls(me.emptyCls);
+ } else if (Ext.supports.Placeholder) {
+ me.inputEl.removeCls(me.emptyCls);
+ }
+ if (me.selectOnFocus || isEmpty) {
+ inputEl.dom.select();
+ }
+ },
+
+ onFocus: function() {
+ var me = this;
+ me.callParent(arguments);
+ if (me.emptyText) {
+ me.autoSize();
+ }
+ },
+
+ // private
+ postBlur : function(){
+ this.applyEmptyText();
+ },
+
+ // private
+ filterKeys : function(e){
+ /*
+ * On European keyboards, the right alt key, Alt Gr, is used to type certain special characters.
+ * JS detects a keypress of this as ctrlKey & altKey. As such, we check that alt isn't pressed
+ * so we can still process these special characters.
+ */
+ if (e.ctrlKey && !e.altKey) {
+ return;
+ }
+ var key = e.getKey(),
+ charCode = String.fromCharCode(e.getCharCode());
+
+ if(Ext.isGecko && (e.isNavKeyPress() || key === e.BACKSPACE || (key === e.DELETE && e.button === -1))){
+ return;
+ }
+
+ if(!Ext.isGecko && e.isSpecialKey() && !charCode){
+ return;
+ }
+ if(!this.maskRe.test(charCode)){
+ e.stopEvent();
+ }
+ },
+
+ /**
+ * Returns the raw String value of the field, without performing any normalization, conversion, or validation. Gets
+ * the current value of the input element if the field has been rendered, ignoring the value if it is the
+ * {@link #emptyText}. To get a normalized and converted value see {@link #getValue}.
+ * @return {String} The raw String value of the field
+ */
+ getRawValue: function() {
+ var me = this,
+ v = me.callParent();
+ if (v === me.emptyText) {
+ v = '';
+ }
+ return v;
+ },
+
+ /**
+ * Sets a data value into the field and runs the change detection and validation. Also applies any configured
+ * {@link #emptyText} for text fields. To set the value directly without these inspections see {@link #setRawValue}.
+ * @param {Object} value The value to set
+ * @return {Ext.form.field.Text} this
+ */
+ setValue: function(value) {
+ var me = this,
+ inputEl = me.inputEl;
+
+ if (inputEl && me.emptyText && !Ext.isEmpty(value)) {
+ inputEl.removeCls(me.emptyCls);
+ }
+
+ me.callParent(arguments);
+
+ me.applyEmptyText();
+ return me;
+ },
+
+ /**
+ * Validates a value according to the field's validation rules and returns an array of errors
+ * for any failing validations. Validation rules are processed in the following order:
+ *
+ * 1. **Field specific validator**
+ *
+ * A validator offers a way to customize and reuse a validation specification.
+ * If a field is configured with a `{@link #validator}`
+ * function, it will be passed the current field value. The `{@link #validator}`
+ * function is expected to return either:
+ *
+ * - Boolean `true` if the value is valid (validation continues).
+ * - a String to represent the invalid message if invalid (validation halts).
+ *
+ * 2. **Basic Validation**
+ *
+ * If the `{@link #validator}` has not halted validation,
+ * basic validation proceeds as follows:
+ *
+ * - `{@link #allowBlank}` : (Invalid message = `{@link #emptyText}`)
+ *
+ * Depending on the configuration of `{@link #allowBlank}`, a
+ * blank field will cause validation to halt at this step and return
+ * Boolean true or false accordingly.
+ *
+ * - `{@link #minLength}` : (Invalid message = `{@link #minLengthText}`)
+ *
+ * If the passed value does not satisfy the `{@link #minLength}`
+ * specified, validation halts.
+ *
+ * - `{@link #maxLength}` : (Invalid message = `{@link #maxLengthText}`)
+ *
+ * If the passed value does not satisfy the `{@link #maxLength}`
+ * specified, validation halts.
+ *
+ * 3. **Preconfigured Validation Types (VTypes)**
+ *
+ * If none of the prior validation steps halts validation, a field
+ * configured with a `{@link #vtype}` will utilize the
+ * corresponding {@link Ext.form.field.VTypes VTypes} validation function.
+ * If invalid, either the field's `{@link #vtypeText}` or
+ * the VTypes vtype Text property will be used for the invalid message.
+ * Keystrokes on the field will be filtered according to the VTypes
+ * vtype Mask property.
+ *
+ * 4. **Field specific regex test**
+ *
+ * If none of the prior validation steps halts validation, a field's
+ * configured <code>{@link #regex}</code> test will be processed.
+ * The invalid message for this test is configured with `{@link #regexText}`
+ *
+ * @param {Object} value The value to validate. The processed raw value will be used if nothing is passed.
+ * @return {String[]} Array of any validation errors
+ */
+ getErrors: function(value) {
+ var me = this,
+ errors = me.callParent(arguments),
+ validator = me.validator,
+ emptyText = me.emptyText,
+ allowBlank = me.allowBlank,
+ vtype = me.vtype,
+ vtypes = Ext.form.field.VTypes,
+ regex = me.regex,
+ format = Ext.String.format,
+ msg;
+
+ value = value || me.processRawValue(me.getRawValue());
+
+ if (Ext.isFunction(validator)) {
+ msg = validator.call(me, value);
+ if (msg !== true) {
+ errors.push(msg);
+ }
+ }
+
+ if (value.length < 1 || value === emptyText) {
+ if (!allowBlank) {
+ errors.push(me.blankText);
+ }
+ //if value is blank, there cannot be any additional errors
+ return errors;
+ }
+
+ if (value.length < me.minLength) {
+ errors.push(format(me.minLengthText, me.minLength));
+ }
+
+ if (value.length > me.maxLength) {
+ errors.push(format(me.maxLengthText, me.maxLength));
+ }
+
+ if (vtype) {
+ if(!vtypes[vtype](value, me)){
+ errors.push(me.vtypeText || vtypes[vtype +'Text']);
+ }
+ }
+
+ if (regex && !regex.test(value)) {
+ errors.push(me.regexText || me.invalidText);
+ }
+
+ return errors;
+ },
+
+ /**
+ * Selects text in this field
+ * @param {Number} [start=0] The index where the selection should start
+ * @param {Number} [end] The index where the selection should end (defaults to the text length)
+ */
+ selectText : function(start, end){
+ var me = this,
+ v = me.getRawValue(),
+ doFocus = true,
+ el = me.inputEl.dom,
+ undef,
+ range;
+
+ if (v.length > 0) {
+ start = start === undef ? 0 : start;
+ end = end === undef ? v.length : end;
+ if (el.setSelectionRange) {
+ el.setSelectionRange(start, end);
+ }
+ else if(el.createTextRange) {
+ range = el.createTextRange();
+ range.moveStart('character', start);
+ range.moveEnd('character', end - v.length);
+ range.select();
+ }
+ doFocus = Ext.isGecko || Ext.isOpera;
+ }
+ if (doFocus) {
+ me.focus();
+ }
+ },
+
+ /**
+ * Automatically grows the field to accomodate the width of the text up to the maximum field width allowed. This
+ * only takes effect if {@link #grow} = true, and fires the {@link #autosize} event if the width changes.
+ */
+ autoSize: function() {
+ var me = this,
+ width;
+ if (me.grow && me.rendered) {
+ me.doComponentLayout();
+ width = me.inputEl.getWidth();
+ if (width !== me.lastInputWidth) {
+ me.fireEvent('autosize', width);
+ me.lastInputWidth = width;
+ }
+ }
+ },
+
+ initAria: function() {
+ this.callParent();
+ this.getActionEl().dom.setAttribute('aria-required', this.allowBlank === false);
+ },
+
+ /**
+ * To get the natural width of the inputEl, we do a simple calculation based on the 'size' config. We use
+ * hard-coded numbers to approximate what browsers do natively, to avoid having to read any styles which would hurt
+ * performance. Overrides Labelable method.
+ * @protected
+ */
+ getBodyNaturalWidth: function() {
+ return Math.round(this.size * 6.5) + 20;
+ }
+
+});
+
+/**
+ * @docauthor Robert Dougan <rob@sencha.com>
+ *
+ * This class creates a multiline text field, which can be used as a direct replacement for traditional
+ * textarea fields. In addition, it supports automatically {@link #grow growing} the height of the textarea to
+ * fit its content.
+ *
+ * All of the configuration options from {@link Ext.form.field.Text} can be used on TextArea.
+ *
+ * Example usage:
+ *
+ * @example
+ * Ext.create('Ext.form.FormPanel', {
+ * title : 'Sample TextArea',
+ * width : 400,
+ * bodyPadding: 10,
+ * renderTo : Ext.getBody(),
+ * items: [{
+ * xtype : 'textareafield',
+ * grow : true,
+ * name : 'message',
+ * fieldLabel: 'Message',
+ * anchor : '100%'
+ * }]
+ * });
+ *
+ * Some other useful configuration options when using {@link #grow} are {@link #growMin} and {@link #growMax}.
+ * These allow you to set the minimum and maximum grow heights for the textarea.
+ */
+Ext.define('Ext.form.field.TextArea', {
+ extend:'Ext.form.field.Text',
+ alias: ['widget.textareafield', 'widget.textarea'],
+ alternateClassName: 'Ext.form.TextArea',
+ requires: ['Ext.XTemplate', 'Ext.layout.component.field.TextArea'],
+
+ fieldSubTpl: [
+ '<textarea id="{id}" ',
+ '<tpl if="name">name="{name}" </tpl>',
+ '<tpl if="rows">rows="{rows}" </tpl>',
+ '<tpl if="cols">cols="{cols}" </tpl>',
+ '<tpl if="tabIdx">tabIndex="{tabIdx}" </tpl>',
+ 'class="{fieldCls} {typeCls}" ',
+ 'autocomplete="off">',
+ '</textarea>',
+ {
+ compiled: true,
+ disableFormats: true
+ }
+ ],
+
+ /**
+ * @cfg {Number} growMin
+ * The minimum height to allow when {@link #grow}=true
+ */
+ growMin: 60,
+
+ /**
+ * @cfg {Number} growMax
+ * The maximum height to allow when {@link #grow}=true
+ */
+ growMax: 1000,
+
+ /**
+ * @cfg {String} growAppend
+ * A string that will be appended to the field's current value for the purposes of calculating the target field
+ * size. Only used when the {@link #grow} config is true. Defaults to a newline for TextArea to ensure there is
+ * always a space below the current line.
+ */
+ growAppend: '\n-',
+
+ /**
+ * @cfg {Number} cols
+ * An initial value for the 'cols' attribute on the textarea element. This is only used if the component has no
+ * configured {@link #width} and is not given a width by its container's layout.
+ */
+ cols: 20,
+
+ /**
+ * @cfg {Number} cols
+ * An initial value for the 'cols' attribute on the textarea element. This is only used if the component has no
+ * configured {@link #width} and is not given a width by its container's layout.
+ */
+ rows: 4,
+
+ /**
+ * @cfg {Boolean} enterIsSpecial
+ * True if you want the enter key to be classed as a special key. Special keys are generally navigation keys
+ * (arrows, space, enter). Setting the config property to true would mean that you could not insert returns into the
+ * textarea.
+ */
+ enterIsSpecial: false,
+
+ /**
+ * @cfg {Boolean} preventScrollbars
+ * true to prevent scrollbars from appearing regardless of how much text is in the field. This option is only
+ * relevant when {@link #grow} is true. Equivalent to setting overflow: hidden.
+ */
+ preventScrollbars: false,
+
+ // private
+ componentLayout: 'textareafield',
+
+ // private
+ onRender: function(ct, position) {
+ var me = this;
+ Ext.applyIf(me.subTplData, {
+ cols: me.cols,
+ rows: me.rows
+ });
+
+ me.callParent(arguments);
+ },
+
+ // private
+ afterRender: function(){
+ var me = this;
+
+ me.callParent(arguments);
+
+ if (me.grow) {
+ if (me.preventScrollbars) {
+ me.inputEl.setStyle('overflow', 'hidden');
+ }
+ me.inputEl.setHeight(me.growMin);
+ }
+ },
+
+ // private
+ fireKey: function(e) {
+ if (e.isSpecialKey() && (this.enterIsSpecial || (e.getKey() !== e.ENTER || e.hasModifier()))) {
+ this.fireEvent('specialkey', this, e);
+ }
+ },
+
+ /**
+ * Automatically grows the field to accomodate the height of the text up to the maximum field height allowed. This
+ * only takes effect if {@link #grow} = true, and fires the {@link #autosize} event if the height changes.
+ */
+ autoSize: function() {
+ var me = this,
+ height;
+
+ if (me.grow && me.rendered) {
+ me.doComponentLayout();
+ height = me.inputEl.getHeight();
+ if (height !== me.lastInputHeight) {
+ me.fireEvent('autosize', height);
+ me.lastInputHeight = height;
+ }
+ }
+ },
+
+ // private
+ initAria: function() {
+ this.callParent(arguments);
+ this.getActionEl().dom.setAttribute('aria-multiline', true);
+ },
+
+ /**
+ * To get the natural width of the textarea element, we do a simple calculation based on the 'cols' config.
+ * We use hard-coded numbers to approximate what browsers do natively, to avoid having to read any styles which
+ * would hurt performance. Overrides Labelable method.
+ * @protected
+ */
+ getBodyNaturalWidth: function() {
+ return Math.round(this.cols * 6.5) + 20;
+ }
+
+});
+
+
+/**
+ * Utility class for generating different styles of message boxes. The singleton instance, Ext.MessageBox
+ * alias `Ext.Msg` can also be used.
+ *
+ * Note that a MessageBox is asynchronous. Unlike a regular JavaScript `alert` (which will halt
+ * browser execution), showing a MessageBox will not cause the code to stop. For this reason, if you have code
+ * that should only run *after* some user feedback from the MessageBox, you must use a callback function
+ * (see the `function` parameter for {@link #show} for more details).
+ *
+ * Basic alert
+ *
+ * @example
+ * Ext.Msg.alert('Status', 'Changes saved successfully.');
+ *
+ * Prompt for user data and process the result using a callback
+ *
+ * @example
+ * Ext.Msg.prompt('Name', 'Please enter your name:', function(btn, text){
+ * if (btn == 'ok'){
+ * // process text value and close...
+ * }
+ * });
+ *
+ * Show a dialog using config options
+ *
+ * @example
+ * Ext.Msg.show({
+ * title:'Save Changes?',
+ * msg: 'You are closing a tab that has unsaved changes. Would you like to save your changes?',
+ * buttons: Ext.Msg.YESNOCANCEL,
+ * icon: Ext.Msg.QUESTION
+ * });
+ */
+Ext.define('Ext.window.MessageBox', {
+ extend: 'Ext.window.Window',
+
+ requires: [
+ 'Ext.toolbar.Toolbar',
+ 'Ext.form.field.Text',
+ 'Ext.form.field.TextArea',
+ 'Ext.button.Button',
+ 'Ext.layout.container.Anchor',
+ 'Ext.layout.container.HBox',
+ 'Ext.ProgressBar'
+ ],
+
+ alias: 'widget.messagebox',
+
+ /**
+ * Button config that displays a single OK button
+ * @type Number
+ */
+ OK : 1,
+ /**
+ * Button config that displays a single Yes button
+ * @type Number
+ */
+ YES : 2,
+ /**
+ * Button config that displays a single No button
+ * @type Number
+ */
+ NO : 4,
+ /**
+ * Button config that displays a single Cancel button
+ * @type Number
+ */
+ CANCEL : 8,
+ /**
+ * Button config that displays OK and Cancel buttons
+ * @type Number
+ */
+ OKCANCEL : 9,
+ /**
+ * Button config that displays Yes and No buttons
+ * @type Number
+ */
+ YESNO : 6,
+ /**
+ * Button config that displays Yes, No and Cancel buttons
+ * @type Number
+ */
+ YESNOCANCEL : 14,
+ /**
+ * The CSS class that provides the INFO icon image
+ * @type String
+ */
+ INFO : 'ext-mb-info',
+ /**
+ * The CSS class that provides the WARNING icon image
+ * @type String
+ */
+ WARNING : 'ext-mb-warning',
+ /**
+ * The CSS class that provides the QUESTION icon image
+ * @type String
+ */
+ QUESTION : 'ext-mb-question',
+ /**
+ * The CSS class that provides the ERROR icon image
+ * @type String
+ */
+ ERROR : 'ext-mb-error',
+
+ // hide it by offsets. Windows are hidden on render by default.
+ hideMode: 'offsets',
+ closeAction: 'hide',
+ resizable: false,
+ title: ' ',
+
+ width: 600,
+ height: 500,
+ minWidth: 250,
+ maxWidth: 600,
+ minHeight: 110,
+ maxHeight: 500,
+ constrain: true,
+
+ cls: Ext.baseCSSPrefix + 'message-box',
+
+ layout: {
+ type: 'anchor'
+ },
+
+ /**
+ * The default height in pixels of the message box's multiline textarea if displayed.
+ * @type Number
+ */
+ defaultTextHeight : 75,
+ /**
+ * 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.
+ * @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.
+ * @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
+ * resource file for handling language support across the framework.
+ * Customize the default text like so: Ext.window.MessageBox.buttonText.yes = "oui"; //french
+ * @type Object
+ */
+ buttonText: {
+ ok: 'OK',
+ yes: 'Yes',
+ no: 'No',
+ cancel: 'Cancel'
+ },
+
+ buttonIds: [
+ 'ok', 'yes', 'no', 'cancel'
+ ],
+
+ titleText: {
+ confirm: 'Confirm',
+ prompt: 'Prompt',
+ wait: 'Loading...',
+ alert: 'Attention'
+ },
+
+ iconHeight: 35,
+
+ makeButton: function(btnIdx) {
+ var btnId = this.buttonIds[btnIdx];
+ return Ext.create('Ext.button.Button', {
+ handler: this.btnCallback,
+ itemId: btnId,
+ scope: this,
+ text: this.buttonText[btnId],
+ minWidth: 75
+ });
+ },
+
+ btnCallback: function(btn) {
+ var me = this,
+ value,
+ field;
+
+ if (me.cfg.prompt || me.cfg.multiline) {
+ if (me.cfg.multiline) {
+ field = me.textArea;
+ } else {
+ field = me.textField;
+ }
+ value = field.getValue();
+ field.reset();
+ }
+
+ // Important not to have focus remain in the hidden Window; Interferes with DnD.
+ btn.blur();
+ me.hide();
+ me.userCallback(btn.itemId, value, me.cfg);
+ },
+
+ hide: function() {
+ var me = this;
+ me.dd.endDrag();
+ me.progressBar.reset();
+ me.removeCls(me.cfg.cls);
+ me.callParent();
+ },
+
+ initComponent: function() {
+ var me = this,
+ i, button;
+
+ me.title = ' ';
+
+ me.topContainer = Ext.create('Ext.container.Container', {
+ anchor: '100%',
+ style: {
+ padding: '10px',
+ overflow: 'hidden'
+ },
+ items: [
+ me.iconComponent = Ext.create('Ext.Component', {
+ cls: 'ext-mb-icon',
+ width: 50,
+ height: me.iconHeight,
+ style: {
+ 'float': 'left'
+ }
+ }),
+ me.promptContainer = Ext.create('Ext.container.Container', {
+ layout: {
+ type: 'anchor'
+ },
+ items: [
+ me.msg = Ext.create('Ext.Component', {
+ autoEl: { tag: 'span' },
+ cls: 'ext-mb-text'
+ }),
+ me.textField = Ext.create('Ext.form.field.Text', {
+ anchor: '100%',
+ enableKeyEvents: true,
+ listeners: {
+ keydown: me.onPromptKey,
+ scope: me
+ }
+ }),
+ me.textArea = Ext.create('Ext.form.field.TextArea', {
+ anchor: '100%',
+ height: 75
+ })
+ ]
+ })
+ ]
+ });
+ me.progressBar = Ext.create('Ext.ProgressBar', {
+ anchor: '-10',
+ style: 'margin-left:10px'
+ });
+
+ me.items = [me.topContainer, me.progressBar];
+
+ // Create the buttons based upon passed bitwise config
+ me.msgButtons = [];
+ for (i = 0; i < 4; i++) {
+ button = me.makeButton(i);
+ me.msgButtons[button.itemId] = button;
+ me.msgButtons.push(button);
+ }
+ me.bottomTb = Ext.create('Ext.toolbar.Toolbar', {
+ ui: 'footer',
+ dock: 'bottom',
+ layout: {
+ pack: 'center'
+ },
+ items: [
+ me.msgButtons[0],
+ me.msgButtons[1],
+ me.msgButtons[2],
+ me.msgButtons[3]
+ ]
+ });
+ me.dockedItems = [me.bottomTb];
+
+ me.callParent();
+ },
+
+ onPromptKey: function(textField, e) {
+ var me = this,
+ blur;
+
+ if (e.keyCode === Ext.EventObject.RETURN || e.keyCode === 10) {
+ if (me.msgButtons.ok.isVisible()) {
+ blur = true;
+ me.msgButtons.ok.handler.call(me, me.msgButtons.ok);
+ } else if (me.msgButtons.yes.isVisible()) {
+ me.msgButtons.yes.handler.call(me, me.msgButtons.yes);
+ blur = true;
+ }
+
+ if (blur) {
+ me.textField.blur();
+ }
+ }
+ },
+
+ reconfigure: function(cfg) {
+ var me = this,
+ buttons = cfg.buttons || 0,
+ hideToolbar = true,
+ initialWidth = me.maxWidth,
+ i;
+
+ cfg = cfg || {};
+ me.cfg = cfg;
+ if (cfg.width) {
+ initialWidth = cfg.width;
+ }
+
+ // Default to allowing the Window to take focus.
+ delete me.defaultFocus;
+
+ // clear any old animateTarget
+ me.animateTarget = cfg.animateTarget || undefined;
+
+ // Defaults to modal
+ me.modal = cfg.modal !== false;
+
+ // Show the title
+ if (cfg.title) {
+ me.setTitle(cfg.title||' ');
+ }
+
+ if (!me.rendered) {
+ me.width = initialWidth;
+ me.render(Ext.getBody());
+ } else {
+ me.setSize(initialWidth, me.maxHeight);
+ }
+ me.setPosition(-10000, -10000);
+
+ // Hide or show the close tool
+ me.closable = cfg.closable && !cfg.wait;
+ me.header.child('[type=close]').setVisible(cfg.closable !== false);
+
+ // Hide or show the header
+ if (!cfg.title && !me.closable) {
+ me.header.hide();
+ } else {
+ me.header.show();
+ }
+
+ // Default to dynamic drag: drag the window, not a ghost
+ me.liveDrag = !cfg.proxyDrag;
+
+ // wrap the user callback
+ me.userCallback = Ext.Function.bind(cfg.callback ||cfg.fn || Ext.emptyFn, cfg.scope || Ext.global);
+
+ // Hide or show the icon Component
+ me.setIcon(cfg.icon);
+
+ // Hide or show the message area
+ if (cfg.msg) {
+ me.msg.update(cfg.msg);
+ me.msg.show();
+ } else {
+ me.msg.hide();
+ }
+
+ // Hide or show the input field
+ if (cfg.prompt || cfg.multiline) {
+ me.multiline = cfg.multiline;
+ if (cfg.multiline) {
+ me.textArea.setValue(cfg.value);
+ me.textArea.setHeight(cfg.defaultTextHeight || me.defaultTextHeight);
+ me.textArea.show();
+ me.textField.hide();
+ me.defaultFocus = me.textArea;
+ } else {
+ me.textField.setValue(cfg.value);
+ me.textArea.hide();
+ me.textField.show();
+ me.defaultFocus = me.textField;
+ }
+ } else {
+ me.textArea.hide();
+ me.textField.hide();
+ }
+
+ // Hide or show the progress bar
+ if (cfg.progress || cfg.wait) {
+ me.progressBar.show();
+ me.updateProgress(0, cfg.progressText);
+ if(cfg.wait === true){
+ me.progressBar.wait(cfg.waitConfig);
+ }
+ } else {
+ me.progressBar.hide();
+ }
+
+ // Hide or show buttons depending on flag value sent.
+ for (i = 0; i < 4; i++) {
+ if (buttons & Math.pow(2, i)) {
+
+ // Default to focus on the first visible button if focus not already set
+ if (!me.defaultFocus) {
+ me.defaultFocus = me.msgButtons[i];
+ }
+ me.msgButtons[i].show();
+ hideToolbar = false;
+ } else {
+ me.msgButtons[i].hide();
+ }
+ }
+
+ // Hide toolbar if no buttons to show
+ if (hideToolbar) {
+ me.bottomTb.hide();
+ } else {
+ me.bottomTb.show();
+ }
+ },
+
+ /**
+ * Displays a new message box, or reinitializes an existing message box, based on the config options
+ * passed in. All display functions (e.g. prompt, alert, etc.) on MessageBox call this function internally,
+ * although those calls are basic shortcuts and do not support all of the config options allowed here.
+ * @param {Object} config The following config options are supported: <ul>
+ * <li><b>animateTarget</b> : String/Element<div class="sub-desc">An id or Element from which the message box should animate as it
+ * opens and closes (defaults to undefined)</div></li>
+ * <li><b>buttons</b> : Number<div class="sub-desc">A bitwise button specifier consisting of the sum of any of the following constants:<ul>
+ * <li>Ext.window.MessageBox.OK</li>
+ * <li>Ext.window.MessageBox.YES</li>
+ * <li>Ext.window.MessageBox.NO</li>
+ * <li>Ext.window.MessageBox.CANCEL</li>
+ * </ul>Or false to not show any buttons (defaults to false)</div></li>
+ * <li><b>closable</b> : Boolean<div class="sub-desc">False to hide the top-right close button (defaults to true). Note that
+ * progress and wait dialogs will ignore this property and always hide the close button as they can only
+ * be closed programmatically.</div></li>
+ * <li><b>cls</b> : String<div class="sub-desc">A custom CSS class to apply to the message box's container element</div></li>
+ * <li><b>defaultTextHeight</b> : Number<div class="sub-desc">The default height in pixels of the message box's multiline textarea
+ * if displayed (defaults to 75)</div></li>
+ * <li><b>fn</b> : Function<div class="sub-desc">A callback function which is called when the dialog is dismissed either
+ * by clicking on the configured buttons, or on the dialog close button, or by pressing
+ * the return button to enter input.
+ * <p>Progress and wait dialogs will ignore this option since they do not respond to user
+ * actions and can only be closed programmatically, so any required function should be called
+ * by the same code after it closes the dialog. Parameters passed:<ul>
+ * <li><b>buttonId</b> : String<div class="sub-desc">The ID of the button pressed, one of:<div class="sub-desc"><ul>
+ * <li><tt>ok</tt></li>
+ * <li><tt>yes</tt></li>
+ * <li><tt>no</tt></li>
+ * <li><tt>cancel</tt></li>
+ * </ul></div></div></li>
+ * <li><b>text</b> : String<div class="sub-desc">Value of the input field if either <tt><a href="#show-option-prompt" ext:member="show-option-prompt" ext:cls="Ext.window.MessageBox">prompt</a></tt>
+ * or <tt><a href="#show-option-multiline" ext:member="show-option-multiline" ext:cls="Ext.window.MessageBox">multiline</a></tt> is true</div></li>
+ * <li><b>opt</b> : Object<div class="sub-desc">The config object passed to show.</div></li>
+ * </ul></p></div></li>
+ * <li><b>scope</b> : Object<div class="sub-desc">The scope (<code>this</code> reference) in which the function will be executed.</div></li>
+ * <li><b>icon</b> : String<div class="sub-desc">A CSS class that provides a background image to be used as the body icon for the
+ * dialog (e.g. Ext.window.MessageBox.WARNING or 'custom-class') (defaults to '')</div></li>
+ * <li><b>iconCls</b> : String<div class="sub-desc">The standard {@link Ext.window.Window#iconCls} to
+ * add an optional header icon (defaults to '')</div></li>
+ * <li><b>maxWidth</b> : Number<div class="sub-desc">The maximum width in pixels of the message box (defaults to 600)</div></li>
+ * <li><b>minWidth</b> : Number<div class="sub-desc">The minimum width in pixels of the message box (defaults to 100)</div></li>
+ * <li><b>modal</b> : Boolean<div class="sub-desc">False to allow user interaction with the page while the message box is
+ * displayed (defaults to true)</div></li>
+ * <li><b>msg</b> : String<div class="sub-desc">A string that will replace the existing message box body text (defaults to the
+ * XHTML-compliant non-breaking space character '&#160;')</div></li>
+ * <li><a id="show-option-multiline"></a><b>multiline</b> : Boolean<div class="sub-desc">
+ * True to prompt the user to enter multi-line text (defaults to false)</div></li>
+ * <li><b>progress</b> : Boolean<div class="sub-desc">True to display a progress bar (defaults to false)</div></li>
+ * <li><b>progressText</b> : String<div class="sub-desc">The text to display inside the progress bar if progress = true (defaults to '')</div></li>
+ * <li><a id="show-option-prompt"></a><b>prompt</b> : Boolean<div class="sub-desc">True to prompt the user to enter single-line text (defaults to false)</div></li>
+ * <li><b>proxyDrag</b> : Boolean<div class="sub-desc">True to display a lightweight proxy while dragging (defaults to false)</div></li>
+ * <li><b>title</b> : String<div class="sub-desc">The title text</div></li>
+ * <li><b>value</b> : String<div class="sub-desc">The string value to set into the active textbox element if displayed</div></li>
+ * <li><b>wait</b> : Boolean<div class="sub-desc">True to display a progress bar (defaults to false)</div></li>
+ * <li><b>waitConfig</b> : Object<div class="sub-desc">A {@link Ext.ProgressBar#wait} config object (applies only if wait = true)</div></li>
+ * <li><b>width</b> : Number<div class="sub-desc">The width of the dialog in pixels</div></li>
+ * </ul>
+ * Example usage:
+ * <pre><code>
+Ext.Msg.show({
+title: 'Address',
+msg: 'Please enter your address:',
+width: 300,
+buttons: Ext.Msg.OKCANCEL,
+multiline: true,
+fn: saveAddress,
+animateTarget: 'addAddressBtn',
+icon: Ext.window.MessageBox.INFO
+});
+</code></pre>
+ * @return {Ext.window.MessageBox} this
+ */
+ show: function(cfg) {
+ var me = this;
+
+ me.reconfigure(cfg);
+ me.addCls(cfg.cls);
+ if (cfg.animateTarget) {
+ me.doAutoSize(true);
+ me.callParent();
+ } else {
+ me.callParent();
+ me.doAutoSize(true);
+ }
+ return me;
+ },
+
+ afterShow: function(){
+ if (this.animateTarget) {
+ this.center();
+ }
+ this.callParent(arguments);
+ },
+
+ doAutoSize: function(center) {
+ var me = this,
+ icon = me.iconComponent,
+ iconHeight = me.iconHeight;
+
+ if (!Ext.isDefined(me.frameWidth)) {
+ me.frameWidth = me.el.getWidth() - me.body.getWidth();
+ }
+
+ // reset to the original dimensions
+ icon.setHeight(iconHeight);
+
+ // Allow per-invocation override of minWidth
+ me.minWidth = me.cfg.minWidth || Ext.getClass(this).prototype.minWidth;
+
+ // Set best possible size based upon allowing the text to wrap in the maximized Window, and
+ // then constraining it to within the max with. Then adding up constituent element heights.
+ me.topContainer.doLayout();
+ if (Ext.isIE6 || Ext.isIEQuirks) {
+ // In IE quirks, the initial full width of the prompt fields will prevent the container element
+ // from collapsing once sized down, so temporarily force them to a small width. They'll get
+ // layed out to their final width later when setting the final window size.
+ me.textField.setCalculatedSize(9);
+ me.textArea.setCalculatedSize(9);
+ }
+ var width = me.cfg.width || me.msg.getWidth() + icon.getWidth() + 25, /* topContainer's layout padding */
+ height = (me.header.rendered ? me.header.getHeight() : 0) +
+ Math.max(me.promptContainer.getHeight(), icon.getHeight()) +
+ me.progressBar.getHeight() +
+ (me.bottomTb.rendered ? me.bottomTb.getHeight() : 0) + 20 ;/* topContainer's layout padding */
+
+ // Update to the size of the content, this way the text won't wrap under the icon.
+ icon.setHeight(Math.max(iconHeight, me.msg.getHeight()));
+ me.setSize(width + me.frameWidth, height + me.frameWidth);
+ if (center) {
+ me.center();
+ }
+ return me;
+ },
+
+ updateText: function(text) {
+ this.msg.update(text);
+ return this.doAutoSize(true);
+ },
+
+ /**
+ * Adds the specified icon to the dialog. By default, the class 'ext-mb-icon' is applied for default
+ * styling, and the class passed in is expected to supply the background image url. Pass in empty string ('')
+ * to clear any existing icon. This method must be called before the MessageBox is shown.
+ * The following built-in icon classes are supported, but you can also pass in a custom class name:
+ * <pre>
+Ext.window.MessageBox.INFO
+Ext.window.MessageBox.WARNING
+Ext.window.MessageBox.QUESTION
+Ext.window.MessageBox.ERROR
+ *</pre>
+ * @param {String} icon A CSS classname specifying the icon's background image url, or empty string to clear the icon
+ * @return {Ext.window.MessageBox} this
+ */
+ setIcon : function(icon) {
+ var me = this;
+ me.iconComponent.removeCls(me.iconCls);
+ if (icon) {
+ me.iconComponent.show();
+ me.iconComponent.addCls(Ext.baseCSSPrefix + 'dlg-icon');
+ me.iconComponent.addCls(me.iconCls = icon);
+ } else {
+ me.iconComponent.removeCls(Ext.baseCSSPrefix + 'dlg-icon');
+ me.iconComponent.hide();
+ }
+ return me;
+ },
+
+ /**
+ * Updates a progress-style message box's text and progress bar. Only relevant on message boxes
+ * initiated via {@link Ext.window.MessageBox#progress} or {@link Ext.window.MessageBox#wait},
+ * or by calling {@link Ext.window.MessageBox#show} with progress: true.
+ * @param {Number} [value=0] Any number between 0 and 1 (e.g., .5)
+ * @param {String} [progressText=''] The progress text to display inside the progress bar.
+ * @param {String} [msg] The message box's body text is replaced with the specified string (defaults to undefined
+ * so that any existing body text will not get overwritten by default unless a new value is passed in)
+ * @return {Ext.window.MessageBox} this
+ */
+ updateProgress : function(value, progressText, msg){
+ this.progressBar.updateProgress(value, progressText);
+ if (msg){
+ this.updateText(msg);
+ }
+ return this;
+ },
+
+ onEsc: function() {
+ if (this.closable !== false) {
+ this.callParent(arguments);
+ }
+ },
+
+ /**
+ * Displays a confirmation message box with Yes and No buttons (comparable to JavaScript's confirm).
+ * If a callback function is passed it will be called after the user clicks either button,
+ * and the id of the button that was clicked will be passed as the only parameter to the callback
+ * (could also be the top-right close button).
+ * @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 (<code>this</code> reference) in which the callback is executed. Defaults to the browser wnidow.
+ * @return {Ext.window.MessageBox} this
+ */
+ confirm: function(cfg, msg, fn, scope) {
+ if (Ext.isString(cfg)) {
+ cfg = {
+ title: cfg,
+ icon: 'ext-mb-question',
+ msg: msg,
+ buttons: this.YESNO,
+ callback: fn,
+ scope: scope
+ };
+ }
+ return this.show(cfg);
+ },
+
+ /**
+ * Displays a message box with OK and Cancel buttons prompting the user to enter some text (comparable to JavaScript's prompt).
+ * The prompt can be a single-line or multi-line textbox. If a callback function is passed it will be called after the user
+ * clicks either button, and the id of the button that was clicked (could also be the top-right
+ * close button) and the text that was entered will be passed as the two parameters to the callback.
+ * @param {String} title The title bar text
+ * @param {String} msg The message box body text
+ * @param {Function} [fn] The callback function invoked after the message box is closed
+ * @param {Object} [scope] The scope (<code>this</code> reference) in which the callback is executed. Defaults to the browser wnidow.
+ * @param {Boolean/Number} [multiline=false] True to create a multiline textbox using the defaultTextHeight
+ * property, or the height in pixels to create the textbox/
+ * @param {String} [value=''] Default value of the text input element
+ * @return {Ext.window.MessageBox} this
+ */
+ prompt : function(cfg, msg, fn, scope, multiline, value){
+ if (Ext.isString(cfg)) {
+ cfg = {
+ prompt: true,
+ title: cfg,
+ minWidth: this.minPromptWidth,
+ msg: msg,
+ buttons: this.OKCANCEL,
+ callback: fn,
+ scope: scope,
+ multiline: multiline,
+ value: value
+ };
+ }
+ return this.show(cfg);
+ },
+
+ /**
+ * Displays a message box with an infinitely auto-updating progress bar. This can be used to block user
+ * interaction while waiting for a long-running process to complete that does not have defined intervals.
+ * You are responsible for closing the message box when the process is complete.
+ * @param {String} msg The message box body text
+ * @param {String} title (optional) The title bar text
+ * @param {Object} config (optional) A {@link Ext.ProgressBar#wait} config object
+ * @return {Ext.window.MessageBox} this
+ */
+ wait : function(cfg, title, config){
+ if (Ext.isString(cfg)) {
+ cfg = {
+ title : title,
+ msg : cfg,
+ closable: false,
+ wait: true,
+ modal: true,
+ minWidth: this.minProgressWidth,
+ waitConfig: config
+ };
+ }
+ return this.show(cfg);
+ },
+
+ /**
+ * Displays a standard read-only message box with an OK button (comparable to the basic JavaScript alert prompt).
+ * If a callback function is passed it will be called after the user clicks the button, and the
+ * id of the button that was clicked will be passed as the only parameter to the callback
+ * (could also be the top-right close button).
+ * @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 (<code>this</code> reference) in which the callback is executed. Defaults to the browser wnidow.
+ * @return {Ext.window.MessageBox} this
+ */
+ alert: function(cfg, msg, fn, scope) {
+ if (Ext.isString(cfg)) {
+ cfg = {
+ title : cfg,
+ msg : msg,
+ buttons: this.OK,
+ fn: fn,
+ scope : scope,
+ minWidth: this.minWidth
+ };
+ }
+ return this.show(cfg);
+ },
+
+ /**
+ * Displays a message box with a progress bar. This message box has no buttons and is not closeable by
+ * the user. You are responsible for updating the progress bar as needed via {@link Ext.window.MessageBox#updateProgress}
+ * and closing the message box when the process is complete.
+ * @param {String} title The title bar text
+ * @param {String} msg The message box body text
+ * @param {String} [progressText=''] The text to display inside the progress bar
+ * @return {Ext.window.MessageBox} this
+ */
+ progress : function(cfg, msg, progressText){
+ if (Ext.isString(cfg)) {
+ cfg = {
+ title: cfg,
+ msg: msg,
+ progress: true,
+ progressText: progressText
+ };
+ }
+ return this.show(cfg);
+ }
+}, function() {
+ /**
+ * @class Ext.MessageBox
+ * @alternateClassName Ext.Msg
+ * @extends Ext.window.MessageBox
+ * @singleton
+ * Singleton instance of {@link Ext.window.MessageBox}.
+ */
+ Ext.MessageBox = Ext.Msg = new this();
+});
+/**
+ * @class Ext.form.Basic
+ * @extends Ext.util.Observable
+ *
+ * Provides input field management, validation, submission, and form loading services for the collection
+ * of {@link Ext.form.field.Field Field} instances within a {@link Ext.container.Container}. It is recommended
+ * that you use a {@link Ext.form.Panel} as the form container, as that has logic to automatically
+ * hook up an instance of {@link Ext.form.Basic} (plus other conveniences related to field configuration.)
+ *
+ * ## Form Actions
+ *
+ * The Basic class delegates the handling of form loads and submits to instances of {@link Ext.form.action.Action}.
+ * See the various Action implementations for specific details of each one's functionality, as well as the
+ * documentation for {@link #doAction} which details the configuration options that can be specified in
+ * each action call.
+ *
+ * The default submit Action is {@link Ext.form.action.Submit}, which uses an Ajax request to submit the
+ * form's values to a configured URL. To enable normal browser submission of an Ext form, use the
+ * {@link #standardSubmit} config option.
+ *
+ * ## File uploads
+ *
+ * File uploads are not performed using normal 'Ajax' techniques; see the description for
+ * {@link #hasUpload} for details. If you're using file uploads you should read the method description.
+ *
+ * ## Example usage:
+ *
+ * Ext.create('Ext.form.Panel', {
+ * title: 'Basic Form',
+ * renderTo: Ext.getBody(),
+ * bodyPadding: 5,
+ * width: 350,
+ *
+ * // Any configuration items here will be automatically passed along to
+ * // the Ext.form.Basic instance when it gets created.
+ *
+ * // The form will submit an AJAX request to this URL when submitted
+ * url: 'save-form.php',
+ *
+ * items: [{
+ * fieldLabel: 'Field',
+ * name: 'theField'
+ * }],
+ *
+ * buttons: [{
+ * text: 'Submit',
+ * handler: function() {
+ * // The getForm() method returns the Ext.form.Basic instance:
+ * var form = this.up('form').getForm();
+ * if (form.isValid()) {
+ * // Submit the Ajax request and handle the response
+ * form.submit({
+ * success: function(form, action) {
+ * Ext.Msg.alert('Success', action.result.msg);
+ * },
+ * failure: function(form, action) {
+ * Ext.Msg.alert('Failed', action.result.msg);
+ * }
+ * });
+ * }
+ * }
+ * }]
+ * });
+ *
+ * @docauthor Jason Johnston <jason@sencha.com>
+ */
+Ext.define('Ext.form.Basic', {
+ extend: 'Ext.util.Observable',
+ alternateClassName: 'Ext.form.BasicForm',
+ requires: ['Ext.util.MixedCollection', 'Ext.form.action.Load', 'Ext.form.action.Submit',
+ 'Ext.window.MessageBox', 'Ext.data.Errors', 'Ext.util.DelayedTask'],
+
+ /**
+ * Creates new form.
+ * @param {Ext.container.Container} owner The component that is the container for the form, usually a {@link Ext.form.Panel}
+ * @param {Object} config Configuration options. These are normally specified in the config to the
+ * {@link Ext.form.Panel} constructor, which passes them along to the BasicForm automatically.
+ */
+ constructor: function(owner, config) {
+ var me = this,
+ onItemAddOrRemove = me.onItemAddOrRemove;
+
+ /**
+ * @property owner
+ * @type Ext.container.Container
+ * The container component to which this BasicForm is attached.
+ */
+ me.owner = owner;
+
+ // Listen for addition/removal of fields in the owner container
+ me.mon(owner, {
+ add: onItemAddOrRemove,
+ remove: onItemAddOrRemove,
+ scope: me
+ });
+
+ Ext.apply(me, config);
+
+ // Normalize the paramOrder to an Array
+ if (Ext.isString(me.paramOrder)) {
+ me.paramOrder = me.paramOrder.split(/[\s,|]/);
+ }
+
+ me.checkValidityTask = Ext.create('Ext.util.DelayedTask', me.checkValidity, me);
+
+ me.addEvents(
+ /**
+ * @event beforeaction
+ * Fires before any action is performed. Return false to cancel the action.
+ * @param {Ext.form.Basic} this
+ * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} to be performed
+ */
+ 'beforeaction',
+ /**
+ * @event actionfailed
+ * Fires when an action fails.
+ * @param {Ext.form.Basic} this
+ * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} that failed
+ */
+ 'actionfailed',
+ /**
+ * @event actioncomplete
+ * Fires when an action is completed.
+ * @param {Ext.form.Basic} this
+ * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} that completed
+ */
+ 'actioncomplete',
+ /**
+ * @event validitychange
+ * Fires when the validity of the entire form changes.
+ * @param {Ext.form.Basic} this
+ * @param {Boolean} valid <tt>true</tt> if the form is now valid, <tt>false</tt> if it is now invalid.
+ */
+ 'validitychange',
+ /**
+ * @event dirtychange
+ * Fires when the dirty state of the entire form changes.
+ * @param {Ext.form.Basic} this
+ * @param {Boolean} dirty <tt>true</tt> if the form is now dirty, <tt>false</tt> if it is no longer dirty.
+ */
+ 'dirtychange'
+ );
+ me.callParent();
+ },
+
+ /**
+ * Do any post constructor initialization
+ * @private
+ */
+ initialize: function(){
+ this.initialized = true;
+ this.onValidityChange(!this.hasInvalidField());
+ },
+
+ /**
+ * @cfg {String} method
+ * The request method to use (GET or POST) for form actions if one isn't supplied in the action options.
+ */
+
+ /**
+ * @cfg {Ext.data.reader.Reader} reader
+ * An Ext.data.DataReader (e.g. {@link Ext.data.reader.Xml}) to be used to read
+ * data when executing 'load' actions. This is optional as there is built-in
+ * support for processing JSON responses.
+ */
+
+ /**
+ * @cfg {Ext.data.reader.Reader} errorReader
+ * <p>An Ext.data.DataReader (e.g. {@link Ext.data.reader.Xml}) to be used to
+ * read field error messages returned from 'submit' actions. This is optional
+ * as there is built-in support for processing JSON responses.</p>
+ * <p>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.</p>
+ * <p>The errorReader does not have to be a full-blown implementation of a
+ * Reader. It simply needs to implement a <tt>read(xhr)</tt> function
+ * which returns an Array of Records in an object with the following
+ * structure:</p><pre><code>
+{
+ records: recordArray
+}
+</code></pre>
+ */
+
+ /**
+ * @cfg {String} url
+ * The URL to use for form actions if one isn't supplied in the
+ * {@link #doAction doAction} options.
+ */
+
+ /**
+ * @cfg {Object} baseParams
+ * <p>Parameters to pass with all requests. e.g. baseParams: {id: '123', foo: 'bar'}.</p>
+ * <p>Parameters are encoded as standard HTTP parameters using {@link Ext.Object#toQueryString}.</p>
+ */
+
+ /**
+ * @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.DirectLoad}.
+ * Methods which have been imported by {@link Ext.direct.Manager} can be specified here to load and submit
+ * forms.
+ * Such as the following:<pre><code>
+api: {
+ load: App.ss.MyProfile.load,
+ submit: App.ss.MyProfile.submit
+}
+</code></pre>
+ * <p>Load actions can use <code>{@link #paramOrder}</code> or <code>{@link #paramsAsHash}</code>
+ * to customize how the load method is invoked.
+ * Submit actions will always use a standard form submit. The <tt>formHandler</tt> configuration must
+ * be set on the associated server-side method which has been imported by {@link Ext.direct.Manager}.</p>
+ */
+
+ /**
+ * @cfg {String/String[]} paramOrder <p>A list of params to be executed server side.
+ * Defaults to <tt>undefined</tt>. Only used for the <code>{@link #api}</code>
+ * <code>load</code> configuration.</p>
+ * <p>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:</p><pre><code>
+paramOrder: ['param1','param2','param3']
+paramOrder: 'param1 param2 param3'
+paramOrder: 'param1,param2,param3'
+paramOrder: 'param1|param2|param'
+ </code></pre>
+ */
+
+ /**
+ * @cfg {Boolean} paramsAsHash
+ * Only used for the <code>{@link #api}</code>
+ * <code>load</code> configuration. If <tt>true</tt>, parameters will be sent as a
+ * single hash collection of named arguments. Providing a
+ * <tt>{@link #paramOrder}</tt> nullifies this configuration.
+ */
+ paramsAsHash: false,
+
+ /**
+ * @cfg {String} waitTitle
+ * The default title to show for the waiting message box
+ */
+ waitTitle: 'Please Wait...',
+
+ /**
+ * @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.
+ */
+ trackResetOnLoad: false,
+
+ /**
+ * @cfg {Boolean} standardSubmit
+ * If set to true, a standard HTML form submit is used instead of a XHR (Ajax) style form submission.
+ * All of the field values, plus any additional params configured via {@link #baseParams}
+ * and/or the `options` to {@link #submit}, will be included in the values submitted in the form.
+ */
+
+ /**
+ * @cfg {String/HTMLElement/Ext.Element} waitMsgTarget
+ * 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.
+ */
+
+
+ // Private
+ wasDirty: false,
+
+
+ /**
+ * Destroys this object.
+ */
+ destroy: function() {
+ this.clearListeners();
+ this.checkValidityTask.cancel();
+ },
+
+ /**
+ * @private
+ * Handle addition or removal of descendant items. Invalidates the cached list of fields
+ * so that {@link #getFields} will do a fresh query next time it is called. Also adds listeners
+ * for state change events on added fields, and tracks components with formBind=true.
+ */
+ onItemAddOrRemove: function(parent, child) {
+ var me = this,
+ isAdding = !!child.ownerCt,
+ isContainer = child.isContainer;
+
+ function handleField(field) {
+ // Listen for state change events on fields
+ me[isAdding ? 'mon' : 'mun'](field, {
+ validitychange: me.checkValidity,
+ dirtychange: me.checkDirty,
+ scope: me,
+ buffer: 100 //batch up sequential calls to avoid excessive full-form validation
+ });
+ // Flush the cached list of fields
+ delete me._fields;
+ }
+
+ if (child.isFormField) {
+ handleField(child);
+ } else if (isContainer) {
+ // Walk down
+ if (child.isDestroyed) {
+ // the container is destroyed, this means we may have child fields, so here
+ // we just invalidate all the fields to be sure.
+ delete me._fields;
+ } else {
+ Ext.Array.forEach(child.query('[isFormField]'), handleField);
+ }
+ }
+
+ // Flush the cached list of formBind components
+ delete this._boundItems;
+
+ // Check form bind, but only after initial add. Batch it to prevent excessive validation
+ // calls when many fields are being added at once.
+ if (me.initialized) {
+ me.checkValidityTask.delay(10);
+ }
+ },
+
+ /**
+ * Return all the {@link Ext.form.field.Field} components in the owner container.
+ * @return {Ext.util.MixedCollection} Collection of the Field objects
+ */
+ getFields: function() {
+ var fields = this._fields;
+ if (!fields) {
+ fields = this._fields = Ext.create('Ext.util.MixedCollection');
+ fields.addAll(this.owner.query('[isFormField]'));
+ }
+ return fields;
+ },
+
+ /**
+ * @private
+ * Finds and returns the set of all items bound to fields inside this form
+ * @return {Ext.util.MixedCollection} The set of all bound form field items
+ */
+ getBoundItems: function() {
+ var boundItems = this._boundItems;
+
+ if (!boundItems || boundItems.getCount() === 0) {
+ boundItems = this._boundItems = Ext.create('Ext.util.MixedCollection');
+ boundItems.addAll(this.owner.query('[formBind]'));
+ }
+
+ return boundItems;
+ },
+
+ /**
+ * Returns true if the form contains any invalid fields. No fields will be marked as invalid
+ * as a result of calling this; to trigger marking of fields use {@link #isValid} instead.
+ */
+ hasInvalidField: function() {
+ return !!this.getFields().findBy(function(field) {
+ var preventMark = field.preventMark,
+ isValid;
+ field.preventMark = true;
+ isValid = field.isValid();
+ field.preventMark = preventMark;
+ return !isValid;
+ });
+ },
+
+ /**
+ * Returns true if client-side validation on the form is successful. Any invalid fields will be
+ * marked as invalid. If you only want to determine overall form validity without marking anything,
+ * use {@link #hasInvalidField} instead.
+ * @return Boolean
+ */
+ isValid: function() {
+ var me = this,
+ invalid;
+ me.batchLayouts(function() {
+ invalid = me.getFields().filterBy(function(field) {
+ return !field.validate();
+ });
+ });
+ return invalid.length < 1;
+ },
+
+ /**
+ * Check whether the validity of the entire form has changed since it was last checked, and
+ * if so fire the {@link #validitychange validitychange} event. This is automatically invoked
+ * when an individual field's validity changes.
+ */
+ checkValidity: function() {
+ var me = this,
+ valid = !me.hasInvalidField();
+ if (valid !== me.wasValid) {
+ me.onValidityChange(valid);
+ me.fireEvent('validitychange', me, valid);
+ me.wasValid = valid;
+ }
+ },
+
+ /**
+ * @private
+ * Handle changes in the form's validity. If there are any sub components with
+ * formBind=true then they are enabled/disabled based on the new validity.
+ * @param {Boolean} valid
+ */
+ onValidityChange: function(valid) {
+ var boundItems = this.getBoundItems();
+ if (boundItems) {
+ boundItems.each(function(cmp) {
+ if (cmp.disabled === valid) {
+ cmp.setDisabled(!valid);
+ }
+ });
+ }
+ },
+
+ /**
+ * <p>Returns true if any fields in this form have changed from their original values.</p>
+ * <p>Note that if this BasicForm was configured with {@link #trackResetOnLoad} then the
+ * Fields' <em>original values</em> are updated when the values are loaded by {@link #setValues}
+ * or {@link #loadRecord}.</p>
+ * @return Boolean
+ */
+ isDirty: function() {
+ return !!this.getFields().findBy(function(f) {
+ return f.isDirty();
+ });
+ },
+
+ /**
+ * Check whether the dirty state of the entire form has changed since it was last checked, and
+ * if so fire the {@link #dirtychange dirtychange} event. This is automatically invoked
+ * when an individual field's dirty state changes.
+ */
+ checkDirty: function() {
+ var dirty = this.isDirty();
+ if (dirty !== this.wasDirty) {
+ this.fireEvent('dirtychange', this, dirty);
+ this.wasDirty = dirty;
+ }
+ },
+
+ /**
+ * <p>Returns true if the form contains a file upload field. This is used to determine the
+ * method for submitting the form: File uploads are not performed using normal 'Ajax' techniques,
+ * that is they are <b>not</b> performed using XMLHttpRequests. Instead a hidden <tt><form></tt>
+ * element containing all the fields is created temporarily and submitted with its
+ * <a href="http://www.w3.org/TR/REC-html40/present/frames.html#adef-target">target</a> set to refer
+ * to a dynamically generated, hidden <tt><iframe></tt> which is inserted into the document
+ * but removed after the return data has been gathered.</p>
+ * <p>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
+ * <a href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17">Content-Type</a> header
+ * must be set to "text/html" in order to tell the browser to insert the text unchanged into the document body.</p>
+ * <p>Characters which are significant to an HTML parser must be sent as HTML entities, so encode
+ * "<" as "&lt;", "&" as "&amp;" etc.</p>
+ * <p>The response text is retrieved from the document, and a fake XMLHttpRequest object
+ * is created containing a <tt>responseText</tt> property in order to conform to the
+ * requirements of event handlers and callbacks.</p>
+ * <p>Be aware that file upload packets are sent with the content type <a href="http://www.faqs.org/rfcs/rfc2388.html">multipart/form</a>
+ * and some server technologies (notably JEE) may require some custom processing in order to
+ * retrieve parameter names and parameter values from the packet content.</p>
+ * @return Boolean
+ */
+ hasUpload: function() {
+ return !!this.getFields().findBy(function(f) {
+ return f.isFileUpload();
+ });
+ },
+
+ /**
+ * Performs a predefined action (an implementation of {@link Ext.form.action.Action})
+ * to perform application-specific processing.
+ * @param {String/Ext.form.action.Action} action The name of the predefined action type,
+ * or instance of {@link Ext.form.action.Action} to perform.
+ * @param {Object} options (optional) The options to pass to the {@link Ext.form.action.Action}
+ * that will get created, if the <tt>action</tt> argument is a String.
+ * <p>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):</p><ul>
+ *
+ * <li><b>url</b> : String<div class="sub-desc">The url for the action (defaults
+ * to the form's {@link #url}.)</div></li>
+ *
+ * <li><b>method</b> : String<div class="sub-desc">The form method to use (defaults
+ * to the form's method, or POST if not defined)</div></li>
+ *
+ * <li><b>params</b> : String/Object<div class="sub-desc"><p>The params to pass
+ * (defaults to the form's baseParams, or none if not defined)</p>
+ * <p>Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode Ext.Object.toQueryString}.</p></div></li>
+ *
+ * <li><b>headers</b> : Object<div class="sub-desc">Request headers to set for the action.</div></li>
+ *
+ * <li><b>success</b> : Function<div class="sub-desc">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:<ul>
+ * <li><tt>form</tt> : The {@link Ext.form.Basic} that requested the action.</li>
+ * <li><tt>action</tt> : The {@link Ext.form.action.Action Action} object which performed the operation.
+ * <div class="sub-desc">The action object contains these properties of interest:<ul>
+ * <li><tt>{@link Ext.form.action.Action#response response}</tt></li>
+ * <li><tt>{@link Ext.form.action.Action#result result}</tt> : interrogate for custom postprocessing</li>
+ * <li><tt>{@link Ext.form.action.Action#type type}</tt></li>
+ * </ul></div></li></ul></div></li>
+ *
+ * <li><b>failure</b> : Function<div class="sub-desc">The callback that will be invoked after a
+ * failed transaction attempt. The function is passed the following parameters:<ul>
+ * <li><tt>form</tt> : The {@link Ext.form.Basic} that requested the action.</li>
+ * <li><tt>action</tt> : The {@link Ext.form.action.Action Action} object which performed the operation.
+ * <div class="sub-desc">The action object contains these properties of interest:<ul>
+ * <li><tt>{@link Ext.form.action.Action#failureType failureType}</tt></li>
+ * <li><tt>{@link Ext.form.action.Action#response response}</tt></li>
+ * <li><tt>{@link Ext.form.action.Action#result result}</tt> : interrogate for custom postprocessing</li>
+ * <li><tt>{@link Ext.form.action.Action#type type}</tt></li>
+ * </ul></div></li></ul></div></li>
+ *
+ * <li><b>scope</b> : Object<div class="sub-desc">The scope in which to call the
+ * callback functions (The <tt>this</tt> reference for the callback functions).</div></li>
+ *
+ * <li><b>clientValidation</b> : Boolean<div class="sub-desc">Submit Action only.
+ * Determines whether a Form's fields are validated in a final call to
+ * {@link Ext.form.Basic#isValid isValid} prior to submission. Set to <tt>false</tt>
+ * to prevent this. If undefined, pre-submission field validation is performed.</div></li></ul>
+ *
+ * @return {Ext.form.Basic} this
+ */
+ doAction: function(action, options) {
+ if (Ext.isString(action)) {
+ action = Ext.ClassManager.instantiateByAlias('formaction.' + action, Ext.apply({}, options, {form: this}));
+ }
+ if (this.fireEvent('beforeaction', this, action) !== false) {
+ this.beforeAction(action);
+ Ext.defer(action.run, 100, action);
+ }
+ return this;
+ },
+
+ /**
+ * Shortcut to {@link #doAction do} a {@link Ext.form.action.Submit submit action}. This will use the
+ * {@link Ext.form.action.Submit AJAX submit action} by default. If the {@link #standardSubmit} config is
+ * enabled it will use a standard form element to submit, or if the {@link #api} config is present it will
+ * use the {@link Ext.form.action.DirectLoad Ext.direct.Direct submit action}.
+ * @param {Object} options The options to pass to the action (see {@link #doAction} for details).<br>
+ * <p>The following code:</p><pre><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.Action.CLIENT_INVALID:
+ Ext.Msg.alert('Failure', 'Form fields may not be submitted with invalid values');
+ break;
+ case Ext.form.action.Action.CONNECT_FAILURE:
+ Ext.Msg.alert('Failure', 'Ajax communication failed');
+ break;
+ case Ext.form.action.Action.SERVER_INVALID:
+ Ext.Msg.alert('Failure', action.result.msg);
+ }
+ }
+});
+</code></pre>
+ * would process the following server response for a successful submission:<pre><code>
+{
+ "success":true, // note this is Boolean, not string
+ "msg":"Consignment updated"
+}
+</code></pre>
+ * and the following server response for a failed submission:<pre><code>
+{
+ "success":false, // note this is Boolean, not string
+ "msg":"You do not have permission to perform this operation"
+}
+</code></pre>
+ * @return {Ext.form.Basic} this
+ */
+ submit: function(options) {
+ return this.doAction(this.standardSubmit ? 'standardsubmit' : this.api ? 'directsubmit' : 'submit', options);
+ },
+
+ /**
+ * 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 {Ext.form.Basic} this
+ */
+ load: function(options) {
+ return this.doAction(this.api ? 'directload' : 'load', options);
+ },
+
+ /**
+ * Persists the values in this form into the passed {@link Ext.data.Model} object in a beginEdit/endEdit block.
+ * @param {Ext.data.Model} record The record to edit
+ * @return {Ext.form.Basic} this
+ */
+ updateRecord: function(record) {
+ var fields = record.fields,
+ values = this.getFieldValues(),
+ name,
+ obj = {};
+
+ fields.each(function(f) {
+ name = f.name;
+ if (name in values) {
+ obj[name] = values[name];
+ }
+ });
+
+ record.beginEdit();
+ record.set(obj);
+ record.endEdit();
+
+ return this;
+ },
+
+ /**
+ * Loads an {@link Ext.data.Model} into this form by calling {@link #setValues} with the
+ * {@link Ext.data.Model#raw record data}.
+ * See also {@link #trackResetOnLoad}.
+ * @param {Ext.data.Model} record The record to load
+ * @return {Ext.form.Basic} this
+ */
+ loadRecord: function(record) {
+ this._record = record;
+ return this.setValues(record.data);
+ },
+
+ /**
+ * Returns the last Ext.data.Model instance that was loaded via {@link #loadRecord}
+ * @return {Ext.data.Model} The record
+ */
+ getRecord: function() {
+ return this._record;
+ },
+
+ /**
+ * @private
+ * Called before an action is performed via {@link #doAction}.
+ * @param {Ext.form.action.Action} action The Action instance that was invoked
+ */
+ beforeAction: function(action) {
+ var waitMsg = action.waitMsg,
+ maskCls = Ext.baseCSSPrefix + 'mask-loading',
+ waitMsgTarget;
+
+ // Call HtmlEditor's syncValue before actions
+ this.getFields().each(function(f) {
+ if (f.isFormField && f.syncValue) {
+ f.syncValue();
+ }
+ });
+
+ if (waitMsg) {
+ waitMsgTarget = this.waitMsgTarget;
+ if (waitMsgTarget === true) {
+ this.owner.el.mask(waitMsg, maskCls);
+ } else if (waitMsgTarget) {
+ waitMsgTarget = this.waitMsgTarget = Ext.get(waitMsgTarget);
+ waitMsgTarget.mask(waitMsg, maskCls);
+ } else {
+ Ext.MessageBox.wait(waitMsg, action.waitTitle || this.waitTitle);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Called after an action is performed via {@link #doAction}.
+ * @param {Ext.form.action.Action} action The Action instance that was invoked
+ * @param {Boolean} success True if the action completed successfully, false, otherwise.
+ */
+ afterAction: function(action, success) {
+ if (action.waitMsg) {
+ var MessageBox = Ext.MessageBox,
+ waitMsgTarget = this.waitMsgTarget;
+ if (waitMsgTarget === true) {
+ this.owner.el.unmask();
+ } else if (waitMsgTarget) {
+ waitMsgTarget.unmask();
+ } else {
+ MessageBox.updateProgress(1);
+ MessageBox.hide();
+ }
+ }
+ if (success) {
+ if (action.reset) {
+ this.reset();
+ }
+ Ext.callback(action.success, action.scope || action, [this, action]);
+ this.fireEvent('actioncomplete', this, action);
+ } else {
+ Ext.callback(action.failure, action.scope || action, [this, action]);
+ this.fireEvent('actionfailed', this, action);
+ }
+ },
+
+
+ /**
+ * Find a specific {@link Ext.form.field.Field} in this form by id or name.
+ * @param {String} id The value to search for (specify either a {@link Ext.Component#id id} or
+ * {@link Ext.form.field.Field#getName name or hiddenName}).
+ * @return Ext.form.field.Field The first matching field, or <tt>null</tt> if none was found.
+ */
+ findField: function(id) {
+ return this.getFields().findBy(function(f) {
+ return f.id === id || f.getName() === id;
+ });
+ },
+
+
+ /**
+ * Mark fields in this form invalid in bulk.
+ * @param {Object/Object[]/Ext.data.Errors} errors
+ * Either an array in the form <code>[{id:'fieldId', msg:'The message'}, ...]</code>,
+ * an object hash of <code>{id: msg, id2: msg2}</code>, or a {@link Ext.data.Errors} object.
+ * @return {Ext.form.Basic} this
+ */
+ markInvalid: function(errors) {
+ var me = this;
+
+ function mark(fieldId, msg) {
+ var field = me.findField(fieldId);
+ if (field) {
+ field.markInvalid(msg);
+ }
+ }
+
+ if (Ext.isArray(errors)) {
+ Ext.each(errors, function(err) {
+ mark(err.id, err.msg);
+ });
+ }
+ else if (errors instanceof Ext.data.Errors) {
+ errors.each(function(err) {
+ mark(err.field, err.message);
+ });
+ }
+ else {
+ Ext.iterate(errors, mark);
+ }
+ return this;
+ },
+
+ /**
+ * Set values for fields in this form in bulk.
+ * @param {Object/Object[]} values Either an array in the form:<pre><code>
+[{id:'clientName', value:'Fred. Olsen Lines'},
+ {id:'portOfLoading', value:'FXT'},
+ {id:'portOfDischarge', value:'OSL'} ]</code></pre>
+ * or an object hash of the form:<pre><code>
+{
+ clientName: 'Fred. Olsen Lines',
+ portOfLoading: 'FXT',
+ portOfDischarge: 'OSL'
+}</code></pre>
+ * @return {Ext.form.Basic} this
+ */
+ setValues: function(values) {
+ var me = this;
+
+ function setVal(fieldId, val) {
+ var field = me.findField(fieldId);
+ if (field) {
+ field.setValue(val);
+ if (me.trackResetOnLoad) {
+ field.resetOriginalValue();
+ }
+ }
+ }
+
+ if (Ext.isArray(values)) {
+ // array of objects
+ Ext.each(values, function(val) {
+ setVal(val.id, val.value);
+ });
+ } else {
+ // object hash
+ Ext.iterate(values, setVal);
+ }
+ return this;
+ },
+
+ /**
+ * Retrieves the fields in the form as a set of key/value pairs, using their
+ * {@link Ext.form.field.Field#getSubmitData getSubmitData()} method to collect the values.
+ * If multiple fields return values under the same name those values will be combined into an Array.
+ * This is similar to {@link #getFieldValues} except that this method collects only String values for
+ * submission, while getFieldValues collects type-specific data values (e.g. Date objects for date fields.)
+ * @param {Boolean} asString (optional) If true, will return the key/value collection as a single
+ * URL-encoded param string. Defaults to false.
+ * @param {Boolean} dirtyOnly (optional) If true, only fields that are dirty will be included in the result.
+ * Defaults to false.
+ * @param {Boolean} includeEmptyText (optional) If true, the configured emptyText of empty fields will be used.
+ * Defaults to false.
+ * @return {String/Object}
+ */
+ getValues: function(asString, dirtyOnly, includeEmptyText, useDataValues) {
+ var values = {};
+
+ this.getFields().each(function(field) {
+ if (!dirtyOnly || field.isDirty()) {
+ var data = field[useDataValues ? 'getModelData' : 'getSubmitData'](includeEmptyText);
+ if (Ext.isObject(data)) {
+ Ext.iterate(data, function(name, val) {
+ if (includeEmptyText && val === '') {
+ val = field.emptyText || '';
+ }
+ if (name in values) {
+ var bucket = values[name],
+ isArray = Ext.isArray;
+ if (!isArray(bucket)) {
+ bucket = values[name] = [bucket];
+ }
+ if (isArray(val)) {
+ values[name] = bucket.concat(val);
+ } else {
+ bucket.push(val);
+ }
+ } else {
+ values[name] = val;
+ }
+ });
+ }
+ }
+ });
+
+ if (asString) {
+ values = Ext.Object.toQueryString(values);
+ }
+ return values;
+ },
+
+ /**
+ * Retrieves the fields in the form as a set of key/value pairs, using their
+ * {@link Ext.form.field.Field#getModelData getModelData()} method to collect the values.
+ * If multiple fields return values under the same name those values will be combined into an Array.
+ * This is similar to {@link #getValues} except that this method collects type-specific data values
+ * (e.g. Date objects for date fields) while getValues returns only String values for submission.
+ * @param {Boolean} dirtyOnly (optional) If true, only fields that are dirty will be included in the result.
+ * Defaults to false.
+ * @return {Object}
+ */
+ getFieldValues: function(dirtyOnly) {
+ return this.getValues(false, dirtyOnly, false, true);
+ },
+
+ /**
+ * Clears all invalid field messages in this form.
+ * @return {Ext.form.Basic} this
+ */
+ clearInvalid: function() {
+ var me = this;
+ me.batchLayouts(function() {
+ me.getFields().each(function(f) {
+ f.clearInvalid();
+ });
+ });
+ return me;
+ },
+
+ /**
+ * Resets all fields in this form.
+ * @return {Ext.form.Basic} this
+ */
+ reset: function() {
+ var me = this;
+ me.batchLayouts(function() {
+ me.getFields().each(function(f) {
+ f.reset();
+ });
+ });
+ return me;
+ },
+
+ /**
+ * Calls {@link Ext#apply Ext.apply} for all fields in this form with the passed object.
+ * @param {Object} obj The object to be applied
+ * @return {Ext.form.Basic} this
+ */
+ applyToFields: function(obj) {
+ this.getFields().each(function(f) {
+ Ext.apply(f, obj);
+ });
+ return this;
+ },
+
+ /**
+ * Calls {@link Ext#applyIf Ext.applyIf} for all field in this form with the passed object.
+ * @param {Object} obj The object to be applied
+ * @return {Ext.form.Basic} this
+ */
+ applyIfToFields: function(obj) {
+ this.getFields().each(function(f) {
+ Ext.applyIf(f, obj);
+ });
+ return this;
+ },
+
+ /**
+ * @private
+ * Utility wrapper that suspends layouts of all field parent containers for the duration of a given
+ * function. Used during full-form validation and resets to prevent huge numbers of layouts.
+ * @param {Function} fn
+ */
+ batchLayouts: function(fn) {
+ var me = this,
+ suspended = new Ext.util.HashMap();
+
+ // Temporarily suspend layout on each field's immediate owner so we don't get a huge layout cascade
+ me.getFields().each(function(field) {
+ var ownerCt = field.ownerCt;
+ if (!suspended.contains(ownerCt)) {
+ suspended.add(ownerCt);
+ ownerCt.oldSuspendLayout = ownerCt.suspendLayout;
+ ownerCt.suspendLayout = true;
+ }
+ });
+
+ // Invoke the function
+ fn();
+
+ // Un-suspend the container layouts
+ suspended.each(function(id, ct) {
+ ct.suspendLayout = ct.oldSuspendLayout;
+ delete ct.oldSuspendLayout;
+ });
+
+ // Trigger a single layout
+ me.owner.doComponentLayout();
+ }
+});
+
+/**
+ * @class Ext.form.FieldAncestor
+
+A mixin for {@link Ext.container.Container} components that are likely to have form fields in their
+items subtree. Adds the following capabilities:
+
+- Methods for handling the addition and removal of {@link Ext.form.Labelable} and {@link Ext.form.field.Field}
+ instances at any depth within the container.
+- Events ({@link #fieldvaliditychange} and {@link #fielderrorchange}) for handling changes to the state
+ of individual fields at the container level.
+- Automatic application of {@link #fieldDefaults} config properties to each field added within the
+ container, to facilitate uniform configuration of all fields.
+
+This mixin is primarily for internal use by {@link Ext.form.Panel} and {@link Ext.form.FieldContainer},
+and should not normally need to be used directly.
+
+ * @markdown
+ * @docauthor Jason Johnston <jason@sencha.com>
+ */
+Ext.define('Ext.form.FieldAncestor', {
+
+ /**
+ * @cfg {Object} fieldDefaults
+ * <p>If specified, the properties in this object are used as default config values for each
+ * {@link Ext.form.Labelable} instance (e.g. {@link Ext.form.field.Base} or {@link Ext.form.FieldContainer})
+ * that is added as a descendant of this container. Corresponding values specified in an individual field's
+ * own configuration, or from the {@link Ext.container.Container#defaults defaults config} of its parent container,
+ * will take precedence. See the documentation for {@link Ext.form.Labelable} to see what config
+ * options may be specified in the <tt>fieldDefaults</tt>.</p>
+ * <p>Example:</p>
+ * <pre><code>new Ext.form.Panel({
+ fieldDefaults: {
+ labelAlign: 'left',
+ labelWidth: 100
+ },
+ items: [{
+ xtype: 'fieldset',
+ defaults: {
+ labelAlign: 'top'
+ },
+ items: [{
+ name: 'field1'
+ }, {
+ name: 'field2'
+ }]
+ }, {
+ xtype: 'fieldset',
+ items: [{
+ name: 'field3',
+ labelWidth: 150
+ }, {
+ name: 'field4'
+ }]
+ }]
+});</code></pre>
+ * <p>In this example, field1 and field2 will get labelAlign:'top' (from the fieldset's <tt>defaults</tt>)
+ * and labelWidth:100 (from <tt>fieldDefaults</tt>), field3 and field4 will both get labelAlign:'left' (from
+ * <tt>fieldDefaults</tt> and field3 will use the labelWidth:150 from its own config.</p>
+ */
+
+
+ /**
+ * @protected Initializes the FieldAncestor's state; this must be called from the initComponent method
+ * of any components importing this mixin.
+ */
+ initFieldAncestor: function() {
+ var me = this,
+ onSubtreeChange = me.onFieldAncestorSubtreeChange;
+
+ me.addEvents(
+ /**
+ * @event fieldvaliditychange
+ * Fires when the validity state of any one of the {@link Ext.form.field.Field} instances within this
+ * container changes.
+ * @param {Ext.form.FieldAncestor} this
+ * @param {Ext.form.Labelable} The Field instance whose validity changed
+ * @param {String} isValid The field's new validity state
+ */
+ 'fieldvaliditychange',
+
+ /**
+ * @event fielderrorchange
+ * Fires when the active error message is changed for any one of the {@link Ext.form.Labelable}
+ * instances within this container.
+ * @param {Ext.form.FieldAncestor} this
+ * @param {Ext.form.Labelable} The Labelable instance whose active error was changed
+ * @param {String} error The active error message
+ */
+ 'fielderrorchange'
+ );
+
+ // Catch addition and removal of descendant fields
+ me.on('add', onSubtreeChange, me);
+ me.on('remove', onSubtreeChange, me);
+
+ me.initFieldDefaults();
+ },
+
+ /**
+ * @private Initialize the {@link #fieldDefaults} object
+ */
+ initFieldDefaults: function() {
+ if (!this.fieldDefaults) {
+ this.fieldDefaults = {};
+ }
+ },
+
+ /**
+ * @private
+ * Handle the addition and removal of components in the FieldAncestor component's child tree.
+ */
+ onFieldAncestorSubtreeChange: function(parent, child) {
+ var me = this,
+ isAdding = !!child.ownerCt;
+
+ function handleCmp(cmp) {
+ var isLabelable = cmp.isFieldLabelable,
+ isField = cmp.isFormField;
+ if (isLabelable || isField) {
+ if (isLabelable) {
+ me['onLabelable' + (isAdding ? 'Added' : 'Removed')](cmp);
+ }
+ if (isField) {
+ me['onField' + (isAdding ? 'Added' : 'Removed')](cmp);
+ }
+ }
+ else if (cmp.isContainer) {
+ Ext.Array.forEach(cmp.getRefItems(), handleCmp);
+ }
+ }
+ handleCmp(child);
+ },
+
+ /**
+ * @protected Called when a {@link Ext.form.Labelable} instance is added to the container's subtree.
+ * @param {Ext.form.Labelable} labelable The instance that was added
+ */
+ onLabelableAdded: function(labelable) {
+ var me = this;
+
+ // buffer slightly to avoid excessive firing while sub-fields are changing en masse
+ me.mon(labelable, 'errorchange', me.handleFieldErrorChange, me, {buffer: 10});
+
+ labelable.setFieldDefaults(me.fieldDefaults);
+ },
+
+ /**
+ * @protected Called when a {@link Ext.form.field.Field} instance is added to the container's subtree.
+ * @param {Ext.form.field.Field} field The field which was added
+ */
+ onFieldAdded: function(field) {
+ var me = this;
+ me.mon(field, 'validitychange', me.handleFieldValidityChange, me);
+ },
+
+ /**
+ * @protected Called when a {@link Ext.form.Labelable} instance is removed from the container's subtree.
+ * @param {Ext.form.Labelable} labelable The instance that was removed
+ */
+ onLabelableRemoved: function(labelable) {
+ var me = this;
+ me.mun(labelable, 'errorchange', me.handleFieldErrorChange, me);
+ },
+
+ /**
+ * @protected Called when a {@link Ext.form.field.Field} instance is removed from the container's subtree.
+ * @param {Ext.form.field.Field} field The field which was removed
+ */
+ onFieldRemoved: function(field) {
+ var me = this;
+ me.mun(field, 'validitychange', me.handleFieldValidityChange, me);
+ },
+
+ /**
+ * @private Handle validitychange events on sub-fields; invoke the aggregated event and method
+ */
+ handleFieldValidityChange: function(field, isValid) {
+ var me = this;
+ me.fireEvent('fieldvaliditychange', me, field, isValid);
+ me.onFieldValidityChange();
+ },
+
+ /**
+ * @private Handle errorchange events on sub-fields; invoke the aggregated event and method
+ */
+ handleFieldErrorChange: function(labelable, activeError) {
+ var me = this;
+ me.fireEvent('fielderrorchange', me, labelable, activeError);
+ me.onFieldErrorChange();
+ },
+
+ /**
+ * @protected Fired when the validity of any field within the container changes.
+ * @param {Ext.form.field.Field} The sub-field whose validity changed
+ * @param {String} The new validity state
+ */
+ onFieldValidityChange: Ext.emptyFn,
+
+ /**
+ * @protected Fired when the error message of any field within the container changes.
+ * @param {Ext.form.Labelable} The sub-field whose active error changed
+ * @param {String} The new active error message
+ */
+ onFieldErrorChange: Ext.emptyFn
+
+});
+/**
+ * @class Ext.layout.container.CheckboxGroup
+ * @extends Ext.layout.container.Container
+ * <p>This layout implements the column arrangement for {@link Ext.form.CheckboxGroup} and {@link Ext.form.RadioGroup}.
+ * It groups the component's sub-items into columns based on the component's
+ * {@link Ext.form.CheckboxGroup#columns columns} and {@link Ext.form.CheckboxGroup#vertical} config properties.</p>
+ *
+ */
+Ext.define('Ext.layout.container.CheckboxGroup', {
+ extend: 'Ext.layout.container.Container',
+ alias: ['layout.checkboxgroup'],
+
+
+ onLayout: function() {
+ var numCols = this.getColCount(),
+ shadowCt = this.getShadowCt(),
+ owner = this.owner,
+ items = owner.items,
+ shadowItems = shadowCt.items,
+ numItems = items.length,
+ colIndex = 0,
+ i, numRows;
+
+ // Distribute the items into the appropriate column containers. We add directly to the
+ // containers' items collection rather than calling container.add(), because we need the
+ // checkboxes to maintain their original ownerCt. The distribution is done on each layout
+ // in case items have been added, removed, or reordered.
+
+ shadowItems.each(function(col) {
+ col.items.clear();
+ });
+
+ // If columns="auto", then the number of required columns may change as checkboxes are added/removed
+ // from the CheckboxGroup; adjust to match.
+ while (shadowItems.length > numCols) {
+ shadowCt.remove(shadowItems.last());
+ }
+ while (shadowItems.length < numCols) {
+ shadowCt.add({
+ xtype: 'container',
+ cls: owner.groupCls,
+ flex: 1
+ });
+ }
+
+ if (owner.vertical) {
+ numRows = Math.ceil(numItems / numCols);
+ for (i = 0; i < numItems; i++) {
+ if (i > 0 && i % numRows === 0) {
+ colIndex++;
+ }
+ shadowItems.getAt(colIndex).items.add(items.getAt(i));
+ }
+ } else {
+ for (i = 0; i < numItems; i++) {
+ colIndex = i % numCols;
+ shadowItems.getAt(colIndex).items.add(items.getAt(i));
+ }
+ }
+
+ if (!shadowCt.rendered) {
+ shadowCt.render(this.getRenderTarget());
+ } else {
+ // Ensure all items are rendered in the correct place in the correct column - this won't
+ // get done by the column containers themselves if their dimensions are not changing.
+ shadowItems.each(function(col) {
+ var layout = col.getLayout();
+ layout.renderItems(layout.getLayoutItems(), layout.getRenderTarget());
+ });
+ }
+
+ shadowCt.doComponentLayout();
+ },
+
+
+ // We don't want to render any items to the owner directly, that gets handled by each column's own layout
+ renderItems: Ext.emptyFn,
+
+
+ /**
+ * @private
+ * Creates and returns the shadow hbox container that will be used to arrange the owner's items
+ * into columns.
+ */
+ getShadowCt: function() {
+ var me = this,
+ shadowCt = me.shadowCt,
+ owner, items, item, columns, columnsIsArray, numCols, i;
+
+ if (!shadowCt) {
+ // Create the column containers based on the owner's 'columns' config
+ owner = me.owner;
+ columns = owner.columns;
+ columnsIsArray = Ext.isArray(columns);
+ numCols = me.getColCount();
+ items = [];
+ for(i = 0; i < numCols; i++) {
+ item = {
+ xtype: 'container',
+ cls: owner.groupCls
+ };
+ if (columnsIsArray) {
+ // Array can contain mixture of whole numbers, used as fixed pixel widths, and fractional
+ // numbers, used as relative flex values.
+ if (columns[i] < 1) {
+ item.flex = columns[i];
+ } else {
+ item.width = columns[i];
+ }
+ }
+ else {
+ // All columns the same width
+ item.flex = 1;
+ }
+ items.push(item);
+ }
+
+ // Create the shadow container; delay rendering until after items are added to the columns
+ shadowCt = me.shadowCt = Ext.createWidget('container', {
+ layout: 'hbox',
+ items: items,
+ ownerCt: owner
+ });
+ }
+
+ return shadowCt;
+ },
+
+
+ /**
+ * @private Get the number of columns in the checkbox group
+ */
+ getColCount: function() {
+ var owner = this.owner,
+ colsCfg = owner.columns;
+ return Ext.isArray(colsCfg) ? colsCfg.length : (Ext.isNumber(colsCfg) ? colsCfg : owner.items.length);
+ }
+
+});
+
+/**
+ * FieldContainer is a derivation of {@link Ext.container.Container Container} that implements the
+ * {@link Ext.form.Labelable Labelable} mixin. This allows it to be configured so that it is rendered with
+ * a {@link #fieldLabel field label} and optional {@link #msgTarget error message} around its sub-items.
+ * This is useful for arranging a group of fields or other components within a single item in a form, so
+ * that it lines up nicely with other fields. A common use is for grouping a set of related fields under
+ * a single label in a form.
+ *
+ * The container's configured {@link #items} will be layed out within the field body area according to the
+ * configured {@link #layout} type. The default layout is `'autocontainer'`.
+ *
+ * Like regular fields, FieldContainer can inherit its decoration configuration from the
+ * {@link Ext.form.Panel#fieldDefaults fieldDefaults} of an enclosing FormPanel. In addition,
+ * FieldContainer itself can pass {@link #fieldDefaults} to any {@link Ext.form.Labelable fields}
+ * it may itself contain.
+ *
+ * If you are grouping a set of {@link Ext.form.field.Checkbox Checkbox} or {@link Ext.form.field.Radio Radio}
+ * fields in a single labeled container, consider using a {@link Ext.form.CheckboxGroup}
+ * or {@link Ext.form.RadioGroup} instead as they are specialized for handling those types.
+ *
+ * # Example
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'FieldContainer Example',
+ * width: 550,
+ * bodyPadding: 10,
+ *
+ * items: [{
+ * xtype: 'fieldcontainer',
+ * fieldLabel: 'Last Three Jobs',
+ * labelWidth: 100,
+ *
+ * // The body area will contain three text fields, arranged
+ * // horizontally, separated by draggable splitters.
+ * layout: 'hbox',
+ * items: [{
+ * xtype: 'textfield',
+ * flex: 1
+ * }, {
+ * xtype: 'splitter'
+ * }, {
+ * xtype: 'textfield',
+ * flex: 1
+ * }, {
+ * xtype: 'splitter'
+ * }, {
+ * xtype: 'textfield',
+ * flex: 1
+ * }]
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * # Usage of fieldDefaults
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'FieldContainer Example',
+ * width: 350,
+ * bodyPadding: 10,
+ *
+ * items: [{
+ * xtype: 'fieldcontainer',
+ * fieldLabel: 'Your Name',
+ * labelWidth: 75,
+ * defaultType: 'textfield',
+ *
+ * // Arrange fields vertically, stretched to full width
+ * layout: 'anchor',
+ * defaults: {
+ * layout: '100%'
+ * },
+ *
+ * // These config values will be applied to both sub-fields, except
+ * // for Last Name which will use its own msgTarget.
+ * fieldDefaults: {
+ * msgTarget: 'under',
+ * labelAlign: 'top'
+ * },
+ *
+ * items: [{
+ * fieldLabel: 'First Name',
+ * name: 'firstName'
+ * }, {
+ * fieldLabel: 'Last Name',
+ * name: 'lastName',
+ * msgTarget: 'under'
+ * }]
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * @docauthor Jason Johnston <jason@sencha.com>
+ */
+Ext.define('Ext.form.FieldContainer', {
+ extend: 'Ext.container.Container',
+ mixins: {
+ labelable: 'Ext.form.Labelable',
+ fieldAncestor: 'Ext.form.FieldAncestor'
+ },
+ alias: 'widget.fieldcontainer',
+
+ componentLayout: 'field',
+
+ /**
+ * @cfg {Boolean} combineLabels
+ * If set to true, and there is no defined {@link #fieldLabel}, the field container will automatically
+ * generate its label by combining the labels of all the fields it contains. Defaults to false.
+ */
+ combineLabels: false,
+
+ /**
+ * @cfg {String} labelConnector
+ * The string to use when joining the labels of individual sub-fields, when {@link #combineLabels} is
+ * set to true. Defaults to ', '.
+ */
+ labelConnector: ', ',
+
+ /**
+ * @cfg {Boolean} combineErrors
+ * If set to true, the field container will automatically combine and display the validation errors from
+ * all the fields it contains as a single error on the container, according to the configured
+ * {@link #msgTarget}. Defaults to false.
+ */
+ combineErrors: false,
+
+ maskOnDisable: false,
+
+ initComponent: function() {
+ var me = this,
+ onSubCmpAddOrRemove = me.onSubCmpAddOrRemove;
+
+ // Init mixins
+ me.initLabelable();
+ me.initFieldAncestor();
+
+ me.callParent();
+ },
+
+ /**
+ * @protected Called when a {@link Ext.form.Labelable} instance is added to the container's subtree.
+ * @param {Ext.form.Labelable} labelable The instance that was added
+ */
+ onLabelableAdded: function(labelable) {
+ var me = this;
+ me.mixins.fieldAncestor.onLabelableAdded.call(this, labelable);
+ me.updateLabel();
+ },
+
+ /**
+ * @protected Called when a {@link Ext.form.Labelable} instance is removed from the container's subtree.
+ * @param {Ext.form.Labelable} labelable The instance that was removed
+ */
+ onLabelableRemoved: function(labelable) {
+ var me = this;
+ me.mixins.fieldAncestor.onLabelableRemoved.call(this, labelable);
+ me.updateLabel();
+ },
+
+ onRender: function() {
+ var me = this;
+
+ me.onLabelableRender();
+
+ me.callParent(arguments);
+ },
+
+ initRenderTpl: function() {
+ var me = this;
+ if (!me.hasOwnProperty('renderTpl')) {
+ me.renderTpl = me.getTpl('labelableRenderTpl');
+ }
+ return me.callParent();
+ },
+
+ initRenderData: function() {
+ return Ext.applyIf(this.callParent(), this.getLabelableRenderData());
+ },
+
+ /**
+ * Returns the combined field label if {@link #combineLabels} is set to true and if there is no
+ * set {@link #fieldLabel}. Otherwise returns the fieldLabel like normal. You can also override
+ * this method to provide a custom generated label.
+ */
+ getFieldLabel: function() {
+ var label = this.fieldLabel || '';
+ if (!label && this.combineLabels) {
+ label = Ext.Array.map(this.query('[isFieldLabelable]'), function(field) {
+ return field.getFieldLabel();
+ }).join(this.labelConnector);
+ }
+ return label;
+ },
+
+ /**
+ * @private Updates the content of the labelEl if it is rendered
+ */
+ updateLabel: function() {
+ var me = this,
+ label = me.labelEl;
+ if (label) {
+ label.update(me.getFieldLabel());
+ }
+ },
+
+
+ /**
+ * @private Fired when the error message of any field within the container changes, and updates the
+ * combined error message to match.
+ */
+ onFieldErrorChange: function(field, activeError) {
+ if (this.combineErrors) {
+ var me = this,
+ oldError = me.getActiveError(),
+ invalidFields = Ext.Array.filter(me.query('[isFormField]'), function(field) {
+ return field.hasActiveError();
+ }),
+ newErrors = me.getCombinedErrors(invalidFields);
+
+ if (newErrors) {
+ me.setActiveErrors(newErrors);
+ } else {
+ me.unsetActiveError();
+ }
+
+ if (oldError !== me.getActiveError()) {
+ me.doComponentLayout();
+ }
+ }
+ },
+
+ /**
+ * Takes an Array of invalid {@link Ext.form.field.Field} objects and builds a combined list of error
+ * messages from them. Defaults to prepending each message by the field name and a colon. This
+ * can be overridden to provide custom combined error message handling, for instance changing
+ * the format of each message or sorting the array (it is sorted in order of appearance by default).
+ * @param {Ext.form.field.Field[]} invalidFields An Array of the sub-fields which are currently invalid.
+ * @return {String[]} The combined list of error messages
+ */
+ getCombinedErrors: function(invalidFields) {
+ var forEach = Ext.Array.forEach,
+ errors = [];
+ forEach(invalidFields, function(field) {
+ forEach(field.getActiveErrors(), function(error) {
+ var label = field.getFieldLabel();
+ errors.push((label ? label + ': ' : '') + error);
+ });
+ });
+ return errors;
+ },
+
+ getTargetEl: function() {
+ return this.bodyEl || this.callParent();
+ }
+});
+
+/**
+ * A {@link Ext.form.FieldContainer field container} which has a specialized layout for arranging
+ * {@link Ext.form.field.Checkbox} controls into columns, and provides convenience
+ * {@link Ext.form.field.Field} methods for {@link #getValue getting}, {@link #setValue setting},
+ * and {@link #validate validating} the group of checkboxes as a whole.
+ *
+ * # Validation
+ *
+ * Individual checkbox fields themselves have no default validation behavior, but
+ * sometimes you want to require a user to select at least one of a group of checkboxes. CheckboxGroup
+ * allows this by setting the config `{@link #allowBlank}:false`; when the user does not check at
+ * least one of the checkboxes, the entire group will be highlighted as invalid and the
+ * {@link #blankText error message} will be displayed according to the {@link #msgTarget} config.
+ *
+ * # Layout
+ *
+ * The default layout for CheckboxGroup makes it easy to arrange the checkboxes into
+ * columns; see the {@link #columns} and {@link #vertical} config documentation for details. You may also
+ * use a completely different layout by setting the {@link #layout} to one of the other supported layout
+ * types; for instance you may wish to use a custom arrangement of hbox and vbox containers. In that case
+ * the checkbox components at any depth will still be managed by the CheckboxGroup's validation.
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'Checkbox Group',
+ * width: 300,
+ * height: 125,
+ * bodyPadding: 10,
+ * renderTo: Ext.getBody(),
+ * items:[{
+ * xtype: 'checkboxgroup',
+ * fieldLabel: 'Two Columns',
+ * // Arrange radio buttons into two columns, distributed vertically
+ * columns: 2,
+ * vertical: true,
+ * items: [
+ * { boxLabel: 'Item 1', name: 'rb', inputValue: '1' },
+ * { boxLabel: 'Item 2', name: 'rb', inputValue: '2', checked: true },
+ * { boxLabel: 'Item 3', name: 'rb', inputValue: '3' },
+ * { boxLabel: 'Item 4', name: 'rb', inputValue: '4' },
+ * { boxLabel: 'Item 5', name: 'rb', inputValue: '5' },
+ * { boxLabel: 'Item 6', name: 'rb', inputValue: '6' }
+ * ]
+ * }]
+ * });
+ */
+Ext.define('Ext.form.CheckboxGroup', {
+ extend:'Ext.form.FieldContainer',
+ mixins: {
+ field: 'Ext.form.field.Field'
+ },
+ alias: 'widget.checkboxgroup',
+ requires: ['Ext.layout.container.CheckboxGroup', 'Ext.form.field.Base'],
+
+ /**
+ * @cfg {String} name
+ * @hide
+ */
+
+ /**
+ * @cfg {Ext.form.field.Checkbox[]/Object[]} items
+ * An Array of {@link Ext.form.field.Checkbox Checkbox}es or Checkbox config objects to arrange in the group.
+ */
+
+ /**
+ * @cfg {String/Number/Number[]} 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 - 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. 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
+ */
+ blankText : "You must select at least one item in this group",
+
+ // private
+ defaultType : 'checkboxfield',
+
+ // private
+ groupCls : Ext.baseCSSPrefix + 'form-check-group',
+
+ /**
+ * @cfg {String} fieldBodyCls
+ * An extra CSS class to be applied to the body content element in addition to {@link #baseBodyCls}.
+ * Defaults to 'x-form-checkboxgroup-body'.
+ */
+ fieldBodyCls: Ext.baseCSSPrefix + 'form-checkboxgroup-body',
+
+ // private
+ layout: 'checkboxgroup',
+
+ initComponent: function() {
+ var me = this;
+ me.callParent();
+ me.initField();
+ },
+
+ /**
+ * Initializes the field's value based on the initial config. If the {@link #value} config is specified then we use
+ * that to set the value; otherwise we initialize the originalValue by querying the values of all sub-checkboxes
+ * after they have been initialized.
+ * @protected
+ */
+ initValue: function() {
+ var me = this,
+ valueCfg = me.value;
+ me.originalValue = me.lastValue = valueCfg || me.getValue();
+ if (valueCfg) {
+ me.setValue(valueCfg);
+ }
+ },
+
+ /**
+ * When a checkbox is added to the group, monitor it for changes
+ * @param {Object} field
+ * @protected
+ */
+ onFieldAdded: function(field) {
+ var me = this;
+ if (field.isCheckbox) {
+ me.mon(field, 'change', me.checkChange, me);
+ }
+ me.callParent(arguments);
+ },
+
+ onFieldRemoved: function(field) {
+ var me = this;
+ if (field.isCheckbox) {
+ me.mun(field, 'change', me.checkChange, me);
+ }
+ me.callParent(arguments);
+ },
+
+ // private override - the group value is a complex object, compare using object serialization
+ isEqual: function(value1, value2) {
+ var toQueryString = Ext.Object.toQueryString;
+ return toQueryString(value1) === toQueryString(value2);
+ },
+
+ /**
+ * Runs CheckboxGroup's validations and returns an array of any errors. The only error by default is if allowBlank
+ * is set to true and no items are checked.
+ * @return {String[]} Array of all validation errors
+ */
+ getErrors: function() {
+ var errors = [];
+ if (!this.allowBlank && Ext.isEmpty(this.getChecked())) {
+ errors.push(this.blankText);
+ }
+ return errors;
+ },
+
+ /**
+ * @private Returns all checkbox components within the container
+ */
+ getBoxes: function() {
+ return this.query('[isCheckbox]');
+ },
+
+ /**
+ * @private Convenience function which calls the given function for every checkbox in the group
+ * @param {Function} fn The function to call
+ * @param {Object} scope (Optional) scope object
+ */
+ eachBox: function(fn, scope) {
+ Ext.Array.forEach(this.getBoxes(), fn, scope || this);
+ },
+
+ /**
+ * Returns an Array of all checkboxes in the container which are currently checked
+ * @return {Ext.form.field.Checkbox[]} Array of Ext.form.field.Checkbox components
+ */
+ getChecked: function() {
+ return Ext.Array.filter(this.getBoxes(), function(cb) {
+ return cb.getValue();
+ });
+ },
+
+ // private override
+ isDirty: function(){
+ return Ext.Array.some(this.getBoxes(), function(cb) {
+ return cb.isDirty();
+ });
+ },
+
+ // private override
+ setReadOnly: function(readOnly) {
+ this.eachBox(function(cb) {
+ cb.setReadOnly(readOnly);
+ });
+ this.readOnly = readOnly;
+ },
+
+ /**
+ * Resets the checked state of all {@link Ext.form.field.Checkbox checkboxes} in the group to their originally
+ * loaded values and clears any validation messages.
+ * See {@link Ext.form.Basic}.{@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad}
+ */
+ reset: function() {
+ var me = this,
+ hadError = me.hasActiveError(),
+ preventMark = me.preventMark;
+ me.preventMark = true;
+ me.batchChanges(function() {
+ me.eachBox(function(cb) {
+ cb.reset();
+ });
+ });
+ me.preventMark = preventMark;
+ me.unsetActiveError();
+ if (hadError) {
+ me.doComponentLayout();
+ }
+ },
+
+ // private override
+ resetOriginalValue: function() {
+ // Defer resetting of originalValue until after all sub-checkboxes have been reset so we get
+ // the correct data from getValue()
+ Ext.defer(function() {
+ this.callParent();
+ }, 1, this);
+ },
+
+
+ /**
+ * Sets the value(s) of all checkboxes in the group. The expected format is an Object of name-value pairs
+ * corresponding to the names of the checkboxes in the group. Each pair can have either a single or multiple values:
+ *
+ * - A single Boolean or String value will be passed to the `setValue` method of the checkbox with that name.
+ * See the rules in {@link Ext.form.field.Checkbox#setValue} for accepted values.
+ * - An Array of String values will be matched against the {@link Ext.form.field.Checkbox#inputValue inputValue}
+ * of checkboxes in the group with that name; those checkboxes whose inputValue exists in the array will be
+ * checked and others will be unchecked.
+ *
+ * If a checkbox's name is not in the mapping at all, it will be unchecked.
+ *
+ * An example:
+ *
+ * var myCheckboxGroup = new Ext.form.CheckboxGroup({
+ * columns: 3,
+ * items: [{
+ * name: 'cb1',
+ * boxLabel: 'Single 1'
+ * }, {
+ * name: 'cb2',
+ * boxLabel: 'Single 2'
+ * }, {
+ * name: 'cb3',
+ * boxLabel: 'Single 3'
+ * }, {
+ * name: 'cbGroup',
+ * boxLabel: 'Grouped 1'
+ * inputValue: 'value1'
+ * }, {
+ * name: 'cbGroup',
+ * boxLabel: 'Grouped 2'
+ * inputValue: 'value2'
+ * }, {
+ * name: 'cbGroup',
+ * boxLabel: 'Grouped 3'
+ * inputValue: 'value3'
+ * }]
+ * });
+ *
+ * myCheckboxGroup.setValue({
+ * cb1: true,
+ * cb3: false,
+ * cbGroup: ['value1', 'value3']
+ * });
+ *
+ * The above code will cause the checkbox named 'cb1' to be checked, as well as the first and third checkboxes named
+ * 'cbGroup'. The other three checkboxes will be unchecked.
+ *
+ * @param {Object} value The mapping of checkbox names to values.
+ * @return {Ext.form.CheckboxGroup} this
+ */
+ setValue: function(value) {
+ var me = this;
+ me.batchChanges(function() {
+ me.eachBox(function(cb) {
+ var name = cb.getName(),
+ cbValue = false;
+ if (value && name in value) {
+ if (Ext.isArray(value[name])) {
+ cbValue = Ext.Array.contains(value[name], cb.inputValue);
+ } else {
+ // single value, let the checkbox's own setValue handle conversion
+ cbValue = value[name];
+ }
+ }
+ cb.setValue(cbValue);
+ });
+ });
+ return me;
+ },
+
+
+ /**
+ * Returns an object containing the values of all checked checkboxes within the group. Each key-value pair in the
+ * object corresponds to a checkbox {@link Ext.form.field.Checkbox#name name}. If there is only one checked checkbox
+ * with a particular name, the value of that pair will be the String {@link Ext.form.field.Checkbox#inputValue
+ * inputValue} of that checkbox. If there are multiple checked checkboxes with that name, the value of that pair
+ * will be an Array of the selected inputValues.
+ *
+ * The object format returned from this method can also be passed directly to the {@link #setValue} method.
+ *
+ * NOTE: In Ext 3, this method returned an array of Checkbox components; this was changed to make it more consistent
+ * with other field components and with the {@link #setValue} argument signature. If you need the old behavior in
+ * Ext 4+, use the {@link #getChecked} method instead.
+ */
+ getValue: function() {
+ var values = {};
+ this.eachBox(function(cb) {
+ var name = cb.getName(),
+ inputValue = cb.inputValue,
+ bucket;
+ if (cb.getValue()) {
+ if (name in values) {
+ bucket = values[name];
+ if (!Ext.isArray(bucket)) {
+ bucket = values[name] = [bucket];
+ }
+ bucket.push(inputValue);
+ } else {
+ values[name] = inputValue;
+ }
+ }
+ });
+ return values;
+ },
+
+ /*
+ * Don't return any data for submit; the form will get the info from the individual checkboxes themselves.
+ */
+ getSubmitData: function() {
+ return null;
+ },
+
+ /*
+ * Don't return any data for the model; the form will get the info from the individual checkboxes themselves.
+ */
+ getModelData: function() {
+ return null;
+ },
+
+ validate: function() {
+ var me = this,
+ errors = me.getErrors(),
+ isValid = Ext.isEmpty(errors),
+ wasValid = !me.hasActiveError();
+
+ if (isValid) {
+ me.unsetActiveError();
+ } else {
+ me.setActiveError(errors);
+ }
+ if (isValid !== wasValid) {
+ me.fireEvent('validitychange', me, isValid);
+ me.doComponentLayout();
+ }
+
+ return isValid;
+ }
+
+}, function() {
+
+ this.borrow(Ext.form.field.Base, ['markInvalid', 'clearInvalid']);
+
+});
+
+
+/**
+ * @private
+ * Private utility class for managing all {@link Ext.form.field.Checkbox} fields grouped by name.
+ */
+Ext.define('Ext.form.CheckboxManager', {
+ extend: 'Ext.util.MixedCollection',
+ singleton: true,
+
+ getByName: function(name) {
+ return this.filterBy(function(item) {
+ return item.name == name;
+ });
+ },
+
+ getWithValue: function(name, value) {
+ return this.filterBy(function(item) {
+ return item.name == name && item.inputValue == value;
+ });
+ },
+
+ getChecked: function(name) {
+ return this.filterBy(function(item) {
+ return item.name == name && item.checked;
+ });
+ }
+});
+
+/**
+ * @docauthor Jason Johnston <jason@sencha.com>
+ *
+ * A container for grouping sets of fields, rendered as a HTML `fieldset` element. The {@link #title}
+ * config will be rendered as the fieldset's `legend`.
+ *
+ * While FieldSets commonly contain simple groups of fields, they are general {@link Ext.container.Container Containers}
+ * and may therefore contain any type of components in their {@link #items}, including other nested containers.
+ * The default {@link #layout} for the FieldSet's items is `'anchor'`, but it can be configured to use any other
+ * layout type.
+ *
+ * FieldSets may also be collapsed if configured to do so; this can be done in two ways:
+ *
+ * 1. Set the {@link #collapsible} config to true; this will result in a collapse button being rendered next to
+ * the {@link #title legend title}, or:
+ * 2. Set the {@link #checkboxToggle} config to true; this is similar to using {@link #collapsible} but renders
+ * a {@link Ext.form.field.Checkbox checkbox} in place of the toggle button. The fieldset will be expanded when the
+ * checkbox is checked and collapsed when it is unchecked. The checkbox will also be included in the
+ * {@link Ext.form.Basic#submit form submit parameters} using the {@link #checkboxName} as its parameter name.
+ *
+ * # Example usage
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * 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: 550,
+ * renderTo: Ext.getBody(),
+ * layout: 'column', // arrange fieldsets side by side
+ * defaults: {
+ * bodyPadding: 4
+ * },
+ * items: [{
+ * // Fieldset in Column 1 - collapsible via toggle button
+ * xtype:'fieldset',
+ * columnWidth: 0.5,
+ * title: 'Fieldset 1',
+ * collapsible: true,
+ * defaultType: 'textfield',
+ * defaults: {anchor: '100%'},
+ * layout: 'anchor',
+ * items :[{
+ * fieldLabel: 'Field 1',
+ * name: 'field1'
+ * }, {
+ * fieldLabel: 'Field 2',
+ * name: 'field2'
+ * }]
+ * }, {
+ * // Fieldset in Column 2 - collapsible via checkbox, collapsed by default, contains a panel
+ * xtype:'fieldset',
+ * title: 'Show Panel', // title or checkboxToggle creates fieldset header
+ * 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: 52
+ * }]
+ * }]
+ * });
+ */
+Ext.define('Ext.form.FieldSet', {
+ extend: 'Ext.container.Container',
+ alias: 'widget.fieldset',
+ uses: ['Ext.form.field.Checkbox', 'Ext.panel.Tool', 'Ext.layout.container.Anchor', 'Ext.layout.component.FieldSet'],
+
+ /**
+ * @cfg {String} title
+ * A title to be displayed in the fieldset's legend. May contain HTML markup.
+ */
+
+ /**
+ * @cfg {Boolean} [checkboxToggle=false]
+ * Set to 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.. This checkbox will be included in form submits using
+ * the {@link #checkboxName}.
+ */
+
+ /**
+ * @cfg {String} checkboxName
+ * The name to assign to the fieldset's checkbox if {@link #checkboxToggle} = true
+ * (defaults to '[fieldset id]-checkbox').
+ */
+
+ /**
+ * @cfg {Boolean} [collapsible=false]
+ * Set to 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.
+ * Another option is to configure {@link #checkboxToggle}. Use the {@link #collapsed} config to collapse the
+ * fieldset by default.
+ */
+
+ /**
+ * @cfg {Boolean} collapsed
+ * Set to true to render the fieldset as collapsed by default. If {@link #checkboxToggle} is specified, the checkbox
+ * will also be unchecked by default.
+ */
+ collapsed: false,
+
+ /**
+ * @property {Ext.Component} legend
+ * The component for the fieldset's legend. Will only be defined if the configuration requires a legend to be
+ * created, by setting the {@link #title} or {@link #checkboxToggle} options.
+ */
+
+ /**
+ * @cfg {String} [baseCls='x-fieldset']
+ * The base CSS class applied to the fieldset.
+ */
+ baseCls: Ext.baseCSSPrefix + 'fieldset',
+
+ /**
+ * @cfg {String} layout
+ * The {@link Ext.container.Container#layout} for the fieldset's immediate child items.
+ */
+ layout: 'anchor',
+
+ componentLayout: 'fieldset',
+
+ // No aria role necessary as fieldset has its own recognized semantics
+ ariaRole: '',
+
+ renderTpl: ['<div id="{id}-body" class="{baseCls}-body"></div>'],
+
+ maskOnDisable: false,
+
+ getElConfig: function(){
+ return {tag: 'fieldset', id: this.id};
+ },
+
+ initComponent: function() {
+ var me = this,
+ baseCls = me.baseCls;
+
+ me.callParent();
+
+ // Create the Legend component if needed
+ me.initLegend();
+
+ // Add body el
+ me.addChildEls('body');
+
+ if (me.collapsed) {
+ me.addCls(baseCls + '-collapsed');
+ me.collapse();
+ }
+ },
+
+ // private
+ onRender: function(container, position) {
+ this.callParent(arguments);
+ // Make sure the legend is created and rendered
+ this.initLegend();
+ },
+
+ /**
+ * @private
+ * Initialize and render the legend component if necessary
+ */
+ initLegend: function() {
+ var me = this,
+ legendItems,
+ legend = me.legend;
+
+ // Create the legend component if needed and it hasn't been already
+ if (!legend && (me.title || me.checkboxToggle || me.collapsible)) {
+ legendItems = [];
+
+ // Checkbox
+ if (me.checkboxToggle) {
+ legendItems.push(me.createCheckboxCmp());
+ }
+ // Toggle button
+ else if (me.collapsible) {
+ legendItems.push(me.createToggleCmp());
+ }
+
+ // Title
+ legendItems.push(me.createTitleCmp());
+
+ legend = me.legend = Ext.create('Ext.container.Container', {
+ baseCls: me.baseCls + '-header',
+ ariaRole: '',
+ ownerCt: this,
+ getElConfig: function(){
+ var result = {
+ tag: 'legend',
+ cls: this.baseCls
+ };
+
+ // Gecko3 will kick every <div> out of <legend> and mess up every thing.
+ // So here we change every <div> into <span>s. Therefore the following
+ // clearer is not needed and since div introduces a lot of subsequent
+ // problems, it is actually harmful.
+ if (!Ext.isGecko3) {
+ result.children = [{
+ cls: Ext.baseCSSPrefix + 'clear'
+ }];
+ }
+ return result;
+ },
+ items: legendItems
+ });
+ }
+
+ // Make sure legend is rendered if the fieldset is rendered
+ if (legend && !legend.rendered && me.rendered) {
+ me.legend.render(me.el, me.body); //insert before body element
+ }
+ },
+
+ /**
+ * Creates the legend title component. This is only called internally, but could be overridden in subclasses to
+ * customize the title component.
+ * @return Ext.Component
+ * @protected
+ */
+ createTitleCmp: function() {
+ var me = this;
+ me.titleCmp = Ext.create('Ext.Component', {
+ html: me.title,
+ getElConfig: function() {
+ return {
+ tag: Ext.isGecko3 ? 'span' : 'div',
+ cls: me.titleCmp.cls,
+ id: me.titleCmp.id
+ };
+ },
+ cls: me.baseCls + '-header-text'
+ });
+ return me.titleCmp;
+ },
+
+ /**
+ * @property {Ext.form.field.Checkbox} checkboxCmp
+ * Refers to the {@link Ext.form.field.Checkbox} component that is added next to the title in the legend. Only
+ * populated if the fieldset is configured with {@link #checkboxToggle}:true.
+ */
+
+ /**
+ * Creates the checkbox component. This is only called internally, but could be overridden in subclasses to
+ * customize the checkbox's configuration or even return an entirely different component type.
+ * @return Ext.Component
+ * @protected
+ */
+ createCheckboxCmp: function() {
+ var me = this,
+ suffix = '-checkbox';
+
+ me.checkboxCmp = Ext.create('Ext.form.field.Checkbox', {
+ getElConfig: function() {
+ return {
+ tag: Ext.isGecko3 ? 'span' : 'div',
+ id: me.checkboxCmp.id,
+ cls: me.checkboxCmp.cls
+ };
+ },
+ name: me.checkboxName || me.id + suffix,
+ cls: me.baseCls + '-header' + suffix,
+ checked: !me.collapsed,
+ listeners: {
+ change: me.onCheckChange,
+ scope: me
+ }
+ });
+ return me.checkboxCmp;
+ },
+
+ /**
+ * @property {Ext.panel.Tool} toggleCmp
+ * Refers to the {@link Ext.panel.Tool} component that is added as the collapse/expand button next to the title in
+ * the legend. Only populated if the fieldset is configured with {@link #collapsible}:true.
+ */
+
+ /**
+ * Creates the toggle button component. This is only called internally, but could be overridden in subclasses to
+ * customize the toggle component.
+ * @return Ext.Component
+ * @protected
+ */
+ createToggleCmp: function() {
+ var me = this;
+ me.toggleCmp = Ext.create('Ext.panel.Tool', {
+ getElConfig: function() {
+ return {
+ tag: Ext.isGecko3 ? 'span' : 'div',
+ id: me.toggleCmp.id,
+ cls: me.toggleCmp.cls
+ };
+ },
+ type: 'toggle',
+ handler: me.toggle,
+ scope: me
+ });
+ return me.toggleCmp;
+ },
+
+ /**
+ * Sets the title of this fieldset
+ * @param {String} title The new title
+ * @return {Ext.form.FieldSet} this
+ */
+ setTitle: function(title) {
+ var me = this;
+ me.title = title;
+ me.initLegend();
+ me.titleCmp.update(title);
+ return me;
+ },
+
+ getTargetEl : function() {
+ return this.body || this.frameBody || this.el;
+ },
+
+ getContentTarget: function() {
+ return this.body;
+ },
+
+ /**
+ * @private
+ * Include the legend component in the items for ComponentQuery
+ */
+ getRefItems: function(deep) {
+ var refItems = this.callParent(arguments),
+ legend = this.legend;
+
+ // Prepend legend items to ensure correct order
+ if (legend) {
+ refItems.unshift(legend);
+ if (deep) {
+ refItems.unshift.apply(refItems, legend.getRefItems(true));
+ }
+ }
+ return refItems;
+ },
+
+ /**
+ * Expands the fieldset.
+ * @return {Ext.form.FieldSet} this
+ */
+ expand : function(){
+ return this.setExpanded(true);
+ },
+
+ /**
+ * Collapses the fieldset.
+ * @return {Ext.form.FieldSet} this
+ */
+ collapse : function() {
+ return this.setExpanded(false);
+ },
+
+ /**
+ * @private Collapse or expand the fieldset
+ */
+ setExpanded: function(expanded) {
+ var me = this,
+ checkboxCmp = me.checkboxCmp;
+
+ expanded = !!expanded;
+
+ if (checkboxCmp) {
+ checkboxCmp.setValue(expanded);
+ }
+
+ if (expanded) {
+ me.removeCls(me.baseCls + '-collapsed');
+ } else {
+ me.addCls(me.baseCls + '-collapsed');
+ }
+ me.collapsed = !expanded;
+ if (expanded) {
+ // ensure subitems will get rendered and layed out when expanding
+ me.getComponentLayout().childrenChanged = true;
+ }
+ me.doComponentLayout();
+ return me;
+ },
+
+ /**
+ * Toggle the fieldset's collapsed state to the opposite of what it is currently
+ */
+ toggle: function() {
+ this.setExpanded(!!this.collapsed);
+ },
+
+ /**
+ * @private
+ * Handle changes in the checkbox checked state
+ */
+ onCheckChange: function(cmp, checked) {
+ this.setExpanded(checked);
+ },
+
+ beforeDestroy : function() {
+ var legend = this.legend;
+ if (legend) {
+ legend.destroy();
+ }
+ this.callParent();
+ }
+});
+
+/**
+ * @docauthor Jason Johnston <jason@sencha.com>
+ *
+ * Produces a standalone `<label />` element which can be inserted into a form and be associated with a field
+ * in that form using the {@link #forId} property.
+ *
+ * **NOTE:** in most cases it will be more appropriate to use the {@link Ext.form.Labelable#fieldLabel fieldLabel}
+ * and associated config properties ({@link Ext.form.Labelable#labelAlign}, {@link Ext.form.Labelable#labelWidth},
+ * etc.) in field components themselves, as that allows labels to be uniformly sized throughout the form.
+ * Ext.form.Label should only be used when your layout can not be achieved with the standard
+ * {@link Ext.form.Labelable field layout}.
+ *
+ * You will likely be associating the label with a field component that extends {@link Ext.form.field.Base}, so
+ * you should make sure the {@link #forId} is set to the same value as the {@link Ext.form.field.Base#inputId inputId}
+ * of that field.
+ *
+ * The label's text can be set using either the {@link #text} or {@link #html} configuration properties; the
+ * difference between the two is that the former will automatically escape HTML characters when rendering, while
+ * the latter will not.
+ *
+ * # Example
+ *
+ * This example creates a Label after its associated Text field, an arrangement that cannot currently
+ * be achieved using the standard Field layout's labelAlign.
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'Field with Label',
+ * width: 400,
+ * bodyPadding: 10,
+ * renderTo: Ext.getBody(),
+ * layout: {
+ * type: 'hbox',
+ * align: 'middle'
+ * },
+ * items: [{
+ * xtype: 'textfield',
+ * hideLabel: true,
+ * flex: 1
+ * }, {
+ * xtype: 'label',
+ * forId: 'myFieldId',
+ * text: 'My Awesome Field',
+ * margins: '0 0 0 10'
+ * }]
+ * });
+ */
+Ext.define('Ext.form.Label', {
+ extend:'Ext.Component',
+ alias: 'widget.label',
+ requires: ['Ext.util.Format'],
+
+ /**
+ * @cfg {String} [text='']
+ * The plain text to display within the label. If you need to include HTML
+ * tags within the label's innerHTML, use the {@link #html} config instead.
+ */
+ /**
+ * @cfg {String} forId
+ * The id of the input element to which this label will be bound via the standard HTML 'for'
+ * attribute. If not specified, the attribute will not be added to the label. In most cases you will be
+ * associating the label with a {@link Ext.form.field.Base} component, so you should make sure this matches
+ * the {@link Ext.form.field.Base#inputId inputId} of that field.
+ */
+ /**
+ * @cfg {String} [html='']
+ * An HTML fragment that will be used as the label's innerHTML.
+ * Note that if {@link #text} is specified it will take precedence and this value will be ignored.
+ */
+
+ maskOnDisable: false,
+ getElConfig: function(){
+ var me = this;
+ return {
+ tag: 'label',
+ id: me.id,
+ htmlFor: me.forId || '',
+ html: me.text ? Ext.util.Format.htmlEncode(me.text) : (me.html || '')
+ };
+ },
+
+ /**
+ * Updates the label's innerHTML with the specified string.
+ * @param {String} text The new label text
+ * @param {Boolean} [encode=true] False to skip HTML-encoding the text when rendering it
+ * to the label. 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 {Ext.form.Label} this
+ */
+ setText : function(text, encode){
+ var me = this;
+
+ encode = encode !== false;
+ if(encode) {
+ me.text = text;
+ delete me.html;
+ } else {
+ me.html = text;
+ delete me.text;
+ }
+
+ if(me.rendered){
+ me.el.dom.innerHTML = encode !== false ? Ext.util.Format.htmlEncode(text) : text;
+ }
+ return this;
+ }
+});
+
+
+/**
+ * @docauthor Jason Johnston <jason@sencha.com>
+ *
+ * FormPanel provides a standard container for forms. It is essentially a standard {@link Ext.panel.Panel} which
+ * automatically creates a {@link Ext.form.Basic BasicForm} for managing any {@link Ext.form.field.Field}
+ * objects that are added as descendants of the panel. It also includes conveniences for configuring and
+ * working with the BasicForm and the collection of Fields.
+ *
+ * # Layout
+ *
+ * By default, FormPanel is configured with `{@link Ext.layout.container.Anchor layout:'anchor'}` for
+ * the layout of its immediate child items. This can be changed to any of the supported container layouts.
+ * The layout of sub-containers is configured in {@link Ext.container.Container#layout the standard way}.
+ *
+ * # BasicForm
+ *
+ * Although **not listed** as configuration options of FormPanel, the FormPanel class accepts all
+ * of the config options supported by the {@link Ext.form.Basic} class, and will pass them along to
+ * the internal BasicForm when it is created.
+ *
+ * **Note**: If subclassing FormPanel, any configuration options for the BasicForm must be applied to
+ * the `initialConfig` property of the FormPanel. Applying {@link Ext.form.Basic BasicForm}
+ * configuration settings to `this` will *not* affect the BasicForm's configuration.
+ *
+ * The following events fired by the BasicForm will be re-fired by the FormPanel and can therefore be
+ * listened for on the FormPanel itself:
+ *
+ * - {@link Ext.form.Basic#beforeaction beforeaction}
+ * - {@link Ext.form.Basic#actionfailed actionfailed}
+ * - {@link Ext.form.Basic#actioncomplete actioncomplete}
+ * - {@link Ext.form.Basic#validitychange validitychange}
+ * - {@link Ext.form.Basic#dirtychange dirtychange}
+ *
+ * # Field Defaults
+ *
+ * The {@link #fieldDefaults} config option conveniently allows centralized configuration of default values
+ * for all fields added as descendants of the FormPanel. Any config option recognized by implementations
+ * of {@link Ext.form.Labelable} may be included in this object. See the {@link #fieldDefaults} documentation
+ * for details of how the defaults are applied.
+ *
+ * # Form Validation
+ *
+ * With the default configuration, form fields are validated on-the-fly while the user edits their values.
+ * This can be controlled on a per-field basis (or via the {@link #fieldDefaults} config) with the field
+ * config properties {@link Ext.form.field.Field#validateOnChange} and {@link Ext.form.field.Base#checkChangeEvents},
+ * and the FormPanel's config properties {@link #pollForChanges} and {@link #pollInterval}.
+ *
+ * Any component within the FormPanel can be configured with `formBind: true`. This will cause that
+ * component to be automatically disabled when the form is invalid, and enabled when it is valid. This is most
+ * commonly used for Button components to prevent submitting the form in an invalid state, but can be used on
+ * any component type.
+ *
+ * For more information on form validation see the following:
+ *
+ * - {@link Ext.form.field.Field#validateOnChange}
+ * - {@link #pollForChanges} and {@link #pollInterval}
+ * - {@link Ext.form.field.VTypes}
+ * - {@link Ext.form.Basic#doAction BasicForm.doAction clientValidation notes}
+ *
+ * # Form Submission
+ *
+ * By default, Ext Forms are submitted through Ajax, using {@link Ext.form.action.Action}. See the documentation for
+ * {@link Ext.form.Basic} for details.
+ *
+ * # Example usage
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'Simple Form',
+ * bodyPadding: 5,
+ * width: 350,
+ *
+ * // The form will submit an AJAX request to this URL when submitted
+ * url: 'save-form.php',
+ *
+ * // Fields will be arranged vertically, stretched to full width
+ * layout: 'anchor',
+ * defaults: {
+ * anchor: '100%'
+ * },
+ *
+ * // The fields
+ * defaultType: 'textfield',
+ * items: [{
+ * fieldLabel: 'First Name',
+ * name: 'first',
+ * allowBlank: false
+ * },{
+ * fieldLabel: 'Last Name',
+ * name: 'last',
+ * allowBlank: false
+ * }],
+ *
+ * // Reset and Submit buttons
+ * buttons: [{
+ * text: 'Reset',
+ * handler: function() {
+ * this.up('form').getForm().reset();
+ * }
+ * }, {
+ * text: 'Submit',
+ * formBind: true, //only enabled once the form is valid
+ * disabled: true,
+ * handler: function() {
+ * var form = this.up('form').getForm();
+ * if (form.isValid()) {
+ * form.submit({
+ * success: function(form, action) {
+ * Ext.Msg.alert('Success', action.result.msg);
+ * },
+ * failure: function(form, action) {
+ * Ext.Msg.alert('Failed', action.result.msg);
+ * }
+ * });
+ * }
+ * }
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ *
+ */
+Ext.define('Ext.form.Panel', {
+ extend:'Ext.panel.Panel',
+ mixins: {
+ fieldAncestor: 'Ext.form.FieldAncestor'
+ },
+ alias: 'widget.form',
+ alternateClassName: ['Ext.FormPanel', 'Ext.form.FormPanel'],
+ requires: ['Ext.form.Basic', 'Ext.util.TaskRunner'],
+
+ /**
+ * @cfg {Boolean} pollForChanges
+ * If set to `true`, sets up an interval task (using the {@link #pollInterval}) in which the
+ * panel's fields are repeatedly checked for changes in their values. This is in addition to the normal detection
+ * each field does on its own input element, and is not needed in most cases. It does, however, provide a
+ * means to absolutely guarantee detection of all changes including some edge cases in some browsers which
+ * do not fire native events. Defaults to `false`.
+ */
+
+ /**
+ * @cfg {Number} pollInterval
+ * Interval in milliseconds at which the form's fields are checked for value changes. Only used if
+ * the {@link #pollForChanges} option is set to `true`. Defaults to 500 milliseconds.
+ */
+
+ /**
+ * @cfg {String} layout
+ * The {@link Ext.container.Container#layout} for the form panel's immediate child items.
+ * Defaults to `'anchor'`.
+ */
+ layout: 'anchor',
+
+ ariaRole: 'form',
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.frame) {
+ me.border = false;
+ }
+
+ me.initFieldAncestor();
+ me.callParent();
+
+ me.relayEvents(me.form, [
+ 'beforeaction',
+ 'actionfailed',
+ 'actioncomplete',
+ 'validitychange',
+ 'dirtychange'
+ ]);
+
+ // Start polling if configured
+ if (me.pollForChanges) {
+ me.startPolling(me.pollInterval || 500);
+ }
+ },
+
+ initItems: function() {
+ // Create the BasicForm
+ var me = this;
+
+ me.form = me.createForm();
+ me.callParent();
+ me.form.initialize();
+ },
+
+ /**
+ * @private
+ */
+ createForm: function() {
+ return Ext.create('Ext.form.Basic', this, Ext.applyIf({listeners: {}}, this.initialConfig));
+ },
+
+ /**
+ * Provides access to the {@link Ext.form.Basic Form} which this Panel contains.
+ * @return {Ext.form.Basic} The {@link Ext.form.Basic Form} which this Panel contains.
+ */
+ getForm: function() {
+ return this.form;
+ },
+
+ /**
+ * Loads an {@link Ext.data.Model} into this form (internally just calls {@link Ext.form.Basic#loadRecord})
+ * See also {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad}.
+ * @param {Ext.data.Model} record The record to load
+ * @return {Ext.form.Basic} The Ext.form.Basic attached to this FormPanel
+ */
+ loadRecord: function(record) {
+ return this.getForm().loadRecord(record);
+ },
+
+ /**
+ * Returns the currently loaded Ext.data.Model instance if one was loaded via {@link #loadRecord}.
+ * @return {Ext.data.Model} The loaded instance
+ */
+ getRecord: function() {
+ return this.getForm().getRecord();
+ },
+
+ /**
+ * Convenience function for fetching the current value of each field in the form. This is the same as calling
+ * {@link Ext.form.Basic#getValues this.getForm().getValues()}
+ * @return {Object} The current form field values, keyed by field name
+ */
+ getValues: function() {
+ return this.getForm().getValues();
+ },
+
+ beforeDestroy: function() {
+ this.stopPolling();
+ this.form.destroy();
+ this.callParent();
+ },
+
+ /**
+ * This is a proxy for the underlying BasicForm's {@link Ext.form.Basic#load} call.
+ * @param {Object} options The options to pass to the action (see {@link Ext.form.Basic#load} and
+ * {@link Ext.form.Basic#doAction} for details)
+ */
+ load: function(options) {
+ this.form.load(options);
+ },
+
+ /**
+ * This is a proxy for the underlying BasicForm's {@link Ext.form.Basic#submit} call.
+ * @param {Object} options The options to pass to the action (see {@link Ext.form.Basic#submit} and
+ * {@link Ext.form.Basic#doAction} for details)
+ */
+ submit: function(options) {
+ this.form.submit(options);
+ },
+
+ /*
+ * Inherit docs, not using onDisable because it only gets fired
+ * when the component is rendered.
+ */
+ disable: function(silent) {
+ this.callParent(arguments);
+ this.form.getFields().each(function(field) {
+ field.disable();
+ });
+ },
+
+ /*
+ * Inherit docs, not using onEnable because it only gets fired
+ * when the component is rendered.
+ */
+ enable: function(silent) {
+ this.callParent(arguments);
+ this.form.getFields().each(function(field) {
+ field.enable();
+ });
+ },
+
+ /**
+ * Start an interval task to continuously poll all the fields in the form for changes in their
+ * values. This is normally started automatically by setting the {@link #pollForChanges} config.
+ * @param {Number} interval The interval in milliseconds at which the check should run.
+ */
+ startPolling: function(interval) {
+ this.stopPolling();
+ var task = Ext.create('Ext.util.TaskRunner', interval);
+ task.start({
+ interval: 0,
+ run: this.checkChange,
+ scope: this
+ });
+ this.pollTask = task;
+ },
+
+ /**
+ * Stop a running interval task that was started by {@link #startPolling}.
+ */
+ stopPolling: function() {
+ var task = this.pollTask;
+ if (task) {
+ task.stopAll();
+ delete this.pollTask;
+ }
+ },
+
+ /**
+ * Forces each field within the form panel to
+ * {@link Ext.form.field.Field#checkChange check if its value has changed}.
+ */
+ checkChange: function() {
+ this.form.getFields().each(function(field) {
+ field.checkChange();
+ });
+ }
+});
+
+/**
+ * A {@link Ext.form.FieldContainer field container} which has a specialized layout for arranging
+ * {@link Ext.form.field.Radio} controls into columns, and provides convenience {@link Ext.form.field.Field}
+ * methods for {@link #getValue getting}, {@link #setValue setting}, and {@link #validate validating} the
+ * group of radio buttons as a whole.
+ *
+ * # Validation
+ *
+ * Individual radio buttons themselves have no default validation behavior, but
+ * sometimes you want to require a user to select one of a group of radios. RadioGroup
+ * allows this by setting the config `{@link #allowBlank}:false`; when the user does not check at
+ * one of the radio buttons, the entire group will be highlighted as invalid and the
+ * {@link #blankText error message} will be displayed according to the {@link #msgTarget} config.</p>
+ *
+ * # Layout
+ *
+ * The default layout for RadioGroup makes it easy to arrange the radio buttons into
+ * columns; see the {@link #columns} and {@link #vertical} config documentation for details. You may also
+ * use a completely different layout by setting the {@link #layout} to one of the other supported layout
+ * types; for instance you may wish to use a custom arrangement of hbox and vbox containers. In that case
+ * the Radio components at any depth will still be managed by the RadioGroup's validation.
+ *
+ * # Example usage
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'RadioGroup Example',
+ * width: 300,
+ * height: 125,
+ * bodyPadding: 10,
+ * renderTo: Ext.getBody(),
+ * items:[{
+ * xtype: 'radiogroup',
+ * fieldLabel: 'Two Columns',
+ * // Arrange radio buttons into two columns, distributed vertically
+ * columns: 2,
+ * vertical: true,
+ * items: [
+ * { boxLabel: 'Item 1', name: 'rb', inputValue: '1' },
+ * { boxLabel: 'Item 2', name: 'rb', inputValue: '2', checked: true},
+ * { boxLabel: 'Item 3', name: 'rb', inputValue: '3' },
+ * { boxLabel: 'Item 4', name: 'rb', inputValue: '4' },
+ * { boxLabel: 'Item 5', name: 'rb', inputValue: '5' },
+ * { boxLabel: 'Item 6', name: 'rb', inputValue: '6' }
+ * ]
+ * }]
+ * });
+ *
+ */
+Ext.define('Ext.form.RadioGroup', {
+ extend: 'Ext.form.CheckboxGroup',
+ alias: 'widget.radiogroup',
+
+ /**
+ * @cfg {Ext.form.field.Radio[]/Object[]} items
+ * An Array of {@link Ext.form.field.Radio Radio}s or Radio config objects to arrange in the group.
+ */
+ /**
+ * @cfg {Boolean} allowBlank True to allow every item in the group to be blank.
+ * 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
+ */
+ blankText : 'You must select one item in this group',
+
+ // private
+ defaultType : 'radiofield',
+
+ // private
+ groupCls : Ext.baseCSSPrefix + 'form-radio-group',
+
+ getBoxes: function() {
+ return this.query('[isRadio]');
+ },
+
+ /**
+ * Sets the value of the radio group. The radio with corresponding name and value will be set.
+ * This method is simpler than {@link Ext.form.CheckboxGroup#setValue} because only 1 value is allowed
+ * for each name.
+ *
+ * @param {Object} value The map from names to values to be set.
+ * @return {Ext.form.CheckboxGroup} this
+ */
+ setValue: function(value) {
+ var me = this;
+ if (Ext.isObject(value)) {
+ Ext.Object.each(value, function(name, cbValue) {
+ var radios = Ext.form.RadioManager.getWithValue(name, cbValue);
+ radios.each(function(cb) {
+ cb.setValue(true);
+ });
+ });
+ }
+ return me;
+ }
+});
+
+/**
+ * @private
+ * Private utility class for managing all {@link Ext.form.field.Radio} fields grouped by name.
+ */
+Ext.define('Ext.form.RadioManager', {
+ extend: 'Ext.util.MixedCollection',
+ singleton: true,
+
+ getByName: function(name) {
+ return this.filterBy(function(item) {
+ return item.name == name;
+ });
+ },
+
+ getWithValue: function(name, value) {
+ return this.filterBy(function(item) {
+ return item.name == name && item.inputValue == value;
+ });
+ },
+
+ getChecked: function(name) {
+ return this.findBy(function(item) {
+ return item.name == name && item.checked;
+ });
+ }
+});
+
+/**
+ * @class Ext.form.action.DirectLoad
+ * @extends Ext.form.action.Load
+ * <p>Provides {@link Ext.direct.Manager} support for loading form data.</p>
+ * <p>This example illustrates usage of Ext.direct.Direct to <b>load</b> a form through Ext.Direct.</p>
+ * <pre><code>
+var myFormPanel = new Ext.form.Panel({
+ // 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
+ }
+});
+ * </code></pre>
+ * The data packet sent to the server will resemble something like:
+ * <pre><code>
+[
+ {
+ "action":"Profile","method":"getBasicInfo","type":"rpc","tid":2,
+ "data":[34,"bar"] // note the order of the params
+ }
+]
+ * </code></pre>
+ * The form will process a data packet returned by the server that is similar
+ * to the following format:
+ * <pre><code>
+[
+ {
+ "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"
+ }
+ }
+ }
+]
+ * </code></pre>
+ */
+Ext.define('Ext.form.action.DirectLoad', {
+ extend:'Ext.form.action.Load',
+ requires: ['Ext.direct.Manager'],
+ alternateClassName: 'Ext.form.Action.DirectLoad',
+ alias: 'formaction.directload',
+
+ type: 'directload',
+
+ run: function() {
+ this.form.api.load.apply(window, this.getArgs());
+ },
+
+ /**
+ * @private
+ * Build the arguments to be sent to the Direct call.
+ * @return Array
+ */
+ getArgs: function() {
+ var me = this,
+ args = [],
+ form = me.form,
+ paramOrder = form.paramOrder,
+ params = me.getParams(),
+ i, len;
+
+ // If a paramOrder was specified, add the params into the argument list in that order.
+ if (paramOrder) {
+ for (i = 0, len = paramOrder.length; i < len; i++) {
+ args.push(params[paramOrder[i]]);
+ }
+ }
+ // If paramsAsHash was specified, add all the params as a single object argument.
+ else if (form.paramsAsHash) {
+ args.push(params);
+ }
+
+ // Add the callback and scope to the end of the arguments list
+ args.push(me.onSuccess, me);
+
+ return args;
+ },
+
+ // 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) {
+ return (this.result = result);
+ },
+
+ onSuccess: function(result, trans) {
+ if (trans.type == Ext.direct.Manager.self.exceptions.SERVER) {
+ result = {};
+ }
+ this.callParent([result]);
+ }
+});
+
+
+
+/**
+ * @class Ext.form.action.DirectSubmit
+ * @extends Ext.form.action.Submit
+ * <p>Provides Ext.direct support for submitting form data.</p>
+ * <p>This example illustrates usage of Ext.direct.Direct to <b>submit</b> a form through Ext.Direct.</p>
+ * <pre><code>
+var myFormPanel = new Ext.form.Panel({
+ // 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']
+});
+ * </code></pre>
+ * The data packet sent to the server will resemble something like:
+ * <pre><code>
+{
+ "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@sencha.com","company":"Sencha Inc."
+ }
+ }
+}
+ * </code></pre>
+ * The form will process a data packet returned by the server that is similar
+ * to the following:
+ * <pre><code>
+// 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"
+ }
+}
+ * </code></pre>
+ * Also see the discussion in {@link Ext.form.action.DirectLoad}.
+ */
+Ext.define('Ext.form.action.DirectSubmit', {
+ extend:'Ext.form.action.Submit',
+ requires: ['Ext.direct.Manager'],
+ alternateClassName: 'Ext.form.Action.DirectSubmit',
+ alias: 'formaction.directsubmit',
+
+ type: 'directsubmit',
+
+ doSubmit: function() {
+ var me = this,
+ callback = Ext.Function.bind(me.onSuccess, me),
+ formEl = me.buildForm();
+ me.form.api.submit(formEl, callback, me);
+ Ext.removeNode(formEl);
+ },
+
+ // 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) {
+ return (this.result = result);
+ },
+
+ onSuccess: function(response, trans) {
+ if (trans.type === Ext.direct.Manager.self.exceptions.SERVER) {
+ response = {};
+ }
+ this.callParent([response]);
+ }
+});
+
+/**
+ * @class Ext.form.action.StandardSubmit
+ * @extends Ext.form.action.Submit
+ * <p>A class which handles submission of data from {@link Ext.form.Basic Form}s using a standard
+ * <tt><form></tt> element submit. It does not handle the response from the submit.</p>
+ * <p>If validation of the form fields fails, the Form's afterAction method
+ * will be called. Otherwise, afterAction will not be called.</p>
+ * <p>Instances of this class are only created by a {@link Ext.form.Basic Form} when
+ * {@link Ext.form.Basic#submit submit}ting, when the form's {@link Ext.form.Basic#standardSubmit}
+ * config option is <tt>true</tt>.</p>
+ */
+Ext.define('Ext.form.action.StandardSubmit', {
+ extend:'Ext.form.action.Submit',
+ alias: 'formaction.standardsubmit',
+
+ /**
+ * @cfg {String} target
+ * Optional <tt>target</tt> attribute to be used for the form when submitting. If not specified,
+ * the target will be the current window/frame.
+ */
+
+ /**
+ * @private
+ * Perform the form submit. Creates and submits a temporary form element containing an input element for each
+ * field value returned by {@link Ext.form.Basic#getValues}, plus any configured {@link #params params} or
+ * {@link Ext.form.Basic#baseParams baseParams}.
+ */
+ doSubmit: function() {
+ var form = this.buildForm();
+ form.submit();
+ Ext.removeNode(form);
+ }
+
+});
+
+/**
+ * @docauthor Robert Dougan <rob@sencha.com>
+ *
+ * Single checkbox field. Can be used as a direct replacement for traditional checkbox fields. Also serves as a
+ * parent class for {@link Ext.form.field.Radio radio buttons}.
+ *
+ * # Labeling
+ *
+ * In addition to the {@link Ext.form.Labelable standard field labeling options}, checkboxes
+ * may be given an optional {@link #boxLabel} which will be displayed immediately after checkbox. Also see
+ * {@link Ext.form.CheckboxGroup} for a convenient method of grouping related checkboxes.
+ *
+ * # Values
+ *
+ * The main value of a checkbox is a boolean, indicating whether or not the checkbox is checked.
+ * The following values will check the checkbox:
+ *
+ * - `true`
+ * - `'true'`
+ * - `'1'`
+ * - `'on'`
+ *
+ * Any other value will uncheck the checkbox.
+ *
+ * In addition to the main boolean value, you may also specify a separate {@link #inputValue}. This will be
+ * sent as the parameter value when the form is {@link Ext.form.Basic#submit submitted}. You will want to set
+ * this value if you have multiple checkboxes with the same {@link #name}. If not specified, the value `on`
+ * will be used.
+ *
+ * # Example usage
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * bodyPadding: 10,
+ * width: 300,
+ * title: 'Pizza Order',
+ * items: [
+ * {
+ * xtype: 'fieldcontainer',
+ * fieldLabel: 'Toppings',
+ * defaultType: 'checkboxfield',
+ * items: [
+ * {
+ * boxLabel : 'Anchovies',
+ * name : 'topping',
+ * inputValue: '1',
+ * id : 'checkbox1'
+ * }, {
+ * boxLabel : 'Artichoke Hearts',
+ * name : 'topping',
+ * inputValue: '2',
+ * checked : true,
+ * id : 'checkbox2'
+ * }, {
+ * boxLabel : 'Bacon',
+ * name : 'topping',
+ * inputValue: '3',
+ * id : 'checkbox3'
+ * }
+ * ]
+ * }
+ * ],
+ * bbar: [
+ * {
+ * text: 'Select Bacon',
+ * handler: function() {
+ * Ext.getCmp('checkbox3').setValue(true);
+ * }
+ * },
+ * '-',
+ * {
+ * text: 'Select All',
+ * handler: function() {
+ * Ext.getCmp('checkbox1').setValue(true);
+ * Ext.getCmp('checkbox2').setValue(true);
+ * Ext.getCmp('checkbox3').setValue(true);
+ * }
+ * },
+ * {
+ * text: 'Deselect All',
+ * handler: function() {
+ * Ext.getCmp('checkbox1').setValue(false);
+ * Ext.getCmp('checkbox2').setValue(false);
+ * Ext.getCmp('checkbox3').setValue(false);
+ * }
+ * }
+ * ],
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.form.field.Checkbox', {
+ extend: 'Ext.form.field.Base',
+ alias: ['widget.checkboxfield', 'widget.checkbox'],
+ alternateClassName: 'Ext.form.Checkbox',
+ requires: ['Ext.XTemplate', 'Ext.form.CheckboxManager'],
+
+ // note: {id} here is really {inputId}, but {cmpId} is available
+ fieldSubTpl: [
+ '<tpl if="boxLabel && boxLabelAlign == \'before\'">',
+ '<label id="{cmpId}-boxLabelEl" class="{boxLabelCls} {boxLabelCls}-{boxLabelAlign}" for="{id}">{boxLabel}</label>',
+ '</tpl>',
+ // Creates not an actual checkbox, but a button which is given aria role="checkbox" and
+ // styled with a custom checkbox image. This allows greater control and consistency in
+ // styling, and using a button allows it to gain focus and handle keyboard nav properly.
+ '<input type="button" id="{id}" ',
+ '<tpl if="tabIdx">tabIndex="{tabIdx}" </tpl>',
+ 'class="{fieldCls} {typeCls}" autocomplete="off" hidefocus="true" />',
+ '<tpl if="boxLabel && boxLabelAlign == \'after\'">',
+ '<label id="{cmpId}-boxLabelEl" class="{boxLabelCls} {boxLabelCls}-{boxLabelAlign}" for="{id}">{boxLabel}</label>',
+ '</tpl>',
+ {
+ disableFormats: true,
+ compiled: true
+ }
+ ],
+
+ isCheckbox: true,
+
+ /**
+ * @cfg {String} [focusCls='x-form-cb-focus']
+ * The CSS class to use when the checkbox receives focus
+ */
+ focusCls: Ext.baseCSSPrefix + 'form-cb-focus',
+
+ /**
+ * @cfg {String} [fieldCls='x-form-field']
+ * The default CSS class for the checkbox
+ */
+
+ /**
+ * @cfg {String} [fieldBodyCls='x-form-cb-wrap']
+ * An extra CSS class to be applied to the body content element in addition to {@link #fieldBodyCls}.
+ * .
+ */
+ fieldBodyCls: Ext.baseCSSPrefix + 'form-cb-wrap',
+
+ /**
+ * @cfg {Boolean} checked
+ * true if the checkbox should render initially checked
+ */
+ checked: false,
+
+ /**
+ * @cfg {String} [checkedCls='x-form-cb-checked']
+ * The CSS class added to the component's main element when it is in the checked state.
+ */
+ checkedCls: Ext.baseCSSPrefix + 'form-cb-checked',
+
+ /**
+ * @cfg {String} boxLabel
+ * An optional text label that will appear next to the checkbox. Whether it appears before or after the checkbox is
+ * determined by the {@link #boxLabelAlign} config.
+ */
+
+ /**
+ * @cfg {String} [boxLabelCls='x-form-cb-label']
+ * The CSS class to be applied to the {@link #boxLabel} element
+ */
+ boxLabelCls: Ext.baseCSSPrefix + 'form-cb-label',
+
+ /**
+ * @cfg {String} boxLabelAlign
+ * The position relative to the checkbox where the {@link #boxLabel} should appear. Recognized values are 'before'
+ * and 'after'.
+ */
+ boxLabelAlign: 'after',
+
+ /**
+ * @cfg {String} inputValue
+ * The value that should go into the generated input element's value attribute and should be used as the parameter
+ * value when submitting as part of a form.
+ */
+ inputValue: 'on',
+
+ /**
+ * @cfg {String} uncheckedValue
+ * If configured, this will be submitted as the checkbox's value during form submit if the checkbox is unchecked. By
+ * default this is undefined, which results in nothing being submitted for the checkbox field when the form is
+ * submitted (the default behavior of HTML checkboxes).
+ */
+
+ /**
+ * @cfg {Function} handler
+ * A function called when the {@link #checked} value changes (can be used instead of handling the {@link #change
+ * change event}).
+ * @cfg {Ext.form.field.Checkbox} handler.checkbox The Checkbox being toggled.
+ * @cfg {Boolean} handler.checked The new checked state of the checkbox.
+ */
+
+ /**
+ * @cfg {Object} scope
+ * An object to use as the scope ('this' reference) of the {@link #handler} function (defaults to this Checkbox).
+ */
+
+ // private overrides
+ checkChangeEvents: [],
+ inputType: 'checkbox',
+ ariaRole: 'checkbox',
+
+ // private
+ onRe: /^on$/i,
+
+ initComponent: function(){
+ this.callParent(arguments);
+ this.getManager().add(this);
+ },
+
+ initValue: function() {
+ var me = this,
+ checked = !!me.checked;
+
+ /**
+ * @property {Object} originalValue
+ * The original value of the field as configured in the {@link #checked} configuration, or as loaded by the last
+ * form load operation if the form's {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} setting is `true`.
+ */
+ me.originalValue = me.lastValue = checked;
+
+ // Set the initial checked state
+ me.setValue(checked);
+ },
+
+ // private
+ onRender : function(ct, position) {
+ var me = this;
+
+ /**
+ * @property {Ext.Element} boxLabelEl
+ * A reference to the label element created for the {@link #boxLabel}. Only present if the component has been
+ * rendered and has a boxLabel configured.
+ */
+ me.addChildEls('boxLabelEl');
+
+ Ext.applyIf(me.subTplData, {
+ boxLabel: me.boxLabel,
+ boxLabelCls: me.boxLabelCls,
+ boxLabelAlign: me.boxLabelAlign
+ });
+
+ me.callParent(arguments);
+ },
+
+ initEvents: function() {
+ var me = this;
+ me.callParent();
+ me.mon(me.inputEl, 'click', me.onBoxClick, me);
+ },
+
+ /**
+ * @private Handle click on the checkbox button
+ */
+ onBoxClick: function(e) {
+ var me = this;
+ if (!me.disabled && !me.readOnly) {
+ this.setValue(!this.checked);
+ }
+ },
+
+ /**
+ * Returns the checked state of the checkbox.
+ * @return {Boolean} True if checked, else false
+ */
+ getRawValue: function() {
+ return this.checked;
+ },
+
+ /**
+ * Returns the checked state of the checkbox.
+ * @return {Boolean} True if checked, else false
+ */
+ getValue: function() {
+ return this.checked;
+ },
+
+ /**
+ * Returns the submit value for the checkbox which can be used when submitting forms.
+ * @return {Boolean/Object} True if checked; otherwise either the {@link #uncheckedValue} or null.
+ */
+ getSubmitValue: function() {
+ var unchecked = this.uncheckedValue,
+ uncheckedVal = Ext.isDefined(unchecked) ? unchecked : null;
+ return this.checked ? this.inputValue : uncheckedVal;
+ },
+
+ /**
+ * Sets the checked state of the checkbox.
+ *
+ * @param {Boolean/String/Number} value The following values will check the checkbox:
+ * `true, 'true', '1', 1, or 'on'`, as well as a String that matches the {@link #inputValue}.
+ * Any other value will uncheck the checkbox.
+ * @return {Boolean} the new checked state of the checkbox
+ */
+ setRawValue: function(value) {
+ var me = this,
+ inputEl = me.inputEl,
+ inputValue = me.inputValue,
+ checked = (value === true || value === 'true' || value === '1' || value === 1 ||
+ (((Ext.isString(value) || Ext.isNumber(value)) && inputValue) ? value == inputValue : me.onRe.test(value)));
+
+ if (inputEl) {
+ inputEl.dom.setAttribute('aria-checked', checked);
+ me[checked ? 'addCls' : 'removeCls'](me.checkedCls);
+ }
+
+ me.checked = me.rawValue = checked;
+ return checked;
+ },
+
+ /**
+ * Sets the checked state of the checkbox, and invokes change detection.
+ * @param {Boolean/String} checked The following values will check the checkbox: `true, 'true', '1', or 'on'`, as
+ * well as a String that matches the {@link #inputValue}. Any other value will uncheck the checkbox.
+ * @return {Ext.form.field.Checkbox} this
+ */
+ setValue: function(checked) {
+ var me = this;
+
+ // If an array of strings is passed, find all checkboxes in the group with the same name as this
+ // one and check all those whose inputValue is in the array, unchecking all the others. This is to
+ // facilitate setting values from Ext.form.Basic#setValues, but is not publicly documented as we
+ // don't want users depending on this behavior.
+ if (Ext.isArray(checked)) {
+ me.getManager().getByName(me.name).each(function(cb) {
+ cb.setValue(Ext.Array.contains(checked, cb.inputValue));
+ });
+ } else {
+ me.callParent(arguments);
+ }
+
+ return me;
+ },
+
+ // private
+ valueToRaw: function(value) {
+ // No extra conversion for checkboxes
+ return value;
+ },
+
+ /**
+ * @private
+ * Called when the checkbox's checked state changes. Invokes the {@link #handler} callback
+ * function if specified.
+ */
+ onChange: function(newVal, oldVal) {
+ var me = this,
+ handler = me.handler;
+ if (handler) {
+ handler.call(me.scope || me, me, newVal);
+ }
+ me.callParent(arguments);
+ },
+
+ // inherit docs
+ beforeDestroy: function(){
+ this.callParent();
+ this.getManager().removeAtKey(this.id);
+ },
+
+ // inherit docs
+ getManager: function() {
+ return Ext.form.CheckboxManager;
+ },
+
+ onEnable: function() {
+ var me = this,
+ inputEl = me.inputEl;
+ me.callParent();
+ if (inputEl) {
+ // Can still be disabled if the field is readOnly
+ inputEl.dom.disabled = me.readOnly;
+ }
+ },
+
+ setReadOnly: function(readOnly) {
+ var me = this,
+ inputEl = me.inputEl;
+ if (inputEl) {
+ // Set the button to disabled when readonly
+ inputEl.dom.disabled = readOnly || me.disabled;
+ }
+ me.readOnly = readOnly;
+ },
+
+ // Calculates and returns the natural width of the bodyEl. It's possible that the initial rendering will
+ // cause the boxLabel to wrap and give us a bad width, so we must prevent wrapping while measuring.
+ getBodyNaturalWidth: function() {
+ var me = this,
+ bodyEl = me.bodyEl,
+ ws = 'white-space',
+ width;
+ bodyEl.setStyle(ws, 'nowrap');
+ width = bodyEl.getWidth();
+ bodyEl.setStyle(ws, '');
+ return width;
+ }
+
+});
+
+/**
+ * @private
+ * @class Ext.layout.component.field.Trigger
+ * @extends Ext.layout.component.field.Field
+ * Layout class for {@link Ext.form.field.Trigger} fields. Adjusts the input field size to accommodate
+ * the trigger button(s).
+ * @private
+ */
+
+Ext.define('Ext.layout.component.field.Trigger', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.triggerfield'],
+
+ extend: 'Ext.layout.component.field.Field',
+
+ /* End Definitions */
+
+ type: 'triggerfield',
+
+ sizeBodyContents: function(width, height) {
+ var me = this,
+ owner = me.owner,
+ inputEl = owner.inputEl,
+ triggerWrap = owner.triggerWrap,
+ triggerWidth = owner.getTriggerWidth();
+
+ // If we or our ancestor is hidden, we can get a triggerWidth calculation
+ // of 0. We don't want to resize in this case.
+ if (owner.hideTrigger || owner.readOnly || triggerWidth > 0) {
+ // Decrease the field's width by the width of the triggers. Both the field and the triggerWrap
+ // are floated left in CSS so they'll stack up side by side.
+ me.setElementSize(inputEl, Ext.isNumber(width) ? width - triggerWidth : width);
+
+ // Explicitly set the triggerWrap's width, to prevent wrapping
+ triggerWrap.setWidth(triggerWidth);
+ }
+ }
+});
+/**
+ * A mechanism for displaying data using custom layout templates and formatting.
+ *
+ * The View uses an {@link Ext.XTemplate} as its internal templating mechanism, and is bound to an
+ * {@link Ext.data.Store} so that as the data in the store changes the view is automatically updated
+ * to reflect the changes. The view also provides built-in behavior for many common events that can
+ * occur for its contained items including click, doubleclick, mouseover, mouseout, etc. as well as a
+ * built-in selection model. **In order to use these features, an {@link #itemSelector} config must
+ * be provided for the DataView to determine what nodes it will be working with.**
+ *
+ * The example below binds a View to a {@link Ext.data.Store} and renders it into an {@link Ext.panel.Panel}.
+ *
+ * @example
+ * Ext.define('Image', {
+ * extend: 'Ext.data.Model',
+ * fields: [
+ * { name:'src', type:'string' },
+ * { name:'caption', type:'string' }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.data.Store', {
+ * id:'imagesStore',
+ * model: 'Image',
+ * data: [
+ * { src:'http://www.sencha.com/img/20110215-feat-drawing.png', caption:'Drawing & Charts' },
+ * { src:'http://www.sencha.com/img/20110215-feat-data.png', caption:'Advanced Data' },
+ * { src:'http://www.sencha.com/img/20110215-feat-html5.png', caption:'Overhauled Theme' },
+ * { src:'http://www.sencha.com/img/20110215-feat-perf.png', caption:'Performance Tuned' }
+ * ]
+ * });
+ *
+ * var imageTpl = new Ext.XTemplate(
+ * '<tpl for=".">',
+ * '<div style="margin-bottom: 10px;" class="thumb-wrap">',
+ * '<img src="{src}" />',
+ * '<br/><span>{caption}</span>',
+ * '</div>',
+ * '</tpl>'
+ * );
+ *
+ * Ext.create('Ext.view.View', {
+ * store: Ext.data.StoreManager.lookup('imagesStore'),
+ * tpl: imageTpl,
+ * itemSelector: 'div.thumb-wrap',
+ * emptyText: 'No images available',
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.view.View', {
+ extend: 'Ext.view.AbstractView',
+ alternateClassName: 'Ext.DataView',
+ alias: 'widget.dataview',
+
+ inheritableStatics: {
+ EventMap: {
+ mousedown: 'MouseDown',
+ mouseup: 'MouseUp',
+ click: 'Click',
+ dblclick: 'DblClick',
+ contextmenu: 'ContextMenu',
+ mouseover: 'MouseOver',
+ mouseout: 'MouseOut',
+ mouseenter: 'MouseEnter',
+ mouseleave: 'MouseLeave',
+ keydown: 'KeyDown',
+ focus: 'Focus'
+ }
+ },
+
+ addCmpEvents: function() {
+ this.addEvents(
+ /**
+ * @event beforeitemmousedown
+ * Fires before the mousedown event on an item is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforeitemmousedown',
+ /**
+ * @event beforeitemmouseup
+ * Fires before the mouseup event on an item is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforeitemmouseup',
+ /**
+ * @event beforeitemmouseenter
+ * Fires before the mouseenter event on an item is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforeitemmouseenter',
+ /**
+ * @event beforeitemmouseleave
+ * Fires before the mouseleave event on an item is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforeitemmouseleave',
+ /**
+ * @event beforeitemclick
+ * Fires before the click event on an item is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforeitemclick',
+ /**
+ * @event beforeitemdblclick
+ * Fires before the dblclick event on an item is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforeitemdblclick',
+ /**
+ * @event beforeitemcontextmenu
+ * Fires before the contextmenu event on an item is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforeitemcontextmenu',
+ /**
+ * @event beforeitemkeydown
+ * Fires before the keydown event on an item is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object. Use {@link Ext.EventObject#getKey getKey()} to retrieve the key that was pressed.
+ */
+ 'beforeitemkeydown',
+ /**
+ * @event itemmousedown
+ * Fires when there is a mouse down on an item
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'itemmousedown',
+ /**
+ * @event itemmouseup
+ * Fires when there is a mouse up on an item
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'itemmouseup',
+ /**
+ * @event itemmouseenter
+ * Fires when the mouse enters an item.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'itemmouseenter',
+ /**
+ * @event itemmouseleave
+ * Fires when the mouse leaves an item.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'itemmouseleave',
+ /**
+ * @event itemclick
+ * Fires when an item is clicked.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'itemclick',
+ /**
+ * @event itemdblclick
+ * Fires when an item is double clicked.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'itemdblclick',
+ /**
+ * @event itemcontextmenu
+ * Fires when an item is right clicked.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'itemcontextmenu',
+ /**
+ * @event itemkeydown
+ * Fires when a key is pressed while an item is currently selected.
+ * @param {Ext.view.View} this
+ * @param {Ext.data.Model} record The record that belongs to the item
+ * @param {HTMLElement} item The item's element
+ * @param {Number} index The item's index
+ * @param {Ext.EventObject} e The raw event object. Use {@link Ext.EventObject#getKey getKey()} to retrieve the key that was pressed.
+ */
+ 'itemkeydown',
+ /**
+ * @event beforecontainermousedown
+ * Fires before the mousedown event on the container is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforecontainermousedown',
+ /**
+ * @event beforecontainermouseup
+ * Fires before the mouseup event on the container is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforecontainermouseup',
+ /**
+ * @event beforecontainermouseover
+ * Fires before the mouseover event on the container is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforecontainermouseover',
+ /**
+ * @event beforecontainermouseout
+ * Fires before the mouseout event on the container is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforecontainermouseout',
+ /**
+ * @event beforecontainerclick
+ * Fires before the click event on the container is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforecontainerclick',
+ /**
+ * @event beforecontainerdblclick
+ * Fires before the dblclick event on the container is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforecontainerdblclick',
+ /**
+ * @event beforecontainercontextmenu
+ * Fires before the contextmenu event on the container is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'beforecontainercontextmenu',
+ /**
+ * @event beforecontainerkeydown
+ * Fires before the keydown event on the container is processed. Returns false to cancel the default action.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object. Use {@link Ext.EventObject#getKey getKey()} to retrieve the key that was pressed.
+ */
+ 'beforecontainerkeydown',
+ /**
+ * @event containermouseup
+ * Fires when there is a mouse up on the container
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'containermouseup',
+ /**
+ * @event containermouseover
+ * Fires when you move the mouse over the container.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'containermouseover',
+ /**
+ * @event containermouseout
+ * Fires when you move the mouse out of the container.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'containermouseout',
+ /**
+ * @event containerclick
+ * Fires when the container is clicked.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'containerclick',
+ /**
+ * @event containerdblclick
+ * Fires when the container is double clicked.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'containerdblclick',
+ /**
+ * @event containercontextmenu
+ * Fires when the container is right clicked.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object
+ */
+ 'containercontextmenu',
+ /**
+ * @event containerkeydown
+ * Fires when a key is pressed while the container is focused, and no item is currently selected.
+ * @param {Ext.view.View} this
+ * @param {Ext.EventObject} e The raw event object. Use {@link Ext.EventObject#getKey getKey()} to retrieve the key that was pressed.
+ */
+ 'containerkeydown',
+
+ /**
+ * @event selectionchange
+ * Fires when the selected nodes change. Relayed event from the underlying selection model.
+ * @param {Ext.view.View} this
+ * @param {HTMLElement[]} selections Array of the selected nodes
+ */
+ 'selectionchange',
+ /**
+ * @event beforeselect
+ * Fires before a selection is made. If any handlers return false, the selection is cancelled.
+ * @param {Ext.view.View} this
+ * @param {HTMLElement} node The node to be selected
+ * @param {HTMLElement[]} selections Array of currently selected nodes
+ */
+ 'beforeselect'
+ );
+ },
+ // private
+ afterRender: function(){
+ var me = this,
+ listeners;
+
+ me.callParent();
+
+ listeners = {
+ scope: me,
+ /*
+ * We need to make copies of this since some of the events fired here will end up triggering
+ * a new event to be called and the shared event object will be mutated. In future we should
+ * investigate if there are any issues with creating a new event object for each event that
+ * is fired.
+ */
+ freezeEvent: true,
+ click: me.handleEvent,
+ mousedown: me.handleEvent,
+ mouseup: me.handleEvent,
+ dblclick: me.handleEvent,
+ contextmenu: me.handleEvent,
+ mouseover: me.handleEvent,
+ mouseout: me.handleEvent,
+ keydown: me.handleEvent
+ };
+
+ me.mon(me.getTargetEl(), listeners);
+
+ if (me.store) {
+ me.bindStore(me.store, true);
+ }
+ },
+
+ handleEvent: function(e) {
+ if (this.processUIEvent(e) !== false) {
+ this.processSpecialEvent(e);
+ }
+ },
+
+ // Private template method
+ processItemEvent: Ext.emptyFn,
+ processContainerEvent: Ext.emptyFn,
+ processSpecialEvent: Ext.emptyFn,
+
+ /*
+ * Returns true if this mouseover/out event is still over the overItem.
+ */
+ stillOverItem: function (event, overItem) {
+ var nowOver;
+
+ // There is this weird bug when you hover over the border of a cell it is saying
+ // the target is the table.
+ // BrowserBug: IE6 & 7. If me.mouseOverItem has been removed and is no longer
+ // in the DOM then accessing .offsetParent will throw an "Unspecified error." exception.
+ // typeof'ng and checking to make sure the offsetParent is an object will NOT throw
+ // this hard exception.
+ if (overItem && typeof(overItem.offsetParent) === "object") {
+ // mouseout : relatedTarget == nowOver, target == wasOver
+ // mouseover: relatedTarget == wasOver, target == nowOver
+ nowOver = (event.type == 'mouseout') ? event.getRelatedTarget() : event.getTarget();
+ return Ext.fly(overItem).contains(nowOver);
+ }
+
+ return false;
+ },
+
+ processUIEvent: function(e) {
+ var me = this,
+ item = e.getTarget(me.getItemSelector(), me.getTargetEl()),
+ map = this.statics().EventMap,
+ index, record,
+ type = e.type,
+ overItem = me.mouseOverItem,
+ newType;
+
+ if (!item) {
+ if (type == 'mouseover' && me.stillOverItem(e, overItem)) {
+ item = overItem;
+ }
+
+ // Try to get the selected item to handle the keydown event, otherwise we'll just fire a container keydown event
+ if (type == 'keydown') {
+ record = me.getSelectionModel().getLastSelected();
+ if (record) {
+ item = me.getNode(record);
+ }
+ }
+ }
+
+ if (item) {
+ index = me.indexOf(item);
+ if (!record) {
+ record = me.getRecord(item);
+ }
+
+ if (me.processItemEvent(record, item, index, e) === false) {
+ return false;
+ }
+
+ newType = me.isNewItemEvent(item, e);
+ if (newType === false) {
+ return false;
+ }
+
+ if (
+ (me['onBeforeItem' + map[newType]](record, item, index, e) === false) ||
+ (me.fireEvent('beforeitem' + newType, me, record, item, index, e) === false) ||
+ (me['onItem' + map[newType]](record, item, index, e) === false)
+ ) {
+ return false;
+ }
+
+ me.fireEvent('item' + newType, me, record, item, index, e);
+ }
+ else {
+ if (
+ (me.processContainerEvent(e) === false) ||
+ (me['onBeforeContainer' + map[type]](e) === false) ||
+ (me.fireEvent('beforecontainer' + type, me, e) === false) ||
+ (me['onContainer' + map[type]](e) === false)
+ ) {
+ return false;
+ }
+
+ me.fireEvent('container' + type, me, e);
+ }
+
+ return true;
+ },
+
+ isNewItemEvent: function (item, e) {
+ var me = this,
+ overItem = me.mouseOverItem,
+ type = e.type;
+
+ switch (type) {
+ case 'mouseover':
+ if (item === overItem) {
+ return false;
+ }
+ me.mouseOverItem = item;
+ return 'mouseenter';
+
+ case 'mouseout':
+ // If the currently mouseovered item contains the mouseover target, it's *NOT* a mouseleave
+ if (me.stillOverItem(e, overItem)) {
+ return false;
+ }
+ me.mouseOverItem = null;
+ return 'mouseleave';
+ }
+ return type;
+ },
+
+ // private
+ onItemMouseEnter: function(record, item, index, e) {
+ if (this.trackOver) {
+ this.highlightItem(item);
+ }
+ },
+
+ // private
+ onItemMouseLeave : function(record, item, index, e) {
+ if (this.trackOver) {
+ this.clearHighlight();
+ }
+ },
+
+ // @private, template methods
+ onItemMouseDown: Ext.emptyFn,
+ onItemMouseUp: Ext.emptyFn,
+ onItemFocus: Ext.emptyFn,
+ onItemClick: Ext.emptyFn,
+ onItemDblClick: Ext.emptyFn,
+ onItemContextMenu: Ext.emptyFn,
+ onItemKeyDown: Ext.emptyFn,
+ onBeforeItemMouseDown: Ext.emptyFn,
+ onBeforeItemMouseUp: Ext.emptyFn,
+ onBeforeItemFocus: Ext.emptyFn,
+ onBeforeItemMouseEnter: Ext.emptyFn,
+ onBeforeItemMouseLeave: Ext.emptyFn,
+ onBeforeItemClick: Ext.emptyFn,
+ onBeforeItemDblClick: Ext.emptyFn,
+ onBeforeItemContextMenu: Ext.emptyFn,
+ onBeforeItemKeyDown: Ext.emptyFn,
+
+ // @private, template methods
+ onContainerMouseDown: Ext.emptyFn,
+ onContainerMouseUp: Ext.emptyFn,
+ onContainerMouseOver: Ext.emptyFn,
+ onContainerMouseOut: Ext.emptyFn,
+ onContainerClick: Ext.emptyFn,
+ onContainerDblClick: Ext.emptyFn,
+ onContainerContextMenu: Ext.emptyFn,
+ onContainerKeyDown: Ext.emptyFn,
+ onBeforeContainerMouseDown: Ext.emptyFn,
+ onBeforeContainerMouseUp: Ext.emptyFn,
+ onBeforeContainerMouseOver: Ext.emptyFn,
+ onBeforeContainerMouseOut: Ext.emptyFn,
+ onBeforeContainerClick: Ext.emptyFn,
+ onBeforeContainerDblClick: Ext.emptyFn,
+ onBeforeContainerContextMenu: Ext.emptyFn,
+ onBeforeContainerKeyDown: Ext.emptyFn,
+
+ /**
+ * Highlights a given item in the DataView. This is called by the mouseover handler if {@link #overItemCls}
+ * and {@link #trackOver} are configured, but can also be called manually by other code, for instance to
+ * handle stepping through the list via keyboard navigation.
+ * @param {HTMLElement} item The item to highlight
+ */
+ highlightItem: function(item) {
+ var me = this;
+ me.clearHighlight();
+ me.highlightedItem = item;
+ Ext.fly(item).addCls(me.overItemCls);
+ },
+
+ /**
+ * Un-highlights the currently highlighted item, if any.
+ */
+ clearHighlight: function() {
+ var me = this,
+ highlighted = me.highlightedItem;
+
+ if (highlighted) {
+ Ext.fly(highlighted).removeCls(me.overItemCls);
+ delete me.highlightedItem;
+ }
+ },
+
+ refresh: function() {
+ var me = this;
+ me.clearHighlight();
+ me.callParent(arguments);
+ if (!me.isFixedHeight()) {
+ me.doComponentLayout();
+ }
+ }
+});
+/**
+ * Component layout for {@link Ext.view.BoundList}. Handles constraining the height to the configured maxHeight.
+ * @class Ext.layout.component.BoundList
+ * @extends Ext.layout.component.Component
+ * @private
+ */
+Ext.define('Ext.layout.component.BoundList', {
+ extend: 'Ext.layout.component.Component',
+ alias: 'layout.boundlist',
+
+ type: 'component',
+
+ beforeLayout: function() {
+ return this.callParent(arguments) || this.owner.refreshed > 0;
+ },
+
+ onLayout : function(width, height) {
+ var me = this,
+ owner = me.owner,
+ floating = owner.floating,
+ el = owner.el,
+ xy = el.getXY(),
+ isNumber = Ext.isNumber,
+ minWidth, maxWidth, minHeight, maxHeight,
+ naturalWidth, naturalHeight, constrainedWidth, constrainedHeight, undef;
+
+ if (floating) {
+ // Position offscreen so the natural width is not affected by the viewport's right edge
+ el.setXY([-9999,-9999]);
+ }
+
+ // Calculate initial layout
+ me.setTargetSize(width, height);
+
+ // Handle min/maxWidth for auto-width
+ if (!isNumber(width)) {
+ minWidth = owner.minWidth;
+ maxWidth = owner.maxWidth;
+ if (isNumber(minWidth) || isNumber(maxWidth)) {
+ naturalWidth = el.getWidth();
+ if (naturalWidth < minWidth) {
+ constrainedWidth = minWidth;
+ }
+ else if (naturalWidth > maxWidth) {
+ constrainedWidth = maxWidth;
+ }
+ if (constrainedWidth) {
+ me.setTargetSize(constrainedWidth);
+ }
+ }
+ }
+ // Handle min/maxHeight for auto-height
+ if (!isNumber(height)) {
+ minHeight = owner.minHeight;
+ maxHeight = owner.maxHeight;
+ if (isNumber(minHeight) || isNumber(maxHeight)) {
+ naturalHeight = el.getHeight();
+ if (naturalHeight < minHeight) {
+ constrainedHeight = minHeight;
+ }
+ else if (naturalHeight > maxHeight) {
+ constrainedHeight = maxHeight;
+ }
+ if (constrainedHeight) {
+ me.setTargetSize(undef, constrainedHeight);
+ }
+ }
+ }
+
+ if (floating) {
+ // Restore position
+ el.setXY(xy);
+ }
+ },
+
+ afterLayout: function() {
+ var me = this,
+ toolbar = me.owner.pagingToolbar;
+ me.callParent();
+ if (toolbar) {
+ toolbar.doComponentLayout();
+ }
+ },
+
+ setTargetSize : function(width, height) {
+ var me = this,
+ owner = me.owner,
+ listHeight = null,
+ toolbar;
+
+ // Size the listEl
+ if (Ext.isNumber(height)) {
+ listHeight = height - owner.el.getFrameWidth('tb');
+ toolbar = owner.pagingToolbar;
+ if (toolbar) {
+ listHeight -= toolbar.getHeight();
+ }
+ }
+ me.setElementSize(owner.listEl, null, listHeight);
+
+ me.callParent(arguments);
+ }
+
+});
+
+/**
+ * A simple class that renders text directly into a toolbar.
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Panel with TextItem',
+ * width: 300,
+ * height: 200,
+ * tbar: [
+ * { xtype: 'tbtext', text: 'Sample Text Item' }
+ * ],
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * @constructor
+ * Creates a new TextItem
+ * @param {Object} text A text string, or a config object containing a <tt>text</tt> property
+ */
+Ext.define('Ext.toolbar.TextItem', {
+ extend: 'Ext.toolbar.Item',
+ requires: ['Ext.XTemplate'],
+ alias: 'widget.tbtext',
+ alternateClassName: 'Ext.Toolbar.TextItem',
+
+ /**
+ * @cfg {String} text The text to be used as innerHTML (html tags are accepted)
+ */
+ text: '',
+
+ renderTpl: '{text}',
+ //
+ baseCls: Ext.baseCSSPrefix + 'toolbar-text',
+
+ onRender : function() {
+ Ext.apply(this.renderData, {
+ text: this.text
+ });
+ this.callParent(arguments);
+ },
+
+ /**
+ * 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);
+ this.ownerCt.doLayout(); // In case an empty text item (centered at zero height) receives new text.
+ } else {
+ this.text = t;
+ }
+ }
+});
+/**
+ * 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 Trigger field directly, as it renders exactly like a combobox for which you
+ * can provide a custom implementation.
+ *
+ * For example:
+ *
+ * @example
+ * Ext.define('Ext.ux.CustomTrigger', {
+ * extend: 'Ext.form.field.Trigger',
+ * alias: 'widget.customtrigger',
+ *
+ * // override onTriggerClick
+ * onTriggerClick: function() {
+ * Ext.Msg.alert('Status', 'You clicked my trigger!');
+ * }
+ * });
+ *
+ * Ext.create('Ext.form.FormPanel', {
+ * title: 'Form with TriggerField',
+ * bodyPadding: 5,
+ * width: 350,
+ * renderTo: Ext.getBody(),
+ * items:[{
+ * xtype: 'customtrigger',
+ * fieldLabel: 'Sample Trigger',
+ * emptyText: 'click the trigger',
+ * }]
+ * });
+ *
+ * However, in general you will most likely want to use Trigger as the base class for a reusable component.
+ * {@link Ext.form.field.Date} and {@link Ext.form.field.ComboBox} are perfect examples of this.
+ */
+Ext.define('Ext.form.field.Trigger', {
+ extend:'Ext.form.field.Text',
+ alias: ['widget.triggerfield', 'widget.trigger'],
+ requires: ['Ext.DomHelper', 'Ext.util.ClickRepeater', 'Ext.layout.component.field.Trigger'],
+ alternateClassName: ['Ext.form.TriggerField', 'Ext.form.TwinTriggerField', 'Ext.form.Trigger'],
+
+ // note: {id} here is really {inputId}, but {cmpId} is available
+ fieldSubTpl: [
+ '<input id="{id}" type="{type}" ',
+ '<tpl if="name">name="{name}" </tpl>',
+ '<tpl if="size">size="{size}" </tpl>',
+ '<tpl if="tabIdx">tabIndex="{tabIdx}" </tpl>',
+ 'class="{fieldCls} {typeCls}" autocomplete="off" />',
+ '<div id="{cmpId}-triggerWrap" class="{triggerWrapCls}" role="presentation">',
+ '{triggerEl}',
+ '<div class="{clearCls}" role="presentation"></div>',
+ '</div>',
+ {
+ compiled: true,
+ disableFormats: true
+ }
+ ],
+
+ /**
+ * @cfg {String} triggerCls
+ * An additional CSS class used to style the trigger button. The trigger will always get the {@link #triggerBaseCls}
+ * by default and triggerCls will be **appended** if specified.
+ */
+
+ /**
+ * @cfg {String} [triggerBaseCls='x-form-trigger']
+ * The base CSS class that is always added to the trigger button. The {@link #triggerCls} will be appended in
+ * addition to this class.
+ */
+ triggerBaseCls: Ext.baseCSSPrefix + 'form-trigger',
+
+ /**
+ * @cfg {String} [triggerWrapCls='x-form-trigger-wrap']
+ * The CSS class that is added to the div wrapping the trigger button(s).
+ */
+ triggerWrapCls: Ext.baseCSSPrefix + 'form-trigger-wrap',
+
+ /**
+ * @cfg {Boolean} hideTrigger
+ * true to hide the trigger element and display only the base text field
+ */
+ hideTrigger: false,
+
+ /**
+ * @cfg {Boolean} editable
+ * false to prevent the user from typing text directly into the field; the field can only have its value set via an
+ * action invoked by the trigger.
+ */
+ editable: true,
+
+ /**
+ * @cfg {Boolean} readOnly
+ * true to prevent the user from changing the field, and hides the trigger. Supercedes the editable and hideTrigger
+ * options if the value is true.
+ */
+ readOnly: false,
+
+ /**
+ * @cfg {Boolean} [selectOnFocus=false]
+ * true to select any existing text in the field immediately on focus. Only applies when
+ * {@link #editable editable} = true
+ */
+
+ /**
+ * @cfg {Boolean} repeatTriggerClick
+ * true to attach a {@link Ext.util.ClickRepeater click repeater} to the trigger.
+ */
+ repeatTriggerClick: false,
+
+
+ /**
+ * @hide
+ * @method autoSize
+ */
+ autoSize: Ext.emptyFn,
+ // private
+ monitorTab: true,
+ // private
+ mimicing: false,
+ // private
+ triggerIndexRe: /trigger-index-(\d+)/,
+
+ componentLayout: 'triggerfield',
+
+ initComponent: function() {
+ this.wrapFocusCls = this.triggerWrapCls + '-focus';
+ this.callParent(arguments);
+ },
+
+ // private
+ onRender: function(ct, position) {
+ var me = this,
+ triggerCls,
+ triggerBaseCls = me.triggerBaseCls,
+ triggerWrapCls = me.triggerWrapCls,
+ triggerConfigs = [],
+ i;
+
+ // triggerCls is a synonym for trigger1Cls, so copy it.
+ // TODO this trigger<n>Cls API design doesn't feel clean, especially where it butts up against the
+ // single triggerCls config. Should rethink this, perhaps something more structured like a list of
+ // trigger config objects that hold cls, handler, etc.
+ if (!me.trigger1Cls) {
+ me.trigger1Cls = me.triggerCls;
+ }
+
+ // Create as many trigger elements as we have trigger<n>Cls configs, but always at least one
+ for (i = 0; (triggerCls = me['trigger' + (i + 1) + 'Cls']) || i < 1; i++) {
+ triggerConfigs.push({
+ cls: [Ext.baseCSSPrefix + 'trigger-index-' + i, triggerBaseCls, triggerCls].join(' '),
+ role: 'button'
+ });
+ }
+ triggerConfigs[i - 1].cls += ' ' + triggerBaseCls + '-last';
+
+ /**
+ * @property {Ext.Element} triggerWrap
+ * A reference to the div element wrapping the trigger button(s). Only set after the field has been rendered.
+ */
+ me.addChildEls('triggerWrap');
+
+ Ext.applyIf(me.subTplData, {
+ triggerWrapCls: triggerWrapCls,
+ triggerEl: Ext.DomHelper.markup(triggerConfigs),
+ clearCls: me.clearCls
+ });
+
+ me.callParent(arguments);
+
+ /**
+ * @property {Ext.CompositeElement} triggerEl
+ * A composite of all the trigger button elements. Only set after the field has been rendered.
+ */
+ me.triggerEl = Ext.select('.' + triggerBaseCls, true, me.triggerWrap.dom);
+
+ me.doc = Ext.getDoc();
+ me.initTrigger();
+ },
+
+ onEnable: function() {
+ this.callParent();
+ this.triggerWrap.unmask();
+ },
+
+ onDisable: function() {
+ this.callParent();
+ this.triggerWrap.mask();
+ },
+
+ afterRender: function() {
+ this.callParent();
+ this.updateEditState();
+ this.triggerEl.unselectable();
+ },
+
+ updateEditState: function() {
+ var me = this,
+ inputEl = me.inputEl,
+ triggerWrap = me.triggerWrap,
+ noeditCls = Ext.baseCSSPrefix + 'trigger-noedit',
+ displayed,
+ readOnly;
+
+ if (me.rendered) {
+ if (me.readOnly) {
+ inputEl.addCls(noeditCls);
+ readOnly = true;
+ displayed = false;
+ } else {
+ if (me.editable) {
+ inputEl.removeCls(noeditCls);
+ readOnly = false;
+ } else {
+ inputEl.addCls(noeditCls);
+ readOnly = true;
+ }
+ displayed = !me.hideTrigger;
+ }
+
+ triggerWrap.setDisplayed(displayed);
+ inputEl.dom.readOnly = readOnly;
+ me.doComponentLayout();
+ }
+ },
+
+ /**
+ * Get the total width of the trigger button area. Only useful after the field has been rendered.
+ * @return {Number} The trigger width
+ */
+ getTriggerWidth: function() {
+ var me = this,
+ triggerWrap = me.triggerWrap,
+ totalTriggerWidth = 0;
+ if (triggerWrap && !me.hideTrigger && !me.readOnly) {
+ me.triggerEl.each(function(trigger) {
+ totalTriggerWidth += trigger.getWidth();
+ });
+ totalTriggerWidth += me.triggerWrap.getFrameWidth('lr');
+ }
+ return totalTriggerWidth;
+ },
+
+ setHideTrigger: function(hideTrigger) {
+ if (hideTrigger != this.hideTrigger) {
+ this.hideTrigger = hideTrigger;
+ this.updateEditState();
+ }
+ },
+
+ /**
+ * Sets the editable state of this field. This method is the runtime equivalent of setting the 'editable' config
+ * option at config time.
+ * @param {Boolean} editable True to allow the user to directly edit 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.
+ */
+ setEditable: function(editable) {
+ if (editable != this.editable) {
+ this.editable = editable;
+ this.updateEditState();
+ }
+ },
+
+ /**
+ * Sets the read-only state of this field. This method is the runtime equivalent of setting the 'readOnly' config
+ * option at config time.
+ * @param {Boolean} readOnly 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.
+ */
+ setReadOnly: function(readOnly) {
+ if (readOnly != this.readOnly) {
+ this.readOnly = readOnly;
+ this.updateEditState();
+ }
+ },
+
+ // private
+ initTrigger: function() {
+ var me = this,
+ triggerWrap = me.triggerWrap,
+ triggerEl = me.triggerEl;
+
+ if (me.repeatTriggerClick) {
+ me.triggerRepeater = Ext.create('Ext.util.ClickRepeater', triggerWrap, {
+ preventDefault: true,
+ handler: function(cr, e) {
+ me.onTriggerWrapClick(e);
+ }
+ });
+ } else {
+ me.mon(me.triggerWrap, 'click', me.onTriggerWrapClick, me);
+ }
+
+ triggerEl.addClsOnOver(me.triggerBaseCls + '-over');
+ triggerEl.each(function(el, c, i) {
+ el.addClsOnOver(me['trigger' + (i + 1) + 'Cls'] + '-over');
+ });
+ triggerEl.addClsOnClick(me.triggerBaseCls + '-click');
+ triggerEl.each(function(el, c, i) {
+ el.addClsOnClick(me['trigger' + (i + 1) + 'Cls'] + '-click');
+ });
+ },
+
+ // private
+ onDestroy: function() {
+ var me = this;
+ Ext.destroyMembers(me, 'triggerRepeater', 'triggerWrap', 'triggerEl');
+ delete me.doc;
+ me.callParent();
+ },
+
+ // private
+ onFocus: function() {
+ var me = this;
+ me.callParent();
+ if (!me.mimicing) {
+ me.bodyEl.addCls(me.wrapFocusCls);
+ me.mimicing = true;
+ me.mon(me.doc, 'mousedown', me.mimicBlur, me, {
+ delay: 10
+ });
+ if (me.monitorTab) {
+ me.on('specialkey', me.checkTab, me);
+ }
+ }
+ },
+
+ // private
+ checkTab: function(me, e) {
+ if (!this.ignoreMonitorTab && e.getKey() == e.TAB) {
+ this.triggerBlur();
+ }
+ },
+
+ // private
+ onBlur: Ext.emptyFn,
+
+ // private
+ mimicBlur: function(e) {
+ if (!this.isDestroyed && !this.bodyEl.contains(e.target) && this.validateBlur(e)) {
+ this.triggerBlur();
+ }
+ },
+
+ // private
+ triggerBlur: function() {
+ var me = this;
+ me.mimicing = false;
+ me.mun(me.doc, 'mousedown', me.mimicBlur, me);
+ if (me.monitorTab && me.inputEl) {
+ me.un('specialkey', me.checkTab, me);
+ }
+ Ext.form.field.Trigger.superclass.onBlur.call(me);
+ if (me.bodyEl) {
+ me.bodyEl.removeCls(me.wrapFocusCls);
+ }
+ },
+
+ beforeBlur: Ext.emptyFn,
+
+ // private
+ // This should be overridden by any subclass that needs to check whether or not the field can be blurred.
+ validateBlur: function(e) {
+ return true;
+ },
+
+ // private
+ // process clicks upon triggers.
+ // determine which trigger index, and dispatch to the appropriate click handler
+ onTriggerWrapClick: function(e) {
+ var me = this,
+ t = e && e.getTarget('.' + Ext.baseCSSPrefix + 'form-trigger', null),
+ match = t && t.className.match(me.triggerIndexRe),
+ idx,
+ triggerClickMethod;
+
+ if (match && !me.readOnly) {
+ idx = parseInt(match[1], 10);
+ triggerClickMethod = me['onTrigger' + (idx + 1) + 'Click'] || me.onTriggerClick;
+ if (triggerClickMethod) {
+ triggerClickMethod.call(me, e);
+ }
+ }
+ },
+
+ /**
+ * @method onTriggerClick
+ * @protected
+ * 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.field.ComboBox and Ext.form.field.Date for sample implementations.
+ * @param {Ext.EventObject} e
+ */
+ onTriggerClick: Ext.emptyFn
+
+ /**
+ * @cfg {Boolean} grow @hide
+ */
+ /**
+ * @cfg {Number} growMin @hide
+ */
+ /**
+ * @cfg {Number} growMax @hide
+ */
+});
+
+/**
+ * An abstract class for fields that have a single trigger which opens a "picker" popup below the field, e.g. a combobox
+ * menu list or a date picker. It provides a base implementation for toggling the picker's visibility when the trigger
+ * is clicked, as well as keyboard navigation and some basic events. Sizing and alignment of the picker can be
+ * controlled via the {@link #matchFieldWidth} and {@link #pickerAlign}/{@link #pickerOffset} config properties
+ * respectively.
+ *
+ * You would not normally use this class directly, but instead use it as the parent class for a specific picker field
+ * implementation. Subclasses must implement the {@link #createPicker} method to create a picker component appropriate
+ * for the field.
+ */
+Ext.define('Ext.form.field.Picker', {
+ extend: 'Ext.form.field.Trigger',
+ alias: 'widget.pickerfield',
+ alternateClassName: 'Ext.form.Picker',
+ requires: ['Ext.util.KeyNav'],
+
+ /**
+ * @cfg {Boolean} matchFieldWidth
+ * Whether the picker dropdown's width should be explicitly set to match the width of the field. Defaults to true.
+ */
+ matchFieldWidth: true,
+
+ /**
+ * @cfg {String} pickerAlign
+ * The {@link Ext.Element#alignTo alignment position} with which to align the picker. Defaults to "tl-bl?"
+ */
+ pickerAlign: 'tl-bl?',
+
+ /**
+ * @cfg {Number[]} pickerOffset
+ * An offset [x,y] to use in addition to the {@link #pickerAlign} when positioning the picker.
+ * Defaults to undefined.
+ */
+
+ /**
+ * @cfg {String} openCls
+ * A class to be added to the field's {@link #bodyEl} element when the picker is opened.
+ * Defaults to 'x-pickerfield-open'.
+ */
+ openCls: Ext.baseCSSPrefix + 'pickerfield-open',
+
+ /**
+ * @property {Boolean} isExpanded
+ * True if the picker is currently expanded, false if not.
+ */
+
+ /**
+ * @cfg {Boolean} editable
+ * False to prevent the user from typing text directly into the field; the field can only have its value set via
+ * selecting a value from the picker. In this state, the picker can also be opened by clicking directly on the input
+ * field itself.
+ */
+ editable: true,
+
+
+ initComponent: function() {
+ this.callParent();
+
+ // Custom events
+ this.addEvents(
+ /**
+ * @event expand
+ * Fires when the field's picker is expanded.
+ * @param {Ext.form.field.Picker} field This field instance
+ */
+ 'expand',
+ /**
+ * @event collapse
+ * Fires when the field's picker is collapsed.
+ * @param {Ext.form.field.Picker} field This field instance
+ */
+ 'collapse',
+ /**
+ * @event select
+ * Fires when a value is selected via the picker.
+ * @param {Ext.form.field.Picker} field This field instance
+ * @param {Object} value The value that was selected. The exact type of this value is dependent on
+ * the individual field and picker implementations.
+ */
+ 'select'
+ );
+ },
+
+
+ initEvents: function() {
+ var me = this;
+ me.callParent();
+
+ // Add handlers for keys to expand/collapse the picker
+ me.keyNav = Ext.create('Ext.util.KeyNav', me.inputEl, {
+ down: function() {
+ if (!me.isExpanded) {
+ // Don't call expand() directly as there may be additional processing involved before
+ // expanding, e.g. in the case of a ComboBox query.
+ me.onTriggerClick();
+ }
+ },
+ esc: me.collapse,
+ scope: me,
+ forceKeyDown: true
+ });
+
+ // Non-editable allows opening the picker by clicking the field
+ if (!me.editable) {
+ me.mon(me.inputEl, 'click', me.onTriggerClick, me);
+ }
+
+ // Disable native browser autocomplete
+ if (Ext.isGecko) {
+ me.inputEl.dom.setAttribute('autocomplete', 'off');
+ }
+ },
+
+
+ /**
+ * Expands this field's picker dropdown.
+ */
+ expand: function() {
+ var me = this,
+ bodyEl, picker, collapseIf;
+
+ if (me.rendered && !me.isExpanded && !me.isDestroyed) {
+ bodyEl = me.bodyEl;
+ picker = me.getPicker();
+ collapseIf = me.collapseIf;
+
+ // show the picker and set isExpanded flag
+ picker.show();
+ me.isExpanded = true;
+ me.alignPicker();
+ bodyEl.addCls(me.openCls);
+
+ // monitor clicking and mousewheel
+ me.mon(Ext.getDoc(), {
+ mousewheel: collapseIf,
+ mousedown: collapseIf,
+ scope: me
+ });
+ Ext.EventManager.onWindowResize(me.alignPicker, me);
+ me.fireEvent('expand', me);
+ me.onExpand();
+ }
+ },
+
+ onExpand: Ext.emptyFn,
+
+ /**
+ * Aligns the picker to the input element
+ * @protected
+ */
+ alignPicker: function() {
+ var me = this,
+ picker;
+
+ if (me.isExpanded) {
+ picker = me.getPicker();
+ if (me.matchFieldWidth) {
+ // Auto the height (it will be constrained by min and max width) unless there are no records to display.
+ picker.setSize(me.bodyEl.getWidth(), picker.store && picker.store.getCount() ? null : 0);
+ }
+ if (picker.isFloating()) {
+ me.doAlign();
+ }
+ }
+ },
+
+ /**
+ * Performs the alignment on the picker using the class defaults
+ * @private
+ */
+ doAlign: function(){
+ var me = this,
+ picker = me.picker,
+ aboveSfx = '-above',
+ isAbove;
+
+ me.picker.alignTo(me.inputEl, me.pickerAlign, me.pickerOffset);
+ // add the {openCls}-above class if the picker was aligned above
+ // the field due to hitting the bottom of the viewport
+ isAbove = picker.el.getY() < me.inputEl.getY();
+ me.bodyEl[isAbove ? 'addCls' : 'removeCls'](me.openCls + aboveSfx);
+ picker[isAbove ? 'addCls' : 'removeCls'](picker.baseCls + aboveSfx);
+ },
+
+ /**
+ * Collapses this field's picker dropdown.
+ */
+ collapse: function() {
+ if (this.isExpanded && !this.isDestroyed) {
+ var me = this,
+ openCls = me.openCls,
+ picker = me.picker,
+ doc = Ext.getDoc(),
+ collapseIf = me.collapseIf,
+ aboveSfx = '-above';
+
+ // hide the picker and set isExpanded flag
+ picker.hide();
+ me.isExpanded = false;
+
+ // remove the openCls
+ me.bodyEl.removeCls([openCls, openCls + aboveSfx]);
+ picker.el.removeCls(picker.baseCls + aboveSfx);
+
+ // remove event listeners
+ doc.un('mousewheel', collapseIf, me);
+ doc.un('mousedown', collapseIf, me);
+ Ext.EventManager.removeResizeListener(me.alignPicker, me);
+ me.fireEvent('collapse', me);
+ me.onCollapse();
+ }
+ },
+
+ onCollapse: Ext.emptyFn,
+
+
+ /**
+ * @private
+ * Runs on mousewheel and mousedown of doc to check to see if we should collapse the picker
+ */
+ collapseIf: function(e) {
+ var me = this;
+ if (!me.isDestroyed && !e.within(me.bodyEl, false, true) && !e.within(me.picker.el, false, true)) {
+ me.collapse();
+ }
+ },
+
+ /**
+ * Returns a reference to the picker component for this field, creating it if necessary by
+ * calling {@link #createPicker}.
+ * @return {Ext.Component} The picker component
+ */
+ getPicker: function() {
+ var me = this;
+ return me.picker || (me.picker = me.createPicker());
+ },
+
+ /**
+ * @method
+ * Creates and returns the component to be used as this field's picker. Must be implemented by subclasses of Picker.
+ * The current field should also be passed as a configuration option to the picker component as the pickerField
+ * property.
+ */
+ createPicker: Ext.emptyFn,
+
+ /**
+ * Handles the trigger click; by default toggles between expanding and collapsing the picker component.
+ * @protected
+ */
+ onTriggerClick: function() {
+ var me = this;
+ if (!me.readOnly && !me.disabled) {
+ if (me.isExpanded) {
+ me.collapse();
+ } else {
+ me.expand();
+ }
+ me.inputEl.focus();
+ }
+ },
+
+ mimicBlur: function(e) {
+ var me = this,
+ picker = me.picker;
+ // ignore mousedown events within the picker element
+ if (!picker || !e.within(picker.el, false, true)) {
+ me.callParent(arguments);
+ }
+ },
+
+ onDestroy : function(){
+ var me = this,
+ picker = me.picker;
+
+ Ext.EventManager.removeResizeListener(me.alignPicker, me);
+ Ext.destroy(me.keyNav);
+ if (picker) {
+ delete picker.pickerField;
+ picker.destroy();
+ }
+ me.callParent();
+ }
+
+});
+
+
+/**
+ * A field with a pair of up/down spinner buttons. This class is not normally instantiated directly,
+ * instead it is subclassed and the {@link #onSpinUp} and {@link #onSpinDown} methods are implemented
+ * to handle when the buttons are clicked. A good example of this is the {@link Ext.form.field.Number}
+ * field which uses the spinner to increment and decrement the field's value by its
+ * {@link Ext.form.field.Number#step step} config value.
+ *
+ * For example:
+ *
+ * @example
+ * Ext.define('Ext.ux.CustomSpinner', {
+ * extend: 'Ext.form.field.Spinner',
+ * alias: 'widget.customspinner',
+ *
+ * // override onSpinUp (using step isn't neccessary)
+ * onSpinUp: function() {
+ * var me = this;
+ * if (!me.readOnly) {
+ * var val = me.step; // set the default value to the step value
+ * if(me.getValue() !== '') {
+ * val = parseInt(me.getValue().slice(0, -5)); // gets rid of " Pack"
+ * }
+ * me.setValue((val + me.step) + ' Pack');
+ * }
+ * },
+ *
+ * // override onSpinDown
+ * onSpinDown: function() {
+ * var val, me = this;
+ * if (!me.readOnly) {
+ * if(me.getValue() !== '') {
+ * val = parseInt(me.getValue().slice(0, -5)); // gets rid of " Pack"
+ * }
+ * me.setValue((val - me.step) + ' Pack');
+ * }
+ * }
+ * });
+ *
+ * Ext.create('Ext.form.FormPanel', {
+ * title: 'Form with SpinnerField',
+ * bodyPadding: 5,
+ * width: 350,
+ * renderTo: Ext.getBody(),
+ * items:[{
+ * xtype: 'customspinner',
+ * fieldLabel: 'How Much Beer?',
+ * step: 6
+ * }]
+ * });
+ *
+ * By default, pressing the up and down arrow keys will also trigger the onSpinUp and onSpinDown methods;
+ * to prevent this, set `{@link #keyNavEnabled} = false`.
+ */
+Ext.define('Ext.form.field.Spinner', {
+ extend: 'Ext.form.field.Trigger',
+ alias: 'widget.spinnerfield',
+ alternateClassName: 'Ext.form.Spinner',
+ requires: ['Ext.util.KeyNav'],
+
+ trigger1Cls: Ext.baseCSSPrefix + 'form-spinner-up',
+ trigger2Cls: Ext.baseCSSPrefix + 'form-spinner-down',
+
+ /**
+ * @cfg {Boolean} spinUpEnabled
+ * Specifies whether the up spinner button is enabled. Defaults to true. To change this after the component is
+ * created, use the {@link #setSpinUpEnabled} method.
+ */
+ spinUpEnabled: true,
+
+ /**
+ * @cfg {Boolean} spinDownEnabled
+ * Specifies whether the down spinner button is enabled. Defaults to true. To change this after the component is
+ * created, use the {@link #setSpinDownEnabled} method.
+ */
+ spinDownEnabled: true,
+
+ /**
+ * @cfg {Boolean} keyNavEnabled
+ * Specifies whether the up and down arrow keys should trigger spinning up and down. Defaults to true.
+ */
+ keyNavEnabled: true,
+
+ /**
+ * @cfg {Boolean} mouseWheelEnabled
+ * Specifies whether the mouse wheel should trigger spinning up and down while the field has focus.
+ * Defaults to true.
+ */
+ mouseWheelEnabled: true,
+
+ /**
+ * @cfg {Boolean} repeatTriggerClick
+ * Whether a {@link Ext.util.ClickRepeater click repeater} should be attached to the spinner buttons.
+ * Defaults to true.
+ */
+ repeatTriggerClick: true,
+
+ /**
+ * @method
+ * @protected
+ * This method is called when the spinner up button is clicked, or when the up arrow key is pressed if
+ * {@link #keyNavEnabled} is true. Must be implemented by subclasses.
+ */
+ onSpinUp: Ext.emptyFn,
+
+ /**
+ * @method
+ * @protected
+ * This method is called when the spinner down button is clicked, or when the down arrow key is pressed if
+ * {@link #keyNavEnabled} is true. Must be implemented by subclasses.
+ */
+ onSpinDown: Ext.emptyFn,
+
+ initComponent: function() {
+ this.callParent();
+
+ this.addEvents(
+ /**
+ * @event spin
+ * Fires when the spinner is made to spin up or down.
+ * @param {Ext.form.field.Spinner} this
+ * @param {String} direction Either 'up' if spinning up, or 'down' if spinning down.
+ */
+ 'spin',
+
+ /**
+ * @event spinup
+ * Fires when the spinner is made to spin up.
+ * @param {Ext.form.field.Spinner} this
+ */
+ 'spinup',
+
+ /**
+ * @event spindown
+ * Fires when the spinner is made to spin down.
+ * @param {Ext.form.field.Spinner} this
+ */
+ 'spindown'
+ );
+ },
+
+ /**
+ * @private
+ * Override.
+ */
+ onRender: function() {
+ var me = this,
+ triggers;
+
+ me.callParent(arguments);
+ triggers = me.triggerEl;
+
+ /**
+ * @property {Ext.Element} spinUpEl
+ * The spinner up button element
+ */
+ me.spinUpEl = triggers.item(0);
+ /**
+ * @property {Ext.Element} spinDownEl
+ * The spinner down button element
+ */
+ me.spinDownEl = triggers.item(1);
+
+ // Set initial enabled/disabled states
+ me.setSpinUpEnabled(me.spinUpEnabled);
+ me.setSpinDownEnabled(me.spinDownEnabled);
+
+ // Init up/down arrow keys
+ if (me.keyNavEnabled) {
+ me.spinnerKeyNav = Ext.create('Ext.util.KeyNav', me.inputEl, {
+ scope: me,
+ up: me.spinUp,
+ down: me.spinDown
+ });
+ }
+
+ // Init mouse wheel
+ if (me.mouseWheelEnabled) {
+ me.mon(me.bodyEl, 'mousewheel', me.onMouseWheel, me);
+ }
+ },
+
+ /**
+ * @private
+ * Override. Since the triggers are stacked, only measure the width of one of them.
+ */
+ getTriggerWidth: function() {
+ return this.hideTrigger || this.readOnly ? 0 : this.spinUpEl.getWidth() + this.triggerWrap.getFrameWidth('lr');
+ },
+
+ /**
+ * @private
+ * Handles the spinner up button clicks.
+ */
+ onTrigger1Click: function() {
+ this.spinUp();
+ },
+
+ /**
+ * @private
+ * Handles the spinner down button clicks.
+ */
+ onTrigger2Click: function() {
+ this.spinDown();
+ },
+
+ /**
+ * Triggers the spinner to step up; fires the {@link #spin} and {@link #spinup} events and calls the
+ * {@link #onSpinUp} method. Does nothing if the field is {@link #disabled} or if {@link #spinUpEnabled}
+ * is false.
+ */
+ spinUp: function() {
+ var me = this;
+ if (me.spinUpEnabled && !me.disabled) {
+ me.fireEvent('spin', me, 'up');
+ me.fireEvent('spinup', me);
+ me.onSpinUp();
+ }
+ },
+
+ /**
+ * Triggers the spinner to step down; fires the {@link #spin} and {@link #spindown} events and calls the
+ * {@link #onSpinDown} method. Does nothing if the field is {@link #disabled} or if {@link #spinDownEnabled}
+ * is false.
+ */
+ spinDown: function() {
+ var me = this;
+ if (me.spinDownEnabled && !me.disabled) {
+ me.fireEvent('spin', me, 'down');
+ me.fireEvent('spindown', me);
+ me.onSpinDown();
+ }
+ },
+
+ /**
+ * Sets whether the spinner up button is enabled.
+ * @param {Boolean} enabled true to enable the button, false to disable it.
+ */
+ setSpinUpEnabled: function(enabled) {
+ var me = this,
+ wasEnabled = me.spinUpEnabled;
+ me.spinUpEnabled = enabled;
+ if (wasEnabled !== enabled && me.rendered) {
+ me.spinUpEl[enabled ? 'removeCls' : 'addCls'](me.trigger1Cls + '-disabled');
+ }
+ },
+
+ /**
+ * Sets whether the spinner down button is enabled.
+ * @param {Boolean} enabled true to enable the button, false to disable it.
+ */
+ setSpinDownEnabled: function(enabled) {
+ var me = this,
+ wasEnabled = me.spinDownEnabled;
+ me.spinDownEnabled = enabled;
+ if (wasEnabled !== enabled && me.rendered) {
+ me.spinDownEl[enabled ? 'removeCls' : 'addCls'](me.trigger2Cls + '-disabled');
+ }
+ },
+
+ /**
+ * @private
+ * Handles mousewheel events on the field
+ */
+ onMouseWheel: function(e) {
+ var me = this,
+ delta;
+ if (me.hasFocus) {
+ delta = e.getWheelDelta();
+ if (delta > 0) {
+ me.spinUp();
+ }
+ else if (delta < 0) {
+ me.spinDown();
+ }
+ e.stopEvent();
+ }
+ },
+
+ onDestroy: function() {
+ Ext.destroyMembers(this, 'spinnerKeyNav', 'spinUpEl', 'spinDownEl');
+ this.callParent();
+ }
+
+});
+/**
+ * @docauthor Jason Johnston <jason@sencha.com>
+ *
+ * A numeric text field that provides automatic keystroke filtering to disallow non-numeric characters,
+ * and numeric validation to limit the value to a range of valid numbers. The range of acceptable number
+ * values can be controlled by setting the {@link #minValue} and {@link #maxValue} configs, and fractional
+ * decimals can be disallowed by setting {@link #allowDecimals} to `false`.
+ *
+ * By default, the number field is also rendered with a set of up/down spinner buttons and has
+ * up/down arrow key and mouse wheel event listeners attached for incrementing/decrementing the value by the
+ * {@link #step} value. To hide the spinner buttons set `{@link #hideTrigger hideTrigger}:true`; to disable
+ * the arrow key and mouse wheel handlers set `{@link #keyNavEnabled keyNavEnabled}:false` and
+ * `{@link #mouseWheelEnabled mouseWheelEnabled}:false`. See the example below.
+ *
+ * # Example usage
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'On The Wall',
+ * width: 300,
+ * bodyPadding: 10,
+ * renderTo: Ext.getBody(),
+ * items: [{
+ * xtype: 'numberfield',
+ * anchor: '100%',
+ * name: 'bottles',
+ * fieldLabel: 'Bottles of Beer',
+ * value: 99,
+ * maxValue: 99,
+ * minValue: 0
+ * }],
+ * buttons: [{
+ * text: 'Take one down, pass it around',
+ * handler: function() {
+ * this.up('form').down('[name=bottles]').spinDown();
+ * }
+ * }]
+ * });
+ *
+ * # Removing UI Enhancements
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'Personal Info',
+ * width: 300,
+ * bodyPadding: 10,
+ * renderTo: Ext.getBody(),
+ * items: [{
+ * xtype: 'numberfield',
+ * anchor: '100%',
+ * name: 'age',
+ * fieldLabel: 'Age',
+ * minValue: 0, //prevents negative numbers
+ *
+ * // Remove spinner buttons, and arrow key and mouse wheel listeners
+ * hideTrigger: true,
+ * keyNavEnabled: false,
+ * mouseWheelEnabled: false
+ * }]
+ * });
+ *
+ * # Using Step
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * renderTo: Ext.getBody(),
+ * title: 'Step',
+ * width: 300,
+ * bodyPadding: 10,
+ * items: [{
+ * xtype: 'numberfield',
+ * anchor: '100%',
+ * name: 'evens',
+ * fieldLabel: 'Even Numbers',
+ *
+ * // Set step so it skips every other number
+ * step: 2,
+ * value: 0,
+ *
+ * // Add change handler to force user-entered numbers to evens
+ * listeners: {
+ * change: function(field, value) {
+ * value = parseInt(value, 10);
+ * field.setValue(value + value % 2);
+ * }
+ * }
+ * }]
+ * });
+ */
+Ext.define('Ext.form.field.Number', {
+ extend:'Ext.form.field.Spinner',
+ alias: 'widget.numberfield',
+ alternateClassName: ['Ext.form.NumberField', 'Ext.form.Number'],
+
+ /**
+ * @cfg {RegExp} stripCharsRe @hide
+ */
+ /**
+ * @cfg {RegExp} maskRe @hide
+ */
+
+ /**
+ * @cfg {Boolean} allowDecimals
+ * False to disallow decimal values
+ */
+ allowDecimals : true,
+
+ /**
+ * @cfg {String} decimalSeparator
+ * Character(s) to allow as the decimal separator
+ */
+ decimalSeparator : '.',
+
+ /**
+ * @cfg {Number} decimalPrecision
+ * The maximum precision to display after the decimal separator
+ */
+ decimalPrecision : 2,
+
+ /**
+ * @cfg {Number} minValue
+ * The minimum allowed value (defaults to Number.NEGATIVE_INFINITY). Will be used by the field's validation logic,
+ * and for {@link Ext.form.field.Spinner#setSpinUpEnabled enabling/disabling the down spinner button}.
+ */
+ minValue: Number.NEGATIVE_INFINITY,
+
+ /**
+ * @cfg {Number} maxValue
+ * The maximum allowed value (defaults to Number.MAX_VALUE). Will be used by the field's validation logic, and for
+ * {@link Ext.form.field.Spinner#setSpinUpEnabled enabling/disabling the up spinner button}.
+ */
+ maxValue: Number.MAX_VALUE,
+
+ /**
+ * @cfg {Number} step
+ * Specifies a numeric interval by which the field's value will be incremented or decremented when the user invokes
+ * the spinner.
+ */
+ step: 1,
+
+ /**
+ * @cfg {String} minText
+ * Error text to display if the minimum value validation fails.
+ */
+ minText : 'The minimum value for this field is {0}',
+
+ /**
+ * @cfg {String} maxText
+ * Error text to display if the maximum value validation fails.
+ */
+ maxText : 'The maximum value for this field is {0}',
+
+ /**
+ * @cfg {String} nanText
+ * Error text to display if the value is not a valid number. For example, this can happen if a valid character like
+ * '.' or '-' is left in the field with no number.
+ */
+ nanText : '{0} is not a valid number',
+
+ /**
+ * @cfg {String} negativeText
+ * Error text to display if the value is negative and {@link #minValue} is set to 0. This is used instead of the
+ * {@link #minText} in that circumstance only.
+ */
+ negativeText : 'The value cannot be negative',
+
+ /**
+ * @cfg {String} baseChars
+ * The base set of characters to evaluate as valid numbers.
+ */
+ baseChars : '0123456789',
+
+ /**
+ * @cfg {Boolean} autoStripChars
+ * True to automatically strip not allowed characters from the field.
+ */
+ autoStripChars: false,
+
+ initComponent: function() {
+ var me = this,
+ allowed;
+
+ me.callParent();
+
+ me.setMinValue(me.minValue);
+ me.setMaxValue(me.maxValue);
+
+ // Build regexes for masking and stripping based on the configured options
+ if (me.disableKeyFilter !== true) {
+ allowed = me.baseChars + '';
+ if (me.allowDecimals) {
+ allowed += me.decimalSeparator;
+ }
+ if (me.minValue < 0) {
+ allowed += '-';
+ }
+ allowed = Ext.String.escapeRegex(allowed);
+ me.maskRe = new RegExp('[' + allowed + ']');
+ if (me.autoStripChars) {
+ me.stripCharsRe = new RegExp('[^' + allowed + ']', 'gi');
+ }
+ }
+ },
+
+ /**
+ * Runs all of Number's validations and returns an array of any errors. Note that this first runs Text's
+ * validations, so the returned array is an amalgamation of all field errors. The additional validations run test
+ * that the value is a number, and that it is within the configured min and max values.
+ * @param {Object} [value] The value to get errors for (defaults to the current field value)
+ * @return {String[]} All validation errors for this field
+ */
+ getErrors: function(value) {
+ var me = this,
+ errors = me.callParent(arguments),
+ format = Ext.String.format,
+ num;
+
+ value = Ext.isDefined(value) ? value : this.processRawValue(this.getRawValue());
+
+ if (value.length < 1) { // if it's blank and textfield didn't flag it then it's valid
+ return errors;
+ }
+
+ value = String(value).replace(me.decimalSeparator, '.');
+
+ if(isNaN(value)){
+ errors.push(format(me.nanText, value));
+ }
+
+ num = me.parseValue(value);
+
+ if (me.minValue === 0 && num < 0) {
+ errors.push(this.negativeText);
+ }
+ else if (num < me.minValue) {
+ errors.push(format(me.minText, me.minValue));
+ }
+
+ if (num > me.maxValue) {
+ errors.push(format(me.maxText, me.maxValue));
+ }
+
+
+ return errors;
+ },
+
+ rawToValue: function(rawValue) {
+ var value = this.fixPrecision(this.parseValue(rawValue));
+ if (value === null) {
+ value = rawValue || null;
+ }
+ return value;
+ },
+
+ valueToRaw: function(value) {
+ var me = this,
+ decimalSeparator = me.decimalSeparator;
+ value = me.parseValue(value);
+ value = me.fixPrecision(value);
+ value = Ext.isNumber(value) ? value : parseFloat(String(value).replace(decimalSeparator, '.'));
+ value = isNaN(value) ? '' : String(value).replace('.', decimalSeparator);
+ return value;
+ },
+
+ onChange: function() {
+ var me = this,
+ value = me.getValue(),
+ valueIsNull = value === null;
+
+ me.callParent(arguments);
+
+ // Update the spinner buttons
+ me.setSpinUpEnabled(valueIsNull || value < me.maxValue);
+ me.setSpinDownEnabled(valueIsNull || value > me.minValue);
+ },
+
+ /**
+ * Replaces any existing {@link #minValue} with the new value.
+ * @param {Number} value The minimum value
+ */
+ setMinValue : function(value) {
+ this.minValue = Ext.Number.from(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.Number.from(value, Number.MAX_VALUE);
+ },
+
+ // private
+ parseValue : function(value) {
+ value = parseFloat(String(value).replace(this.decimalSeparator, '.'));
+ return isNaN(value) ? null : value;
+ },
+
+ /**
+ * @private
+ */
+ fixPrecision : function(value) {
+ var me = this,
+ nan = isNaN(value),
+ precision = me.decimalPrecision;
+
+ if (nan || !value) {
+ return nan ? '' : value;
+ } else if (!me.allowDecimals || precision <= 0) {
+ precision = 0;
+ }
+
+ return parseFloat(Ext.Number.toFixed(parseFloat(value), precision));
+ },
+
+ beforeBlur : function() {
+ var me = this,
+ v = me.parseValue(me.getRawValue());
+
+ if (!Ext.isEmpty(v)) {
+ me.setValue(v);
+ }
+ },
+
+ onSpinUp: function() {
+ var me = this;
+ if (!me.readOnly) {
+ me.setValue(Ext.Number.constrain(me.getValue() + me.step, me.minValue, me.maxValue));
+ }
+ },
+
+ onSpinDown: function() {
+ var me = this;
+ if (!me.readOnly) {
+ me.setValue(Ext.Number.constrain(me.getValue() - me.step, me.minValue, me.maxValue));
+ }
+ }
+});
+
+/**
+ * As the number of records increases, the time required for the browser to render them increases. Paging is used to
+ * reduce the amount of data exchanged with the client. Note: if there are more records/rows than can be viewed in the
+ * available screen area, vertical scrollbars will be added.
+ *
+ * Paging is typically handled on the server side (see exception below). The client sends parameters to the server side,
+ * which the server needs to interpret and then respond with the appropriate data.
+ *
+ * Ext.toolbar.Paging is a specialized toolbar that is bound to a {@link Ext.data.Store} and provides automatic
+ * paging control. This Component {@link Ext.data.Store#load load}s blocks of data into the {@link #store} by passing
+ * parameters used for paging criteria.
+ *
+ * {@img Ext.toolbar.Paging/Ext.toolbar.Paging.png Ext.toolbar.Paging component}
+ *
+ * Paging Toolbar is typically used as one of the Grid's toolbars:
+ *
+ * @example
+ * var itemsPerPage = 2; // set the number of items you want per page
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * id:'simpsonsStore',
+ * autoLoad: false,
+ * fields:['name', 'email', 'phone'],
+ * pageSize: itemsPerPage, // items per page
+ * proxy: {
+ * type: 'ajax',
+ * url: 'pagingstore.js', // url that will load data with respect to start and limit params
+ * reader: {
+ * type: 'json',
+ * root: 'items',
+ * totalProperty: 'total'
+ * }
+ * }
+ * });
+ *
+ * // specify segment of data you want to load using params
+ * store.load({
+ * params:{
+ * start:0,
+ * limit: itemsPerPage
+ * }
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Simpsons',
+ * store: store,
+ * columns: [
+ * { header: 'Name', dataIndex: 'name' },
+ * { header: 'Email', dataIndex: 'email', flex: 1 },
+ * { header: 'Phone', dataIndex: 'phone' }
+ * ],
+ * width: 400,
+ * height: 125,
+ * dockedItems: [{
+ * xtype: 'pagingtoolbar',
+ * store: store, // same store GridPanel is using
+ * dock: 'bottom',
+ * displayInfo: true
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * To use paging, pass the paging requirements to the server when the store is first loaded.
+ *
+ * store.load({
+ * params: {
+ * // specify params for the first page load if using paging
+ * start: 0,
+ * limit: myPageSize,
+ * // other params
+ * foo: 'bar'
+ * }
+ * });
+ *
+ * If using {@link Ext.data.Store#autoLoad store's autoLoad} configuration:
+ *
+ * var myStore = Ext.create('Ext.data.Store', {
+ * {@link Ext.data.Store#autoLoad autoLoad}: {start: 0, limit: 25},
+ * ...
+ * });
+ *
+ * The packet sent back from the server would have this form:
+ *
+ * {
+ * "success": true,
+ * "results": 2000,
+ * "rows": [ // ***Note:** this must be an Array
+ * { "id": 1, "name": "Bill", "occupation": "Gardener" },
+ * { "id": 2, "name": "Ben", "occupation": "Horticulturalist" },
+ * ...
+ * { "id": 25, "name": "Sue", "occupation": "Botanist" }
+ * ]
+ * }
+ *
+ * ## Paging with Local Data
+ *
+ * Paging can also be accomplished with local data using extensions:
+ *
+ * - [Ext.ux.data.PagingStore][1]
+ * - Paging Memory Proxy (examples/ux/PagingMemoryProxy.js)
+ *
+ * [1]: http://sencha.com/forum/showthread.php?t=71532
+ */
+Ext.define('Ext.toolbar.Paging', {
+ extend: 'Ext.toolbar.Toolbar',
+ alias: 'widget.pagingtoolbar',
+ alternateClassName: 'Ext.PagingToolbar',
+ requires: ['Ext.toolbar.TextItem', 'Ext.form.field.Number'],
+ /**
+ * @cfg {Ext.data.Store} store (required)
+ * The {@link Ext.data.Store} the paging toolbar should use as its data source.
+ */
+
+ /**
+ * @cfg {Boolean} displayInfo
+ * true to display the displayMsg
+ */
+ displayInfo: false,
+
+ /**
+ * @cfg {Boolean} prependButtons
+ * true to insert any configured items _before_ the paging buttons.
+ */
+ prependButtons: false,
+
+ /**
+ * @cfg {String} displayMsg
+ * The paging status message to display. Note that this string is
+ * formatted using the braced numbers {0}-{2} as tokens that are replaced by the values for start, end and total
+ * respectively. These tokens should be preserved when overriding this string if showing those values is desired.
+ */
+ displayMsg : 'Displaying {0} - {1} of {2}',
+
+ /**
+ * @cfg {String} emptyMsg
+ * The message to display when no records are found.
+ */
+ emptyMsg : 'No data to display',
+
+ /**
+ * @cfg {String} beforePageText
+ * The text displayed before the input item.
+ */
+ beforePageText : 'Page',
+
+ /**
+ * @cfg {String} afterPageText
+ * Customizable piece of the default paging text. Note that this string is formatted using
+ * {0} as a token that is replaced by the number of total pages. This token should be preserved when overriding this
+ * string if showing the total page count is desired.
+ */
+ afterPageText : 'of {0}',
+
+ /**
+ * @cfg {String} firstText
+ * The quicktip text displayed for the first page button.
+ * **Note**: quick tips must be initialized for the quicktip to show.
+ */
+ firstText : 'First Page',
+
+ /**
+ * @cfg {String} prevText
+ * The quicktip text displayed for the previous page button.
+ * **Note**: quick tips must be initialized for the quicktip to show.
+ */
+ prevText : 'Previous Page',
+
+ /**
+ * @cfg {String} nextText
+ * The quicktip text displayed for the next page button.
+ * **Note**: quick tips must be initialized for the quicktip to show.
+ */
+ nextText : 'Next Page',
+
+ /**
+ * @cfg {String} lastText
+ * The quicktip text displayed for the last page button.
+ * **Note**: quick tips must be initialized for the quicktip to show.
+ */
+ lastText : 'Last Page',
+
+ /**
+ * @cfg {String} refreshText
+ * The quicktip text displayed for the Refresh button.
+ * **Note**: quick tips must be initialized for the quicktip to show.
+ */
+ refreshText : 'Refresh',
+
+ /**
+ * @cfg {Number} inputItemWidth
+ * The width in pixels of the input field used to display and change the current page number.
+ */
+ inputItemWidth : 30,
+
+ /**
+ * Gets the standard paging items in the toolbar
+ * @private
+ */
+ getPagingItems: function() {
+ var me = this;
+
+ return [{
+ itemId: 'first',
+ tooltip: me.firstText,
+ overflowText: me.firstText,
+ iconCls: Ext.baseCSSPrefix + 'tbar-page-first',
+ disabled: true,
+ handler: me.moveFirst,
+ scope: me
+ },{
+ itemId: 'prev',
+ tooltip: me.prevText,
+ overflowText: me.prevText,
+ iconCls: Ext.baseCSSPrefix + 'tbar-page-prev',
+ disabled: true,
+ handler: me.movePrevious,
+ scope: me
+ },
+ '-',
+ me.beforePageText,
+ {
+ xtype: 'numberfield',
+ itemId: 'inputItem',
+ name: 'inputItem',
+ cls: Ext.baseCSSPrefix + 'tbar-page-number',
+ allowDecimals: false,
+ minValue: 1,
+ hideTrigger: true,
+ enableKeyEvents: true,
+ selectOnFocus: true,
+ submitValue: false,
+ width: me.inputItemWidth,
+ margins: '-1 2 3 2',
+ listeners: {
+ scope: me,
+ keydown: me.onPagingKeyDown,
+ blur: me.onPagingBlur
+ }
+ },{
+ xtype: 'tbtext',
+ itemId: 'afterTextItem',
+ text: Ext.String.format(me.afterPageText, 1)
+ },
+ '-',
+ {
+ itemId: 'next',
+ tooltip: me.nextText,
+ overflowText: me.nextText,
+ iconCls: Ext.baseCSSPrefix + 'tbar-page-next',
+ disabled: true,
+ handler: me.moveNext,
+ scope: me
+ },{
+ itemId: 'last',
+ tooltip: me.lastText,
+ overflowText: me.lastText,
+ iconCls: Ext.baseCSSPrefix + 'tbar-page-last',
+ disabled: true,
+ handler: me.moveLast,
+ scope: me
+ },
+ '-',
+ {
+ itemId: 'refresh',
+ tooltip: me.refreshText,
+ overflowText: me.refreshText,
+ iconCls: Ext.baseCSSPrefix + 'tbar-loading',
+ handler: me.doRefresh,
+ scope: me
+ }];
+ },
+
+ initComponent : function(){
+ var me = this,
+ pagingItems = me.getPagingItems(),
+ userItems = me.items || me.buttons || [];
+
+ if (me.prependButtons) {
+ me.items = userItems.concat(pagingItems);
+ } else {
+ me.items = pagingItems.concat(userItems);
+ }
+ delete me.buttons;
+
+ if (me.displayInfo) {
+ me.items.push('->');
+ me.items.push({xtype: 'tbtext', itemId: 'displayItem'});
+ }
+
+ me.callParent();
+
+ me.addEvents(
+ /**
+ * @event change
+ * Fires after the active page has been changed.
+ * @param {Ext.toolbar.Paging} this
+ * @param {Object} pageData An object that has these properties:
+ *
+ * - `total` : Number
+ *
+ * The total number of records in the dataset as returned by the server
+ *
+ * - `currentPage` : Number
+ *
+ * The current page number
+ *
+ * - `pageCount` : Number
+ *
+ * The total number of pages (calculated from the total number of records in the dataset as returned by the
+ * server and the current {@link Ext.data.Store#pageSize pageSize})
+ *
+ * - `toRecord` : Number
+ *
+ * The starting record index for the current page
+ *
+ * - `fromRecord` : Number
+ *
+ * The ending record index for the current page
+ */
+ 'change',
+
+ /**
+ * @event beforechange
+ * Fires just before the active page is changed. Return false to prevent the active page from being changed.
+ * @param {Ext.toolbar.Paging} this
+ * @param {Number} page The page number that will be loaded on change
+ */
+ 'beforechange'
+ );
+ me.on('afterlayout', me.onLoad, me, {single: true});
+
+ me.bindStore(me.store || 'ext-empty-store', true);
+ },
+ // private
+ updateInfo : function(){
+ var me = this,
+ displayItem = me.child('#displayItem'),
+ store = me.store,
+ pageData = me.getPageData(),
+ count, msg;
+
+ if (displayItem) {
+ count = store.getCount();
+ if (count === 0) {
+ msg = me.emptyMsg;
+ } else {
+ msg = Ext.String.format(
+ me.displayMsg,
+ pageData.fromRecord,
+ pageData.toRecord,
+ pageData.total
+ );
+ }
+ displayItem.setText(msg);
+ me.doComponentLayout();
+ }
+ },
+
+ // private
+ onLoad : function(){
+ var me = this,
+ pageData,
+ currPage,
+ pageCount,
+ afterText;
+
+ if (!me.rendered) {
+ return;
+ }
+
+ pageData = me.getPageData();
+ currPage = pageData.currentPage;
+ pageCount = pageData.pageCount;
+ afterText = Ext.String.format(me.afterPageText, isNaN(pageCount) ? 1 : pageCount);
+
+ me.child('#afterTextItem').setText(afterText);
+ me.child('#inputItem').setValue(currPage);
+ me.child('#first').setDisabled(currPage === 1);
+ me.child('#prev').setDisabled(currPage === 1);
+ me.child('#next').setDisabled(currPage === pageCount);
+ me.child('#last').setDisabled(currPage === pageCount);
+ me.child('#refresh').enable();
+ me.updateInfo();
+ me.fireEvent('change', me, pageData);
+ },
+
+ // private
+ getPageData : function(){
+ var store = this.store,
+ totalCount = store.getTotalCount();
+
+ return {
+ total : totalCount,
+ currentPage : store.currentPage,
+ pageCount: Math.ceil(totalCount / store.pageSize),
+ fromRecord: ((store.currentPage - 1) * store.pageSize) + 1,
+ toRecord: Math.min(store.currentPage * store.pageSize, totalCount)
+
+ };
+ },
+
+ // private
+ onLoadError : function(){
+ if (!this.rendered) {
+ return;
+ }
+ this.child('#refresh').enable();
+ },
+
+ // private
+ readPageFromInput : function(pageData){
+ var v = this.child('#inputItem').getValue(),
+ pageNum = parseInt(v, 10);
+
+ if (!v || isNaN(pageNum)) {
+ this.child('#inputItem').setValue(pageData.currentPage);
+ return false;
+ }
+ return pageNum;
+ },
+
+ onPagingFocus : function(){
+ this.child('#inputItem').select();
+ },
+
+ //private
+ onPagingBlur : function(e){
+ var curPage = this.getPageData().currentPage;
+ this.child('#inputItem').setValue(curPage);
+ },
+
+ // private
+ onPagingKeyDown : function(field, e){
+ var me = this,
+ k = e.getKey(),
+ pageData = me.getPageData(),
+ increment = e.shiftKey ? 10 : 1,
+ pageNum;
+
+ if (k == e.RETURN) {
+ e.stopEvent();
+ pageNum = me.readPageFromInput(pageData);
+ if (pageNum !== false) {
+ pageNum = Math.min(Math.max(1, pageNum), pageData.pageCount);
+ if(me.fireEvent('beforechange', me, pageNum) !== false){
+ me.store.loadPage(pageNum);
+ }
+ }
+ } else if (k == e.HOME || k == e.END) {
+ e.stopEvent();
+ pageNum = k == e.HOME ? 1 : pageData.pageCount;
+ field.setValue(pageNum);
+ } else if (k == e.UP || k == e.PAGEUP || k == e.DOWN || k == e.PAGEDOWN) {
+ e.stopEvent();
+ pageNum = me.readPageFromInput(pageData);
+ if (pageNum) {
+ if (k == e.DOWN || k == e.PAGEDOWN) {
+ increment *= -1;
+ }
+ pageNum += increment;
+ if (pageNum >= 1 && pageNum <= pageData.pages) {
+ field.setValue(pageNum);
+ }
+ }
+ }
+ },
+
+ // private
+ beforeLoad : function(){
+ if(this.rendered && this.refresh){
+ this.refresh.disable();
+ }
+ },
+
+ // private
+ doLoad : function(start){
+ if(this.fireEvent('beforechange', this, o) !== false){
+ this.store.load();
+ }
+ },
+
+ /**
+ * Move to the first page, has the same effect as clicking the 'first' button.
+ */
+ moveFirst : function(){
+ if (this.fireEvent('beforechange', this, 1) !== false){
+ this.store.loadPage(1);
+ }
+ },
+
+ /**
+ * Move to the previous page, has the same effect as clicking the 'previous' button.
+ */
+ movePrevious : function(){
+ var me = this,
+ prev = me.store.currentPage - 1;
+
+ if (prev > 0) {
+ if (me.fireEvent('beforechange', me, prev) !== false) {
+ me.store.previousPage();
+ }
+ }
+ },
+
+ /**
+ * Move to the next page, has the same effect as clicking the 'next' button.
+ */
+ moveNext : function(){
+ var me = this,
+ total = me.getPageData().pageCount,
+ next = me.store.currentPage + 1;
+
+ if (next <= total) {
+ if (me.fireEvent('beforechange', me, next) !== false) {
+ me.store.nextPage();
+ }
+ }
+ },
+
+ /**
+ * Move to the last page, has the same effect as clicking the 'last' button.
+ */
+ moveLast : function(){
+ var me = this,
+ last = me.getPageData().pageCount;
+
+ if (me.fireEvent('beforechange', me, last) !== false) {
+ me.store.loadPage(last);
+ }
+ },
+
+ /**
+ * Refresh the current page, has the same effect as clicking the 'refresh' button.
+ */
+ doRefresh : function(){
+ var me = this,
+ current = me.store.currentPage;
+
+ if (me.fireEvent('beforechange', me, current) !== false) {
+ me.store.loadPage(current);
+ }
+ },
+
+ /**
+ * Binds the paging toolbar to the specified {@link Ext.data.Store}
+ * @param {Ext.data.Store} store The store to bind to this toolbar
+ * @param {Boolean} initial (Optional) true to not remove listeners
+ */
+ bindStore : function(store, initial){
+ var me = this;
+
+ if (!initial && me.store) {
+ if(store !== me.store && me.store.autoDestroy){
+ me.store.destroyStore();
+ }else{
+ me.store.un('beforeload', me.beforeLoad, me);
+ me.store.un('load', me.onLoad, me);
+ me.store.un('exception', me.onLoadError, me);
+ }
+ if(!store){
+ me.store = null;
+ }
+ }
+ if (store) {
+ store = Ext.data.StoreManager.lookup(store);
+ store.on({
+ scope: me,
+ beforeload: me.beforeLoad,
+ load: me.onLoad,
+ exception: me.onLoadError
+ });
+ }
+ me.store = store;
+ },
+
+ /**
+ * Unbinds the paging toolbar from the specified {@link Ext.data.Store} **(deprecated)**
+ * @param {Ext.data.Store} store The data store to unbind
+ */
+ unbind : function(store){
+ this.bindStore(null);
+ },
+
+ /**
+ * Binds the paging toolbar to the specified {@link Ext.data.Store} **(deprecated)**
+ * @param {Ext.data.Store} store The data store to bind
+ */
+ bind : function(store){
+ this.bindStore(store);
+ },
+
+ // private
+ onDestroy : function(){
+ this.bindStore(null);
+ this.callParent();
+ }
+});
+
+/**
+ * An internally used DataView for {@link Ext.form.field.ComboBox ComboBox}.
+ */
+Ext.define('Ext.view.BoundList', {
+ extend: 'Ext.view.View',
+ alias: 'widget.boundlist',
+ alternateClassName: 'Ext.BoundList',
+ requires: ['Ext.layout.component.BoundList', 'Ext.toolbar.Paging'],
+
+ /**
+ * @cfg {Number} pageSize
+ * If greater than `0`, a {@link Ext.toolbar.Paging} is displayed at the bottom of the list and store
+ * queries will execute with page {@link Ext.data.Operation#start start} and
+ * {@link Ext.data.Operation#limit limit} parameters. Defaults to `0`.
+ */
+ pageSize: 0,
+
+ /**
+ * @property {Ext.toolbar.Paging} pagingToolbar
+ * A reference to the PagingToolbar instance in this view. Only populated if {@link #pageSize} is greater
+ * than zero and the BoundList has been rendered.
+ */
+
+ // private overrides
+ autoScroll: true,
+ baseCls: Ext.baseCSSPrefix + 'boundlist',
+ itemCls: Ext.baseCSSPrefix + 'boundlist-item',
+ listItemCls: '',
+ shadow: false,
+ trackOver: true,
+ refreshed: 0,
+
+ ariaRole: 'listbox',
+
+ componentLayout: 'boundlist',
+
+ renderTpl: ['<div id="{id}-listEl" class="list-ct"></div>'],
+
+ initComponent: function() {
+ var me = this,
+ baseCls = me.baseCls,
+ itemCls = me.itemCls;
+
+ me.selectedItemCls = baseCls + '-selected';
+ me.overItemCls = baseCls + '-item-over';
+ me.itemSelector = "." + itemCls;
+
+ if (me.floating) {
+ me.addCls(baseCls + '-floating');
+ }
+
+ if (!me.tpl) {
+ // should be setting aria-posinset based on entire set of data
+ // not filtered set
+ me.tpl = Ext.create('Ext.XTemplate',
+ '<ul><tpl for=".">',
+ '<li role="option" class="' + itemCls + '">' + me.getInnerTpl(me.displayField) + '</li>',
+ '</tpl></ul>'
+ );
+ } else if (Ext.isString(me.tpl)) {
+ me.tpl = Ext.create('Ext.XTemplate', me.tpl);
+ }
+
+ if (me.pageSize) {
+ me.pagingToolbar = me.createPagingToolbar();
+ }
+
+ me.callParent();
+
+ me.addChildEls('listEl');
+ },
+
+ createPagingToolbar: function() {
+ return Ext.widget('pagingtoolbar', {
+ pageSize: this.pageSize,
+ store: this.store,
+ border: false
+ });
+ },
+
+ onRender: function() {
+ var me = this,
+ toolbar = me.pagingToolbar;
+ me.callParent(arguments);
+ if (toolbar) {
+ toolbar.render(me.el);
+ }
+ },
+
+ bindStore : function(store, initial) {
+ var me = this,
+ toolbar = me.pagingToolbar;
+ me.callParent(arguments);
+ if (toolbar) {
+ toolbar.bindStore(store, initial);
+ }
+ },
+
+ getTargetEl: function() {
+ return this.listEl || this.el;
+ },
+
+ getInnerTpl: function(displayField) {
+ return '{' + displayField + '}';
+ },
+
+ refresh: function() {
+ var me = this;
+ me.callParent();
+ if (me.isVisible()) {
+ me.refreshed++;
+ me.doComponentLayout();
+ me.refreshed--;
+ }
+ },
+
+ initAria: function() {
+ this.callParent();
+
+ var selModel = this.getSelectionModel(),
+ mode = selModel.getSelectionMode(),
+ actionEl = this.getActionEl();
+
+ // TODO: subscribe to mode changes or allow the selModel to manipulate this attribute.
+ if (mode !== 'SINGLE') {
+ actionEl.dom.setAttribute('aria-multiselectable', true);
+ }
+ },
+
+ onDestroy: function() {
+ Ext.destroyMembers(this, 'pagingToolbar', 'listEl');
+ this.callParent();
+ }
+});
+
+/**
+ * @class Ext.view.BoundListKeyNav
+ * @extends Ext.util.KeyNav
+ * A specialized {@link Ext.util.KeyNav} implementation for navigating a {@link Ext.view.BoundList} using
+ * the keyboard. The up, down, pageup, pagedown, home, and end keys move the active highlight
+ * through the list. The enter key invokes the selection model's select action using the highlighted item.
+ */
+Ext.define('Ext.view.BoundListKeyNav', {
+ extend: 'Ext.util.KeyNav',
+ requires: 'Ext.view.BoundList',
+
+ /**
+ * @cfg {Ext.view.BoundList} boundList (required)
+ * The {@link Ext.view.BoundList} instance for which key navigation will be managed.
+ */
+
+ constructor: function(el, config) {
+ var me = this;
+ me.boundList = config.boundList;
+ me.callParent([el, Ext.apply({}, config, me.defaultHandlers)]);
+ },
+
+ defaultHandlers: {
+ up: function() {
+ var me = this,
+ boundList = me.boundList,
+ allItems = boundList.all,
+ oldItem = boundList.highlightedItem,
+ oldItemIdx = oldItem ? boundList.indexOf(oldItem) : -1,
+ newItemIdx = oldItemIdx > 0 ? oldItemIdx - 1 : allItems.getCount() - 1; //wraps around
+ me.highlightAt(newItemIdx);
+ },
+
+ down: function() {
+ var me = this,
+ boundList = me.boundList,
+ allItems = boundList.all,
+ oldItem = boundList.highlightedItem,
+ oldItemIdx = oldItem ? boundList.indexOf(oldItem) : -1,
+ newItemIdx = oldItemIdx < allItems.getCount() - 1 ? oldItemIdx + 1 : 0; //wraps around
+ me.highlightAt(newItemIdx);
+ },
+
+ pageup: function() {
+ //TODO
+ },
+
+ pagedown: function() {
+ //TODO
+ },
+
+ home: function() {
+ this.highlightAt(0);
+ },
+
+ end: function() {
+ var me = this;
+ me.highlightAt(me.boundList.all.getCount() - 1);
+ },
+
+ enter: function(e) {
+ this.selectHighlighted(e);
+ }
+ },
+
+ /**
+ * Highlights the item at the given index.
+ * @param {Number} index
+ */
+ highlightAt: function(index) {
+ var boundList = this.boundList,
+ item = boundList.all.item(index);
+ if (item) {
+ item = item.dom;
+ boundList.highlightItem(item);
+ boundList.getTargetEl().scrollChildIntoView(item, false);
+ }
+ },
+
+ /**
+ * Triggers selection of the currently highlighted item according to the behavior of
+ * the configured SelectionModel.
+ */
+ selectHighlighted: function(e) {
+ var me = this,
+ boundList = me.boundList,
+ highlighted = boundList.highlightedItem,
+ selModel = boundList.getSelectionModel();
+ if (highlighted) {
+ selModel.selectWithEvent(boundList.getRecord(highlighted), e);
+ }
+ }
+
+});
+/**
+ * @docauthor Jason Johnston <jason@sencha.com>
+ *
+ * A combobox control with support for autocomplete, remote loading, and many other features.
+ *
+ * A ComboBox is like a combination of a traditional HTML text `<input>` field and a `<select>`
+ * field; the user is able to type freely into the field, and/or pick values from a dropdown selection
+ * list. The user can input any value by default, even if it does not appear in the selection list;
+ * to prevent free-form values and restrict them to items in the list, set {@link #forceSelection} to `true`.
+ *
+ * The selection list's options are populated from any {@link Ext.data.Store}, including remote
+ * stores. The data items in the store are mapped to each option's displayed text and backing value via
+ * the {@link #valueField} and {@link #displayField} configurations, respectively.
+ *
+ * If your store is not remote, i.e. it depends only on local data and is loaded up front, you should be
+ * sure to set the {@link #queryMode} to `'local'`, as this will improve responsiveness for the user.
+ *
+ * # Example usage:
+ *
+ * @example
+ * // The data store containing the list of states
+ * var states = Ext.create('Ext.data.Store', {
+ * fields: ['abbr', 'name'],
+ * data : [
+ * {"abbr":"AL", "name":"Alabama"},
+ * {"abbr":"AK", "name":"Alaska"},
+ * {"abbr":"AZ", "name":"Arizona"}
+ * //...
+ * ]
+ * });
+ *
+ * // Create the combo box, attached to the states data store
+ * Ext.create('Ext.form.ComboBox', {
+ * fieldLabel: 'Choose State',
+ * store: states,
+ * queryMode: 'local',
+ * displayField: 'name',
+ * valueField: 'abbr',
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * # Events
+ *
+ * To do something when something in ComboBox is selected, configure the select event:
+ *
+ * var cb = Ext.create('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.field.ComboBox(yourOptions);
+ * cb.on('select', yourFunction, yourScope);
+ *
+ * # Multiple Selection
+ *
+ * ComboBox also allows selection of multiple items from the list; to enable multi-selection set the
+ * {@link #multiSelect} config to `true`.
+ */
+Ext.define('Ext.form.field.ComboBox', {
+ extend:'Ext.form.field.Picker',
+ requires: ['Ext.util.DelayedTask', 'Ext.EventObject', 'Ext.view.BoundList', 'Ext.view.BoundListKeyNav', 'Ext.data.StoreManager'],
+ alternateClassName: 'Ext.form.ComboBox',
+ alias: ['widget.combobox', 'widget.combo'],
+
+ /**
+ * @cfg {String} [triggerCls='x-form-arrow-trigger']
+ * An additional CSS class used to style the trigger button. The trigger will always get the {@link #triggerBaseCls}
+ * by default and `triggerCls` will be **appended** if specified.
+ */
+ triggerCls: Ext.baseCSSPrefix + 'form-arrow-trigger',
+
+ /**
+ * @private
+ * @cfg {String}
+ * CSS class used to find the {@link #hiddenDataEl}
+ */
+ hiddenDataCls: Ext.baseCSSPrefix + 'hide-display ' + Ext.baseCSSPrefix + 'form-data-hidden',
+
+ /**
+ * @override
+ */
+ fieldSubTpl: [
+ '<div class="{hiddenDataCls}" role="presentation"></div>',
+ '<input id="{id}" type="{type}" ',
+ '<tpl if="size">size="{size}" </tpl>',
+ '<tpl if="tabIdx">tabIndex="{tabIdx}" </tpl>',
+ 'class="{fieldCls} {typeCls}" autocomplete="off" />',
+ '<div id="{cmpId}-triggerWrap" class="{triggerWrapCls}" role="presentation">',
+ '{triggerEl}',
+ '<div class="{clearCls}" role="presentation"></div>',
+ '</div>',
+ {
+ compiled: true,
+ disableFormats: true
+ }
+ ],
+
+ getSubTplData: function(){
+ var me = this;
+ Ext.applyIf(me.subTplData, {
+ hiddenDataCls: me.hiddenDataCls
+ });
+ return me.callParent(arguments);
+ },
+
+ afterRender: function(){
+ var me = this;
+ me.callParent(arguments);
+ me.setHiddenValue(me.value);
+ },
+
+ /**
+ * @cfg {Ext.data.Store/Array} store
+ * The data source to which this combo is bound. Acceptable values for this property are:
+ *
+ * - **any {@link Ext.data.Store Store} subclass**
+ * - **an Array** : Arrays will be converted to a {@link Ext.data.Store} 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 #queryMode}.
+ */
+
+ /**
+ * @cfg {Boolean} multiSelect
+ * If set to `true`, allows the combo field to hold more than one value at a time, and allows selecting multiple
+ * items from the dropdown list. The combo's text field will show all selected values separated by the
+ * {@link #delimiter}.
+ */
+ multiSelect: false,
+
+ /**
+ * @cfg {String} delimiter
+ * The character(s) used to separate the {@link #displayField display values} of multiple selected items when
+ * `{@link #multiSelect} = true`.
+ */
+ delimiter: ', ',
+
+ /**
+ * @cfg {String} displayField
+ * The underlying {@link Ext.data.Field#name data field name} to bind to this ComboBox.
+ *
+ * See also `{@link #valueField}`.
+ */
+ displayField: 'text',
+
+ /**
+ * @cfg {String} valueField (required)
+ * The underlying {@link Ext.data.Field#name data value name} to bind to this ComboBox (defaults to match
+ * the value of the {@link #displayField} config).
+ *
+ * **Note**: use of a `valueField` requires the user to make a selection in order for a value to be mapped. See also
+ * `{@link #displayField}`.
+ */
+
+ /**
+ * @cfg {String} triggerAction
+ * The action to execute when the trigger is clicked.
+ *
+ * - **`'all'`** :
+ *
+ * {@link #doQuery run the query} specified by the `{@link #allQuery}` config option
+ *
+ * - **`'query'`** :
+ *
+ * {@link #doQuery run the query} using the {@link Ext.form.field.Base#getRawValue raw value}.
+ *
+ * See also `{@link #queryParam}`.
+ */
+ triggerAction: 'all',
+
+ /**
+ * @cfg {String} allQuery
+ * The text query to send to the server to return all records for the list with no filtering
+ */
+ allQuery: '',
+
+ /**
+ * @cfg {String} queryParam
+ * Name of the parameter used by the Store to pass the typed string when the ComboBox is configured with
+ * `{@link #queryMode}: 'remote'`. If explicitly set to a falsy value it will not be sent.
+ */
+ queryParam: 'query',
+
+ /**
+ * @cfg {String} queryMode
+ * The mode in which the ComboBox uses the configured Store. Acceptable values are:
+ *
+ * - **`'remote'`** :
+ *
+ * In `queryMode: 'remote'`, the ComboBox loads its Store dynamically based upon user interaction.
+ *
+ * This is typically used for "autocomplete" type inputs, and after the user finishes typing, the Store is {@link
+ * Ext.data.Store#load load}ed.
+ *
+ * A parameter containing the typed string is sent in the load request. The default parameter name for the input
+ * string is `query`, but this can be configured using the {@link #queryParam} config.
+ *
+ * In `queryMode: 'remote'`, the Store may be configured with `{@link Ext.data.Store#remoteFilter remoteFilter}:
+ * true`, and further filters may be _programatically_ added to the Store which are then passed with every load
+ * request which allows the server to further refine the returned dataset.
+ *
+ * Typically, in an autocomplete situation, {@link #hideTrigger} is configured `true` because it has no meaning for
+ * autocomplete.
+ *
+ * - **`'local'`** :
+ *
+ * ComboBox loads local data
+ *
+ * var combo = new Ext.form.field.ComboBox({
+ * renderTo: document.body,
+ * queryMode: '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'
+ * });
+ */
+ queryMode: 'remote',
+
+ queryCaching: true,
+
+ /**
+ * @cfg {Number} pageSize
+ * If greater than `0`, a {@link Ext.toolbar.Paging} is displayed in the footer of the dropdown list and the
+ * {@link #doQuery filter queries} will execute with page start and {@link Ext.view.BoundList#pageSize limit}
+ * parameters. Only applies when `{@link #queryMode} = 'remote'`.
+ */
+ pageSize: 0,
+
+ /**
+ * @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 #queryMode} = 'remote'` or `10` if `{@link #queryMode} = 'local'`)
+ */
+
+ /**
+ * @cfg {Number} minChars
+ * The minimum number of characters the user must type before autocomplete and {@link #typeAhead} activate (defaults
+ * to `4` if `{@link #queryMode} = 'remote'` or `0` if `{@link #queryMode} = 'local'`, does not apply if
+ * `{@link Ext.form.field.Trigger#editable editable} = false`).
+ */
+
+ /**
+ * @cfg {Boolean} autoSelect
+ * `true` to automatically highlight the first result gathered by the data store in the dropdown list when it is
+ * opened. A false value would cause nothing in the list to be highlighted automatically, so
+ * the user would have to manually highlight an item before pressing the enter or {@link #selectOnTab tab} key to
+ * select it (unless the value of ({@link #typeAhead}) were true), or use the mouse to select a value.
+ */
+ 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.
+ */
+ typeAhead: false,
+
+ /**
+ * @cfg {Number} typeAheadDelay
+ * The length of time in milliseconds to wait until the typeahead text is displayed if `{@link #typeAhead} = true`
+ */
+ typeAheadDelay: 250,
+
+ /**
+ * @cfg {Boolean} selectOnTab
+ * Whether the Tab key should select the currently highlighted item.
+ */
+ selectOnTab: true,
+
+ /**
+ * @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.
+ */
+ forceSelection: false,
+
+ /**
+ * @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. If this default text is used, it means there
+ * is no value set and no validation will occur on this field.
+ */
+
+ /**
+ * @property {String} lastQuery
+ * 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.field.ComboBox({
+ * ...
+ * queryMode: '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.field.ComboBox({
+ * ...
+ * queryMode: 'local',
+ * triggerAction: 'all',
+ * lastQuery: ''
+ * });
+ */
+
+ /**
+ * @cfg {Object} defaultListConfig
+ * Set of options that will be used as defaults for the user-configured {@link #listConfig} object.
+ */
+ defaultListConfig: {
+ emptyText: '',
+ loadingText: 'Loading...',
+ loadingHeight: 70,
+ minWidth: 70,
+ maxHeight: 300,
+ shadow: 'sides'
+ },
+
+ /**
+ * @cfg {String/HTMLElement/Ext.Element} transform
+ * The id, DOM node or {@link Ext.Element} of an existing HTML `<select>` element to convert into a ComboBox. The
+ * target select's options will be used to build the options in the ComboBox dropdown; a configured {@link #store}
+ * will take precedence over this.
+ */
+
+ /**
+ * @cfg {Object} listConfig
+ * An optional set of configuration properties that will be passed to the {@link Ext.view.BoundList}'s constructor.
+ * Any configuration that is valid for BoundList can be included. Some of the more useful ones are:
+ *
+ * - {@link Ext.view.BoundList#cls} - defaults to empty
+ * - {@link Ext.view.BoundList#emptyText} - defaults to empty string
+ * - {@link Ext.view.BoundList#itemSelector} - defaults to the value defined in BoundList
+ * - {@link Ext.view.BoundList#loadingText} - defaults to `'Loading...'`
+ * - {@link Ext.view.BoundList#minWidth} - defaults to `70`
+ * - {@link Ext.view.BoundList#maxWidth} - defaults to `undefined`
+ * - {@link Ext.view.BoundList#maxHeight} - defaults to `300`
+ * - {@link Ext.view.BoundList#resizable} - defaults to `false`
+ * - {@link Ext.view.BoundList#shadow} - defaults to `'sides'`
+ * - {@link Ext.view.BoundList#width} - defaults to `undefined` (automatically set to the width of the ComboBox
+ * field if {@link #matchFieldWidth} is true)
+ */
+
+ //private
+ ignoreSelection: 0,
+
+ initComponent: function() {
+ var me = this,
+ isDefined = Ext.isDefined,
+ store = me.store,
+ transform = me.transform,
+ transformSelect, isLocalMode;
+
+ Ext.applyIf(me.renderSelectors, {
+ hiddenDataEl: '.' + me.hiddenDataCls.split(' ').join('.')
+ });
+
+
+ this.addEvents(
+ /**
+ * @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.field.ComboBox
+ *
+ * This combo box
+ *
+ * - `query` : String
+ *
+ * The query string
+ *
+ * - `forceAll` : Boolean
+ *
+ * True to force "all" query
+ *
+ * - `cancel` : Boolean
+ *
+ * Set to true to cancel the query
+ */
+ 'beforequery',
+
+ /**
+ * @event select
+ * Fires when at least one list item is selected.
+ * @param {Ext.form.field.ComboBox} combo This combo box
+ * @param {Array} records The selected records
+ */
+ 'select',
+
+ /**
+ * @event beforeselect
+ * Fires before the selected item is added to the collection
+ * @param {Ext.form.field.ComboBox} combo This combo box
+ * @param {Ext.data.Record} record The selected record
+ * @param {Number} index The index of the selected record
+ */
+ 'beforeselect',
+
+ /**
+ * @event beforedeselect
+ * Fires before the deselected item is removed from the collection
+ * @param {Ext.form.field.ComboBox} combo This combo box
+ * @param {Ext.data.Record} record The deselected record
+ * @param {Number} index The index of the deselected record
+ */
+ 'beforedeselect'
+ );
+
+ // Build store from 'transform' HTML select element's options
+ if (transform) {
+ transformSelect = Ext.getDom(transform);
+ if (transformSelect) {
+ store = Ext.Array.map(Ext.Array.from(transformSelect.options), function(option) {
+ return [option.value, option.text];
+ });
+ if (!me.name) {
+ me.name = transformSelect.name;
+ }
+ if (!('value' in me)) {
+ me.value = transformSelect.value;
+ }
+ }
+ }
+
+ me.bindStore(store || 'ext-empty-store', true);
+ store = me.store;
+ if (store.autoCreated) {
+ me.queryMode = 'local';
+ me.valueField = me.displayField = 'field1';
+ if (!store.expanded) {
+ me.displayField = 'field2';
+ }
+ }
+
+
+ if (!isDefined(me.valueField)) {
+ me.valueField = me.displayField;
+ }
+
+ isLocalMode = me.queryMode === 'local';
+ if (!isDefined(me.queryDelay)) {
+ me.queryDelay = isLocalMode ? 10 : 500;
+ }
+ if (!isDefined(me.minChars)) {
+ me.minChars = isLocalMode ? 0 : 4;
+ }
+
+ if (!me.displayTpl) {
+ me.displayTpl = Ext.create('Ext.XTemplate',
+ '<tpl for=".">' +
+ '{[typeof values === "string" ? values : values["' + me.displayField + '"]]}' +
+ '<tpl if="xindex < xcount">' + me.delimiter + '</tpl>' +
+ '</tpl>'
+ );
+ } else if (Ext.isString(me.displayTpl)) {
+ me.displayTpl = Ext.create('Ext.XTemplate', me.displayTpl);
+ }
+
+ me.callParent();
+
+ me.doQueryTask = Ext.create('Ext.util.DelayedTask', me.doRawQuery, me);
+
+ // store has already been loaded, setValue
+ if (me.store.getCount() > 0) {
+ me.setValue(me.value);
+ }
+
+ // render in place of 'transform' select
+ if (transformSelect) {
+ me.render(transformSelect.parentNode, transformSelect);
+ Ext.removeNode(transformSelect);
+ delete me.renderTo;
+ }
+ },
+
+ /**
+ * Returns the store associated with this ComboBox.
+ * @return {Ext.data.Store} The store
+ */
+ getStore : function(){
+ return this.store;
+ },
+
+ beforeBlur: function() {
+ this.doQueryTask.cancel();
+ this.assertValue();
+ },
+
+ // private
+ assertValue: function() {
+ var me = this,
+ value = me.getRawValue(),
+ rec;
+
+ if (me.forceSelection) {
+ if (me.multiSelect) {
+ // For multiselect, check that the current displayed value matches the current
+ // selection, if it does not then revert to the most recent selection.
+ if (value !== me.getDisplayValue()) {
+ me.setValue(me.lastSelection);
+ }
+ } else {
+ // For single-select, match the displayed value to a record and select it,
+ // if it does not match a record then revert to the most recent selection.
+ rec = me.findRecordByDisplay(value);
+ if (rec) {
+ me.select(rec);
+ } else {
+ me.setValue(me.lastSelection);
+ }
+ }
+ }
+ me.collapse();
+ },
+
+ onTypeAhead: function() {
+ var me = this,
+ displayField = me.displayField,
+ record = me.store.findRecord(displayField, me.getRawValue()),
+ boundList = me.getPicker(),
+ newValue, len, selStart;
+
+ if (record) {
+ newValue = record.get(displayField);
+ len = newValue.length;
+ selStart = me.getRawValue().length;
+
+ boundList.highlightItem(boundList.getNode(record));
+
+ if (selStart !== 0 && selStart !== len) {
+ me.setRawValue(newValue);
+ me.selectText(selStart, newValue.length);
+ }
+ }
+ },
+
+ // invoked when a different store is bound to this combo
+ // than the original
+ resetToDefault: function() {
+
+ },
+
+ bindStore: function(store, initial) {
+ var me = this,
+ oldStore = me.store;
+
+ // this code directly accesses this.picker, bc invoking getPicker
+ // would create it when we may be preping to destroy it
+ if (oldStore && !initial) {
+ if (oldStore !== store && oldStore.autoDestroy) {
+ oldStore.destroyStore();
+ } else {
+ oldStore.un({
+ scope: me,
+ load: me.onLoad,
+ exception: me.collapse
+ });
+ }
+ if (!store) {
+ me.store = null;
+ if (me.picker) {
+ me.picker.bindStore(null);
+ }
+ }
+ }
+ if (store) {
+ if (!initial) {
+ me.resetToDefault();
+ }
+
+ me.store = Ext.data.StoreManager.lookup(store);
+ me.store.on({
+ scope: me,
+ load: me.onLoad,
+ exception: me.collapse
+ });
+
+ if (me.picker) {
+ me.picker.bindStore(store);
+ }
+ }
+ },
+
+ onLoad: function() {
+ var me = this,
+ value = me.value;
+
+ // If performing a remote query upon the raw value...
+ if (me.rawQuery) {
+ me.rawQuery = false;
+ me.syncSelection();
+ if (me.picker && !me.picker.getSelectionModel().hasSelection()) {
+ me.doAutoSelect();
+ }
+ }
+ // If store initial load or triggerAction: 'all' trigger click.
+ else {
+ // Set the value on load
+ if (me.value) {
+ me.setValue(me.value);
+ } else {
+ // There's no value.
+ // Highlight the first item in the list if autoSelect: true
+ if (me.store.getCount()) {
+ me.doAutoSelect();
+ } else {
+ me.setValue('');
+ }
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Execute the query with the raw contents within the textfield.
+ */
+ doRawQuery: function() {
+ this.doQuery(this.getRawValue(), false, true);
+ },
+
+ /**
+ * Executes 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} queryString The SQL query to execute
+ * @param {Boolean} [forceAll=false] `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.
+ * @param {Boolean} [rawQuery=false] Pass as true if the raw typed value is being used as the query string. This causes the
+ * resulting store load to leave the raw value undisturbed.
+ * @return {Boolean} true if the query was permitted to run, false if it was cancelled by a {@link #beforequery}
+ * handler.
+ */
+ doQuery: function(queryString, forceAll, rawQuery) {
+ queryString = queryString || '';
+
+ // store in object and pass by reference in 'beforequery'
+ // so that client code can modify values.
+ var me = this,
+ qe = {
+ query: queryString,
+ forceAll: forceAll,
+ combo: me,
+ cancel: false
+ },
+ store = me.store,
+ isLocalMode = me.queryMode === 'local';
+
+ if (me.fireEvent('beforequery', qe) === false || qe.cancel) {
+ return false;
+ }
+
+ // get back out possibly modified values
+ queryString = qe.query;
+ forceAll = qe.forceAll;
+
+ // query permitted to run
+ if (forceAll || (queryString.length >= me.minChars)) {
+ // expand before starting query so LoadMask can position itself correctly
+ me.expand();
+
+ // make sure they aren't querying the same thing
+ if (!me.queryCaching || me.lastQuery !== queryString) {
+ me.lastQuery = queryString;
+
+ if (isLocalMode) {
+ // forceAll means no filtering - show whole dataset.
+ if (forceAll) {
+ store.clearFilter();
+ } else {
+ // Clear filter, but supress event so that the BoundList is not immediately updated.
+ store.clearFilter(true);
+ store.filter(me.displayField, queryString);
+ }
+ } else {
+ // Set flag for onLoad handling to know how the Store was loaded
+ me.rawQuery = rawQuery;
+
+ // In queryMode: 'remote', we assume Store filters are added by the developer as remote filters,
+ // and these are automatically passed as params with every load call, so we do *not* call clearFilter.
+ if (me.pageSize) {
+ // if we're paging, we've changed the query so start at page 1.
+ me.loadPage(1);
+ } else {
+ store.load({
+ params: me.getParams(queryString)
+ });
+ }
+ }
+ }
+
+ // Clear current selection if it does not match the current value in the field
+ if (me.getRawValue() !== me.getDisplayValue()) {
+ me.ignoreSelection++;
+ me.picker.getSelectionModel().deselectAll();
+ me.ignoreSelection--;
+ }
+
+ if (isLocalMode) {
+ me.doAutoSelect();
+ }
+ if (me.typeAhead) {
+ me.doTypeAhead();
+ }
+ }
+ return true;
+ },
+
+ loadPage: function(pageNum){
+ this.store.loadPage(pageNum, {
+ params: this.getParams(this.lastQuery)
+ });
+ },
+
+ onPageChange: function(toolbar, newPage){
+ /*
+ * Return false here so we can call load ourselves and inject the query param.
+ * We don't want to do this for every store load since the developer may load
+ * the store through some other means so we won't add the query param.
+ */
+ this.loadPage(newPage);
+ return false;
+ },
+
+ // private
+ getParams: function(queryString) {
+ var params = {},
+ param = this.queryParam;
+
+ if (param) {
+ params[param] = queryString;
+ }
+ return params;
+ },
+
+ /**
+ * @private
+ * If the autoSelect config is true, and the picker is open, highlights the first item.
+ */
+ doAutoSelect: function() {
+ var me = this,
+ picker = me.picker,
+ lastSelected, itemNode;
+ if (picker && me.autoSelect && me.store.getCount() > 0) {
+ // Highlight the last selected item and scroll it into view
+ lastSelected = picker.getSelectionModel().lastSelected;
+ itemNode = picker.getNode(lastSelected || 0);
+ if (itemNode) {
+ picker.highlightItem(itemNode);
+ picker.listEl.scrollChildIntoView(itemNode, false);
+ }
+ }
+ },
+
+ doTypeAhead: function() {
+ if (!this.typeAheadTask) {
+ this.typeAheadTask = Ext.create('Ext.util.DelayedTask', this.onTypeAhead, this);
+ }
+ if (this.lastKey != Ext.EventObject.BACKSPACE && this.lastKey != Ext.EventObject.DELETE) {
+ this.typeAheadTask.delay(this.typeAheadDelay);
+ }
+ },
+
+ onTriggerClick: function() {
+ var me = this;
+ if (!me.readOnly && !me.disabled) {
+ if (me.isExpanded) {
+ me.collapse();
+ } else {
+ me.onFocus({});
+ if (me.triggerAction === 'all') {
+ me.doQuery(me.allQuery, true);
+ } else {
+ me.doQuery(me.getRawValue(), false, true);
+ }
+ }
+ me.inputEl.focus();
+ }
+ },
+
+
+ // store the last key and doQuery if relevant
+ onKeyUp: function(e, t) {
+ var me = this,
+ key = e.getKey();
+
+ if (!me.readOnly && !me.disabled && me.editable) {
+ me.lastKey = key;
+ // we put this in a task so that we can cancel it if a user is
+ // in and out before the queryDelay elapses
+
+ // perform query w/ any normal key or backspace or delete
+ if (!e.isSpecialKey() || key == e.BACKSPACE || key == e.DELETE) {
+ me.doQueryTask.delay(me.queryDelay);
+ }
+ }
+
+ if (me.enableKeyEvents) {
+ me.callParent(arguments);
+ }
+ },
+
+ initEvents: function() {
+ var me = this;
+ me.callParent();
+
+ /*
+ * Setup keyboard handling. If enableKeyEvents is true, we already have
+ * a listener on the inputEl for keyup, so don't create a second.
+ */
+ if (!me.enableKeyEvents) {
+ me.mon(me.inputEl, 'keyup', me.onKeyUp, me);
+ }
+ },
+
+ onDestroy: function(){
+ this.bindStore(null);
+ this.callParent();
+ },
+
+ createPicker: function() {
+ var me = this,
+ picker,
+ menuCls = Ext.baseCSSPrefix + 'menu',
+ opts = Ext.apply({
+ pickerField: me,
+ selModel: {
+ mode: me.multiSelect ? 'SIMPLE' : 'SINGLE'
+ },
+ floating: true,
+ hidden: true,
+ ownerCt: me.ownerCt,
+ cls: me.el.up('.' + menuCls) ? menuCls : '',
+ store: me.store,
+ displayField: me.displayField,
+ focusOnToFront: false,
+ pageSize: me.pageSize,
+ tpl: me.tpl
+ }, me.listConfig, me.defaultListConfig);
+
+ picker = me.picker = Ext.create('Ext.view.BoundList', opts);
+ if (me.pageSize) {
+ picker.pagingToolbar.on('beforechange', me.onPageChange, me);
+ }
+
+ me.mon(picker, {
+ itemclick: me.onItemClick,
+ refresh: me.onListRefresh,
+ scope: me
+ });
+
+ me.mon(picker.getSelectionModel(), {
+ 'beforeselect': me.onBeforeSelect,
+ 'beforedeselect': me.onBeforeDeselect,
+ 'selectionchange': me.onListSelectionChange,
+ scope: me
+ });
+
+ return picker;
+ },
+
+ alignPicker: function(){
+ var me = this,
+ picker = me.picker,
+ heightAbove = me.getPosition()[1] - Ext.getBody().getScroll().top,
+ heightBelow = Ext.Element.getViewHeight() - heightAbove - me.getHeight(),
+ space = Math.max(heightAbove, heightBelow);
+
+ me.callParent();
+ if (picker.getHeight() > space) {
+ picker.setHeight(space - 5); // have some leeway so we aren't flush against
+ me.doAlign();
+ }
+ },
+
+ onListRefresh: function() {
+ this.alignPicker();
+ this.syncSelection();
+ },
+
+ onItemClick: function(picker, record){
+ /*
+ * If we're doing single selection, the selection change events won't fire when
+ * clicking on the selected element. Detect it here.
+ */
+ var me = this,
+ lastSelection = me.lastSelection,
+ valueField = me.valueField,
+ selected;
+
+ if (!me.multiSelect && lastSelection) {
+ selected = lastSelection[0];
+ if (selected && (record.get(valueField) === selected.get(valueField))) {
+ // Make sure we also update the display value if it's only partial
+ me.displayTplData = [record.data];
+ me.setRawValue(me.getDisplayValue());
+ me.collapse();
+ }
+ }
+ },
+
+ onBeforeSelect: function(list, record) {
+ return this.fireEvent('beforeselect', this, record, record.index);
+ },
+
+ onBeforeDeselect: function(list, record) {
+ return this.fireEvent('beforedeselect', this, record, record.index);
+ },
+
+ onListSelectionChange: function(list, selectedRecords) {
+ var me = this,
+ isMulti = me.multiSelect,
+ hasRecords = selectedRecords.length > 0;
+ // Only react to selection if it is not called from setValue, and if our list is
+ // expanded (ignores changes to the selection model triggered elsewhere)
+ if (!me.ignoreSelection && me.isExpanded) {
+ if (!isMulti) {
+ Ext.defer(me.collapse, 1, me);
+ }
+ /*
+ * Only set the value here if we're in multi selection mode or we have
+ * a selection. Otherwise setValue will be called with an empty value
+ * which will cause the change event to fire twice.
+ */
+ if (isMulti || hasRecords) {
+ me.setValue(selectedRecords, false);
+ }
+ if (hasRecords) {
+ me.fireEvent('select', me, selectedRecords);
+ }
+ me.inputEl.focus();
+ }
+ },
+
+ /**
+ * @private
+ * Enables the key nav for the BoundList when it is expanded.
+ */
+ onExpand: function() {
+ var me = this,
+ keyNav = me.listKeyNav,
+ selectOnTab = me.selectOnTab,
+ picker = me.getPicker();
+
+ // Handle BoundList navigation from the input field. Insert a tab listener specially to enable selectOnTab.
+ if (keyNav) {
+ keyNav.enable();
+ } else {
+ keyNav = me.listKeyNav = Ext.create('Ext.view.BoundListKeyNav', this.inputEl, {
+ boundList: picker,
+ forceKeyDown: true,
+ tab: function(e) {
+ if (selectOnTab) {
+ this.selectHighlighted(e);
+ me.triggerBlur();
+ }
+ // Tab key event is allowed to propagate to field
+ return true;
+ }
+ });
+ }
+
+ // While list is expanded, stop tab monitoring from Ext.form.field.Trigger so it doesn't short-circuit selectOnTab
+ if (selectOnTab) {
+ me.ignoreMonitorTab = true;
+ }
+
+ Ext.defer(keyNav.enable, 1, keyNav); //wait a bit so it doesn't react to the down arrow opening the picker
+ me.inputEl.focus();
+ },
+
+ /**
+ * @private
+ * Disables the key nav for the BoundList when it is collapsed.
+ */
+ onCollapse: function() {
+ var me = this,
+ keyNav = me.listKeyNav;
+ if (keyNav) {
+ keyNav.disable();
+ me.ignoreMonitorTab = false;
+ }
+ },
+
+ /**
+ * Selects an item by a {@link Ext.data.Model Model}, or by a key value.
+ * @param {Object} r
+ */
+ select: function(r) {
+ this.setValue(r, true);
+ },
+
+ /**
+ * Finds the record by searching for a specific field/value combination.
+ * @param {String} field The name of the field to test.
+ * @param {Object} value The value to match the field against.
+ * @return {Ext.data.Model} The matched record or false.
+ */
+ findRecord: function(field, value) {
+ var ds = this.store,
+ idx = ds.findExact(field, value);
+ return idx !== -1 ? ds.getAt(idx) : false;
+ },
+
+ /**
+ * Finds the record by searching values in the {@link #valueField}.
+ * @param {Object} value The value to match the field against.
+ * @return {Ext.data.Model} The matched record or false.
+ */
+ findRecordByValue: function(value) {
+ return this.findRecord(this.valueField, value);
+ },
+
+ /**
+ * Finds the record by searching values in the {@link #displayField}.
+ * @param {Object} value The value to match the field against.
+ * @return {Ext.data.Model} The matched record or false.
+ */
+ findRecordByDisplay: function(value) {
+ return this.findRecord(this.displayField, value);
+ },
+
+ /**
+ * Sets the specified value(s) into the field. For each value, if a record is found in the {@link #store} that
+ * matches based on the {@link #valueField}, then that record's {@link #displayField} will be displayed in the
+ * field. If no match is found, and the {@link #valueNotFoundText} config option is defined, then that will be
+ * displayed as the default field text. Otherwise a blank value will be shown, although the value will still be set.
+ * @param {String/String[]} value The value(s) to be set. Can be either a single String or {@link Ext.data.Model},
+ * or an Array of Strings or Models.
+ * @return {Ext.form.field.Field} this
+ */
+ setValue: function(value, doSelect) {
+ var me = this,
+ valueNotFoundText = me.valueNotFoundText,
+ inputEl = me.inputEl,
+ i, len, record,
+ models = [],
+ displayTplData = [],
+ processedValue = [];
+
+ if (me.store.loading) {
+ // Called while the Store is loading. Ensure it is processed by the onLoad method.
+ me.value = value;
+ me.setHiddenValue(me.value);
+ return me;
+ }
+
+ // This method processes multi-values, so ensure value is an array.
+ value = Ext.Array.from(value);
+
+ // Loop through values
+ for (i = 0, len = value.length; i < len; i++) {
+ record = value[i];
+ if (!record || !record.isModel) {
+ record = me.findRecordByValue(record);
+ }
+ // record found, select it.
+ if (record) {
+ models.push(record);
+ displayTplData.push(record.data);
+ processedValue.push(record.get(me.valueField));
+ }
+ // record was not found, this could happen because
+ // store is not loaded or they set a value not in the store
+ else {
+ // If we are allowing insertion of values not represented in the Store, then set the value, and the display value
+ if (!me.forceSelection) {
+ displayTplData.push(value[i]);
+ processedValue.push(value[i]);
+ }
+ // Else, if valueNotFoundText is defined, display it, otherwise display nothing for this value
+ else if (Ext.isDefined(valueNotFoundText)) {
+ displayTplData.push(valueNotFoundText);
+ }
+ }
+ }
+
+ // Set the value of this field. If we are multiselecting, then that is an array.
+ me.setHiddenValue(processedValue);
+ me.value = me.multiSelect ? processedValue : processedValue[0];
+ if (!Ext.isDefined(me.value)) {
+ me.value = null;
+ }
+ me.displayTplData = displayTplData; //store for getDisplayValue method
+ me.lastSelection = me.valueModels = models;
+
+ if (inputEl && me.emptyText && !Ext.isEmpty(value)) {
+ inputEl.removeCls(me.emptyCls);
+ }
+
+ // Calculate raw value from the collection of Model data
+ me.setRawValue(me.getDisplayValue());
+ me.checkChange();
+
+ if (doSelect !== false) {
+ me.syncSelection();
+ }
+ me.applyEmptyText();
+
+ return me;
+ },
+
+ /**
+ * @private
+ * Set the value of {@link #hiddenDataEl}
+ * Dynamically adds and removes input[type=hidden] elements
+ */
+ setHiddenValue: function(values){
+ var me = this, i;
+ if (!me.hiddenDataEl) {
+ return;
+ }
+ values = Ext.Array.from(values);
+ var dom = me.hiddenDataEl.dom,
+ childNodes = dom.childNodes,
+ input = childNodes[0],
+ valueCount = values.length,
+ childrenCount = childNodes.length;
+
+ if (!input && valueCount > 0) {
+ me.hiddenDataEl.update(Ext.DomHelper.markup({tag:'input', type:'hidden', name:me.name}));
+ childrenCount = 1;
+ input = dom.firstChild;
+ }
+ while (childrenCount > valueCount) {
+ dom.removeChild(childNodes[0]);
+ -- childrenCount;
+ }
+ while (childrenCount < valueCount) {
+ dom.appendChild(input.cloneNode(true));
+ ++ childrenCount;
+ }
+ for (i = 0; i < valueCount; i++) {
+ childNodes[i].value = values[i];
+ }
+ },
+
+ /**
+ * @private Generates the string value to be displayed in the text field for the currently stored value
+ */
+ getDisplayValue: function() {
+ return this.displayTpl.apply(this.displayTplData);
+ },
+
+ getValue: function() {
+ // If the user has not changed the raw field value since a value was selected from the list,
+ // then return the structured value from the selection. If the raw field value is different
+ // than what would be displayed due to selection, return that raw value.
+ var me = this,
+ picker = me.picker,
+ rawValue = me.getRawValue(), //current value of text field
+ value = me.value; //stored value from last selection or setValue() call
+
+ if (me.getDisplayValue() !== rawValue) {
+ value = rawValue;
+ me.value = me.displayTplData = me.valueModels = null;
+ if (picker) {
+ me.ignoreSelection++;
+ picker.getSelectionModel().deselectAll();
+ me.ignoreSelection--;
+ }
+ }
+
+ return value;
+ },
+
+ getSubmitValue: function() {
+ return this.getValue();
+ },
+
+ isEqual: function(v1, v2) {
+ var fromArray = Ext.Array.from,
+ i, len;
+
+ v1 = fromArray(v1);
+ v2 = fromArray(v2);
+ len = v1.length;
+
+ if (len !== v2.length) {
+ return false;
+ }
+
+ for(i = 0; i < len; i++) {
+ if (v2[i] !== v1[i]) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+
+ /**
+ * Clears any value currently set in the ComboBox.
+ */
+ clearValue: function() {
+ this.setValue([]);
+ },
+
+ /**
+ * @private Synchronizes the selection in the picker to match the current value of the combobox.
+ */
+ syncSelection: function() {
+ var me = this,
+ ExtArray = Ext.Array,
+ picker = me.picker,
+ selection, selModel;
+ if (picker) {
+ // From the value, find the Models that are in the store's current data
+ selection = [];
+ ExtArray.forEach(me.valueModels || [], function(value) {
+ if (value && value.isModel && me.store.indexOf(value) >= 0) {
+ selection.push(value);
+ }
+ });
+
+ // Update the selection to match
+ me.ignoreSelection++;
+ selModel = picker.getSelectionModel();
+ selModel.deselectAll();
+ if (selection.length) {
+ selModel.select(selection);
+ }
+ me.ignoreSelection--;
+ }
+ }
+});
+
+/**
+ * A month picker component. This class is used by the {@link Ext.picker.Date Date picker} class
+ * to allow browsing and selection of year/months combinations.
+ */
+Ext.define('Ext.picker.Month', {
+ extend: 'Ext.Component',
+ requires: ['Ext.XTemplate', 'Ext.util.ClickRepeater', 'Ext.Date', 'Ext.button.Button'],
+ alias: 'widget.monthpicker',
+ alternateClassName: 'Ext.MonthPicker',
+
+ renderTpl: [
+ '<div id="{id}-bodyEl" class="{baseCls}-body">',
+ '<div class="{baseCls}-months">',
+ '<tpl for="months">',
+ '<div class="{parent.baseCls}-item {parent.baseCls}-month"><a href="#" hidefocus="on">{.}</a></div>',
+ '</tpl>',
+ '</div>',
+ '<div class="{baseCls}-years">',
+ '<div class="{baseCls}-yearnav">',
+ '<button id="{id}-prevEl" class="{baseCls}-yearnav-prev"></button>',
+ '<button id="{id}-nextEl" class="{baseCls}-yearnav-next"></button>',
+ '</div>',
+ '<tpl for="years">',
+ '<div class="{parent.baseCls}-item {parent.baseCls}-year"><a href="#" hidefocus="on">{.}</a></div>',
+ '</tpl>',
+ '</div>',
+ '<div class="' + Ext.baseCSSPrefix + 'clear"></div>',
+ '</div>',
+ '<tpl if="showButtons">',
+ '<div id="{id}-buttonsEl" class="{baseCls}-buttons"></div>',
+ '</tpl>'
+ ],
+
+ /**
+ * @cfg {String} okText The text to display on the ok button.
+ */
+ okText: 'OK',
+
+ /**
+ * @cfg {String} cancelText The text to display on the cancel button.
+ */
+ cancelText: 'Cancel',
+
+ /**
+ * @cfg {String} baseCls The base CSS class to apply to the picker element. Defaults to <tt>'x-monthpicker'</tt>
+ */
+ baseCls: Ext.baseCSSPrefix + 'monthpicker',
+
+ /**
+ * @cfg {Boolean} showButtons True to show ok and cancel buttons below the picker.
+ */
+ showButtons: true,
+
+ /**
+ * @cfg {String} selectedCls The class to be added to selected items in the picker. Defaults to
+ * <tt>'x-monthpicker-selected'</tt>
+ */
+
+ /**
+ * @cfg {Date/Number[]} value The default value to set. See {@link #setValue}
+ */
+ width: 178,
+
+ // used when attached to date picker which isnt showing buttons
+ smallCls: Ext.baseCSSPrefix + 'monthpicker-small',
+
+ // private
+ totalYears: 10,
+ yearOffset: 5, // 10 years in total, 2 per row
+ monthOffset: 6, // 12 months, 2 per row
+
+ // private, inherit docs
+ initComponent: function(){
+ var me = this;
+
+ me.selectedCls = me.baseCls + '-selected';
+ me.addEvents(
+ /**
+ * @event cancelclick
+ * Fires when the cancel button is pressed.
+ * @param {Ext.picker.Month} this
+ */
+ 'cancelclick',
+
+ /**
+ * @event monthclick
+ * Fires when a month is clicked.
+ * @param {Ext.picker.Month} this
+ * @param {Array} value The current value
+ */
+ 'monthclick',
+
+ /**
+ * @event monthdblclick
+ * Fires when a month is clicked.
+ * @param {Ext.picker.Month} this
+ * @param {Array} value The current value
+ */
+ 'monthdblclick',
+
+ /**
+ * @event okclick
+ * Fires when the ok button is pressed.
+ * @param {Ext.picker.Month} this
+ * @param {Array} value The current value
+ */
+ 'okclick',
+
+ /**
+ * @event select
+ * Fires when a month/year is selected.
+ * @param {Ext.picker.Month} this
+ * @param {Array} value The current value
+ */
+ 'select',
+
+ /**
+ * @event yearclick
+ * Fires when a year is clicked.
+ * @param {Ext.picker.Month} this
+ * @param {Array} value The current value
+ */
+ 'yearclick',
+
+ /**
+ * @event yeardblclick
+ * Fires when a year is clicked.
+ * @param {Ext.picker.Month} this
+ * @param {Array} value The current value
+ */
+ 'yeardblclick'
+ );
+ if (me.small) {
+ me.addCls(me.smallCls);
+ }
+ me.setValue(me.value);
+ me.activeYear = me.getYear(new Date().getFullYear() - 4, -4);
+ this.callParent();
+ },
+
+ // private, inherit docs
+ onRender: function(ct, position){
+ var me = this,
+ i = 0,
+ months = [],
+ shortName = Ext.Date.getShortMonthName,
+ monthLen = me.monthOffset;
+
+ for (; i < monthLen; ++i) {
+ months.push(shortName(i), shortName(i + monthLen));
+ }
+
+ Ext.apply(me.renderData, {
+ months: months,
+ years: me.getYears(),
+ showButtons: me.showButtons
+ });
+
+ me.addChildEls('bodyEl', 'prevEl', 'nextEl', 'buttonsEl');
+
+ me.callParent(arguments);
+ },
+
+ // private, inherit docs
+ afterRender: function(){
+ var me = this,
+ body = me.bodyEl,
+ buttonsEl = me.buttonsEl;
+
+ me.callParent();
+
+ me.mon(body, 'click', me.onBodyClick, me);
+ me.mon(body, 'dblclick', me.onBodyClick, me);
+
+ // keep a reference to the year/month elements since we'll be re-using them
+ me.years = body.select('.' + me.baseCls + '-year a');
+ me.months = body.select('.' + me.baseCls + '-month a');
+
+ if (me.showButtons) {
+ me.okBtn = Ext.create('Ext.button.Button', {
+ text: me.okText,
+ renderTo: buttonsEl,
+ handler: me.onOkClick,
+ scope: me
+ });
+ me.cancelBtn = Ext.create('Ext.button.Button', {
+ text: me.cancelText,
+ renderTo: buttonsEl,
+ handler: me.onCancelClick,
+ scope: me
+ });
+ }
+
+ me.backRepeater = Ext.create('Ext.util.ClickRepeater', me.prevEl, {
+ handler: Ext.Function.bind(me.adjustYear, me, [-me.totalYears])
+ });
+
+ me.prevEl.addClsOnOver(me.baseCls + '-yearnav-prev-over');
+ me.nextRepeater = Ext.create('Ext.util.ClickRepeater', me.nextEl, {
+ handler: Ext.Function.bind(me.adjustYear, me, [me.totalYears])
+ });
+ me.nextEl.addClsOnOver(me.baseCls + '-yearnav-next-over');
+ me.updateBody();
+ },
+
+ /**
+ * Set the value for the picker.
+ * @param {Date/Number[]} value The value to set. It can be a Date object, where the month/year will be extracted, or
+ * it can be an array, with the month as the first index and the year as the second.
+ * @return {Ext.picker.Month} this
+ */
+ setValue: function(value){
+ var me = this,
+ active = me.activeYear,
+ offset = me.monthOffset,
+ year,
+ index;
+
+ if (!value) {
+ me.value = [null, null];
+ } else if (Ext.isDate(value)) {
+ me.value = [value.getMonth(), value.getFullYear()];
+ } else {
+ me.value = [value[0], value[1]];
+ }
+
+ if (me.rendered) {
+ year = me.value[1];
+ if (year !== null) {
+ if ((year < active || year > active + me.yearOffset)) {
+ me.activeYear = year - me.yearOffset + 1;
+ }
+ }
+ me.updateBody();
+ }
+
+ return me;
+ },
+
+ /**
+ * Gets the selected value. It is returned as an array [month, year]. It may
+ * be a partial value, for example [null, 2010]. The month is returned as
+ * 0 based.
+ * @return {Number[]} The selected value
+ */
+ getValue: function(){
+ return this.value;
+ },
+
+ /**
+ * Checks whether the picker has a selection
+ * @return {Boolean} Returns true if both a month and year have been selected
+ */
+ hasSelection: function(){
+ var value = this.value;
+ return value[0] !== null && value[1] !== null;
+ },
+
+ /**
+ * Get an array of years to be pushed in the template. It is not in strict
+ * numerical order because we want to show them in columns.
+ * @private
+ * @return {Number[]} An array of years
+ */
+ getYears: function(){
+ var me = this,
+ offset = me.yearOffset,
+ start = me.activeYear, // put the "active" year on the left
+ end = start + offset,
+ i = start,
+ years = [];
+
+ for (; i < end; ++i) {
+ years.push(i, i + offset);
+ }
+
+ return years;
+ },
+
+ /**
+ * Update the years in the body based on any change
+ * @private
+ */
+ updateBody: function(){
+ var me = this,
+ years = me.years,
+ months = me.months,
+ yearNumbers = me.getYears(),
+ cls = me.selectedCls,
+ value = me.getYear(null),
+ month = me.value[0],
+ monthOffset = me.monthOffset,
+ year;
+
+ if (me.rendered) {
+ years.removeCls(cls);
+ months.removeCls(cls);
+ years.each(function(el, all, index){
+ year = yearNumbers[index];
+ el.dom.innerHTML = year;
+ if (year == value) {
+ el.dom.className = cls;
+ }
+ });
+ if (month !== null) {
+ if (month < monthOffset) {
+ month = month * 2;
+ } else {
+ month = (month - monthOffset) * 2 + 1;
+ }
+ months.item(month).addCls(cls);
+ }
+ }
+ },
+
+ /**
+ * Gets the current year value, or the default.
+ * @private
+ * @param {Number} defaultValue The default value to use if the year is not defined.
+ * @param {Number} offset A number to offset the value by
+ * @return {Number} The year value
+ */
+ getYear: function(defaultValue, offset) {
+ var year = this.value[1];
+ offset = offset || 0;
+ return year === null ? defaultValue : year + offset;
+ },
+
+ /**
+ * React to clicks on the body
+ * @private
+ */
+ onBodyClick: function(e, t) {
+ var me = this,
+ isDouble = e.type == 'dblclick';
+
+ if (e.getTarget('.' + me.baseCls + '-month')) {
+ e.stopEvent();
+ me.onMonthClick(t, isDouble);
+ } else if (e.getTarget('.' + me.baseCls + '-year')) {
+ e.stopEvent();
+ me.onYearClick(t, isDouble);
+ }
+ },
+
+ /**
+ * Modify the year display by passing an offset.
+ * @param {Number} [offset=10] The offset to move by.
+ */
+ adjustYear: function(offset){
+ if (typeof offset != 'number') {
+ offset = this.totalYears;
+ }
+ this.activeYear += offset;
+ this.updateBody();
+ },
+
+ /**
+ * React to the ok button being pressed
+ * @private
+ */
+ onOkClick: function(){
+ this.fireEvent('okclick', this, this.value);
+ },
+
+ /**
+ * React to the cancel button being pressed
+ * @private
+ */
+ onCancelClick: function(){
+ this.fireEvent('cancelclick', this);
+ },
+
+ /**
+ * React to a month being clicked
+ * @private
+ * @param {HTMLElement} target The element that was clicked
+ * @param {Boolean} isDouble True if the event was a doubleclick
+ */
+ onMonthClick: function(target, isDouble){
+ var me = this;
+ me.value[0] = me.resolveOffset(me.months.indexOf(target), me.monthOffset);
+ me.updateBody();
+ me.fireEvent('month' + (isDouble ? 'dbl' : '') + 'click', me, me.value);
+ me.fireEvent('select', me, me.value);
+ },
+
+ /**
+ * React to a year being clicked
+ * @private
+ * @param {HTMLElement} target The element that was clicked
+ * @param {Boolean} isDouble True if the event was a doubleclick
+ */
+ onYearClick: function(target, isDouble){
+ var me = this;
+ me.value[1] = me.activeYear + me.resolveOffset(me.years.indexOf(target), me.yearOffset);
+ me.updateBody();
+ me.fireEvent('year' + (isDouble ? 'dbl' : '') + 'click', me, me.value);
+ me.fireEvent('select', me, me.value);
+
+ },
+
+ /**
+ * Returns an offsetted number based on the position in the collection. Since our collections aren't
+ * numerically ordered, this function helps to normalize those differences.
+ * @private
+ * @param {Object} index
+ * @param {Object} offset
+ * @return {Number} The correctly offsetted number
+ */
+ resolveOffset: function(index, offset){
+ if (index % 2 === 0) {
+ return (index / 2);
+ } else {
+ return offset + Math.floor(index / 2);
+ }
+ },
+
+ // private, inherit docs
+ beforeDestroy: function(){
+ var me = this;
+ me.years = me.months = null;
+ Ext.destroyMembers(me, 'backRepeater', 'nextRepeater', 'okBtn', 'cancelBtn');
+ me.callParent();
+ }
+});
+
+/**
+ * A date picker. This class is used by the Ext.form.field.Date field to allow browsing and selection of valid
+ * dates in a popup next to the field, but may also be used with other components.
+ *
+ * Typically you will need to implement a handler function to be notified when the user chooses a date from the picker;
+ * you can register the handler using the {@link #select} event, or by implementing the {@link #handler} method.
+ *
+ * By default the user will be allowed to pick any date; this can be changed by using the {@link #minDate},
+ * {@link #maxDate}, {@link #disabledDays}, {@link #disabledDatesRE}, and/or {@link #disabledDates} configs.
+ *
+ * All the string values documented below may be overridden by including an Ext locale file in your page.
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Choose a future date:',
+ * width: 200,
+ * bodyPadding: 10,
+ * renderTo: Ext.getBody(),
+ * items: [{
+ * xtype: 'datepicker',
+ * minDate: new Date(),
+ * handler: function(picker, date) {
+ * // do something with the selected date
+ * }
+ * }]
+ * });
+ */
+Ext.define('Ext.picker.Date', {
+ extend: 'Ext.Component',
+ requires: [
+ 'Ext.XTemplate',
+ 'Ext.button.Button',
+ 'Ext.button.Split',
+ 'Ext.util.ClickRepeater',
+ 'Ext.util.KeyNav',
+ 'Ext.EventObject',
+ 'Ext.fx.Manager',
+ 'Ext.picker.Month'
+ ],
+ alias: 'widget.datepicker',
+ alternateClassName: 'Ext.DatePicker',
+
+ renderTpl: [
+ '<div class="{cls}" id="{id}" role="grid" title="{ariaTitle} {value:this.longDay}">',
+ '<div role="presentation" class="{baseCls}-header">',
+ '<div class="{baseCls}-prev"><a id="{id}-prevEl" href="#" role="button" title="{prevText}"></a></div>',
+ '<div class="{baseCls}-month" id="{id}-middleBtnEl"></div>',
+ '<div class="{baseCls}-next"><a id="{id}-nextEl" href="#" role="button" title="{nextText}"></a></div>',
+ '</div>',
+ '<table id="{id}-eventEl" class="{baseCls}-inner" cellspacing="0" role="presentation">',
+ '<thead role="presentation"><tr role="presentation">',
+ '<tpl for="dayNames">',
+ '<th role="columnheader" title="{.}"><span>{.:this.firstInitial}</span></th>',
+ '</tpl>',
+ '</tr></thead>',
+ '<tbody role="presentation"><tr role="presentation">',
+ '<tpl for="days">',
+ '{#:this.isEndOfWeek}',
+ '<td role="gridcell" id="{[Ext.id()]}">',
+ '<a role="presentation" href="#" hidefocus="on" class="{parent.baseCls}-date" tabIndex="1">',
+ '<em role="presentation"><span role="presentation"></span></em>',
+ '</a>',
+ '</td>',
+ '</tpl>',
+ '</tr></tbody>',
+ '</table>',
+ '<tpl if="showToday">',
+ '<div id="{id}-footerEl" role="presentation" class="{baseCls}-footer"></div>',
+ '</tpl>',
+ '</div>',
+ {
+ firstInitial: function(value) {
+ return value.substr(0,1);
+ },
+ isEndOfWeek: function(value) {
+ // convert from 1 based index to 0 based
+ // by decrementing value once.
+ value--;
+ var end = value % 7 === 0 && value !== 0;
+ return end ? '</tr><tr role="row">' : '';
+ },
+ longDay: function(value){
+ return Ext.Date.format(value, this.longDayFormat);
+ }
+ }
+ ],
+
+ ariaTitle: 'Date Picker',
+
+ /**
+ * @cfg {String} todayText
+ * The text to display on the button that selects the current date
+ */
+ todayText : 'Today',
+
+ /**
+ * @cfg {Function} handler
+ * Optional. A function that will handle the select event of this picker. The handler is passed the following
+ * parameters:
+ *
+ * - `picker` : Ext.picker.Date
+ *
+ * This Date picker.
+ *
+ * - `date` : Date
+ *
+ * The selected date.
+ */
+
+ /**
+ * @cfg {Object} scope
+ * The scope (`this` reference) in which the `{@link #handler}` function will be called. Defaults to this
+ * DatePicker instance.
+ */
+
+ /**
+ * @cfg {String} todayTip
+ * A string used to format the message for displaying in a tooltip over the button that selects the current date.
+ * The `{0}` token in string is replaced by today's date.
+ */
+ todayTip : '{0} (Spacebar)',
+
+ /**
+ * @cfg {String} minText
+ * The error text to display if the minDate validation fails.
+ */
+ minText : 'This date is before the minimum date',
+
+ /**
+ * @cfg {String} maxText
+ * The error text to display if the maxDate validation fails.
+ */
+ maxText : 'This date is after the maximum date',
+
+ /**
+ * @cfg {String} format
+ * The default date format string which can be overriden for localization support. The format must be valid
+ * according to {@link Ext.Date#parse} (defaults to {@link Ext.Date#defaultFormat}).
+ */
+
+ /**
+ * @cfg {String} disabledDaysText
+ * The tooltip to display when the date falls on a disabled day.
+ */
+ disabledDaysText : 'Disabled',
+
+ /**
+ * @cfg {String} disabledDatesText
+ * The tooltip text to display when the date falls on a disabled date.
+ */
+ disabledDatesText : 'Disabled',
+
+ /**
+ * @cfg {String[]} monthNames
+ * An array of textual month names which can be overriden for localization support (defaults to Ext.Date.monthNames)
+ */
+
+ /**
+ * @cfg {String[]} dayNames
+ * An array of textual day names which can be overriden for localization support (defaults to Ext.Date.dayNames)
+ */
+
+ /**
+ * @cfg {String} nextText
+ * The next month navigation button tooltip
+ */
+ nextText : 'Next Month (Control+Right)',
+
+ /**
+ * @cfg {String} prevText
+ * The previous month navigation button tooltip
+ */
+ prevText : 'Previous Month (Control+Left)',
+
+ /**
+ * @cfg {String} monthYearText
+ * The header month selector tooltip
+ */
+ monthYearText : 'Choose a month (Control+Up/Down to move years)',
+
+ /**
+ * @cfg {Number} startDay
+ * Day index at which the week should begin, 0-based (defaults to Sunday)
+ */
+ startDay : 0,
+
+ /**
+ * @cfg {Boolean} showToday
+ * False to hide the footer area containing the Today button and disable the keyboard handler for spacebar that
+ * selects the current date.
+ */
+ showToday : true,
+
+ /**
+ * @cfg {Date} [minDate=null]
+ * Minimum allowable date (JavaScript date object)
+ */
+
+ /**
+ * @cfg {Date} [maxDate=null]
+ * Maximum allowable date (JavaScript date object)
+ */
+
+ /**
+ * @cfg {Number[]} [disabledDays=null]
+ * An array of days to disable, 0-based. For example, [0, 6] disables Sunday and Saturday.
+ */
+
+ /**
+ * @cfg {RegExp} [disabledDatesRE=null]
+ * JavaScript regular expression used to disable a pattern of dates. The {@link #disabledDates}
+ * config will generate this regex internally, but if you specify disabledDatesRE it will take precedence over the
+ * disabledDates value.
+ */
+
+ /**
+ * @cfg {String[]} disabledDates
+ * An array of 'dates' to disable, as strings. These strings will be used to build a dynamic regular expression so
+ * they are very powerful. Some examples:
+ *
+ * - ['03/08/2003', '09/16/2003'] would disable those exact dates
+ * - ['03/08', '09/16'] would disable those days for every year
+ * - ['^03/08'] would only match the beginning (useful if you are using short years)
+ * - ['03/../2006'] would disable every day in March 2006
+ * - ['^03'] would disable every day in every March
+ *
+ * Note that the format of the dates included in the array should exactly match the {@link #format} config. In order
+ * to support regular expressions, if you are using a date format that has '.' in it, you will have to escape the
+ * dot when restricting dates. For example: ['03\\.08\\.03'].
+ */
+
+ /**
+ * @cfg {Boolean} disableAnim
+ * True to disable animations when showing the month picker.
+ */
+ disableAnim: false,
+
+ /**
+ * @cfg {String} [baseCls='x-datepicker']
+ * The base CSS class to apply to this components element.
+ */
+ baseCls: Ext.baseCSSPrefix + 'datepicker',
+
+ /**
+ * @cfg {String} [selectedCls='x-datepicker-selected']
+ * The class to apply to the selected cell.
+ */
+
+ /**
+ * @cfg {String} [disabledCellCls='x-datepicker-disabled']
+ * The class to apply to disabled cells.
+ */
+
+ /**
+ * @cfg {String} longDayFormat
+ * The format for displaying a date in a longer format.
+ */
+ longDayFormat: 'F d, Y',
+
+ /**
+ * @cfg {Object} keyNavConfig
+ * Specifies optional custom key event handlers for the {@link Ext.util.KeyNav} attached to this date picker. Must
+ * conform to the config format recognized by the {@link Ext.util.KeyNav} constructor. Handlers specified in this
+ * object will replace default handlers of the same name.
+ */
+
+ /**
+ * @cfg {Boolean} focusOnShow
+ * True to automatically focus the picker on show.
+ */
+ focusOnShow: false,
+
+ // private
+ // Set by other components to stop the picker focus being updated when the value changes.
+ focusOnSelect: true,
+
+ width: 178,
+
+ // default value used to initialise each date in the DatePicker
+ // (note: 12 noon was chosen because it steers well clear of all DST timezone changes)
+ initHour: 12, // 24-hour format
+
+ numDays: 42,
+
+ // private, inherit docs
+ initComponent : function() {
+ var me = this,
+ clearTime = Ext.Date.clearTime;
+
+ me.selectedCls = me.baseCls + '-selected';
+ me.disabledCellCls = me.baseCls + '-disabled';
+ me.prevCls = me.baseCls + '-prevday';
+ me.activeCls = me.baseCls + '-active';
+ me.nextCls = me.baseCls + '-prevday';
+ me.todayCls = me.baseCls + '-today';
+ me.dayNames = me.dayNames.slice(me.startDay).concat(me.dayNames.slice(0, me.startDay));
+ this.callParent();
+
+ me.value = me.value ?
+ clearTime(me.value, true) : clearTime(new Date());
+
+ me.addEvents(
+ /**
+ * @event select
+ * Fires when a date is selected
+ * @param {Ext.picker.Date} this DatePicker
+ * @param {Date} date The selected date
+ */
+ 'select'
+ );
+
+ me.initDisabledDays();
+ },
+
+ // private, inherit docs
+ onRender : function(container, position){
+ /*
+ * days array for looping through 6 full weeks (6 weeks * 7 days)
+ * Note that we explicitly force the size here so the template creates
+ * all the appropriate cells.
+ */
+
+ var me = this,
+ days = new Array(me.numDays),
+ today = Ext.Date.format(new Date(), me.format);
+
+ Ext.applyIf(me, {
+ renderData: {}
+ });
+
+ Ext.apply(me.renderData, {
+ dayNames: me.dayNames,
+ ariaTitle: me.ariaTitle,
+ value: me.value,
+ showToday: me.showToday,
+ prevText: me.prevText,
+ nextText: me.nextText,
+ days: days
+ });
+ me.getTpl('renderTpl').longDayFormat = me.longDayFormat;
+
+ me.addChildEls('eventEl', 'prevEl', 'nextEl', 'middleBtnEl', 'footerEl');
+
+ this.callParent(arguments);
+ me.el.unselectable();
+
+ me.cells = me.eventEl.select('tbody td');
+ me.textNodes = me.eventEl.query('tbody td span');
+
+ me.monthBtn = Ext.create('Ext.button.Split', {
+ text: '',
+ tooltip: me.monthYearText,
+ renderTo: me.middleBtnEl
+ });
+ //~ me.middleBtnEl.down('button').addCls(Ext.baseCSSPrefix + 'btn-arrow');
+
+
+ me.todayBtn = Ext.create('Ext.button.Button', {
+ renderTo: me.footerEl,
+ text: Ext.String.format(me.todayText, today),
+ tooltip: Ext.String.format(me.todayTip, today),
+ handler: me.selectToday,
+ scope: me
+ });
+ },
+
+ // private, inherit docs
+ initEvents: function(){
+ var me = this,
+ eDate = Ext.Date,
+ day = eDate.DAY;
+
+ this.callParent();
+
+ me.prevRepeater = Ext.create('Ext.util.ClickRepeater', me.prevEl, {
+ handler: me.showPrevMonth,
+ scope: me,
+ preventDefault: true,
+ stopDefault: true
+ });
+
+ me.nextRepeater = Ext.create('Ext.util.ClickRepeater', me.nextEl, {
+ handler: me.showNextMonth,
+ scope: me,
+ preventDefault:true,
+ stopDefault:true
+ });
+
+ me.keyNav = Ext.create('Ext.util.KeyNav', me.eventEl, Ext.apply({
+ scope: me,
+ 'left' : function(e){
+ if(e.ctrlKey){
+ me.showPrevMonth();
+ }else{
+ me.update(eDate.add(me.activeDate, day, -1));
+ }
+ },
+
+ 'right' : function(e){
+ if(e.ctrlKey){
+ me.showNextMonth();
+ }else{
+ me.update(eDate.add(me.activeDate, day, 1));
+ }
+ },
+
+ 'up' : function(e){
+ if(e.ctrlKey){
+ me.showNextYear();
+ }else{
+ me.update(eDate.add(me.activeDate, day, -7));
+ }
+ },
+
+ 'down' : function(e){
+ if(e.ctrlKey){
+ me.showPrevYear();
+ }else{
+ me.update(eDate.add(me.activeDate, day, 7));
+ }
+ },
+ 'pageUp' : me.showNextMonth,
+ 'pageDown' : me.showPrevMonth,
+ 'enter' : function(e){
+ e.stopPropagation();
+ return true;
+ }
+ }, me.keyNavConfig));
+
+ if(me.showToday){
+ me.todayKeyListener = me.eventEl.addKeyListener(Ext.EventObject.SPACE, me.selectToday, me);
+ }
+ me.mon(me.eventEl, 'mousewheel', me.handleMouseWheel, me);
+ me.mon(me.eventEl, 'click', me.handleDateClick, me, {delegate: 'a.' + me.baseCls + '-date'});
+ me.mon(me.monthBtn, 'click', me.showMonthPicker, me);
+ me.mon(me.monthBtn, 'arrowclick', me.showMonthPicker, me);
+ me.update(me.value);
+ },
+
+ /**
+ * Setup the disabled dates regex based on config options
+ * @private
+ */
+ initDisabledDays : function(){
+ var me = this,
+ dd = me.disabledDates,
+ re = '(?:',
+ len;
+
+ if(!me.disabledDatesRE && dd){
+ len = dd.length - 1;
+
+ Ext.each(dd, function(d, i){
+ re += Ext.isDate(d) ? '^' + Ext.String.escapeRegex(Ext.Date.dateFormat(d, me.format)) + '$' : dd[i];
+ if(i != len){
+ re += '|';
+ }
+ }, me);
+ me.disabledDatesRE = new RegExp(re + ')');
+ }
+ },
+
+ /**
+ * Replaces any existing disabled dates with new values and refreshes the DatePicker.
+ * @param {String[]/RegExp} disabledDates An array of date strings (see the {@link #disabledDates} config for
+ * details on supported values), or a JavaScript regular expression used to disable a pattern of dates.
+ * @return {Ext.picker.Date} this
+ */
+ setDisabledDates : function(dd){
+ var me = this;
+
+ if(Ext.isArray(dd)){
+ me.disabledDates = dd;
+ me.disabledDatesRE = null;
+ }else{
+ me.disabledDatesRE = dd;
+ }
+ me.initDisabledDays();
+ me.update(me.value, true);
+ return me;
+ },
+
+ /**
+ * Replaces any existing disabled days (by index, 0-6) with new values and refreshes the DatePicker.
+ * @param {Number[]} disabledDays An array of disabled day indexes. See the {@link #disabledDays} config for details
+ * on supported values.
+ * @return {Ext.picker.Date} this
+ */
+ setDisabledDays : function(dd){
+ this.disabledDays = dd;
+ return this.update(this.value, true);
+ },
+
+ /**
+ * Replaces any existing {@link #minDate} with the new value and refreshes the DatePicker.
+ * @param {Date} value The minimum date that can be selected
+ * @return {Ext.picker.Date} this
+ */
+ setMinDate : function(dt){
+ this.minDate = dt;
+ return this.update(this.value, true);
+ },
+
+ /**
+ * Replaces any existing {@link #maxDate} with the new value and refreshes the DatePicker.
+ * @param {Date} value The maximum date that can be selected
+ * @return {Ext.picker.Date} this
+ */
+ setMaxDate : function(dt){
+ this.maxDate = dt;
+ return this.update(this.value, true);
+ },
+
+ /**
+ * Sets the value of the date field
+ * @param {Date} value The date to set
+ * @return {Ext.picker.Date} this
+ */
+ setValue : function(value){
+ this.value = Ext.Date.clearTime(value, true);
+ return this.update(this.value);
+ },
+
+ /**
+ * Gets the current selected value of the date field
+ * @return {Date} The selected date
+ */
+ getValue : function(){
+ return this.value;
+ },
+
+ // private
+ focus : function(){
+ this.update(this.activeDate);
+ },
+
+ // private, inherit docs
+ onEnable: function(){
+ this.callParent();
+ this.setDisabledStatus(false);
+ this.update(this.activeDate);
+
+ },
+
+ // private, inherit docs
+ onDisable : function(){
+ this.callParent();
+ this.setDisabledStatus(true);
+ },
+
+ /**
+ * Set the disabled state of various internal components
+ * @private
+ * @param {Boolean} disabled
+ */
+ setDisabledStatus : function(disabled){
+ var me = this;
+
+ me.keyNav.setDisabled(disabled);
+ me.prevRepeater.setDisabled(disabled);
+ me.nextRepeater.setDisabled(disabled);
+ if (me.showToday) {
+ me.todayKeyListener.setDisabled(disabled);
+ me.todayBtn.setDisabled(disabled);
+ }
+ },
+
+ /**
+ * Get the current active date.
+ * @private
+ * @return {Date} The active date
+ */
+ getActive: function(){
+ return this.activeDate || this.value;
+ },
+
+ /**
+ * Run any animation required to hide/show the month picker.
+ * @private
+ * @param {Boolean} isHide True if it's a hide operation
+ */
+ runAnimation: function(isHide){
+ var picker = this.monthPicker,
+ options = {
+ duration: 200,
+ callback: function(){
+ if (isHide) {
+ picker.hide();
+ } else {
+ picker.show();
+ }
+ }
+ };
+
+ if (isHide) {
+ picker.el.slideOut('t', options);
+ } else {
+ picker.el.slideIn('t', options);
+ }
+ },
+
+ /**
+ * Hides the month picker, if it's visible.
+ * @param {Boolean} [animate] Indicates whether to animate this action. If the animate
+ * parameter is not specified, the behavior will use {@link #disableAnim} to determine
+ * whether to animate or not.
+ * @return {Ext.picker.Date} this
+ */
+ hideMonthPicker : function(animate){
+ var me = this,
+ picker = me.monthPicker;
+
+ if (picker) {
+ if (me.shouldAnimate(animate)) {
+ me.runAnimation(true);
+ } else {
+ picker.hide();
+ }
+ }
+ return me;
+ },
+
+ /**
+ * Show the month picker
+ * @param {Boolean} [animate] Indicates whether to animate this action. If the animate
+ * parameter is not specified, the behavior will use {@link #disableAnim} to determine
+ * whether to animate or not.
+ * @return {Ext.picker.Date} this
+ */
+ showMonthPicker : function(animate){
+ var me = this,
+ picker;
+
+ if (me.rendered && !me.disabled) {
+ picker = me.createMonthPicker();
+ picker.setValue(me.getActive());
+ picker.setSize(me.getSize());
+ picker.setPosition(-1, -1);
+ if (me.shouldAnimate(animate)) {
+ me.runAnimation(false);
+ } else {
+ picker.show();
+ }
+ }
+ return me;
+ },
+
+ /**
+ * Checks whether a hide/show action should animate
+ * @private
+ * @param {Boolean} [animate] A possible animation value
+ * @return {Boolean} Whether to animate the action
+ */
+ shouldAnimate: function(animate){
+ return Ext.isDefined(animate) ? animate : !this.disableAnim;
+ },
+
+ /**
+ * Create the month picker instance
+ * @private
+ * @return {Ext.picker.Month} picker
+ */
+ createMonthPicker: function(){
+ var me = this,
+ picker = me.monthPicker;
+
+ if (!picker) {
+ me.monthPicker = picker = Ext.create('Ext.picker.Month', {
+ renderTo: me.el,
+ floating: true,
+ shadow: false,
+ small: me.showToday === false,
+ listeners: {
+ scope: me,
+ cancelclick: me.onCancelClick,
+ okclick: me.onOkClick,
+ yeardblclick: me.onOkClick,
+ monthdblclick: me.onOkClick
+ }
+ });
+ if (!me.disableAnim) {
+ // hide the element if we're animating to prevent an initial flicker
+ picker.el.setStyle('display', 'none');
+ }
+ me.on('beforehide', Ext.Function.bind(me.hideMonthPicker, me, [false]));
+ }
+ return picker;
+ },
+
+ /**
+ * Respond to an ok click on the month picker
+ * @private
+ */
+ onOkClick: function(picker, value){
+ var me = this,
+ month = value[0],
+ year = value[1],
+ date = new Date(year, month, me.getActive().getDate());
+
+ if (date.getMonth() !== month) {
+ // 'fix' the JS rolling date conversion if needed
+ date = new Date(year, month, 1).getLastDateOfMonth();
+ }
+ me.update(date);
+ me.hideMonthPicker();
+ },
+
+ /**
+ * Respond to a cancel click on the month picker
+ * @private
+ */
+ onCancelClick: function(){
+ this.hideMonthPicker();
+ },
+
+ /**
+ * Show the previous month.
+ * @param {Object} e
+ * @return {Ext.picker.Date} this
+ */
+ showPrevMonth : function(e){
+ return this.update(Ext.Date.add(this.activeDate, Ext.Date.MONTH, -1));
+ },
+
+ /**
+ * Show the next month.
+ * @param {Object} e
+ * @return {Ext.picker.Date} this
+ */
+ showNextMonth : function(e){
+ return this.update(Ext.Date.add(this.activeDate, Ext.Date.MONTH, 1));
+ },
+
+ /**
+ * Show the previous year.
+ * @return {Ext.picker.Date} this
+ */
+ showPrevYear : function(){
+ this.update(Ext.Date.add(this.activeDate, Ext.Date.YEAR, -1));
+ },
+
+ /**
+ * Show the next year.
+ * @return {Ext.picker.Date} this
+ */
+ showNextYear : function(){
+ this.update(Ext.Date.add(this.activeDate, Ext.Date.YEAR, 1));
+ },
+
+ /**
+ * Respond to the mouse wheel event
+ * @private
+ * @param {Ext.EventObject} e
+ */
+ handleMouseWheel : function(e){
+ e.stopEvent();
+ if(!this.disabled){
+ var delta = e.getWheelDelta();
+ if(delta > 0){
+ this.showPrevMonth();
+ } else if(delta < 0){
+ this.showNextMonth();
+ }
+ }
+ },
+
+ /**
+ * Respond to a date being clicked in the picker
+ * @private
+ * @param {Ext.EventObject} e
+ * @param {HTMLElement} t
+ */
+ handleDateClick : function(e, t){
+ var me = this,
+ handler = me.handler;
+
+ e.stopEvent();
+ if(!me.disabled && t.dateValue && !Ext.fly(t.parentNode).hasCls(me.disabledCellCls)){
+ me.cancelFocus = me.focusOnSelect === false;
+ me.setValue(new Date(t.dateValue));
+ delete me.cancelFocus;
+ me.fireEvent('select', me, me.value);
+ if (handler) {
+ handler.call(me.scope || me, me, me.value);
+ }
+ // event handling is turned off on hide
+ // when we are using the picker in a field
+ // therefore onSelect comes AFTER the select
+ // event.
+ me.onSelect();
+ }
+ },
+
+ /**
+ * Perform any post-select actions
+ * @private
+ */
+ onSelect: function() {
+ if (this.hideOnSelect) {
+ this.hide();
+ }
+ },
+
+ /**
+ * Sets the current value to today.
+ * @return {Ext.picker.Date} this
+ */
+ selectToday : function(){
+ var me = this,
+ btn = me.todayBtn,
+ handler = me.handler;
+
+ if(btn && !btn.disabled){
+ me.setValue(Ext.Date.clearTime(new Date()));
+ me.fireEvent('select', me, me.value);
+ if (handler) {
+ handler.call(me.scope || me, me, me.value);
+ }
+ me.onSelect();
+ }
+ return me;
+ },
+
+ /**
+ * Update the selected cell
+ * @private
+ * @param {Date} date The new date
+ * @param {Date} active The active date
+ */
+ selectedUpdate: function(date, active){
+ var me = this,
+ t = date.getTime(),
+ cells = me.cells,
+ cls = me.selectedCls;
+
+ cells.removeCls(cls);
+ cells.each(function(c){
+ if (c.dom.firstChild.dateValue == t) {
+ me.el.dom.setAttribute('aria-activedescendent', c.dom.id);
+ c.addCls(cls);
+ if(me.isVisible() && !me.cancelFocus){
+ Ext.fly(c.dom.firstChild).focus(50);
+ }
+ return false;
+ }
+ }, this);
+ },
+
+ /**
+ * Update the contents of the picker for a new month
+ * @private
+ * @param {Date} date The new date
+ * @param {Date} active The active date
+ */
+ fullUpdate: function(date, active){
+ var me = this,
+ cells = me.cells.elements,
+ textNodes = me.textNodes,
+ disabledCls = me.disabledCellCls,
+ eDate = Ext.Date,
+ i = 0,
+ extraDays = 0,
+ visible = me.isVisible(),
+ sel = +eDate.clearTime(date, true),
+ today = +eDate.clearTime(new Date()),
+ min = me.minDate ? eDate.clearTime(me.minDate, true) : Number.NEGATIVE_INFINITY,
+ max = me.maxDate ? eDate.clearTime(me.maxDate, true) : Number.POSITIVE_INFINITY,
+ ddMatch = me.disabledDatesRE,
+ ddText = me.disabledDatesText,
+ ddays = me.disabledDays ? me.disabledDays.join('') : false,
+ ddaysText = me.disabledDaysText,
+ format = me.format,
+ days = eDate.getDaysInMonth(date),
+ firstOfMonth = eDate.getFirstDateOfMonth(date),
+ startingPos = firstOfMonth.getDay() - me.startDay,
+ previousMonth = eDate.add(date, eDate.MONTH, -1),
+ longDayFormat = me.longDayFormat,
+ prevStart,
+ current,
+ disableToday,
+ tempDate,
+ setCellClass,
+ html,
+ cls,
+ formatValue,
+ value;
+
+ if (startingPos < 0) {
+ startingPos += 7;
+ }
+
+ days += startingPos;
+ prevStart = eDate.getDaysInMonth(previousMonth) - startingPos;
+ current = new Date(previousMonth.getFullYear(), previousMonth.getMonth(), prevStart, me.initHour);
+
+ if (me.showToday) {
+ tempDate = eDate.clearTime(new Date());
+ disableToday = (tempDate < min || tempDate > max ||
+ (ddMatch && format && ddMatch.test(eDate.dateFormat(tempDate, format))) ||
+ (ddays && ddays.indexOf(tempDate.getDay()) != -1));
+
+ if (!me.disabled) {
+ me.todayBtn.setDisabled(disableToday);
+ me.todayKeyListener.setDisabled(disableToday);
+ }
+ }
+
+ setCellClass = function(cell){
+ value = +eDate.clearTime(current, true);
+ cell.title = eDate.format(current, longDayFormat);
+ // store dateValue number as an expando
+ cell.firstChild.dateValue = value;
+ if(value == today){
+ cell.className += ' ' + me.todayCls;
+ cell.title = me.todayText;
+ }
+ if(value == sel){
+ cell.className += ' ' + me.selectedCls;
+ me.el.dom.setAttribute('aria-activedescendant', cell.id);
+ if (visible && me.floating) {
+ Ext.fly(cell.firstChild).focus(50);
+ }
+ }
+ // disabling
+ if(value < min) {
+ cell.className = disabledCls;
+ cell.title = me.minText;
+ return;
+ }
+ if(value > max) {
+ cell.className = disabledCls;
+ cell.title = me.maxText;
+ return;
+ }
+ if(ddays){
+ if(ddays.indexOf(current.getDay()) != -1){
+ cell.title = ddaysText;
+ cell.className = disabledCls;
+ }
+ }
+ if(ddMatch && format){
+ formatValue = eDate.dateFormat(current, format);
+ if(ddMatch.test(formatValue)){
+ cell.title = ddText.replace('%0', formatValue);
+ cell.className = disabledCls;
+ }
+ }
+ };
+
+ for(; i < me.numDays; ++i) {
+ if (i < startingPos) {
+ html = (++prevStart);
+ cls = me.prevCls;
+ } else if (i >= days) {
+ html = (++extraDays);
+ cls = me.nextCls;
+ } else {
+ html = i - startingPos + 1;
+ cls = me.activeCls;
+ }
+ textNodes[i].innerHTML = html;
+ cells[i].className = cls;
+ current.setDate(current.getDate() + 1);
+ setCellClass(cells[i]);
+ }
+
+ me.monthBtn.setText(me.monthNames[date.getMonth()] + ' ' + date.getFullYear());
+ },
+
+ /**
+ * Update the contents of the picker
+ * @private
+ * @param {Date} date The new date
+ * @param {Boolean} forceRefresh True to force a full refresh
+ */
+ update : function(date, forceRefresh){
+ var me = this,
+ active = me.activeDate;
+
+ if (me.rendered) {
+ me.activeDate = date;
+ if(!forceRefresh && active && me.el && active.getMonth() == date.getMonth() && active.getFullYear() == date.getFullYear()){
+ me.selectedUpdate(date, active);
+ } else {
+ me.fullUpdate(date, active);
+ }
+ }
+ return me;
+ },
+
+ // private, inherit docs
+ beforeDestroy : function() {
+ var me = this;
+
+ if (me.rendered) {
+ Ext.destroy(
+ me.todayKeyListener,
+ me.keyNav,
+ me.monthPicker,
+ me.monthBtn,
+ me.nextRepeater,
+ me.prevRepeater,
+ me.todayBtn
+ );
+ delete me.textNodes;
+ delete me.cells.elements;
+ }
+ me.callParent();
+ },
+
+ // private, inherit docs
+ onShow: function() {
+ this.callParent(arguments);
+ if (this.focusOnShow) {
+ this.focus();
+ }
+ }
+},
+
+// After dependencies have loaded:
+function() {
+ var proto = this.prototype;
+
+ proto.monthNames = Ext.Date.monthNames;
+
+ proto.dayNames = Ext.Date.dayNames;
+
+ proto.format = Ext.Date.defaultFormat;
+});
+
+/**
+ * @docauthor Jason Johnston <jason@sencha.com>
+ *
+ * Provides a date input field with a {@link Ext.picker.Date date picker} dropdown and automatic date
+ * validation.
+ *
+ * This field recognizes and uses the JavaScript Date object as its main {@link #value} type. In addition,
+ * it recognizes string values which are parsed according to the {@link #format} and/or {@link #altFormats}
+ * configs. These may be reconfigured to use date formats appropriate for the user's locale.
+ *
+ * The field may be limited to a certain range of dates by using the {@link #minValue}, {@link #maxValue},
+ * {@link #disabledDays}, and {@link #disabledDates} config parameters. These configurations will be used both
+ * in the field's validation, and in the date picker dropdown by preventing invalid dates from being selected.
+ *
+ * # Example usage
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * renderTo: Ext.getBody(),
+ * width: 300,
+ * bodyPadding: 10,
+ * title: 'Dates',
+ * items: [{
+ * xtype: 'datefield',
+ * anchor: '100%',
+ * fieldLabel: 'From',
+ * name: 'from_date',
+ * maxValue: new Date() // limited to the current date or prior
+ * }, {
+ * xtype: 'datefield',
+ * anchor: '100%',
+ * fieldLabel: 'To',
+ * name: 'to_date',
+ * value: new Date() // defaults to today
+ * }]
+ * });
+ *
+ * # Date Formats Examples
+ *
+ * This example shows a couple of different date format parsing scenarios. Both use custom date format
+ * configurations; the first one matches the configured `format` while the second matches the `altFormats`.
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * renderTo: Ext.getBody(),
+ * width: 300,
+ * bodyPadding: 10,
+ * title: 'Dates',
+ * items: [{
+ * xtype: 'datefield',
+ * anchor: '100%',
+ * fieldLabel: 'Date',
+ * name: 'date',
+ * // The value matches the format; will be parsed and displayed using that format.
+ * format: 'm d Y',
+ * value: '2 4 1978'
+ * }, {
+ * xtype: 'datefield',
+ * anchor: '100%',
+ * fieldLabel: 'Date',
+ * name: 'date',
+ * // The value does not match the format, but does match an altFormat; will be parsed
+ * // using the altFormat and displayed using the format.
+ * format: 'm d Y',
+ * altFormats: 'm,d,Y|m.d.Y',
+ * value: '2.4.1978'
+ * }]
+ * });
+ */
+Ext.define('Ext.form.field.Date', {
+ extend:'Ext.form.field.Picker',
+ alias: 'widget.datefield',
+ requires: ['Ext.picker.Date'],
+ alternateClassName: ['Ext.form.DateField', 'Ext.form.Date'],
+
+ /**
+ * @cfg {String} format
+ * The default date format string which can be overriden for localization support. The format must be valid
+ * according to {@link Ext.Date#parse}.
+ */
+ format : "m/d/Y",
+ /**
+ * @cfg {String} altFormats
+ * Multiple date formats separated by "|" to try when parsing a user input value and it does not match the defined
+ * format.
+ */
+ altFormats : "m/d/Y|n/j/Y|n/j/y|m/j/y|n/d/y|m/j/Y|n/d/Y|m-d-y|m-d-Y|m/d|m-d|md|mdy|mdY|d|Y-m-d|n-j|n/j",
+ /**
+ * @cfg {String} disabledDaysText
+ * The tooltip to display when the date falls on a disabled day.
+ */
+ disabledDaysText : "Disabled",
+ /**
+ * @cfg {String} disabledDatesText
+ * The tooltip text to display when the date falls on a disabled date.
+ */
+ disabledDatesText : "Disabled",
+ /**
+ * @cfg {String} minText
+ * The error text to display when the date in the cell is before {@link #minValue}.
+ */
+ minText : "The date in this field must be equal to or after {0}",
+ /**
+ * @cfg {String} maxText
+ * The error text to display when the date in the cell is after {@link #maxValue}.
+ */
+ maxText : "The date in this field must be equal to or before {0}",
+ /**
+ * @cfg {String} invalidText
+ * The error text to display when the date in the field is invalid.
+ */
+ invalidText : "{0} is not a valid date - it must be in the format {1}",
+ /**
+ * @cfg {String} [triggerCls='x-form-date-trigger']
+ * An additional CSS class used to style the trigger button. The trigger will always get the class 'x-form-trigger'
+ * and triggerCls will be **appended** if specified (default class displays a calendar icon).
+ */
+ triggerCls : Ext.baseCSSPrefix + 'form-date-trigger',
+ /**
+ * @cfg {Boolean} showToday
+ * false to hide the footer area of the Date picker containing the Today button and disable the keyboard handler for
+ * spacebar that selects the current date.
+ */
+ showToday : true,
+ /**
+ * @cfg {Date/String} minValue
+ * The minimum allowed date. Can be either a Javascript date object or a string date in a valid format.
+ */
+ /**
+ * @cfg {Date/String} maxValue
+ * The maximum allowed date. Can be either a Javascript date object or a string date in a valid format.
+ */
+ /**
+ * @cfg {Number[]} disabledDays
+ * An array of days to disable, 0 based. Some examples:
+ *
+ * // disable Sunday and Saturday:
+ * disabledDays: [0, 6]
+ * // disable weekdays:
+ * disabledDays: [1,2,3,4,5]
+ */
+ /**
+ * @cfg {String[]} disabledDates
+ * An array of "dates" to disable, as strings. These strings will be used to build a dynamic regular expression so
+ * they are very powerful. Some examples:
+ *
+ * // disable these exact dates:
+ * disabledDates: ["03/08/2003", "09/16/2003"]
+ * // disable these days for every year:
+ * disabledDates: ["03/08", "09/16"]
+ * // only match the beginning (useful if you are using short years):
+ * disabledDates: ["^03/08"]
+ * // disable every day in March 2006:
+ * disabledDates: ["03/../2006"]
+ * // disable every day in every March:
+ * disabledDates: ["^03"]
+ *
+ * Note that the format of the dates included in the array should exactly match the {@link #format} config. In order
+ * to support regular expressions, if you are using a {@link #format date format} that has "." in it, you will have
+ * to escape the dot when restricting dates. For example: `["03\\.08\\.03"]`.
+ */
+
+ /**
+ * @cfg {String} submitFormat
+ * The date format string which will be submitted to the server. The format must be valid according to {@link
+ * Ext.Date#parse} (defaults to {@link #format}).
+ */
+
+ // in the absence of a time value, a default value of 12 noon will be used
+ // (note: 12 noon was chosen because it steers well clear of all DST timezone changes)
+ initTime: '12', // 24 hour format
+
+ initTimeFormat: 'H',
+
+ matchFieldWidth: false,
+ /**
+ * @cfg {Number} startDay
+ * Day index at which the week should begin, 0-based (defaults to Sunday)
+ */
+ startDay: 0,
+
+ initComponent : function(){
+ var me = this,
+ isString = Ext.isString,
+ min, max;
+
+ min = me.minValue;
+ max = me.maxValue;
+ if(isString(min)){
+ me.minValue = me.parseDate(min);
+ }
+ if(isString(max)){
+ me.maxValue = me.parseDate(max);
+ }
+ me.disabledDatesRE = null;
+ me.initDisabledDays();
+
+ me.callParent();
+ },
+
+ initValue: function() {
+ var me = this,
+ value = me.value;
+
+ // If a String value was supplied, try to convert it to a proper Date
+ if (Ext.isString(value)) {
+ me.value = me.rawToValue(value);
+ }
+
+ me.callParent();
+ },
+
+ // private
+ initDisabledDays : function(){
+ if(this.disabledDates){
+ var dd = this.disabledDates,
+ len = dd.length - 1,
+ re = "(?:";
+
+ Ext.each(dd, function(d, i){
+ re += Ext.isDate(d) ? '^' + Ext.String.escapeRegex(d.dateFormat(this.format)) + '$' : dd[i];
+ if (i !== len) {
+ re += '|';
+ }
+ }, this);
+ this.disabledDatesRE = new RegExp(re + ')');
+ }
+ },
+
+ /**
+ * Replaces any existing disabled dates with new values and refreshes the Date picker.
+ * @param {String[]} disabledDates An array of date strings (see the {@link #disabledDates} config for details on
+ * supported values) used to disable a pattern of dates.
+ */
+ setDisabledDates : function(dd){
+ var me = this,
+ picker = me.picker;
+
+ me.disabledDates = dd;
+ me.initDisabledDays();
+ if (picker) {
+ picker.setDisabledDates(me.disabledDatesRE);
+ }
+ },
+
+ /**
+ * Replaces any existing disabled days (by index, 0-6) with new values and refreshes the Date picker.
+ * @param {Number[]} disabledDays An array of disabled day indexes. See the {@link #disabledDays} config for details on
+ * supported values.
+ */
+ setDisabledDays : function(dd){
+ var picker = this.picker;
+
+ this.disabledDays = dd;
+ if (picker) {
+ picker.setDisabledDays(dd);
+ }
+ },
+
+ /**
+ * Replaces any existing {@link #minValue} with the new value and refreshes the Date picker.
+ * @param {Date} value The minimum date that can be selected
+ */
+ setMinValue : function(dt){
+ var me = this,
+ picker = me.picker,
+ minValue = (Ext.isString(dt) ? me.parseDate(dt) : dt);
+
+ me.minValue = minValue;
+ if (picker) {
+ picker.minText = Ext.String.format(me.minText, me.formatDate(me.minValue));
+ picker.setMinDate(minValue);
+ }
+ },
+
+ /**
+ * Replaces any existing {@link #maxValue} with the new value and refreshes the Date picker.
+ * @param {Date} value The maximum date that can be selected
+ */
+ setMaxValue : function(dt){
+ var me = this,
+ picker = me.picker,
+ maxValue = (Ext.isString(dt) ? me.parseDate(dt) : dt);
+
+ me.maxValue = maxValue;
+ if (picker) {
+ picker.maxText = Ext.String.format(me.maxText, me.formatDate(me.maxValue));
+ picker.setMaxDate(maxValue);
+ }
+ },
+
+ /**
+ * Runs all of Date's validations and returns an array of any errors. Note that this first runs Text's validations,
+ * so the returned array is an amalgamation of all field errors. The additional validation checks are testing that
+ * the date format is valid, that the chosen date is within the min and max date constraints set, that the date
+ * chosen is not in the disabledDates regex and that the day chosed is not one of the disabledDays.
+ * @param {Object} [value] The value to get errors for (defaults to the current field value)
+ * @return {String[]} All validation errors for this field
+ */
+ getErrors: function(value) {
+ var me = this,
+ format = Ext.String.format,
+ clearTime = Ext.Date.clearTime,
+ errors = me.callParent(arguments),
+ disabledDays = me.disabledDays,
+ disabledDatesRE = me.disabledDatesRE,
+ minValue = me.minValue,
+ maxValue = me.maxValue,
+ len = disabledDays ? disabledDays.length : 0,
+ i = 0,
+ svalue,
+ fvalue,
+ day,
+ time;
+
+ value = me.formatDate(value || me.processRawValue(me.getRawValue()));
+
+ if (value === null || value.length < 1) { // if it's blank and textfield didn't flag it then it's valid
+ return errors;
+ }
+
+ svalue = value;
+ value = me.parseDate(value);
+ if (!value) {
+ errors.push(format(me.invalidText, svalue, me.format));
+ return errors;
+ }
+
+ time = value.getTime();
+ if (minValue && time < clearTime(minValue).getTime()) {
+ errors.push(format(me.minText, me.formatDate(minValue)));
+ }
+
+ if (maxValue && time > clearTime(maxValue).getTime()) {
+ errors.push(format(me.maxText, me.formatDate(maxValue)));
+ }
+
+ if (disabledDays) {
+ day = value.getDay();
+
+ for(; i < len; i++) {
+ if (day === disabledDays[i]) {
+ errors.push(me.disabledDaysText);
+ break;
+ }
+ }
+ }
+
+ fvalue = me.formatDate(value);
+ if (disabledDatesRE && disabledDatesRE.test(fvalue)) {
+ errors.push(format(me.disabledDatesText, fvalue));
+ }
+
+ return errors;
+ },
+
+ rawToValue: function(rawValue) {
+ return this.parseDate(rawValue) || rawValue || null;
+ },
+
+ valueToRaw: function(value) {
+ return this.formatDate(this.parseDate(value));
+ },
+
+ /**
+ * @method setValue
+ * Sets the value of the date field. You can pass a date object or any string that can be parsed into a valid date,
+ * using {@link #format} as the date format, according to the same rules as {@link Ext.Date#parse} (the default
+ * format used is "m/d/Y").
+ *
+ * Usage:
+ *
+ * //All of these calls set the same date value (May 4, 2006)
+ *
+ * //Pass a date object:
+ * var dt = new Date('5/4/2006');
+ * dateField.setValue(dt);
+ *
+ * //Pass a date string (default format):
+ * dateField.setValue('05/04/2006');
+ *
+ * //Pass a date string (custom format):
+ * dateField.format = 'Y-m-d';
+ * dateField.setValue('2006-05-04');
+ *
+ * @param {String/Date} date The date or valid date string
+ * @return {Ext.form.field.Date} this
+ */
+
+ /**
+ * Attempts to parse a given string value using a given {@link Ext.Date#parse date format}.
+ * @param {String} value The value to attempt to parse
+ * @param {String} format A valid date format (see {@link Ext.Date#parse})
+ * @return {Date} The parsed Date object, or null if the value could not be successfully parsed.
+ */
+ safeParse : function(value, format) {
+ var me = this,
+ utilDate = Ext.Date,
+ parsedDate,
+ result = null;
+
+ if (utilDate.formatContainsHourInfo(format)) {
+ // if parse format contains hour information, no DST adjustment is necessary
+ result = utilDate.parse(value, format);
+ } else {
+ // set time to 12 noon, then clear the time
+ parsedDate = utilDate.parse(value + ' ' + me.initTime, format + ' ' + me.initTimeFormat);
+ if (parsedDate) {
+ result = utilDate.clearTime(parsedDate);
+ }
+ }
+ return result;
+ },
+
+ // @private
+ getSubmitValue: function() {
+ var format = this.submitFormat || this.format,
+ value = this.getValue();
+
+ return value ? Ext.Date.format(value, format) : '';
+ },
+
+ /**
+ * @private
+ */
+ parseDate : function(value) {
+ if(!value || Ext.isDate(value)){
+ return value;
+ }
+
+ var me = this,
+ val = me.safeParse(value, me.format),
+ altFormats = me.altFormats,
+ altFormatsArray = me.altFormatsArray,
+ i = 0,
+ len;
+
+ if (!val && altFormats) {
+ altFormatsArray = altFormatsArray || altFormats.split('|');
+ len = altFormatsArray.length;
+ for (; i < len && !val; ++i) {
+ val = me.safeParse(value, altFormatsArray[i]);
+ }
+ }
+ return val;
+ },
+
+ // private
+ formatDate : function(date){
+ return Ext.isDate(date) ? Ext.Date.dateFormat(date, this.format) : date;
+ },
+
+ createPicker: function() {
+ var me = this,
+ format = Ext.String.format;
+
+ return Ext.create('Ext.picker.Date', {
+ pickerField: me,
+ ownerCt: me.ownerCt,
+ renderTo: document.body,
+ floating: true,
+ hidden: true,
+ focusOnShow: true,
+ minDate: me.minValue,
+ maxDate: me.maxValue,
+ disabledDatesRE: me.disabledDatesRE,
+ disabledDatesText: me.disabledDatesText,
+ disabledDays: me.disabledDays,
+ disabledDaysText: me.disabledDaysText,
+ format: me.format,
+ showToday: me.showToday,
+ startDay: me.startDay,
+ minText: format(me.minText, me.formatDate(me.minValue)),
+ maxText: format(me.maxText, me.formatDate(me.maxValue)),
+ listeners: {
+ scope: me,
+ select: me.onSelect
+ },
+ keyNavConfig: {
+ esc: function() {
+ me.collapse();
+ }
+ }
+ });
+ },
+
+ onSelect: function(m, d) {
+ var me = this;
+
+ me.setValue(d);
+ me.fireEvent('select', me, d);
+ me.collapse();
+ },
+
+ /**
+ * @private
+ * Sets the Date picker's value to match the current field value when expanding.
+ */
+ onExpand: function() {
+ var value = this.getValue();
+ this.picker.setValue(Ext.isDate(value) ? value : new Date());
+ },
+
+ /**
+ * @private
+ * Focuses the field when collapsing the Date picker.
+ */
+ onCollapse: function() {
+ this.focus(false, 60);
+ },
+
+ // private
+ beforeBlur : function(){
+ var me = this,
+ v = me.parseDate(me.getRawValue()),
+ focusTask = me.focusTask;
+
+ if (focusTask) {
+ focusTask.cancel();
+ }
+
+ if (v) {
+ me.setValue(v);
+ }
+ }
+
+ /**
+ * @hide
+ * @cfg {Boolean} grow
+ */
+ /**
+ * @hide
+ * @cfg {Number} growMin
+ */
+ /**
+ * @hide
+ * @cfg {Number} growMax
+ */
+ /**
+ * @hide
+ * @method autoSize
+ */
+});
+
+/**
+ * A display-only text field which is not validated and not submitted. This is useful for when you want to display a
+ * value from a form's {@link Ext.form.Basic#load loaded data} but do not want to allow the user to edit or submit that
+ * value. The value can be optionally {@link #htmlEncode HTML encoded} if it contains HTML markup that you do not want
+ * to be rendered.
+ *
+ * If you have more complex content, or need to include components within the displayed content, also consider using a
+ * {@link Ext.form.FieldContainer} instead.
+ *
+ * Example:
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * renderTo: Ext.getBody(),
+ * width: 175,
+ * height: 120,
+ * bodyPadding: 10,
+ * title: 'Final Score',
+ * items: [{
+ * xtype: 'displayfield',
+ * fieldLabel: 'Home',
+ * name: 'home_score',
+ * value: '10'
+ * }, {
+ * xtype: 'displayfield',
+ * fieldLabel: 'Visitor',
+ * name: 'visitor_score',
+ * value: '11'
+ * }],
+ * buttons: [{
+ * text: 'Update',
+ * }]
+ * });
+ */
+Ext.define('Ext.form.field.Display', {
+ extend:'Ext.form.field.Base',
+ alias: 'widget.displayfield',
+ requires: ['Ext.util.Format', 'Ext.XTemplate'],
+ alternateClassName: ['Ext.form.DisplayField', 'Ext.form.Display'],
+ fieldSubTpl: [
+ '<div id="{id}" class="{fieldCls}"></div>',
+ {
+ compiled: true,
+ disableFormats: true
+ }
+ ],
+
+ /**
+ * @cfg {String} [fieldCls="x-form-display-field"]
+ * The default CSS class for the field.
+ */
+ fieldCls: Ext.baseCSSPrefix + 'form-display-field',
+
+ /**
+ * @cfg {Boolean} htmlEncode
+ * false to skip HTML-encoding the text when rendering it. This might be useful if you want to
+ * include tags in the field's innerHTML rather than rendering them as string literals per the default logic.
+ */
+ htmlEncode: false,
+
+ validateOnChange: false,
+
+ initEvents: Ext.emptyFn,
+
+ submitValue: false,
+
+ isValid: function() {
+ return true;
+ },
+
+ validate: function() {
+ return true;
+ },
+
+ getRawValue: function() {
+ return this.rawValue;
+ },
+
+ setRawValue: function(value) {
+ var me = this;
+ value = Ext.value(value, '');
+ me.rawValue = value;
+ if (me.rendered) {
+ me.inputEl.dom.innerHTML = me.htmlEncode ? Ext.util.Format.htmlEncode(value) : value;
+ }
+ return value;
+ },
+
+ // private
+ getContentTarget: function() {
+ return this.inputEl;
+ }
+
+ /**
+ * @cfg {String} inputType
+ * @hide
+ */
+ /**
+ * @cfg {Boolean} disabled
+ * @hide
+ */
+ /**
+ * @cfg {Boolean} readOnly
+ * @hide
+ */
+ /**
+ * @cfg {Boolean} validateOnChange
+ * @hide
+ */
+ /**
+ * @cfg {Number} checkChangeEvents
+ * @hide
+ */
+ /**
+ * @cfg {Number} checkChangeBuffer
+ * @hide
+ */
+});
+
+/**
+ * @docauthor Jason Johnston <jason@sencha.com>
+ *
+ * A file upload field which has custom styling and allows control over the button text and other
+ * features of {@link Ext.form.field.Text text fields} like {@link Ext.form.field.Text#emptyText empty text}.
+ * It uses a hidden file input element behind the scenes to allow user selection of a file and to
+ * perform the actual upload during {@link Ext.form.Basic#submit form submit}.
+ *
+ * Because there is no secure cross-browser way to programmatically set the value of a file input,
+ * the standard Field `setValue` method is not implemented. The `{@link #getValue}` method will return
+ * a value that is browser-dependent; some have just the file name, some have a full path, some use
+ * a fake path.
+ *
+ * **IMPORTANT:** File uploads are not performed using normal 'Ajax' techniques; see the description for
+ * {@link Ext.form.Basic#hasUpload} for details.
+ *
+ * # Example Usage
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'Upload a Photo',
+ * width: 400,
+ * bodyPadding: 10,
+ * frame: true,
+ * renderTo: Ext.getBody(),
+ * items: [{
+ * xtype: 'filefield',
+ * name: 'photo',
+ * fieldLabel: 'Photo',
+ * labelWidth: 50,
+ * msgTarget: 'side',
+ * allowBlank: false,
+ * anchor: '100%',
+ * buttonText: 'Select Photo...'
+ * }],
+ *
+ * buttons: [{
+ * text: 'Upload',
+ * handler: function() {
+ * var form = this.up('form').getForm();
+ * if(form.isValid()){
+ * form.submit({
+ * url: 'photo-upload.php',
+ * waitMsg: 'Uploading your photo...',
+ * success: function(fp, o) {
+ * Ext.Msg.alert('Success', 'Your photo "' + o.result.file + '" has been uploaded.');
+ * }
+ * });
+ * }
+ * }
+ * }]
+ * });
+ */
+Ext.define("Ext.form.field.File", {
+ extend: 'Ext.form.field.Text',
+ alias: ['widget.filefield', 'widget.fileuploadfield'],
+ alternateClassName: ['Ext.form.FileUploadField', 'Ext.ux.form.FileUploadField', 'Ext.form.File'],
+ uses: ['Ext.button.Button', 'Ext.layout.component.field.File'],
+
+ /**
+ * @cfg {String} buttonText
+ * The button text to display on the upload button. Note that if you supply a value for
+ * {@link #buttonConfig}, the buttonConfig.text value will be used instead if available.
+ */
+ buttonText: 'Browse...',
+
+ /**
+ * @cfg {Boolean} buttonOnly
+ * True to display the file upload field as a button with no visible text field. If true, all
+ * inherited Text members will still be available.
+ */
+ buttonOnly: false,
+
+ /**
+ * @cfg {Number} buttonMargin
+ * The number of pixels of space reserved between the button and the text field. Note that this only
+ * applies if {@link #buttonOnly} = false.
+ */
+ buttonMargin: 3,
+
+ /**
+ * @cfg {Object} buttonConfig
+ * A standard {@link Ext.button.Button} config object.
+ */
+
+ /**
+ * @event change
+ * Fires when the underlying file input field's value has changed from the user selecting a new file from the system
+ * file selection dialog.
+ * @param {Ext.ux.form.FileUploadField} this
+ * @param {String} value The file value returned by the underlying file input field
+ */
+
+ /**
+ * @property {Ext.Element} fileInputEl
+ * A reference to the invisible file input element created for this upload field. Only populated after this
+ * component is rendered.
+ */
+
+ /**
+ * @property {Ext.button.Button} button
+ * A reference to the trigger Button component created for this upload field. Only populated after this component is
+ * rendered.
+ */
+
+ /**
+ * @cfg {String} [fieldBodyCls='x-form-file-wrap']
+ * An extra CSS class to be applied to the body content element in addition to {@link #fieldBodyCls}.
+ */
+ fieldBodyCls: Ext.baseCSSPrefix + 'form-file-wrap',
+
+ /**
+ * @cfg {Boolean} readOnly
+ * Unlike with other form fields, the readOnly config defaults to true in File field.
+ */
+ readOnly: true,
+
+ // private
+ componentLayout: 'filefield',
+
+ // private
+ onRender: function() {
+ var me = this,
+ inputEl;
+
+ me.callParent(arguments);
+
+ me.createButton();
+ me.createFileInput();
+
+ // we don't create the file/button til after onRender, the initial disable() is
+ // called in the onRender of the component.
+ if (me.disabled) {
+ me.disableItems();
+ }
+
+ inputEl = me.inputEl;
+ inputEl.dom.removeAttribute('name'); //name goes on the fileInput, not the text input
+ if (me.buttonOnly) {
+ inputEl.setDisplayed(false);
+ }
+ },
+
+ /**
+ * @private
+ * Creates the custom trigger Button component. The fileInput will be inserted into this.
+ */
+ createButton: function() {
+ var me = this;
+ me.button = Ext.widget('button', Ext.apply({
+ ui: me.ui,
+ renderTo: me.bodyEl,
+ text: me.buttonText,
+ cls: Ext.baseCSSPrefix + 'form-file-btn',
+ preventDefault: false,
+ style: me.buttonOnly ? '' : 'margin-left:' + me.buttonMargin + 'px'
+ }, me.buttonConfig));
+ },
+
+ /**
+ * @private
+ * Creates the file input element. It is inserted into the trigger button component, made
+ * invisible, and floated on top of the button's other content so that it will receive the
+ * button's clicks.
+ */
+ createFileInput : function() {
+ var me = this;
+ me.fileInputEl = me.button.el.createChild({
+ name: me.getName(),
+ cls: Ext.baseCSSPrefix + 'form-file-input',
+ tag: 'input',
+ type: 'file',
+ size: 1
+ }).on('change', me.onFileChange, me);
+ },
+
+ /**
+ * @private Event handler fired when the user selects a file.
+ */
+ onFileChange: function() {
+ this.lastValue = null; // force change event to get fired even if the user selects a file with the same name
+ Ext.form.field.File.superclass.setValue.call(this, this.fileInputEl.dom.value);
+ },
+
+ /**
+ * Overridden to do nothing
+ * @hide
+ */
+ setValue: Ext.emptyFn,
+
+ reset : function(){
+ var me = this;
+ if (me.rendered) {
+ me.fileInputEl.remove();
+ me.createFileInput();
+ me.inputEl.dom.value = '';
+ }
+ me.callParent();
+ },
+
+ onDisable: function(){
+ this.callParent();
+ this.disableItems();
+ },
+
+ disableItems: function(){
+ var file = this.fileInputEl,
+ button = this.button;
+
+ if (file) {
+ file.dom.disabled = true;
+ }
+ if (button) {
+ button.disable();
+ }
+ },
+
+ onEnable: function(){
+ var me = this;
+ me.callParent();
+ me.fileInputEl.dom.disabled = false;
+ me.button.enable();
+ },
+
+ isFileUpload: function() {
+ return true;
+ },
+
+ extractFileInput: function() {
+ var fileInput = this.fileInputEl.dom;
+ this.reset();
+ return fileInput;
+ },
+
+ onDestroy: function(){
+ Ext.destroyMembers(this, 'fileInputEl', 'button');
+ this.callParent();
+ }
+
+
+});
+
+/**
+ * A basic hidden field for storing hidden values in forms that need to be passed in the form submit.
+ *
+ * This creates an actual input element with type="submit" in the DOM. While its label is
+ * {@link #hideLabel not rendered} by default, it is still a real component and may be sized according
+ * to its owner container's layout.
+ *
+ * Because of this, in most cases it is more convenient and less problematic to simply
+ * {@link Ext.form.action.Action#params pass hidden parameters} directly when
+ * {@link Ext.form.Basic#submit submitting the form}.
+ *
+ * Example:
+ *
+ * new Ext.form.Panel({
+ * title: 'My Form',
+ * items: [{
+ * xtype: 'textfield',
+ * fieldLabel: 'Text Field',
+ * name: 'text_field',
+ * value: 'value from text field'
+ * }, {
+ * xtype: 'hiddenfield',
+ * name: 'hidden_field_1',
+ * value: 'value from hidden field'
+ * }],
+ *
+ * buttons: [{
+ * text: 'Submit',
+ * handler: function() {
+ * this.up('form').getForm().submit({
+ * params: {
+ * hidden_field_2: 'value from submit call'
+ * }
+ * });
+ * }
+ * }]
+ * });
+ *
+ * Submitting the above form will result in three values sent to the server:
+ *
+ * text_field=value+from+text+field&hidden;_field_1=value+from+hidden+field&hidden_field_2=value+from+submit+call
+ *
+ */
+Ext.define('Ext.form.field.Hidden', {
+ extend:'Ext.form.field.Base',
+ alias: ['widget.hiddenfield', 'widget.hidden'],
+ alternateClassName: 'Ext.form.Hidden',
+
+ // private
+ inputType : 'hidden',
+ hideLabel: true,
+
+ initComponent: function(){
+ this.formItemCls += '-hidden';
+ this.callParent();
+ },
+
+ /**
+ * @private
+ * Override. Treat undefined and null values as equal to an empty string value.
+ */
+ isEqual: function(value1, value2) {
+ return this.isEqualAsString(value1, value2);
+ },
+
+ // These are all private overrides
+ initEvents: Ext.emptyFn,
+ setSize : Ext.emptyFn,
+ setWidth : Ext.emptyFn,
+ setHeight : Ext.emptyFn,
+ setPosition : Ext.emptyFn,
+ setPagePosition : Ext.emptyFn,
+ markInvalid : Ext.emptyFn,
+ clearInvalid : Ext.emptyFn
+});
+
+/**
+ * Color picker provides a simple color palette for choosing colors. The picker can be rendered to any container. The
+ * available default to a standard 40-color palette; this can be customized with the {@link #colors} config.
+ *
+ * Typically you will need to implement a handler function to be notified when the user chooses a color from the picker;
+ * you can register the handler using the {@link #select} event, or by implementing the {@link #handler} method.
+ *
+ * @example
+ * Ext.create('Ext.picker.Color', {
+ * value: '993300', // initial selected color
+ * renderTo: Ext.getBody(),
+ * listeners: {
+ * select: function(picker, selColor) {
+ * alert(selColor);
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.picker.Color', {
+ extend: 'Ext.Component',
+ requires: 'Ext.XTemplate',
+ alias: 'widget.colorpicker',
+ alternateClassName: 'Ext.ColorPalette',
+
+ /**
+ * @cfg {String} [componentCls='x-color-picker']
+ * The CSS class to apply to the containing element.
+ */
+ componentCls : Ext.baseCSSPrefix + 'color-picker',
+
+ /**
+ * @cfg {String} [selectedCls='x-color-picker-selected']
+ * The CSS class to apply to the selected element
+ */
+ selectedCls: Ext.baseCSSPrefix + 'color-picker-selected',
+
+ /**
+ * @cfg {String} value
+ * The initial color to highlight (should be a valid 6-digit color hex code without the # symbol). Note that the hex
+ * codes are case-sensitive.
+ */
+ value : null,
+
+ /**
+ * @cfg {String} clickEvent
+ * The DOM event that will cause a color to be selected. This can be any valid event name (dblclick, contextmenu).
+ */
+ clickEvent :'click',
+
+ /**
+ * @cfg {Boolean} allowReselect
+ * If set to true then reselecting a color that is already selected fires the {@link #select} event
+ */
+ allowReselect : false,
+
+ /**
+ * @property {String[]} colors
+ * An array of 6-digit color hex code strings (without the # symbol). This array can contain any number of colors,
+ * and each hex code should be unique. The width of the picker is controlled via CSS by adjusting the width property
+ * of the 'x-color-picker' class (or assigning a custom class), so you can balance the number of colors with the
+ * width setting until the box is symmetrical.
+ *
+ * You can override individual colors if needed:
+ *
+ * var cp = new Ext.picker.Color();
+ * cp.colors[0] = 'FF0000'; // change the first box to red
+ *
+ * Or you can provide a custom array of your own for complete control:
+ *
+ * var cp = new Ext.picker.Color();
+ * cp.colors = ['000000', '993300', '333300'];
+ */
+ colors : [
+ '000000', '993300', '333300', '003300', '003366', '000080', '333399', '333333',
+ '800000', 'FF6600', '808000', '008000', '008080', '0000FF', '666699', '808080',
+ 'FF0000', 'FF9900', '99CC00', '339966', '33CCCC', '3366FF', '800080', '969696',
+ 'FF00FF', 'FFCC00', 'FFFF00', '00FF00', '00FFFF', '00CCFF', '993366', 'C0C0C0',
+ 'FF99CC', 'FFCC99', 'FFFF99', 'CCFFCC', 'CCFFFF', '99CCFF', 'CC99FF', 'FFFFFF'
+ ],
+
+ /**
+ * @cfg {Function} handler
+ * A function that will handle the select event of this picker. The handler is passed the following parameters:
+ *
+ * - `picker` : ColorPicker
+ *
+ * The {@link Ext.picker.Color picker}.
+ *
+ * - `color` : String
+ *
+ * The 6-digit color hex code (without the # symbol).
+ */
+
+ /**
+ * @cfg {Object} scope
+ * The scope (`this` reference) in which the `{@link #handler}` function will be called. Defaults to this
+ * Color picker instance.
+ */
+
+ colorRe: /(?:^|\s)color-(.{6})(?:\s|$)/,
+
+ renderTpl: [
+ '<tpl for="colors">',
+ '<a href="#" class="color-{.}" hidefocus="on">',
+ '<em><span style="background:#{.}" unselectable="on"> </span></em>',
+ '</a>',
+ '</tpl>'
+ ],
+
+ // private
+ initComponent : function(){
+ var me = this;
+
+ me.callParent(arguments);
+ me.addEvents(
+ /**
+ * @event select
+ * Fires when a color is selected
+ * @param {Ext.picker.Color} this
+ * @param {String} color The 6-digit color hex code (without the # symbol)
+ */
+ 'select'
+ );
+
+ if (me.handler) {
+ me.on('select', me.handler, me.scope, true);
+ }
+ },
+
+
+ // private
+ onRender : function(container, position){
+ var me = this,
+ clickEvent = me.clickEvent;
+
+ Ext.apply(me.renderData, {
+ itemCls: me.itemCls,
+ colors: me.colors
+ });
+ me.callParent(arguments);
+
+ me.mon(me.el, clickEvent, me.handleClick, me, {delegate: 'a'});
+ // always stop following the anchors
+ if(clickEvent != 'click'){
+ me.mon(me.el, 'click', Ext.emptyFn, me, {delegate: 'a', stopEvent: true});
+ }
+ },
+
+ // private
+ afterRender : function(){
+ var me = this,
+ value;
+
+ me.callParent(arguments);
+ if (me.value) {
+ value = me.value;
+ me.value = null;
+ me.select(value, true);
+ }
+ },
+
+ // private
+ handleClick : function(event, target){
+ var me = this,
+ color;
+
+ event.stopEvent();
+ if (!me.disabled) {
+ color = target.className.match(me.colorRe)[1];
+ me.select(color.toUpperCase());
+ }
+ },
+
+ /**
+ * Selects the specified color in the picker (fires the {@link #select} event)
+ * @param {String} color A valid 6-digit color hex code (# will be stripped if included)
+ * @param {Boolean} suppressEvent (optional) True to stop the select event from firing. Defaults to false.
+ */
+ select : function(color, suppressEvent){
+
+ var me = this,
+ selectedCls = me.selectedCls,
+ value = me.value,
+ el;
+
+ color = color.replace('#', '');
+ if (!me.rendered) {
+ me.value = color;
+ return;
+ }
+
+
+ if (color != value || me.allowReselect) {
+ el = me.el;
+
+ if (me.value) {
+ el.down('a.color-' + value).removeCls(selectedCls);
+ }
+ el.down('a.color-' + color).addCls(selectedCls);
+ me.value = color;
+ if (suppressEvent !== true) {
+ me.fireEvent('select', me, color);
+ }
+ }
+ },
+
+ /**
+ * Get the currently selected color value.
+ * @return {String} value The selected value. Null if nothing is selected.
+ */
+ getValue: function(){
+ return this.value || null;
+ }
+});
+
+/**
+ * @private
+ * @class Ext.layout.component.field.HtmlEditor
+ * @extends Ext.layout.component.field.Field
+ * Layout class for {@link Ext.form.field.HtmlEditor} fields. Sizes the toolbar, textarea, and iframe elements.
+ * @private
+ */
+
+Ext.define('Ext.layout.component.field.HtmlEditor', {
+ extend: 'Ext.layout.component.field.Field',
+ alias: ['layout.htmleditor'],
+
+ type: 'htmleditor',
+
+ sizeBodyContents: function(width, height) {
+ var me = this,
+ owner = me.owner,
+ bodyEl = owner.bodyEl,
+ toolbar = owner.getToolbar(),
+ textarea = owner.textareaEl,
+ iframe = owner.iframeEl,
+ editorHeight;
+
+ if (Ext.isNumber(width)) {
+ width -= bodyEl.getFrameWidth('lr');
+ }
+ toolbar.setWidth(width);
+ textarea.setWidth(width);
+ iframe.setWidth(width);
+
+ // If fixed height, subtract toolbar height from the input area height
+ if (Ext.isNumber(height)) {
+ editorHeight = height - toolbar.getHeight() - bodyEl.getFrameWidth('tb');
+ textarea.setHeight(editorHeight);
+ iframe.setHeight(editorHeight);
+ }
+ }
+});
+/**
+ * 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.tip.QuickTipManager} singleton is
+ * {@link Ext.tip.QuickTipManager#init initialized}.
+ *
+ * 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:
+ *
+ * @example
+ * Ext.tip.QuickTipManager.init(); // enable tooltips
+ * Ext.create('Ext.form.HtmlEditor', {
+ * width: 580,
+ * height: 250,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * Passed via xtype into a container and with custom options:
+ *
+ * @example
+ * Ext.tip.QuickTipManager.init(); // enable tooltips
+ * new Ext.panel.Panel({
+ * title: 'HTML Editor',
+ * renderTo: Ext.getBody(),
+ * width: 550,
+ * height: 250,
+ * frame: true,
+ * layout: 'fit',
+ * items: {
+ * xtype: 'htmleditor',
+ * enableColors: false,
+ * enableAlignments: false
+ * }
+ * });
+ */
+Ext.define('Ext.form.field.HtmlEditor', {
+ extend:'Ext.Component',
+ mixins: {
+ labelable: 'Ext.form.Labelable',
+ field: 'Ext.form.field.Field'
+ },
+ alias: 'widget.htmleditor',
+ alternateClassName: 'Ext.form.HtmlEditor',
+ requires: [
+ 'Ext.tip.QuickTipManager',
+ 'Ext.picker.Color',
+ 'Ext.toolbar.Item',
+ 'Ext.toolbar.Toolbar',
+ 'Ext.util.Format',
+ 'Ext.layout.component.field.HtmlEditor'
+ ],
+
+ fieldSubTpl: [
+ '<div id="{cmpId}-toolbarWrap" class="{toolbarWrapCls}"></div>',
+ '<textarea id="{cmpId}-textareaEl" name="{name}" tabIndex="-1" class="{textareaCls}" ',
+ 'style="{size}" autocomplete="off"></textarea>',
+ '<iframe id="{cmpId}-iframeEl" name="{iframeName}" frameBorder="0" style="overflow:auto;{size}" src="{iframeSrc}"></iframe>',
+ {
+ compiled: true,
+ disableFormats: true
+ }
+ ],
+
+ /**
+ * @cfg {Boolean} enableFormat
+ * Enable the bold, italic and underline buttons
+ */
+ enableFormat : true,
+ /**
+ * @cfg {Boolean} enableFontSize
+ * Enable the increase/decrease font size buttons
+ */
+ enableFontSize : true,
+ /**
+ * @cfg {Boolean} enableColors
+ * Enable the fore/highlight color buttons
+ */
+ enableColors : true,
+ /**
+ * @cfg {Boolean} enableAlignments
+ * Enable the left, center, right alignment buttons
+ */
+ enableAlignments : true,
+ /**
+ * @cfg {Boolean} enableLists
+ * Enable the bullet and numbered list buttons. Not available in Safari.
+ */
+ enableLists : true,
+ /**
+ * @cfg {Boolean} enableSourceEdit
+ * Enable the switch to source edit button. Not available in Safari.
+ */
+ enableSourceEdit : true,
+ /**
+ * @cfg {Boolean} enableLinks
+ * Enable the create link button. Not available in Safari.
+ */
+ enableLinks : true,
+ /**
+ * @cfg {Boolean} enableFont
+ * Enable font selection. Not available in Safari.
+ */
+ enableFont : true,
+ /**
+ * @cfg {String} createLinkText
+ * The default text for the create link prompt
+ */
+ createLinkText : 'Please enter the URL for the link:',
+ /**
+ * @cfg {String} [defaultLinkValue='http://']
+ * The default value for the create link prompt
+ */
+ defaultLinkValue : 'http:/'+'/',
+ /**
+ * @cfg {String[]} 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) ? ' ' : '​',
+
+ fieldBodyCls: Ext.baseCSSPrefix + 'html-editor-wrap',
+
+ componentLayout: 'htmleditor',
+
+ // private properties
+ initialized : false,
+ activated : false,
+ sourceEditMode : false,
+ iframePad:3,
+ hideMode:'offsets',
+
+ maskOnDisable: true,
+
+ // private
+ initComponent : function(){
+ var me = this;
+
+ me.addEvents(
+ /**
+ * @event initialize
+ * Fires when the editor is fully initialized (including the iframe)
+ * @param {Ext.form.field.HtmlEditor} this
+ */
+ 'initialize',
+ /**
+ * @event activate
+ * Fires when the editor is first receives the focus. Any insertion must wait until after this event.
+ * @param {Ext.form.field.HtmlEditor} this
+ */
+ 'activate',
+ /**
+ * @event beforesync
+ * Fires before the textarea is updated with content from the editor iframe. Return false to cancel the
+ * sync.
+ * @param {Ext.form.field.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 {Ext.form.field.HtmlEditor} this
+ * @param {String} html
+ */
+ 'beforepush',
+ /**
+ * @event sync
+ * Fires when the textarea is updated with content from the editor iframe.
+ * @param {Ext.form.field.HtmlEditor} this
+ * @param {String} html
+ */
+ 'sync',
+ /**
+ * @event push
+ * Fires when the iframe editor is updated with content from the textarea.
+ * @param {Ext.form.field.HtmlEditor} this
+ * @param {String} html
+ */
+ 'push',
+ /**
+ * @event editmodechange
+ * Fires when the editor switches edit modes
+ * @param {Ext.form.field.HtmlEditor} this
+ * @param {Boolean} sourceEdit True if source edit, false if standard editing.
+ */
+ 'editmodechange'
+ );
+
+ me.callParent(arguments);
+
+ // Init mixins
+ me.initLabelable();
+ me.initField();
+ },
+
+ /**
+ * Called when the editor creates its toolbar. Override this method if you need to
+ * add custom toolbar buttons.
+ * @param {Ext.form.field.HtmlEditor} editor
+ * @protected
+ */
+ createToolbar : function(editor){
+ var me = this,
+ items = [],
+ tipsEnabled = Ext.tip.QuickTipManager && Ext.tip.QuickTipManager.isEnabled(),
+ baseCSSPrefix = Ext.baseCSSPrefix,
+ fontSelectItem, toolbar, undef;
+
+ function btn(id, toggle, handler){
+ return {
+ itemId : id,
+ cls : baseCSSPrefix + 'btn-icon',
+ iconCls: baseCSSPrefix + 'edit-'+id,
+ enableToggle:toggle !== false,
+ scope: editor,
+ handler:handler||editor.relayBtnCmd,
+ clickEvent:'mousedown',
+ tooltip: tipsEnabled ? editor.buttonTips[id] || undef : undef,
+ overflowText: editor.buttonTips[id].title || undef,
+ tabIndex:-1
+ };
+ }
+
+
+ if (me.enableFont && !Ext.isSafari2) {
+ fontSelectItem = Ext.widget('component', {
+ renderTpl: [
+ '<select id="{id}-selectEl" class="{cls}">',
+ '<tpl for="fonts">',
+ '<option value="{[values.toLowerCase()]}" style="font-family:{.}"<tpl if="values.toLowerCase()==parent.defaultFont"> selected</tpl>>{.}</option>',
+ '</tpl>',
+ '</select>'
+ ],
+ renderData: {
+ cls: baseCSSPrefix + 'font-select',
+ fonts: me.fontFamilies,
+ defaultFont: me.defaultFont
+ },
+ childEls: ['selectEl'],
+ onDisable: function() {
+ var selectEl = this.selectEl;
+ if (selectEl) {
+ selectEl.dom.disabled = true;
+ }
+ Ext.Component.superclass.onDisable.apply(this, arguments);
+ },
+ onEnable: function() {
+ var selectEl = this.selectEl;
+ if (selectEl) {
+ selectEl.dom.disabled = false;
+ }
+ Ext.Component.superclass.onEnable.apply(this, arguments);
+ }
+ });
+
+ items.push(
+ fontSelectItem,
+ '-'
+ );
+ }
+
+ if (me.enableFormat) {
+ items.push(
+ btn('bold'),
+ btn('italic'),
+ btn('underline')
+ );
+ }
+
+ if (me.enableFontSize) {
+ items.push(
+ '-',
+ btn('increasefontsize', false, me.adjustFont),
+ btn('decreasefontsize', false, me.adjustFont)
+ );
+ }
+
+ if (me.enableColors) {
+ items.push(
+ '-', {
+ itemId: 'forecolor',
+ cls: baseCSSPrefix + 'btn-icon',
+ iconCls: baseCSSPrefix + 'edit-forecolor',
+ overflowText: editor.buttonTips.forecolor.title,
+ tooltip: tipsEnabled ? editor.buttonTips.forecolor || undef : undef,
+ tabIndex:-1,
+ menu : Ext.widget('menu', {
+ plain: true,
+ items: [{
+ xtype: 'colorpicker',
+ allowReselect: true,
+ focus: Ext.emptyFn,
+ value: '000000',
+ plain: true,
+ clickEvent: 'mousedown',
+ handler: function(cp, color) {
+ me.execCmd('forecolor', Ext.isWebKit || Ext.isIE ? '#'+color : color);
+ me.deferFocus();
+ this.up('menu').hide();
+ }
+ }]
+ })
+ }, {
+ itemId: 'backcolor',
+ cls: baseCSSPrefix + 'btn-icon',
+ iconCls: baseCSSPrefix + 'edit-backcolor',
+ overflowText: editor.buttonTips.backcolor.title,
+ tooltip: tipsEnabled ? editor.buttonTips.backcolor || undef : undef,
+ tabIndex:-1,
+ menu : Ext.widget('menu', {
+ plain: true,
+ items: [{
+ xtype: 'colorpicker',
+ focus: Ext.emptyFn,
+ value: 'FFFFFF',
+ plain: true,
+ allowReselect: true,
+ clickEvent: 'mousedown',
+ handler: function(cp, color) {
+ if (Ext.isGecko) {
+ me.execCmd('useCSS', false);
+ me.execCmd('hilitecolor', color);
+ me.execCmd('useCSS', true);
+ me.deferFocus();
+ } else {
+ me.execCmd(Ext.isOpera ? 'hilitecolor' : 'backcolor', Ext.isWebKit || Ext.isIE ? '#'+color : color);
+ me.deferFocus();
+ }
+ this.up('menu').hide();
+ }
+ }]
+ })
+ }
+ );
+ }
+
+ if (me.enableAlignments) {
+ items.push(
+ '-',
+ btn('justifyleft'),
+ btn('justifycenter'),
+ btn('justifyright')
+ );
+ }
+
+ if (!Ext.isSafari2) {
+ if (me.enableLinks) {
+ items.push(
+ '-',
+ btn('createlink', false, me.createLink)
+ );
+ }
+
+ if (me.enableLists) {
+ items.push(
+ '-',
+ btn('insertorderedlist'),
+ btn('insertunorderedlist')
+ );
+ }
+ if (me.enableSourceEdit) {
+ items.push(
+ '-',
+ btn('sourceedit', true, function(btn){
+ me.toggleSourceEdit(!me.sourceEditMode);
+ })
+ );
+ }
+ }
+
+ // build the toolbar
+ toolbar = Ext.widget('toolbar', {
+ renderTo: me.toolbarWrap,
+ enableOverflow: true,
+ items: items
+ });
+
+ if (fontSelectItem) {
+ me.fontSelect = fontSelectItem.selectEl;
+
+ me.mon(me.fontSelect, 'change', function(){
+ me.relayCmd('fontname', me.fontSelect.dom.value);
+ me.deferFocus();
+ });
+ }
+
+ // stop form submits
+ me.mon(toolbar.el, 'click', function(e){
+ e.preventDefault();
+ });
+
+ me.toolbar = toolbar;
+ },
+
+ onDisable: function() {
+ this.bodyEl.mask();
+ this.callParent(arguments);
+ },
+
+ onEnable: function() {
+ this.bodyEl.unmask();
+ this.callParent(arguments);
+ },
+
+ /**
+ * Sets the read only state of this field.
+ * @param {Boolean} readOnly Whether the field should be read only.
+ */
+ setReadOnly: function(readOnly) {
+ var me = this,
+ textareaEl = me.textareaEl,
+ iframeEl = me.iframeEl,
+ body;
+
+ me.readOnly = readOnly;
+
+ if (textareaEl) {
+ textareaEl.dom.readOnly = readOnly;
+ }
+
+ if (me.initialized) {
+ body = me.getEditorBody();
+ if (Ext.isIE) {
+ // Hide the iframe while setting contentEditable so it doesn't grab focus
+ iframeEl.setDisplayed(false);
+ body.contentEditable = !readOnly;
+ iframeEl.setDisplayed(true);
+ } else {
+ me.setDesignMode(!readOnly);
+ }
+ if (body) {
+ body.style.cursor = readOnly ? 'default' : 'text';
+ }
+ me.disableItems(readOnly);
+ }
+ },
+
+ /**
+ * 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.
+ * Also note that forcing IE7 mode works when the page is loaded normally, but if you are using IE's Web
+ * Developer Tools to manually set the document mode, that will take precedence and override what this
+ * code sets by default. This can be confusing when developing, but is not a user-facing issue.
+ * @protected
+ */
+ getDocMarkup: function() {
+ var me = this,
+ h = me.iframeEl.getHeight() - me.iframePad * 2;
+ return Ext.String.format('<html><head><style type="text/css">body{border:0;margin:0;padding:{0}px;height:{1}px;box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box;cursor:text}</style></head><body></body></html>', me.iframePad, h);
+ },
+
+ // private
+ getEditorBody: function() {
+ var doc = this.getDoc();
+ return doc.body || doc.documentElement;
+ },
+
+ // private
+ getDoc: function() {
+ return (!Ext.isIE && this.iframeEl.dom.contentDocument) || this.getWin().document;
+ },
+
+ // private
+ getWin: function() {
+ return Ext.isIE ? this.iframeEl.dom.contentWindow : window.frames[this.iframeEl.dom.name];
+ },
+
+ // private
+ onRender: function() {
+ var me = this;
+
+ me.onLabelableRender();
+
+ me.addChildEls('toolbarWrap', 'iframeEl', 'textareaEl');
+
+ me.callParent(arguments);
+
+ me.textareaEl.dom.value = me.value || '';
+
+ // Start polling for when the iframe document is ready to be manipulated
+ me.monitorTask = Ext.TaskManager.start({
+ run: me.checkDesignMode,
+ scope: me,
+ interval:100
+ });
+
+ me.createToolbar(me);
+ me.disableItems(true);
+ },
+
+ initRenderTpl: function() {
+ var me = this;
+ if (!me.hasOwnProperty('renderTpl')) {
+ me.renderTpl = me.getTpl('labelableRenderTpl');
+ }
+ return me.callParent();
+ },
+
+ initRenderData: function() {
+ return Ext.applyIf(this.callParent(), this.getLabelableRenderData());
+ },
+
+ getSubTplData: function() {
+ var cssPrefix = Ext.baseCSSPrefix;
+ return {
+ cmpId: this.id,
+ id: this.getInputId(),
+ toolbarWrapCls: cssPrefix + 'html-editor-tb',
+ textareaCls: cssPrefix + 'hidden',
+ iframeName: Ext.id(),
+ iframeSrc: Ext.SSL_SECURE_URL,
+ size: 'height:100px;'
+ };
+ },
+
+ getSubTplMarkup: function() {
+ var data = this.getSubTplData();
+ return this.getTpl('fieldSubTpl').apply(data);
+ },
+
+ getBodyNaturalWidth: function() {
+ return 565;
+ },
+
+ initFrameDoc: function() {
+ var me = this,
+ doc, task;
+
+ Ext.TaskManager.stop(me.monitorTask);
+
+ doc = me.getDoc();
+ me.win = me.getWin();
+
+ doc.open();
+ doc.write(me.getDocMarkup());
+ doc.close();
+
+ task = { // must defer to wait for browser to be ready
+ run: function() {
+ var doc = me.getDoc();
+ if (doc.body || doc.readyState === 'complete') {
+ Ext.TaskManager.stop(task);
+ me.setDesignMode(true);
+ Ext.defer(me.initEditor, 10, me);
+ }
+ },
+ interval : 10,
+ duration:10000,
+ scope: me
+ };
+ Ext.TaskManager.start(task);
+ },
+
+ checkDesignMode: function() {
+ var me = this,
+ doc = me.getDoc();
+ if (doc && (!doc.editorInitialized || me.getDesignMode() !== 'on')) {
+ me.initFrameDoc();
+ }
+ },
+
+ /**
+ * @private
+ * Sets current design mode. To enable, mode can be true or 'on', off otherwise
+ */
+ setDesignMode: function(mode) {
+ var me = this,
+ doc = me.getDoc();
+ if (doc) {
+ if (me.readOnly) {
+ mode = false;
+ }
+ doc.designMode = (/on|true/i).test(String(mode).toLowerCase()) ?'on':'off';
+ }
+ },
+
+ // private
+ getDesignMode: function() {
+ var doc = this.getDoc();
+ return !doc ? '' : String(doc.designMode).toLowerCase();
+ },
+
+ disableItems: function(disabled) {
+ this.getToolbar().items.each(function(item){
+ if(item.getItemId() !== 'sourceedit'){
+ item.setDisabled(disabled);
+ }
+ });
+ },
+
+ /**
+ * Toggles the editor between standard and source edit mode.
+ * @param {Boolean} sourceEditMode (optional) True for source edit, false for standard
+ */
+ toggleSourceEdit: function(sourceEditMode) {
+ var me = this,
+ iframe = me.iframeEl,
+ textarea = me.textareaEl,
+ hiddenCls = Ext.baseCSSPrefix + 'hidden',
+ btn = me.getToolbar().getComponent('sourceedit');
+
+ if (!Ext.isBoolean(sourceEditMode)) {
+ sourceEditMode = !me.sourceEditMode;
+ }
+ me.sourceEditMode = sourceEditMode;
+
+ if (btn.pressed !== sourceEditMode) {
+ btn.toggle(sourceEditMode);
+ }
+ if (sourceEditMode) {
+ me.disableItems(true);
+ me.syncValue();
+ iframe.addCls(hiddenCls);
+ textarea.removeCls(hiddenCls);
+ textarea.dom.removeAttribute('tabIndex');
+ textarea.focus();
+ }
+ else {
+ if (me.initialized) {
+ me.disableItems(me.readOnly);
+ }
+ me.pushValue();
+ iframe.removeCls(hiddenCls);
+ textarea.addCls(hiddenCls);
+ textarea.dom.setAttribute('tabIndex', -1);
+ me.deferFocus();
+ }
+ me.fireEvent('editmodechange', me, sourceEditMode);
+ me.doComponentLayout();
+ },
+
+ // private used internally
+ createLink : function() {
+ var url = prompt(this.createLinkText, this.defaultLinkValue);
+ if (url && url !== 'http:/'+'/') {
+ this.relayCmd('createlink', url);
+ }
+ },
+
+ clearInvalid: Ext.emptyFn,
+
+ // docs inherit from Field
+ setValue: function(value) {
+ var me = this,
+ textarea = me.textareaEl;
+ me.mixins.field.setValue.call(me, value);
+ if (value === null || value === undefined) {
+ value = '';
+ }
+ if (textarea) {
+ textarea.dom.value = value;
+ }
+ me.pushValue();
+ return this;
+ },
+
+ /**
+ * 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
+ * @protected
+ */
+ 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;
+ },
+
+ /**
+ * Syncs the contents of the editor iframe with the textarea.
+ * @protected
+ */
+ syncValue : function(){
+ var me = this,
+ body, html, bodyStyle, match;
+ if (me.initialized) {
+ body = me.getEditorBody();
+ html = body.innerHTML;
+ if (Ext.isWebKit) {
+ bodyStyle = body.getAttribute('style'); // Safari puts text-align styles on the body element!
+ match = bodyStyle.match(/text-align:(.*?);/i);
+ if (match && match[1]) {
+ html = '<div style="' + match[0] + '">' + html + '</div>';
+ }
+ }
+ html = me.cleanHtml(html);
+ if (me.fireEvent('beforesync', me, html) !== false) {
+ me.textareaEl.dom.value = html;
+ me.fireEvent('sync', me, html);
+ }
+ }
+ },
+
+ //docs inherit from Field
+ getValue : function() {
+ var me = this,
+ value;
+ if (!me.sourceEditMode) {
+ me.syncValue();
+ }
+ value = me.rendered ? me.textareaEl.dom.value : me.value;
+ me.value = value;
+ return value;
+ },
+
+ /**
+ * Pushes the value of the textarea into the iframe editor.
+ * @protected
+ */
+ pushValue: function() {
+ var me = this,
+ v;
+ if(me.initialized){
+ v = me.textareaEl.dom.value || '';
+ if (!me.activated && v.length < 1) {
+ v = me.defaultValue;
+ }
+ if (me.fireEvent('beforepush', me, v) !== false) {
+ me.getEditorBody().innerHTML = v;
+ if (Ext.isGecko) {
+ // Gecko hack, see: https://bugzilla.mozilla.org/show_bug.cgi?id=232791#c8
+ me.setDesignMode(false); //toggle off first
+ me.setDesignMode(true);
+ }
+ me.fireEvent('push', me, v);
+ }
+ }
+ },
+
+ // private
+ deferFocus : function(){
+ this.focus(false, true);
+ },
+
+ getFocusEl: function() {
+ var me = this,
+ win = me.win;
+ return win && !me.sourceEditMode ? win : me.textareaEl;
+ },
+
+ // private
+ initEditor : function(){
+ //Destroying the component during/before initEditor can cause issues.
+ try {
+ var me = this,
+ dbody = me.getEditorBody(),
+ ss = me.textareaEl.getStyles('font-size', 'font-family', 'background-image', 'background-repeat', 'background-color', 'color'),
+ doc,
+ fn;
+
+ ss['background-attachment'] = 'fixed'; // w3c
+ dbody.bgProperties = 'fixed'; // ie
+
+ Ext.DomHelper.applyStyles(dbody, ss);
+
+ doc = me.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 = Ext.Function.bind(me.onEditorEvent, me);
+ Ext.EventManager.on(doc, {
+ mousedown: fn,
+ dblclick: fn,
+ click: fn,
+ keyup: fn,
+ buffer:100
+ });
+
+ // These events need to be relayed from the inner document (where they stop
+ // bubbling) up to the outer document. This has to be done at the DOM level so
+ // the event reaches listeners on elements like the document body. The effected
+ // mechanisms that depend on this bubbling behavior are listed to the right
+ // of the event.
+ fn = me.onRelayedEvent;
+ Ext.EventManager.on(doc, {
+ mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront)
+ mousemove: fn, // window resize drag detection
+ mouseup: fn, // window resize termination
+ click: fn, // not sure, but just to be safe
+ dblclick: fn, // not sure again
+ scope: me
+ });
+
+ if (Ext.isGecko) {
+ Ext.EventManager.on(doc, 'keypress', me.applyCommand, me);
+ }
+ if (me.fixKeys) {
+ Ext.EventManager.on(doc, 'keydown', me.fixKeys, me);
+ }
+
+ // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK!
+ Ext.EventManager.on(window, 'unload', me.beforeDestroy, me);
+ doc.editorInitialized = true;
+
+ me.initialized = true;
+ me.pushValue();
+ me.setReadOnly(me.readOnly);
+ me.fireEvent('initialize', me);
+ } catch(ex) {
+ // ignore (why?)
+ }
+ },
+
+ // private
+ beforeDestroy : function(){
+ var me = this,
+ monitorTask = me.monitorTask,
+ doc, prop;
+
+ if (monitorTask) {
+ Ext.TaskManager.stop(monitorTask);
+ }
+ if (me.rendered) {
+ try {
+ doc = me.getDoc();
+ if (doc) {
+ Ext.EventManager.removeAll(doc);
+ for (prop in doc) {
+ if (doc.hasOwnProperty(prop)) {
+ delete doc[prop];
+ }
+ }
+ }
+ } catch(e) {
+ // ignore (why?)
+ }
+ Ext.destroyMembers(me, 'tb', 'toolbarWrap', 'iframeEl', 'textareaEl');
+ }
+ me.callParent();
+ },
+
+ // private
+ onRelayedEvent: function (event) {
+ // relay event from the iframe's document to the document that owns the iframe...
+
+ var iframeEl = this.iframeEl,
+ iframeXY = iframeEl.getXY(),
+ eventXY = event.getXY();
+
+ // the event from the inner document has XY relative to that document's origin,
+ // so adjust it to use the origin of the iframe in the outer document:
+ event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]];
+
+ event.injectEvent(iframeEl); // blame the iframe for the event...
+
+ event.xy = eventXY; // restore the original XY (just for safety)
+ },
+
+ // private
+ onFirstFocus : function(){
+ var me = this,
+ selection, range;
+ me.activated = true;
+ me.disableItems(me.readOnly);
+ if (Ext.isGecko) { // prevent silly gecko errors
+ me.win.focus();
+ selection = me.win.getSelection();
+ if (!selection.focusNode || selection.focusNode.nodeType !== 3) {
+ range = selection.getRangeAt(0);
+ range.selectNodeContents(me.getEditorBody());
+ range.collapse(true);
+ me.deferFocus();
+ }
+ try {
+ me.execCmd('useCSS', true);
+ me.execCmd('styleWithCSS', false);
+ } catch(e) {
+ // ignore (why?)
+ }
+ }
+ me.fireEvent('activate', me);
+ },
+
+ // private
+ adjustFont: function(btn) {
+ var adjust = btn.getItemId() === 'increasefontsize' ? 1 : -1,
+ size = this.getDoc().queryCommandValue('FontSize') || '2',
+ isPxSize = Ext.isString(size) && size.indexOf('px') !== -1,
+ isSafari;
+ size = parseInt(size, 10);
+ if (isPxSize) {
+ // Safari 3 values
+ // 1 = 10px, 2 = 13px, 3 = 16px, 4 = 18px, 5 = 24px, 6 = 32px
+ if (size <= 10) {
+ size = 1 + adjust;
+ }
+ else if (size <= 13) {
+ size = 2 + adjust;
+ }
+ else if (size <= 16) {
+ size = 3 + adjust;
+ }
+ else if (size <= 18) {
+ size = 4 + adjust;
+ }
+ else if (size <= 24) {
+ size = 5 + adjust;
+ }
+ else {
+ size = 6 + adjust;
+ }
+ size = Ext.Number.constrain(size, 1, 6);
+ } else {
+ isSafari = Ext.isSafari;
+ if (isSafari) { // safari
+ adjust *= 2;
+ }
+ size = Math.max(1, size + adjust) + (isSafari ? 'px' : 0);
+ }
+ this.execCmd('FontSize', size);
+ },
+
+ // private
+ onEditorEvent: function(e) {
+ this.updateToolbar();
+ },
+
+ /**
+ * Triggers a toolbar update by reading the markup state of the current selection in the editor.
+ * @protected
+ */
+ updateToolbar: function() {
+ var me = this,
+ btns, doc, name, fontSelect;
+
+ if (me.readOnly) {
+ return;
+ }
+
+ if (!me.activated) {
+ me.onFirstFocus();
+ return;
+ }
+
+ btns = me.getToolbar().items.map;
+ doc = me.getDoc();
+
+ if (me.enableFont && !Ext.isSafari2) {
+ name = (doc.queryCommandValue('FontName') || me.defaultFont).toLowerCase();
+ fontSelect = me.fontSelect.dom;
+ if (name !== fontSelect.value) {
+ fontSelect.value = name;
+ }
+ }
+
+ function updateButtons() {
+ Ext.Array.forEach(Ext.Array.toArray(arguments), function(name) {
+ btns[name].toggle(doc.queryCommandState(name));
+ });
+ }
+ if(me.enableFormat){
+ updateButtons('bold', 'italic', 'underline');
+ }
+ if(me.enableAlignments){
+ updateButtons('justifyleft', 'justifycenter', 'justifyright');
+ }
+ if(!Ext.isSafari2 && me.enableLists){
+ updateButtons('insertorderedlist', 'insertunorderedlist');
+ }
+
+ Ext.menu.Manager.hideAll();
+
+ me.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=null] The value to pass to the command
+ */
+ relayCmd: function(cmd, value) {
+ Ext.defer(function() {
+ var me = this;
+ me.focus();
+ me.execCmd(cmd, value);
+ me.updateToolbar();
+ }, 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 me = this,
+ doc = me.getDoc(),
+ undef;
+ doc.execCommand(cmd, false, value === undef ? null : value);
+ me.syncValue();
+ },
+
+ // private
+ applyCommand : function(e){
+ if (e.ctrlKey) {
+ var me = this,
+ 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) {
+ me.win.focus();
+ me.execCmd(cmd);
+ me.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){
+ var me = this,
+ range;
+
+ if (me.activated) {
+ me.win.focus();
+ if (Ext.isIE) {
+ range = me.getDoc().selection.createRange();
+ if (range) {
+ range.pasteHTML(text);
+ me.syncValue();
+ me.deferFocus();
+ }
+ }else{
+ me.execCmd('InsertHTML', text);
+ me.deferFocus();
+ }
+ }
+ },
+
+ // private
+ fixKeys: function() { // load time branching for fastest keydown performance
+ if (Ext.isIE) {
+ return function(e){
+ var me = this,
+ k = e.getKey(),
+ doc = me.getDoc(),
+ range, target;
+ if (k === e.TAB) {
+ e.stopEvent();
+ range = doc.selection.createRange();
+ if(range){
+ range.collapse(true);
+ range.pasteHTML(' ');
+ me.deferFocus();
+ }
+ }
+ else if (k === e.ENTER) {
+ range = doc.selection.createRange();
+ if (range) {
+ target = range.parentElement();
+ if(!target || target.tagName.toLowerCase() !== 'li'){
+ e.stopEvent();
+ range.pasteHTML('<br />');
+ range.collapse(false);
+ range.select();
+ }
+ }
+ }
+ };
+ }
+
+ if (Ext.isOpera) {
+ return function(e){
+ var me = this;
+ if (e.getKey() === e.TAB) {
+ e.stopEvent();
+ me.win.focus();
+ me.execCmd('InsertHTML',' ');
+ me.deferFocus();
+ }
+ };
+ }
+
+ if (Ext.isWebKit) {
+ return function(e){
+ var me = this,
+ k = e.getKey();
+ if (k === e.TAB) {
+ e.stopEvent();
+ me.execCmd('InsertText','\t');
+ me.deferFocus();
+ }
+ else if (k === e.ENTER) {
+ e.stopEvent();
+ me.execCmd('InsertHtml','<br /><br />');
+ me.deferFocus();
+ }
+ };
+ }
+
+ return null; // not needed, so null
+ }(),
+
+ /**
+ * Returns the editor's toolbar. **This is only available after the editor has been rendered.**
+ * @return {Ext.toolbar.Toolbar}
+ */
+ getToolbar : function(){
+ return this.toolbar;
+ },
+
+ /**
+ * @property {Object} buttonTips
+ * 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'
+ * },
+ * ...
+ */
+ buttonTips : {
+ bold : {
+ title: 'Bold (Ctrl+B)',
+ text: 'Make the selected text bold.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ italic : {
+ title: 'Italic (Ctrl+I)',
+ text: 'Make the selected text italic.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ underline : {
+ title: 'Underline (Ctrl+U)',
+ text: 'Underline the selected text.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ increasefontsize : {
+ title: 'Grow Text',
+ text: 'Increase the font size.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ decreasefontsize : {
+ title: 'Shrink Text',
+ text: 'Decrease the font size.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ backcolor : {
+ title: 'Text Highlight Color',
+ text: 'Change the background color of the selected text.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ forecolor : {
+ title: 'Font Color',
+ text: 'Change the color of the selected text.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ justifyleft : {
+ title: 'Align Text Left',
+ text: 'Align text to the left.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ justifycenter : {
+ title: 'Center Text',
+ text: 'Center text in the editor.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ justifyright : {
+ title: 'Align Text Right',
+ text: 'Align text to the right.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ insertunorderedlist : {
+ title: 'Bullet List',
+ text: 'Start a bulleted list.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ insertorderedlist : {
+ title: 'Numbered List',
+ text: 'Start a numbered list.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ createlink : {
+ title: 'Hyperlink',
+ text: 'Make the selected text a hyperlink.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ },
+ sourceedit : {
+ title: 'Source Edit',
+ text: 'Switch to source editing mode.',
+ cls: Ext.baseCSSPrefix + 'html-editor-tip'
+ }
+ }
+
+ // hide stuff that is not compatible
+ /**
+ * @event blur
+ * @hide
+ */
+ /**
+ * @event change
+ * @hide
+ */
+ /**
+ * @event focus
+ * @hide
+ */
+ /**
+ * @event specialkey
+ * @hide
+ */
+ /**
+ * @cfg {String} fieldCls @hide
+ */
+ /**
+ * @cfg {String} focusCls @hide
+ */
+ /**
+ * @cfg {String} autoCreate @hide
+ */
+ /**
+ * @cfg {String} inputType @hide
+ */
+ /**
+ * @cfg {String} invalidCls @hide
+ */
+ /**
+ * @cfg {String} invalidText @hide
+ */
+ /**
+ * @cfg {String} msgFx @hide
+ */
+ /**
+ * @cfg {Boolean} allowDomMove @hide
+ */
+ /**
+ * @cfg {String} applyTo @hide
+ */
+ /**
+ * @cfg {String} readOnly @hide
+ */
+ /**
+ * @cfg {String} tabIndex @hide
+ */
+ /**
+ * @method validate
+ * @hide
+ */
+});
+
+/**
+ * @docauthor Robert Dougan <rob@sencha.com>
+ *
+ * Single radio field. Similar to checkbox, but automatically handles making sure only one radio is checked
+ * at a time within a group of radios with the same name.
+ *
+ * # Labeling
+ *
+ * In addition to the {@link Ext.form.Labelable standard field labeling options}, radio buttons
+ * may be given an optional {@link #boxLabel} which will be displayed immediately to the right of the input. Also
+ * see {@link Ext.form.RadioGroup} for a convenient method of grouping related radio buttons.
+ *
+ * # Values
+ *
+ * The main value of a Radio field is a boolean, indicating whether or not the radio is checked.
+ *
+ * The following values will check the radio:
+ *
+ * - `true`
+ * - `'true'`
+ * - `'1'`
+ * - `'on'`
+ *
+ * Any other value will uncheck it.
+ *
+ * In addition to the main boolean value, you may also specify a separate {@link #inputValue}. This will be sent
+ * as the parameter value when the form is {@link Ext.form.Basic#submit submitted}. You will want to set this
+ * value if you have multiple radio buttons with the same {@link #name}, as is almost always the case.
+ *
+ * # Example usage
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title : 'Order Form',
+ * width : 300,
+ * bodyPadding: 10,
+ * renderTo : Ext.getBody(),
+ * items: [
+ * {
+ * xtype : 'fieldcontainer',
+ * fieldLabel : 'Size',
+ * defaultType: 'radiofield',
+ * defaults: {
+ * flex: 1
+ * },
+ * layout: 'hbox',
+ * items: [
+ * {
+ * boxLabel : 'M',
+ * name : 'size',
+ * inputValue: 'm',
+ * id : 'radio1'
+ * }, {
+ * boxLabel : 'L',
+ * name : 'size',
+ * inputValue: 'l',
+ * id : 'radio2'
+ * }, {
+ * boxLabel : 'XL',
+ * name : 'size',
+ * inputValue: 'xl',
+ * id : 'radio3'
+ * }
+ * ]
+ * },
+ * {
+ * xtype : 'fieldcontainer',
+ * fieldLabel : 'Color',
+ * defaultType: 'radiofield',
+ * defaults: {
+ * flex: 1
+ * },
+ * layout: 'hbox',
+ * items: [
+ * {
+ * boxLabel : 'Blue',
+ * name : 'color',
+ * inputValue: 'blue',
+ * id : 'radio4'
+ * }, {
+ * boxLabel : 'Grey',
+ * name : 'color',
+ * inputValue: 'grey',
+ * id : 'radio5'
+ * }, {
+ * boxLabel : 'Black',
+ * name : 'color',
+ * inputValue: 'black',
+ * id : 'radio6'
+ * }
+ * ]
+ * }
+ * ],
+ * bbar: [
+ * {
+ * text: 'Smaller Size',
+ * handler: function() {
+ * var radio1 = Ext.getCmp('radio1'),
+ * radio2 = Ext.getCmp('radio2'),
+ * radio3 = Ext.getCmp('radio3');
+ *
+ * //if L is selected, change to M
+ * if (radio2.getValue()) {
+ * radio1.setValue(true);
+ * return;
+ * }
+ *
+ * //if XL is selected, change to L
+ * if (radio3.getValue()) {
+ * radio2.setValue(true);
+ * return;
+ * }
+ *
+ * //if nothing is set, set size to S
+ * radio1.setValue(true);
+ * }
+ * },
+ * {
+ * text: 'Larger Size',
+ * handler: function() {
+ * var radio1 = Ext.getCmp('radio1'),
+ * radio2 = Ext.getCmp('radio2'),
+ * radio3 = Ext.getCmp('radio3');
+ *
+ * //if M is selected, change to L
+ * if (radio1.getValue()) {
+ * radio2.setValue(true);
+ * return;
+ * }
+ *
+ * //if L is selected, change to XL
+ * if (radio2.getValue()) {
+ * radio3.setValue(true);
+ * return;
+ * }
+ *
+ * //if nothing is set, set size to XL
+ * radio3.setValue(true);
+ * }
+ * },
+ * '-',
+ * {
+ * text: 'Select color',
+ * menu: {
+ * indent: false,
+ * items: [
+ * {
+ * text: 'Blue',
+ * handler: function() {
+ * var radio = Ext.getCmp('radio4');
+ * radio.setValue(true);
+ * }
+ * },
+ * {
+ * text: 'Grey',
+ * handler: function() {
+ * var radio = Ext.getCmp('radio5');
+ * radio.setValue(true);
+ * }
+ * },
+ * {
+ * text: 'Black',
+ * handler: function() {
+ * var radio = Ext.getCmp('radio6');
+ * radio.setValue(true);
+ * }
+ * }
+ * ]
+ * }
+ * }
+ * ]
+ * });
+ */
+Ext.define('Ext.form.field.Radio', {
+ extend:'Ext.form.field.Checkbox',
+ alias: ['widget.radiofield', 'widget.radio'],
+ alternateClassName: 'Ext.form.Radio',
+ requires: ['Ext.form.RadioManager'],
+
+ isRadio: true,
+
+ /**
+ * @cfg {String} uncheckedValue @hide
+ */
+
+ // private
+ inputType: 'radio',
+ ariaRole: 'radio',
+
+ /**
+ * If this radio is part of a group, it will return the selected value
+ * @return {String}
+ */
+ getGroupValue: function() {
+ var selected = this.getManager().getChecked(this.name);
+ return selected ? selected.inputValue : null;
+ },
+
+ /**
+ * @private Handle click on the radio button
+ */
+ onBoxClick: function(e) {
+ var me = this;
+ if (!me.disabled && !me.readOnly) {
+ this.setValue(true);
+ }
+ },
+
+ /**
+ * 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 {String/Boolean} value Checked value, or the value of the sibling radio button to check.
+ * @return {Ext.form.field.Radio} this
+ */
+ setValue: function(v) {
+ var me = this,
+ active;
+
+ if (Ext.isBoolean(v)) {
+ me.callParent(arguments);
+ } else {
+ active = me.getManager().getWithValue(me.name, v).getAt(0);
+ if (active) {
+ active.setValue(true);
+ }
+ }
+ return me;
+ },
+
+ /**
+ * Returns the submit value for the checkbox which can be used when submitting forms.
+ * @return {Boolean/Object} True if checked, null if not.
+ */
+ getSubmitValue: function() {
+ return this.checked ? this.inputValue : null;
+ },
+
+ getModelData: function() {
+ return this.getSubmitData();
+ },
+
+ // inherit docs
+ onChange: function(newVal, oldVal) {
+ var me = this;
+ me.callParent(arguments);
+
+ if (newVal) {
+ this.getManager().getByName(me.name).each(function(item){
+ if (item !== me) {
+ item.setValue(false);
+ }
+ }, me);
+ }
+ },
+
+ // inherit docs
+ getManager: function() {
+ return Ext.form.RadioManager;
+ }
+});
+
+/**
+ * A time picker which provides a list of times from which to choose. This is used by the Ext.form.field.Time
+ * class to allow browsing and selection of valid times, but could also be used with other components.
+ *
+ * By default, all times starting at midnight and incrementing every 15 minutes will be presented. This list of
+ * available times can be controlled using the {@link #minValue}, {@link #maxValue}, and {@link #increment}
+ * configuration properties. The format of the times presented in the list can be customized with the {@link #format}
+ * config.
+ *
+ * To handle when the user selects a time from the list, you can subscribe to the {@link #selectionchange} event.
+ *
+ * @example
+ * Ext.create('Ext.picker.Time', {
+ * width: 60,
+ * minValue: Ext.Date.parse('04:30:00 AM', 'h:i:s A'),
+ * maxValue: Ext.Date.parse('08:00:00 AM', 'h:i:s A'),
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.picker.Time', {
+ extend: 'Ext.view.BoundList',
+ alias: 'widget.timepicker',
+ requires: ['Ext.data.Store', 'Ext.Date'],
+
+ /**
+ * @cfg {Date} minValue
+ * The minimum time to be shown in the list of times. This must be a Date object (only the time fields will be
+ * used); no parsing of String values will be done.
+ */
+
+ /**
+ * @cfg {Date} maxValue
+ * The maximum time to be shown in the list of times. This must be a Date object (only the time fields will be
+ * used); no parsing of String values will be done.
+ */
+
+ /**
+ * @cfg {Number} increment
+ * The number of minutes between each time value in the list.
+ */
+ increment: 15,
+
+ /**
+ * @cfg {String} format
+ * The default time format string which can be overriden for localization support. The format must be valid
+ * according to {@link Ext.Date#parse} (defaults to 'g:i A', e.g., '3:15 PM'). For 24-hour time format try 'H:i'
+ * instead.
+ */
+ format : "g:i A",
+
+ /**
+ * @hide
+ * The field in the implicitly-generated Model objects that gets displayed in the list. This is
+ * an internal field name only and is not useful to change via config.
+ */
+ displayField: 'disp',
+
+ /**
+ * @private
+ * Year, month, and day that all times will be normalized into internally.
+ */
+ initDate: [2008,0,1],
+
+ componentCls: Ext.baseCSSPrefix + 'timepicker',
+
+ /**
+ * @hide
+ */
+ loadMask: false,
+
+ initComponent: function() {
+ var me = this,
+ dateUtil = Ext.Date,
+ clearTime = dateUtil.clearTime,
+ initDate = me.initDate;
+
+ // Set up absolute min and max for the entire day
+ me.absMin = clearTime(new Date(initDate[0], initDate[1], initDate[2]));
+ me.absMax = dateUtil.add(clearTime(new Date(initDate[0], initDate[1], initDate[2])), 'mi', (24 * 60) - 1);
+
+ me.store = me.createStore();
+ me.updateList();
+
+ me.callParent();
+ },
+
+ /**
+ * Set the {@link #minValue} and update the list of available times. This must be a Date object (only the time
+ * fields will be used); no parsing of String values will be done.
+ * @param {Date} value
+ */
+ setMinValue: function(value) {
+ this.minValue = value;
+ this.updateList();
+ },
+
+ /**
+ * Set the {@link #maxValue} and update the list of available times. This must be a Date object (only the time
+ * fields will be used); no parsing of String values will be done.
+ * @param {Date} value
+ */
+ setMaxValue: function(value) {
+ this.maxValue = value;
+ this.updateList();
+ },
+
+ /**
+ * @private
+ * Sets the year/month/day of the given Date object to the {@link #initDate}, so that only
+ * the time fields are significant. This makes values suitable for time comparison.
+ * @param {Date} date
+ */
+ normalizeDate: function(date) {
+ var initDate = this.initDate;
+ date.setFullYear(initDate[0], initDate[1], initDate[2]);
+ return date;
+ },
+
+ /**
+ * Update the list of available times in the list to be constrained within the {@link #minValue}
+ * and {@link #maxValue}.
+ */
+ updateList: function() {
+ var me = this,
+ min = me.normalizeDate(me.minValue || me.absMin),
+ max = me.normalizeDate(me.maxValue || me.absMax);
+
+ me.store.filterBy(function(record) {
+ var date = record.get('date');
+ return date >= min && date <= max;
+ });
+ },
+
+ /**
+ * @private
+ * Creates the internal {@link Ext.data.Store} that contains the available times. The store
+ * is loaded with all possible times, and it is later filtered to hide those times outside
+ * the minValue/maxValue.
+ */
+ createStore: function() {
+ var me = this,
+ utilDate = Ext.Date,
+ times = [],
+ min = me.absMin,
+ max = me.absMax;
+
+ while(min <= max){
+ times.push({
+ disp: utilDate.dateFormat(min, me.format),
+ date: min
+ });
+ min = utilDate.add(min, 'mi', me.increment);
+ }
+
+ return Ext.create('Ext.data.Store', {
+ fields: ['disp', 'date'],
+ data: times
+ });
+ }
+
+});
+
+/**
+ * Provides a time input field with a time dropdown and automatic time validation.
+ *
+ * This field recognizes and uses JavaScript Date objects as its main {@link #value} type (only the time portion of the
+ * date is used; the month/day/year are ignored). In addition, it recognizes string values which are parsed according to
+ * the {@link #format} and/or {@link #altFormats} configs. These may be reconfigured to use time formats appropriate for
+ * the user's locale.
+ *
+ * The field may be limited to a certain range of times by using the {@link #minValue} and {@link #maxValue} configs,
+ * and the interval between time options in the dropdown can be changed with the {@link #increment} config.
+ *
+ * Example usage:
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'Time Card',
+ * width: 300,
+ * bodyPadding: 10,
+ * renderTo: Ext.getBody(),
+ * items: [{
+ * xtype: 'timefield',
+ * name: 'in',
+ * fieldLabel: 'Time In',
+ * minValue: '6:00 AM',
+ * maxValue: '8:00 PM',
+ * increment: 30,
+ * anchor: '100%'
+ * }, {
+ * xtype: 'timefield',
+ * name: 'out',
+ * fieldLabel: 'Time Out',
+ * minValue: '6:00 AM',
+ * maxValue: '8:00 PM',
+ * increment: 30,
+ * anchor: '100%'
+ * }]
+ * });
+ */
+Ext.define('Ext.form.field.Time', {
+ extend:'Ext.form.field.Picker',
+ alias: 'widget.timefield',
+ requires: ['Ext.form.field.Date', 'Ext.picker.Time', 'Ext.view.BoundListKeyNav', 'Ext.Date'],
+ alternateClassName: ['Ext.form.TimeField', 'Ext.form.Time'],
+
+ /**
+ * @cfg {String} triggerCls
+ * An additional CSS class used to style the trigger button. The trigger will always get the {@link #triggerBaseCls}
+ * by default and triggerCls will be **appended** if specified. Defaults to 'x-form-time-trigger' for the Time field
+ * trigger.
+ */
+ triggerCls: Ext.baseCSSPrefix + 'form-time-trigger',
+
+ /**
+ * @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}.
+ */
+
+ /**
+ * @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}.
+ */
+
+ /**
+ * @cfg {String} minText
+ * The error text to display when the entered time is before {@link #minValue}.
+ */
+ minText : "The time in this field must be equal to or after {0}",
+
+ /**
+ * @cfg {String} maxText
+ * The error text to display when the entered time is after {@link #maxValue}.
+ */
+ maxText : "The time in this field must be equal to or before {0}",
+
+ /**
+ * @cfg {String} invalidText
+ * The error text to display when the time in the field is invalid.
+ */
+ invalidText : "{0} is not a valid time",
+
+ /**
+ * @cfg {String} format
+ * The default time format string which can be overriden for localization support. The format must be valid
+ * according to {@link Ext.Date#parse} (defaults to 'g:i A', e.g., '3:15 PM'). For 24-hour time format try 'H:i'
+ * instead.
+ */
+ format : "g:i A",
+
+ /**
+ * @cfg {String} submitFormat
+ * The date format string which will be submitted to the server. The format must be valid according to {@link
+ * Ext.Date#parse} (defaults to {@link #format}).
+ */
+
+ /**
+ * @cfg {String} altFormats
+ * Multiple date formats separated by "|" to try when parsing a user input value and it doesn't match the defined
+ * format.
+ */
+ altFormats : "g:ia|g:iA|g:i a|g:i A|h:i|g:i|H:i|ga|ha|gA|h a|g a|g A|gi|hi|gia|hia|g|H|gi a|hi a|giA|hiA|gi A|hi A",
+
+ /**
+ * @cfg {Number} increment
+ * The number of minutes between each time value in the list.
+ */
+ increment: 15,
+
+ /**
+ * @cfg {Number} pickerMaxHeight
+ * The maximum height of the {@link Ext.picker.Time} dropdown.
+ */
+ pickerMaxHeight: 300,
+
+ /**
+ * @cfg {Boolean} selectOnTab
+ * Whether the Tab key should select the currently highlighted item.
+ */
+ selectOnTab: true,
+
+ /**
+ * @private
+ * This is the date to use when generating time values in the absence of either minValue
+ * or maxValue. Using the current date causes DST issues on DST boundary dates, so this is an
+ * arbitrary "safe" date that can be any date aside from DST boundary dates.
+ */
+ initDate: '1/1/2008',
+ initDateFormat: 'j/n/Y',
+
+
+ initComponent: function() {
+ var me = this,
+ min = me.minValue,
+ max = me.maxValue;
+ if (min) {
+ me.setMinValue(min);
+ }
+ if (max) {
+ me.setMaxValue(max);
+ }
+ this.callParent();
+ },
+
+ initValue: function() {
+ var me = this,
+ value = me.value;
+
+ // If a String value was supplied, try to convert it to a proper Date object
+ if (Ext.isString(value)) {
+ me.value = me.rawToValue(value);
+ }
+
+ me.callParent();
+ },
+
+ /**
+ * Replaces any existing {@link #minValue} with the new time and refreshes the picker's range.
+ * @param {Date/String} value The minimum time that can be selected
+ */
+ setMinValue: function(value) {
+ var me = this,
+ picker = me.picker;
+ me.setLimit(value, true);
+ if (picker) {
+ picker.setMinValue(me.minValue);
+ }
+ },
+
+ /**
+ * Replaces any existing {@link #maxValue} with the new time and refreshes the picker's range.
+ * @param {Date/String} value The maximum time that can be selected
+ */
+ setMaxValue: function(value) {
+ var me = this,
+ picker = me.picker;
+ me.setLimit(value, false);
+ if (picker) {
+ picker.setMaxValue(me.maxValue);
+ }
+ },
+
+ /**
+ * @private
+ * Updates either the min or max value. Converts the user's value into a Date object whose
+ * year/month/day is set to the {@link #initDate} so that only the time fields are significant.
+ */
+ setLimit: function(value, isMin) {
+ var me = this,
+ d, val;
+ if (Ext.isString(value)) {
+ d = me.parseDate(value);
+ }
+ else if (Ext.isDate(value)) {
+ d = value;
+ }
+ if (d) {
+ val = Ext.Date.clearTime(new Date(me.initDate));
+ val.setHours(d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds());
+ me[isMin ? 'minValue' : 'maxValue'] = val;
+ }
+ },
+
+ rawToValue: function(rawValue) {
+ return this.parseDate(rawValue) || rawValue || null;
+ },
+
+ valueToRaw: function(value) {
+ return this.formatDate(this.parseDate(value));
+ },
+
+ /**
+ * Runs all of Time's validations and returns an array of any errors. Note that this first runs Text's validations,
+ * so the returned array is an amalgamation of all field errors. The additional validation checks are testing that
+ * the time format is valid, that the chosen time is within the {@link #minValue} and {@link #maxValue} constraints
+ * set.
+ * @param {Object} [value] The value to get errors for (defaults to the current field value)
+ * @return {String[]} All validation errors for this field
+ */
+ getErrors: function(value) {
+ var me = this,
+ format = Ext.String.format,
+ errors = me.callParent(arguments),
+ minValue = me.minValue,
+ maxValue = me.maxValue,
+ date;
+
+ value = me.formatDate(value || me.processRawValue(me.getRawValue()));
+
+ if (value === null || value.length < 1) { // if it's blank and textfield didn't flag it then it's valid
+ return errors;
+ }
+
+ date = me.parseDate(value);
+ if (!date) {
+ errors.push(format(me.invalidText, value, me.format));
+ return errors;
+ }
+
+ if (minValue && date < minValue) {
+ errors.push(format(me.minText, me.formatDate(minValue)));
+ }
+
+ if (maxValue && date > maxValue) {
+ errors.push(format(me.maxText, me.formatDate(maxValue)));
+ }
+
+ return errors;
+ },
+
+ formatDate: function() {
+ return Ext.form.field.Date.prototype.formatDate.apply(this, arguments);
+ },
+
+ /**
+ * @private
+ * Parses an input value into a valid Date object.
+ * @param {String/Date} value
+ */
+ parseDate: function(value) {
+ if (!value || Ext.isDate(value)) {
+ return value;
+ }
+
+ var me = this,
+ val = me.safeParse(value, me.format),
+ altFormats = me.altFormats,
+ altFormatsArray = me.altFormatsArray,
+ i = 0,
+ len;
+
+ if (!val && altFormats) {
+ altFormatsArray = altFormatsArray || altFormats.split('|');
+ len = altFormatsArray.length;
+ for (; i < len && !val; ++i) {
+ val = me.safeParse(value, altFormatsArray[i]);
+ }
+ }
+ return val;
+ },
+
+ safeParse: function(value, format){
+ var me = this,
+ utilDate = Ext.Date,
+ parsedDate,
+ result = null;
+
+ if (utilDate.formatContainsDateInfo(format)) {
+ // assume we've been given a full date
+ result = utilDate.parse(value, format);
+ } else {
+ // Use our initial safe date
+ parsedDate = utilDate.parse(me.initDate + ' ' + value, me.initDateFormat + ' ' + format);
+ if (parsedDate) {
+ result = parsedDate;
+ }
+ }
+ return result;
+ },
+
+ // @private
+ getSubmitValue: function() {
+ var me = this,
+ format = me.submitFormat || me.format,
+ value = me.getValue();
+
+ return value ? Ext.Date.format(value, format) : null;
+ },
+
+ /**
+ * @private
+ * Creates the {@link Ext.picker.Time}
+ */
+ createPicker: function() {
+ var me = this,
+ picker = Ext.create('Ext.picker.Time', {
+ pickerField: me,
+ selModel: {
+ mode: 'SINGLE'
+ },
+ floating: true,
+ hidden: true,
+ minValue: me.minValue,
+ maxValue: me.maxValue,
+ increment: me.increment,
+ format: me.format,
+ ownerCt: this.ownerCt,
+ renderTo: document.body,
+ maxHeight: me.pickerMaxHeight,
+ focusOnToFront: false
+ });
+
+ me.mon(picker.getSelectionModel(), {
+ selectionchange: me.onListSelect,
+ scope: me
+ });
+
+ return picker;
+ },
+
+ /**
+ * @private
+ * Enables the key nav for the Time picker when it is expanded.
+ * TODO this is largely the same logic as ComboBox, should factor out.
+ */
+ onExpand: function() {
+ var me = this,
+ keyNav = me.pickerKeyNav,
+ selectOnTab = me.selectOnTab,
+ picker = me.getPicker(),
+ lastSelected = picker.getSelectionModel().lastSelected,
+ itemNode;
+
+ if (!keyNav) {
+ keyNav = me.pickerKeyNav = Ext.create('Ext.view.BoundListKeyNav', this.inputEl, {
+ boundList: picker,
+ forceKeyDown: true,
+ tab: function(e) {
+ if (selectOnTab) {
+ if(me.picker.highlightedItem) {
+ this.selectHighlighted(e);
+ } else {
+ me.collapse();
+ }
+ me.triggerBlur();
+ }
+ // Tab key event is allowed to propagate to field
+ return true;
+ }
+ });
+ // stop tab monitoring from Ext.form.field.Trigger so it doesn't short-circuit selectOnTab
+ if (selectOnTab) {
+ me.ignoreMonitorTab = true;
+ }
+ }
+ Ext.defer(keyNav.enable, 1, keyNav); //wait a bit so it doesn't react to the down arrow opening the picker
+
+ // Highlight the last selected item and scroll it into view
+ if (lastSelected) {
+ itemNode = picker.getNode(lastSelected);
+ if (itemNode) {
+ picker.highlightItem(itemNode);
+ picker.el.scrollChildIntoView(itemNode, false);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Disables the key nav for the Time picker when it is collapsed.
+ */
+ onCollapse: function() {
+ var me = this,
+ keyNav = me.pickerKeyNav;
+ if (keyNav) {
+ keyNav.disable();
+ me.ignoreMonitorTab = false;
+ }
+ },
+
+ /**
+ * @private
+ * Clears the highlighted item in the picker on change.
+ * This prevents the highlighted item from being selected instead of the custom typed in value when the tab key is pressed.
+ */
+ onChange: function() {
+ var me = this,
+ picker = me.picker;
+
+ me.callParent(arguments);
+ if(picker) {
+ picker.clearHighlight();
+ }
+ },
+
+ /**
+ * @private
+ * Handles a time being selected from the Time picker.
+ */
+ onListSelect: function(list, recordArray) {
+ var me = this,
+ record = recordArray[0],
+ val = record ? record.get('date') : null;
+ me.setValue(val);
+ me.fireEvent('select', me, val);
+ me.picker.clearHighlight();
+ me.collapse();
+ me.inputEl.focus();
+ }
+});
+
+
+/**
+ * @class Ext.grid.CellEditor
+ * @extends Ext.Editor
+ * Internal utility class that provides default configuration for cell editing.
+ * @ignore
+ */
+Ext.define('Ext.grid.CellEditor', {
+ extend: 'Ext.Editor',
+ constructor: function(config) {
+ config = Ext.apply({}, config);
+
+ if (config.field) {
+ config.field.monitorTab = false;
+ }
+ if (!Ext.isDefined(config.autoSize)) {
+ config.autoSize = {
+ width: 'boundEl'
+ };
+ }
+ this.callParent([config]);
+ },
+
+ /**
+ * @private
+ * Hide the grid cell when editor is shown.
+ */
+ onShow: function() {
+ var first = this.boundEl.first();
+ if (first) {
+ first.hide();
+ }
+ this.callParent(arguments);
+ },
+
+ /**
+ * @private
+ * Show grid cell when editor is hidden.
+ */
+ onHide: function() {
+ var first = this.boundEl.first();
+ if (first) {
+ first.show();
+ }
+ this.callParent(arguments);
+ },
+
+ /**
+ * @private
+ * Fix checkbox blur when it is clicked.
+ */
+ afterRender: function() {
+ this.callParent(arguments);
+ var field = this.field;
+ if (field.isXType('checkboxfield')) {
+ field.mon(field.inputEl, 'mousedown', this.onCheckBoxMouseDown, this);
+ field.mon(field.inputEl, 'click', this.onCheckBoxClick, this);
+ }
+ },
+
+ /**
+ * @private
+ * Because when checkbox is clicked it loses focus completeEdit is bypassed.
+ */
+ onCheckBoxMouseDown: function() {
+ this.completeEdit = Ext.emptyFn;
+ },
+
+ /**
+ * @private
+ * Restore checkbox focus and completeEdit method.
+ */
+ onCheckBoxClick: function() {
+ delete this.completeEdit;
+ this.field.focus(false, 10);
+ },
+
+ alignment: "tl-tl",
+ hideEl : false,
+ cls: Ext.baseCSSPrefix + "small-editor " + Ext.baseCSSPrefix + "grid-editor",
+ shim: false,
+ shadow: false
+});
+/**
+ * @class Ext.grid.ColumnLayout
+ * @extends Ext.layout.container.HBox
+ * @private
+ *
+ * <p>This class is used only by the grid's HeaderContainer docked child.</p>
+ *
+ * <p>It adds the ability to shrink the vertical size of the inner container element back if a grouped
+ * column header has all its child columns dragged out, and the whole HeaderContainer needs to shrink back down.</p>
+ *
+ * <p>Also, after every layout, after all headers have attained their 'stretchmax' height, it goes through and calls
+ * <code>setPadding</code> on the columns so that they lay out correctly.</p>
+ */
+Ext.define('Ext.grid.ColumnLayout', {
+ extend: 'Ext.layout.container.HBox',
+ alias: 'layout.gridcolumn',
+ type : 'column',
+
+ reserveOffset: false,
+
+ shrinkToFit: false,
+
+ // Height-stretched innerCt must be able to revert back to unstretched height
+ clearInnerCtOnLayout: true,
+
+ beforeLayout: function() {
+ var me = this,
+ i = 0,
+ items = me.getLayoutItems(),
+ len = items.length,
+ item, returnValue,
+ s;
+
+ // Scrollbar offset defined by width of any vertical scroller in the owning grid
+ if (!Ext.isDefined(me.availableSpaceOffset)) {
+ s = me.owner.up('tablepanel').verticalScroller;
+ me.availableSpaceOffset = s ? s.width-1 : 0;
+ }
+
+ returnValue = me.callParent(arguments);
+
+ // Size to a sane minimum height before possibly being stretched to accommodate grouped headers
+ me.innerCt.setHeight(23);
+
+ // Unstretch child items before the layout which stretches them.
+ for (; i < len; i++) {
+ item = items[i];
+ item.el.setStyle({
+ height: 'auto'
+ });
+ item.titleContainer.setStyle({
+ height: 'auto',
+ paddingTop: '0'
+ });
+ if (item.componentLayout && item.componentLayout.lastComponentSize) {
+ item.componentLayout.lastComponentSize.height = item.el.dom.offsetHeight;
+ }
+ }
+ return returnValue;
+ },
+
+ // Override to enforce the forceFit config.
+ calculateChildBoxes: function(visibleItems, targetSize) {
+ var me = this,
+ calculations = me.callParent(arguments),
+ boxes = calculations.boxes,
+ metaData = calculations.meta,
+ len = boxes.length, i = 0, box, item;
+
+ if (targetSize.width && !me.isHeader) {
+ // If configured forceFit then all columns will be flexed
+ if (me.owner.forceFit) {
+
+ for (; i < len; i++) {
+ box = boxes[i];
+ item = box.component;
+
+ // Set a sane minWidth for the Box layout to be able to squeeze flexed Headers down to.
+ item.minWidth = Ext.grid.plugin.HeaderResizer.prototype.minColWidth;
+
+ // For forceFit, just use allocated width as the flex value, and the proportions
+ // will end up the same whatever HeaderContainer width they are being forced into.
+ item.flex = box.width;
+ }
+
+ // Recalculate based upon all columns now being flexed instead of sized.
+ calculations = me.callParent(arguments);
+ }
+ else if (metaData.tooNarrow) {
+ targetSize.width = metaData.desiredSize;
+ }
+ }
+
+ return calculations;
+ },
+
+ afterLayout: function() {
+ var me = this,
+ owner = me.owner,
+ topGrid,
+ bothHeaderCts,
+ otherHeaderCt,
+ thisHeight,
+ otherHeight,
+ modifiedGrid,
+ i = 0,
+ items,
+ len,
+ headerHeight;
+
+ me.callParent(arguments);
+
+ // Set up padding in items
+ if (!me.owner.hideHeaders) {
+
+ // If this is one HeaderContainer of a pair in a side-by-side locking view, then find the height
+ // of the highest one, and sync the other one to that height.
+ if (owner.lockableInjected) {
+ topGrid = owner.up('tablepanel').up('tablepanel');
+ bothHeaderCts = topGrid.query('headercontainer:not([isHeader])');
+ otherHeaderCt = (bothHeaderCts[0] === owner) ? bothHeaderCts[1] : bothHeaderCts[0];
+
+ // Both sides must be rendered for this syncing operation to work.
+ if (!otherHeaderCt.rendered) {
+ return;
+ }
+
+ // Get the height of the highest of both HeaderContainers
+ otherHeight = otherHeaderCt.layout.getRenderTarget().getViewSize().height;
+ if (!otherHeight) {
+ return;
+ }
+ thisHeight = this.getRenderTarget().getViewSize().height;
+ if (!thisHeight) {
+ return;
+ }
+
+ // Prevent recursion back into here when the "other" grid, after adjusting to the new hight of its headerCt, attempts to inform its ownerCt
+ // Block the upward notification by flagging the top grid's component layout as busy.
+ topGrid.componentLayout.layoutBusy = true;
+
+ // Assume that the correct header height is the height of this HeaderContainer
+ headerHeight = thisHeight;
+
+ // Synch the height of the smaller HeaderContainer to the height of the highest one.
+ if (thisHeight > otherHeight) {
+ otherHeaderCt.layout.align = 'stretch';
+ otherHeaderCt.setCalculatedSize(otherHeaderCt.getWidth(), owner.getHeight(), otherHeaderCt.ownerCt);
+ delete otherHeaderCt.layout.align;
+ modifiedGrid = otherHeaderCt.up('tablepanel');
+ } else if (otherHeight > thisHeight) {
+ headerHeight = otherHeight;
+ this.align = 'stretch';
+ owner.setCalculatedSize(owner.getWidth(), otherHeaderCt.getHeight(), owner.ownerCt);
+ delete this.align;
+ modifiedGrid = owner.up('tablepanel');
+ }
+ topGrid.componentLayout.layoutBusy = false;
+
+ // Gather all Header items across both Grids.
+ items = bothHeaderCts[0].layout.getLayoutItems().concat(bothHeaderCts[1].layout.getLayoutItems());
+ } else {
+ headerHeight = this.getRenderTarget().getViewSize().height;
+ items = me.getLayoutItems();
+ }
+
+ len = items.length;
+ for (; i < len; i++) {
+ items[i].setPadding(headerHeight);
+ }
+
+ // Size the View within the grid which has had its HeaderContainer entallened (That's a perfectly cromulent word BTW)
+ if (modifiedGrid) {
+ setTimeout(function() {
+ modifiedGrid.doLayout();
+ }, 1);
+ }
+ }
+ },
+
+ // FIX: when flexing we actually don't have enough space as we would
+ // typically because of the scrollOffset on the GridView, must reserve this
+ updateInnerCtSize: function(tSize, calcs) {
+ var me = this,
+ extra;
+
+ // Columns must not account for scroll offset
+ if (!me.isHeader) {
+ me.tooNarrow = calcs.meta.tooNarrow;
+ extra = (me.reserveOffset ? me.availableSpaceOffset : 0);
+
+ if (calcs.meta.tooNarrow) {
+ tSize.width = calcs.meta.desiredSize + extra;
+ } else {
+ tSize.width += extra;
+ }
+ }
+
+ return me.callParent(arguments);
+ },
+
+ doOwnerCtLayouts: function() {
+ var ownerCt = this.owner.ownerCt;
+ if (!ownerCt.componentLayout.layoutBusy) {
+ ownerCt.doComponentLayout();
+ }
+ }
+});
+/**
+ * @class Ext.grid.LockingView
+ * This class is used internally to provide a single interface when using
+ * a locking grid. Internally, the locking grid creates two separate grids,
+ * so this class is used to map calls appropriately.
+ * @ignore
+ */
+Ext.define('Ext.grid.LockingView', {
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ eventRelayRe: /^(beforeitem|beforecontainer|item|container|cell)/,
+
+ constructor: function(config){
+ var me = this,
+ eventNames = [],
+ eventRe = me.eventRelayRe,
+ locked = config.locked.getView(),
+ normal = config.normal.getView(),
+ events,
+ event;
+
+ Ext.apply(me, {
+ lockedView: locked,
+ normalView: normal,
+ lockedGrid: config.locked,
+ normalGrid: config.normal,
+ panel: config.panel
+ });
+ me.mixins.observable.constructor.call(me, config);
+
+ // relay events
+ events = locked.events;
+ for (event in events) {
+ if (events.hasOwnProperty(event) && eventRe.test(event)) {
+ eventNames.push(event);
+ }
+ }
+ me.relayEvents(locked, eventNames);
+ me.relayEvents(normal, eventNames);
+
+ normal.on({
+ scope: me,
+ itemmouseleave: me.onItemMouseLeave,
+ itemmouseenter: me.onItemMouseEnter
+ });
+
+ locked.on({
+ scope: me,
+ itemmouseleave: me.onItemMouseLeave,
+ itemmouseenter: me.onItemMouseEnter
+ });
+ },
+
+ getGridColumns: function() {
+ var cols = this.lockedGrid.headerCt.getGridColumns();
+ return cols.concat(this.normalGrid.headerCt.getGridColumns());
+ },
+
+ getEl: function(column){
+ return this.getViewForColumn(column).getEl();
+ },
+
+ getViewForColumn: function(column) {
+ var view = this.lockedView,
+ inLocked;
+
+ view.headerCt.cascade(function(col){
+ if (col === column) {
+ inLocked = true;
+ return false;
+ }
+ });
+
+ return inLocked ? view : this.normalView;
+ },
+
+ onItemMouseEnter: function(view, record){
+ var me = this,
+ locked = me.lockedView,
+ other = me.normalView,
+ item;
+
+ if (view.trackOver) {
+ if (view !== locked) {
+ other = locked;
+ }
+ item = other.getNode(record);
+ other.highlightItem(item);
+ }
+ },
+
+ onItemMouseLeave: function(view, record){
+ var me = this,
+ locked = me.lockedView,
+ other = me.normalView;
+
+ if (view.trackOver) {
+ if (view !== locked) {
+ other = locked;
+ }
+ other.clearHighlight();
+ }
+ },
+
+ relayFn: function(name, args){
+ args = args || [];
+
+ var view = this.lockedView;
+ view[name].apply(view, args || []);
+ view = this.normalView;
+ view[name].apply(view, args || []);
+ },
+
+ getSelectionModel: function(){
+ return this.panel.getSelectionModel();
+ },
+
+ getStore: function(){
+ return this.panel.store;
+ },
+
+ getNode: function(nodeInfo){
+ // default to the normal view
+ return this.normalView.getNode(nodeInfo);
+ },
+
+ getCell: function(record, column){
+ var view = this.getViewForColumn(column),
+ row;
+
+ row = view.getNode(record);
+ return Ext.fly(row).down(column.getCellSelector());
+ },
+
+ getRecord: function(node){
+ var result = this.lockedView.getRecord(node);
+ if (!node) {
+ result = this.normalView.getRecord(node);
+ }
+ return result;
+ },
+
+ addElListener: function(eventName, fn, scope){
+ this.relayFn('addElListener', arguments);
+ },
+
+ refreshNode: function(){
+ this.relayFn('refreshNode', arguments);
+ },
+
+ refresh: function(){
+ this.relayFn('refresh', arguments);
+ },
+
+ bindStore: function(){
+ this.relayFn('bindStore', arguments);
+ },
+
+ addRowCls: function(){
+ this.relayFn('addRowCls', arguments);
+ },
+
+ removeRowCls: function(){
+ this.relayFn('removeRowCls', arguments);
+ }
+
+});
+/**
+ * @class Ext.grid.Lockable
+ * @private
+ *
+ * Lockable is a private mixin which injects lockable behavior into any
+ * TablePanel subclass such as GridPanel or TreePanel. TablePanel will
+ * automatically inject the Ext.grid.Lockable mixin in when one of the
+ * these conditions are met:
+ *
+ * - The TablePanel has the lockable configuration set to true
+ * - One of the columns in the TablePanel has locked set to true/false
+ *
+ * Each TablePanel subclass must register an alias. It should have an array
+ * of configurations to copy to the 2 separate tablepanel's that will be generated
+ * to note what configurations should be copied. These are named normalCfgCopy and
+ * lockedCfgCopy respectively.
+ *
+ * Columns which are locked must specify a fixed width. They do NOT support a
+ * flex width.
+ *
+ * Configurations which are specified in this class will be available on any grid or
+ * tree which is using the lockable functionality.
+ */
+Ext.define('Ext.grid.Lockable', {
+
+ requires: ['Ext.grid.LockingView'],
+
+ /**
+ * @cfg {Boolean} syncRowHeight Synchronize rowHeight between the normal and
+ * locked grid view. This is turned on by default. If your grid is guaranteed
+ * to have rows of all the same height, you should set this to false to
+ * optimize performance.
+ */
+ syncRowHeight: true,
+
+ /**
+ * @cfg {String} subGridXType The xtype of the subgrid to specify. If this is
+ * not specified lockable will determine the subgrid xtype to create by the
+ * following rule. Use the superclasses xtype if the superclass is NOT
+ * tablepanel, otherwise use the xtype itself.
+ */
+
+ /**
+ * @cfg {Object} lockedViewConfig A view configuration to be applied to the
+ * locked side of the grid. Any conflicting configurations between lockedViewConfig
+ * and viewConfig will be overwritten by the lockedViewConfig.
+ */
+
+ /**
+ * @cfg {Object} normalViewConfig A view configuration to be applied to the
+ * normal/unlocked side of the grid. Any conflicting configurations between normalViewConfig
+ * and viewConfig will be overwritten by the normalViewConfig.
+ */
+
+ // private variable to track whether or not the spacer is hidden/visible
+ spacerHidden: true,
+
+ headerCounter: 0,
+
+ // i8n text
+ unlockText: 'Unlock',
+ lockText: 'Lock',
+
+ determineXTypeToCreate: function() {
+ var me = this,
+ typeToCreate;
+
+ if (me.subGridXType) {
+ typeToCreate = me.subGridXType;
+ } else {
+ var xtypes = this.getXTypes().split('/'),
+ xtypesLn = xtypes.length,
+ xtype = xtypes[xtypesLn - 1],
+ superxtype = xtypes[xtypesLn - 2];
+
+ if (superxtype !== 'tablepanel') {
+ typeToCreate = superxtype;
+ } else {
+ typeToCreate = xtype;
+ }
+ }
+
+ return typeToCreate;
+ },
+
+ // injectLockable will be invoked before initComponent's parent class implementation
+ // is called, so throughout this method this. are configurations
+ injectLockable: function() {
+ // ensure lockable is set to true in the TablePanel
+ this.lockable = true;
+ // Instruct the TablePanel it already has a view and not to create one.
+ // We are going to aggregate 2 copies of whatever TablePanel we are using
+ this.hasView = true;
+
+ var me = this,
+ // xtype of this class, 'treepanel' or 'gridpanel'
+ // (Note: this makes it a requirement that any subclass that wants to use lockable functionality needs to register an
+ // alias.)
+ xtype = me.determineXTypeToCreate(),
+ // share the selection model
+ selModel = me.getSelectionModel(),
+ lockedGrid = {
+ xtype: xtype,
+ // Lockable does NOT support animations for Tree
+ enableAnimations: false,
+ scroll: false,
+ scrollerOwner: false,
+ selModel: selModel,
+ border: false,
+ cls: Ext.baseCSSPrefix + 'grid-inner-locked'
+ },
+ normalGrid = {
+ xtype: xtype,
+ enableAnimations: false,
+ scrollerOwner: false,
+ selModel: selModel,
+ border: false
+ },
+ i = 0,
+ columns,
+ lockedHeaderCt,
+ normalHeaderCt;
+
+ me.addCls(Ext.baseCSSPrefix + 'grid-locked');
+
+ // copy appropriate configurations to the respective
+ // aggregated tablepanel instances and then delete them
+ // from the master tablepanel.
+ Ext.copyTo(normalGrid, me, me.normalCfgCopy);
+ Ext.copyTo(lockedGrid, me, me.lockedCfgCopy);
+ for (; i < me.normalCfgCopy.length; i++) {
+ delete me[me.normalCfgCopy[i]];
+ }
+ for (i = 0; i < me.lockedCfgCopy.length; i++) {
+ delete me[me.lockedCfgCopy[i]];
+ }
+
+ me.addEvents(
+ /**
+ * @event lockcolumn
+ * Fires when a column is locked.
+ * @param {Ext.grid.Panel} this The gridpanel.
+ * @param {Ext.grid.column.Column} column The column being locked.
+ */
+ 'lockcolumn',
+
+ /**
+ * @event unlockcolumn
+ * Fires when a column is unlocked.
+ * @param {Ext.grid.Panel} this The gridpanel.
+ * @param {Ext.grid.column.Column} column The column being unlocked.
+ */
+ 'unlockcolumn'
+ );
+
+ me.addStateEvents(['lockcolumn', 'unlockcolumn']);
+
+ me.lockedHeights = [];
+ me.normalHeights = [];
+
+ columns = me.processColumns(me.columns);
+
+ lockedGrid.width = columns.lockedWidth + Ext.num(selModel.headerWidth, 0);
+ lockedGrid.columns = columns.locked;
+ normalGrid.columns = columns.normal;
+
+ me.store = Ext.StoreManager.lookup(me.store);
+ lockedGrid.store = me.store;
+ normalGrid.store = me.store;
+
+ // normal grid should flex the rest of the width
+ normalGrid.flex = 1;
+ lockedGrid.viewConfig = me.lockedViewConfig || {};
+ lockedGrid.viewConfig.loadingUseMsg = false;
+ normalGrid.viewConfig = me.normalViewConfig || {};
+
+ Ext.applyIf(lockedGrid.viewConfig, me.viewConfig);
+ Ext.applyIf(normalGrid.viewConfig, me.viewConfig);
+
+ me.normalGrid = Ext.ComponentManager.create(normalGrid);
+ me.lockedGrid = Ext.ComponentManager.create(lockedGrid);
+
+ me.view = Ext.create('Ext.grid.LockingView', {
+ locked: me.lockedGrid,
+ normal: me.normalGrid,
+ panel: me
+ });
+
+ if (me.syncRowHeight) {
+ me.lockedGrid.getView().on({
+ refresh: me.onLockedGridAfterRefresh,
+ itemupdate: me.onLockedGridAfterUpdate,
+ scope: me
+ });
+
+ me.normalGrid.getView().on({
+ refresh: me.onNormalGridAfterRefresh,
+ itemupdate: me.onNormalGridAfterUpdate,
+ scope: me
+ });
+ }
+
+ lockedHeaderCt = me.lockedGrid.headerCt;
+ normalHeaderCt = me.normalGrid.headerCt;
+
+ lockedHeaderCt.lockedCt = true;
+ lockedHeaderCt.lockableInjected = true;
+ normalHeaderCt.lockableInjected = true;
+
+ lockedHeaderCt.on({
+ columnshow: me.onLockedHeaderShow,
+ columnhide: me.onLockedHeaderHide,
+ columnmove: me.onLockedHeaderMove,
+ sortchange: me.onLockedHeaderSortChange,
+ columnresize: me.onLockedHeaderResize,
+ scope: me
+ });
+
+ normalHeaderCt.on({
+ columnmove: me.onNormalHeaderMove,
+ sortchange: me.onNormalHeaderSortChange,
+ scope: me
+ });
+
+ me.normalGrid.on({
+ scrollershow: me.onScrollerShow,
+ scrollerhide: me.onScrollerHide,
+ scope: me
+ });
+
+ me.lockedGrid.on('afterlayout', me.onLockedGridAfterLayout, me, {single: true});
+
+ me.modifyHeaderCt();
+ me.items = [me.lockedGrid, me.normalGrid];
+
+ me.relayHeaderCtEvents(lockedHeaderCt);
+ me.relayHeaderCtEvents(normalHeaderCt);
+
+ me.layout = {
+ type: 'hbox',
+ align: 'stretch'
+ };
+ },
+
+ processColumns: function(columns){
+ // split apart normal and lockedWidths
+ var i = 0,
+ len = columns.length,
+ lockedWidth = 1,
+ lockedHeaders = [],
+ normalHeaders = [],
+ column;
+
+ for (; i < len; ++i) {
+ column = columns[i];
+ // mark the column as processed so that the locked attribute does not
+ // trigger trying to aggregate the columns again.
+ column.processed = true;
+ if (column.locked) {
+ if (!column.hidden) {
+ lockedWidth += column.width || Ext.grid.header.Container.prototype.defaultWidth;
+ }
+ lockedHeaders.push(column);
+ } else {
+ normalHeaders.push(column);
+ }
+ if (!column.headerId) {
+ column.headerId = (column.initialConfig || column).id || ('L' + (++this.headerCounter));
+ }
+ }
+ return {
+ lockedWidth: lockedWidth,
+ locked: lockedHeaders,
+ normal: normalHeaders
+ };
+ },
+
+ // create a new spacer after the table is refreshed
+ onLockedGridAfterLayout: function() {
+ var me = this,
+ lockedView = me.lockedGrid.getView();
+ lockedView.on({
+ beforerefresh: me.destroySpacer,
+ scope: me
+ });
+ },
+
+ // trigger a pseudo refresh on the normal side
+ onLockedHeaderMove: function() {
+ if (this.syncRowHeight) {
+ this.onNormalGridAfterRefresh();
+ }
+ },
+
+ // trigger a pseudo refresh on the locked side
+ onNormalHeaderMove: function() {
+ if (this.syncRowHeight) {
+ this.onLockedGridAfterRefresh();
+ }
+ },
+
+ // create a spacer in lockedsection and store a reference
+ // TODO: Should destroy before refreshing content
+ getSpacerEl: function() {
+ var me = this,
+ w,
+ view,
+ el;
+
+ if (!me.spacerEl) {
+ // This affects scrolling all the way to the bottom of a locked grid
+ // additional test, sort a column and make sure it synchronizes
+ w = Ext.getScrollBarWidth() + (Ext.isIE ? 2 : 0);
+ view = me.lockedGrid.getView();
+ el = view.el;
+
+ me.spacerEl = Ext.DomHelper.append(el, {
+ cls: me.spacerHidden ? (Ext.baseCSSPrefix + 'hidden') : '',
+ style: 'height: ' + w + 'px;'
+ }, true);
+ }
+ return me.spacerEl;
+ },
+
+ destroySpacer: function() {
+ var me = this;
+ if (me.spacerEl) {
+ me.spacerEl.destroy();
+ delete me.spacerEl;
+ }
+ },
+
+ // cache the heights of all locked rows and sync rowheights
+ onLockedGridAfterRefresh: function() {
+ var me = this,
+ view = me.lockedGrid.getView(),
+ el = view.el,
+ rowEls = el.query(view.getItemSelector()),
+ ln = rowEls.length,
+ i = 0;
+
+ // reset heights each time.
+ me.lockedHeights = [];
+
+ for (; i < ln; i++) {
+ me.lockedHeights[i] = rowEls[i].clientHeight;
+ }
+ me.syncRowHeights();
+ },
+
+ // cache the heights of all normal rows and sync rowheights
+ onNormalGridAfterRefresh: function() {
+ var me = this,
+ view = me.normalGrid.getView(),
+ el = view.el,
+ rowEls = el.query(view.getItemSelector()),
+ ln = rowEls.length,
+ i = 0;
+
+ // reset heights each time.
+ me.normalHeights = [];
+
+ for (; i < ln; i++) {
+ me.normalHeights[i] = rowEls[i].clientHeight;
+ }
+ me.syncRowHeights();
+ },
+
+ // rows can get bigger/smaller
+ onLockedGridAfterUpdate: function(record, index, node) {
+ this.lockedHeights[index] = node.clientHeight;
+ this.syncRowHeights();
+ },
+
+ // rows can get bigger/smaller
+ onNormalGridAfterUpdate: function(record, index, node) {
+ this.normalHeights[index] = node.clientHeight;
+ this.syncRowHeights();
+ },
+
+ // match the rowheights to the biggest rowheight on either
+ // side
+ syncRowHeights: function() {
+ var me = this,
+ lockedHeights = me.lockedHeights,
+ normalHeights = me.normalHeights,
+ calcHeights = [],
+ ln = lockedHeights.length,
+ i = 0,
+ lockedView, normalView,
+ lockedRowEls, normalRowEls,
+ vertScroller = me.getVerticalScroller(),
+ scrollTop;
+
+ // ensure there are an equal num of locked and normal
+ // rows before synchronization
+ if (lockedHeights.length && normalHeights.length) {
+ lockedView = me.lockedGrid.getView();
+ normalView = me.normalGrid.getView();
+ lockedRowEls = lockedView.el.query(lockedView.getItemSelector());
+ normalRowEls = normalView.el.query(normalView.getItemSelector());
+
+ // loop thru all of the heights and sync to the other side
+ for (; i < ln; i++) {
+ // ensure both are numbers
+ if (!isNaN(lockedHeights[i]) && !isNaN(normalHeights[i])) {
+ if (lockedHeights[i] > normalHeights[i]) {
+ Ext.fly(normalRowEls[i]).setHeight(lockedHeights[i]);
+ } else if (lockedHeights[i] < normalHeights[i]) {
+ Ext.fly(lockedRowEls[i]).setHeight(normalHeights[i]);
+ }
+ }
+ }
+
+ // invalidate the scroller and sync the scrollers
+ me.normalGrid.invalidateScroller();
+
+ // synchronize the view with the scroller, if we have a virtualScrollTop
+ // then the user is using a PagingScroller
+ if (vertScroller && vertScroller.setViewScrollTop) {
+ vertScroller.setViewScrollTop(me.virtualScrollTop);
+ } else {
+ // We don't use setScrollTop here because if the scrollTop is
+ // set to the exact same value some browsers won't fire the scroll
+ // event. Instead, we directly set the scrollTop.
+ scrollTop = normalView.el.dom.scrollTop;
+ normalView.el.dom.scrollTop = scrollTop;
+ lockedView.el.dom.scrollTop = scrollTop;
+ }
+
+ // reset the heights
+ me.lockedHeights = [];
+ me.normalHeights = [];
+ }
+ },
+
+ // track when scroller is shown
+ onScrollerShow: function(scroller, direction) {
+ if (direction === 'horizontal') {
+ this.spacerHidden = false;
+ this.getSpacerEl().removeCls(Ext.baseCSSPrefix + 'hidden');
+ }
+ },
+
+ // track when scroller is hidden
+ onScrollerHide: function(scroller, direction) {
+ if (direction === 'horizontal') {
+ this.spacerHidden = true;
+ if (this.spacerEl) {
+ this.spacerEl.addCls(Ext.baseCSSPrefix + 'hidden');
+ }
+ }
+ },
+
+
+ // inject Lock and Unlock text
+ modifyHeaderCt: function() {
+ var me = this;
+ me.lockedGrid.headerCt.getMenuItems = me.getMenuItems(true);
+ me.normalGrid.headerCt.getMenuItems = me.getMenuItems(false);
+ },
+
+ onUnlockMenuClick: function() {
+ this.unlock();
+ },
+
+ onLockMenuClick: function() {
+ this.lock();
+ },
+
+ getMenuItems: function(locked) {
+ var me = this,
+ unlockText = me.unlockText,
+ lockText = me.lockText,
+ unlockCls = Ext.baseCSSPrefix + 'hmenu-unlock',
+ lockCls = Ext.baseCSSPrefix + 'hmenu-lock',
+ unlockHandler = Ext.Function.bind(me.onUnlockMenuClick, me),
+ lockHandler = Ext.Function.bind(me.onLockMenuClick, me);
+
+ // runs in the scope of headerCt
+ return function() {
+ var o = Ext.grid.header.Container.prototype.getMenuItems.call(this);
+ o.push('-',{
+ cls: unlockCls,
+ text: unlockText,
+ handler: unlockHandler,
+ disabled: !locked
+ });
+ o.push({
+ cls: lockCls,
+ text: lockText,
+ handler: lockHandler,
+ disabled: locked
+ });
+ return o;
+ };
+ },
+
+ // going from unlocked section to locked
+ /**
+ * Locks the activeHeader as determined by which menu is open OR a header
+ * as specified.
+ * @param {Ext.grid.column.Column} header (Optional) Header to unlock from the locked section. Defaults to the header which has the menu open currently.
+ * @param {Number} toIdx (Optional) The index to move the unlocked header to. Defaults to appending as the last item.
+ * @private
+ */
+ lock: function(activeHd, toIdx) {
+ var me = this,
+ normalGrid = me.normalGrid,
+ lockedGrid = me.lockedGrid,
+ normalHCt = normalGrid.headerCt,
+ lockedHCt = lockedGrid.headerCt;
+
+ activeHd = activeHd || normalHCt.getMenu().activeHeader;
+
+ // if column was previously flexed, get/set current width
+ // and remove the flex
+ if (activeHd.flex) {
+ activeHd.width = activeHd.getWidth();
+ delete activeHd.flex;
+ }
+
+ normalHCt.remove(activeHd, false);
+ lockedHCt.suspendLayout = true;
+ activeHd.locked = true;
+ if (Ext.isDefined(toIdx)) {
+ lockedHCt.insert(toIdx, activeHd);
+ } else {
+ lockedHCt.add(activeHd);
+ }
+ lockedHCt.suspendLayout = false;
+ me.syncLockedSection();
+
+ me.fireEvent('lockcolumn', me, activeHd);
+ },
+
+ syncLockedSection: function() {
+ var me = this;
+ me.syncLockedWidth();
+ me.lockedGrid.getView().refresh();
+ me.normalGrid.getView().refresh();
+ },
+
+ // adjust the locked section to the width of its respective
+ // headerCt
+ syncLockedWidth: function() {
+ var me = this,
+ width = me.lockedGrid.headerCt.getFullWidth(true);
+ me.lockedGrid.setWidth(width+1); // +1 for border pixel
+ me.doComponentLayout();
+ },
+
+ onLockedHeaderResize: function() {
+ this.syncLockedWidth();
+ },
+
+ onLockedHeaderHide: function() {
+ this.syncLockedWidth();
+ },
+
+ onLockedHeaderShow: function() {
+ this.syncLockedWidth();
+ },
+
+ onLockedHeaderSortChange: function(headerCt, header, sortState) {
+ if (sortState) {
+ // no real header, and silence the event so we dont get into an
+ // infinite loop
+ this.normalGrid.headerCt.clearOtherSortStates(null, true);
+ }
+ },
+
+ onNormalHeaderSortChange: function(headerCt, header, sortState) {
+ if (sortState) {
+ // no real header, and silence the event so we dont get into an
+ // infinite loop
+ this.lockedGrid.headerCt.clearOtherSortStates(null, true);
+ }
+ },
+
+ // going from locked section to unlocked
+ /**
+ * Unlocks the activeHeader as determined by which menu is open OR a header
+ * as specified.
+ * @param {Ext.grid.column.Column} header (Optional) Header to unlock from the locked section. Defaults to the header which has the menu open currently.
+ * @param {Number} toIdx (Optional) The index to move the unlocked header to. Defaults to 0.
+ * @private
+ */
+ unlock: function(activeHd, toIdx) {
+ var me = this,
+ normalGrid = me.normalGrid,
+ lockedGrid = me.lockedGrid,
+ normalHCt = normalGrid.headerCt,
+ lockedHCt = lockedGrid.headerCt;
+
+ if (!Ext.isDefined(toIdx)) {
+ toIdx = 0;
+ }
+ activeHd = activeHd || lockedHCt.getMenu().activeHeader;
+
+ lockedHCt.remove(activeHd, false);
+ me.syncLockedWidth();
+ me.lockedGrid.getView().refresh();
+ activeHd.locked = false;
+ normalHCt.insert(toIdx, activeHd);
+ me.normalGrid.getView().refresh();
+
+ me.fireEvent('unlockcolumn', me, activeHd);
+ },
+
+ applyColumnsState: function (columns) {
+ var me = this,
+ lockedGrid = me.lockedGrid,
+ lockedHeaderCt = lockedGrid.headerCt,
+ normalHeaderCt = me.normalGrid.headerCt,
+ lockedCols = lockedHeaderCt.items,
+ normalCols = normalHeaderCt.items,
+ existing,
+ locked = [],
+ normal = [],
+ lockedDefault,
+ lockedWidth = 1;
+
+ Ext.each(columns, function (col) {
+ function matches (item) {
+ return item.headerId == col.id;
+ }
+
+ lockedDefault = true;
+ if (!(existing = lockedCols.findBy(matches))) {
+ existing = normalCols.findBy(matches);
+ lockedDefault = false;
+ }
+
+ if (existing) {
+ if (existing.applyColumnState) {
+ existing.applyColumnState(col);
+ }
+ if (!Ext.isDefined(existing.locked)) {
+ existing.locked = lockedDefault;
+ }
+ if (existing.locked) {
+ locked.push(existing);
+ if (!existing.hidden && Ext.isNumber(existing.width)) {
+ lockedWidth += existing.width;
+ }
+ } else {
+ normal.push(existing);
+ }
+ }
+ });
+
+ // state and config must have the same columns (compare counts for now):
+ if (locked.length + normal.length == lockedCols.getCount() + normalCols.getCount()) {
+ lockedHeaderCt.removeAll(false);
+ normalHeaderCt.removeAll(false);
+
+ lockedHeaderCt.add(locked);
+ normalHeaderCt.add(normal);
+
+ lockedGrid.setWidth(lockedWidth);
+ }
+ },
+
+ getColumnsState: function () {
+ var me = this,
+ locked = me.lockedGrid.headerCt.getColumnsState(),
+ normal = me.normalGrid.headerCt.getColumnsState();
+
+ return locked.concat(normal);
+ },
+
+ // we want to totally override the reconfigure behaviour here, since we're creating 2 sub-grids
+ reconfigureLockable: function(store, columns) {
+ var me = this,
+ lockedGrid = me.lockedGrid,
+ normalGrid = me.normalGrid;
+
+ if (columns) {
+ lockedGrid.headerCt.suspendLayout = true;
+ normalGrid.headerCt.suspendLayout = true;
+ lockedGrid.headerCt.removeAll();
+ normalGrid.headerCt.removeAll();
+
+ columns = me.processColumns(columns);
+ lockedGrid.setWidth(columns.lockedWidth);
+ lockedGrid.headerCt.add(columns.locked);
+ normalGrid.headerCt.add(columns.normal);
+ }
+
+ if (store) {
+ store = Ext.data.StoreManager.lookup(store);
+ me.store = store;
+ lockedGrid.bindStore(store);
+ normalGrid.bindStore(store);
+ } else {
+ lockedGrid.getView().refresh();
+ normalGrid.getView().refresh();
+ }
+
+ if (columns) {
+ lockedGrid.headerCt.suspendLayout = false;
+ normalGrid.headerCt.suspendLayout = false;
+ lockedGrid.headerCt.forceComponentLayout();
+ normalGrid.headerCt.forceComponentLayout();
+ }
+ }
+});
+
+/**
+ * Docked in an Ext.grid.Panel, controls virtualized scrolling and synchronization
+ * across different sections.
+ */
+Ext.define('Ext.grid.Scroller', {
+ extend: 'Ext.Component',
+ alias: 'widget.gridscroller',
+ weight: 110,
+ baseCls: Ext.baseCSSPrefix + 'scroller',
+ focusable: false,
+ reservedSpace: 0,
+
+ renderTpl: [
+ '<div class="' + Ext.baseCSSPrefix + 'scroller-ct" id="{baseId}_ct">',
+ '<div class="' + Ext.baseCSSPrefix + 'stretcher" id="{baseId}_stretch"></div>',
+ '</div>'
+ ],
+
+ initComponent: function() {
+ var me = this,
+ dock = me.dock,
+ cls = Ext.baseCSSPrefix + 'scroller-vertical';
+
+ me.offsets = {bottom: 0};
+ me.scrollProp = 'scrollTop';
+ me.vertical = true;
+ me.sizeProp = 'width';
+
+ if (dock === 'top' || dock === 'bottom') {
+ cls = Ext.baseCSSPrefix + 'scroller-horizontal';
+ me.sizeProp = 'height';
+ me.scrollProp = 'scrollLeft';
+ me.vertical = false;
+ me.weight += 5;
+ }
+
+ me.cls += (' ' + cls);
+
+ Ext.applyIf(me.renderSelectors, {
+ stretchEl: '.' + Ext.baseCSSPrefix + 'stretcher',
+ scrollEl: '.' + Ext.baseCSSPrefix + 'scroller-ct'
+ });
+ me.callParent();
+ },
+
+ ensureDimension: function(){
+ var me = this,
+ sizeProp = me.sizeProp;
+
+ me[sizeProp] = me.scrollerSize = Ext.getScrollbarSize()[sizeProp];
+ },
+
+ initRenderData: function () {
+ var me = this,
+ ret = me.callParent(arguments) || {};
+
+ ret.baseId = me.id;
+
+ return ret;
+ },
+
+ afterRender: function() {
+ var me = this;
+ me.callParent();
+
+ me.mon(me.scrollEl, 'scroll', me.onElScroll, me);
+ Ext.cache[me.el.id].skipGarbageCollection = true;
+ },
+
+ onAdded: function(container) {
+ // Capture the controlling grid Panel so that we can use it even when we are undocked, and don't have an ownerCt
+ this.ownerGrid = container;
+ this.callParent(arguments);
+ },
+
+ getSizeCalculation: function() {
+ var me = this,
+ owner = me.getPanel(),
+ width = 1,
+ height = 1,
+ view, tbl;
+
+ if (!me.vertical) {
+ // TODO: Must gravitate to a single region..
+ // Horizontal scrolling only scrolls virtualized region
+ var items = owner.query('tableview'),
+ center = items[1] || items[0];
+
+ if (!center) {
+ return false;
+ }
+ // center is not guaranteed to have content, such as when there
+ // are zero rows in the grid/tree. We read the width from the
+ // headerCt instead.
+ width = center.headerCt.getFullWidth();
+
+ if (Ext.isIEQuirks) {
+ width--;
+ }
+ } else {
+ view = owner.down('tableview:not([lockableInjected])');
+ if (!view || !view.el) {
+ return false;
+ }
+ tbl = view.el.child('table', true);
+ if (!tbl) {
+ return false;
+ }
+
+ // needs to also account for header and scroller (if still in picture)
+ // should calculate from headerCt.
+ height = tbl.offsetHeight;
+ }
+ if (isNaN(width)) {
+ width = 1;
+ }
+ if (isNaN(height)) {
+ height = 1;
+ }
+ return {
+ width: width,
+ height: height
+ };
+ },
+
+ invalidate: function(firstPass) {
+ var me = this,
+ stretchEl = me.stretchEl;
+
+ if (!stretchEl || !me.ownerCt) {
+ return;
+ }
+
+ var size = me.getSizeCalculation(),
+ scrollEl = me.scrollEl,
+ elDom = scrollEl.dom,
+ reservedSpace = me.reservedSpace,
+ pos,
+ extra = 5;
+
+ if (size) {
+ stretchEl.setSize(size);
+
+ size = me.el.getSize(true);
+
+ if (me.vertical) {
+ size.width += extra;
+ size.height -= reservedSpace;
+ pos = 'left';
+ } else {
+ size.width -= reservedSpace;
+ size.height += extra;
+ pos = 'top';
+ }
+
+ scrollEl.setSize(size);
+ elDom.style[pos] = (-extra) + 'px';
+
+ // BrowserBug: IE7
+ // This makes the scroller enabled, when initially rendering.
+ elDom.scrollTop = elDom.scrollTop;
+ }
+ },
+
+ afterComponentLayout: function() {
+ this.callParent(arguments);
+ this.invalidate();
+ },
+
+ restoreScrollPos: function () {
+ var me = this,
+ el = this.scrollEl,
+ elDom = el && el.dom;
+
+ if (me._scrollPos !== null && elDom) {
+ elDom[me.scrollProp] = me._scrollPos;
+ me._scrollPos = null;
+ }
+ },
+
+ setReservedSpace: function (reservedSpace) {
+ var me = this;
+ if (me.reservedSpace !== reservedSpace) {
+ me.reservedSpace = reservedSpace;
+ me.invalidate();
+ }
+ },
+
+ saveScrollPos: function () {
+ var me = this,
+ el = this.scrollEl,
+ elDom = el && el.dom;
+
+ me._scrollPos = elDom ? elDom[me.scrollProp] : null;
+ },
+
+ /**
+ * Sets the scrollTop and constrains the value between 0 and max.
+ * @param {Number} scrollTop
+ * @return {Number} The resulting scrollTop value after being constrained
+ */
+ setScrollTop: function(scrollTop) {
+ var el = this.scrollEl,
+ elDom = el && el.dom;
+
+ if (elDom) {
+ return elDom.scrollTop = Ext.Number.constrain(scrollTop, 0, elDom.scrollHeight - elDom.clientHeight);
+ }
+ },
+
+ /**
+ * Sets the scrollLeft and constrains the value between 0 and max.
+ * @param {Number} scrollLeft
+ * @return {Number} The resulting scrollLeft value after being constrained
+ */
+ setScrollLeft: function(scrollLeft) {
+ var el = this.scrollEl,
+ elDom = el && el.dom;
+
+ if (elDom) {
+ return elDom.scrollLeft = Ext.Number.constrain(scrollLeft, 0, elDom.scrollWidth - elDom.clientWidth);
+ }
+ },
+
+ /**
+ * Scroll by deltaY
+ * @param {Number} delta
+ * @return {Number} The resulting scrollTop value
+ */
+ scrollByDeltaY: function(delta) {
+ var el = this.scrollEl,
+ elDom = el && el.dom;
+
+ if (elDom) {
+ return this.setScrollTop(elDom.scrollTop + delta);
+ }
+ },
+
+ /**
+ * Scroll by deltaX
+ * @param {Number} delta
+ * @return {Number} The resulting scrollLeft value
+ */
+ scrollByDeltaX: function(delta) {
+ var el = this.scrollEl,
+ elDom = el && el.dom;
+
+ if (elDom) {
+ return this.setScrollLeft(elDom.scrollLeft + delta);
+ }
+ },
+
+
+ /**
+ * Scroll to the top.
+ */
+ scrollToTop : function(){
+ this.setScrollTop(0);
+ },
+
+ // synchronize the scroller with the bound gridviews
+ onElScroll: function(event, target) {
+ this.fireEvent('bodyscroll', event, target);
+ },
+
+ getPanel: function() {
+ var me = this;
+ if (!me.panel) {
+ me.panel = this.up('[scrollerOwner]');
+ }
+ return me.panel;
+ }
+});
+
+
+/**
+ * @class Ext.grid.PagingScroller
+ * @extends Ext.grid.Scroller
+ */
+Ext.define('Ext.grid.PagingScroller', {
+ extend: 'Ext.grid.Scroller',
+ alias: 'widget.paginggridscroller',
+ //renderTpl: null,
+ //tpl: [
+ // '<tpl for="pages">',
+ // '<div class="' + Ext.baseCSSPrefix + 'stretcher" style="width: {width}px;height: {height}px;"></div>',
+ // '</tpl>'
+ //],
+ /**
+ * @cfg {Number} percentageFromEdge This is a number above 0 and less than 1 which specifies
+ * at what percentage to begin fetching the next page. For example if the pageSize is 100
+ * and the percentageFromEdge is the default of 0.35, the paging scroller will prefetch pages
+ * when scrolling up between records 0 and 34 and when scrolling down between records 65 and 99.
+ */
+ percentageFromEdge: 0.35,
+
+ /**
+ * @cfg {Number} scrollToLoadBuffer This is the time in milliseconds to buffer load requests
+ * when scrolling the PagingScrollbar.
+ */
+ scrollToLoadBuffer: 200,
+
+ activePrefetch: true,
+
+ chunkSize: 50,
+ snapIncrement: 25,
+
+ syncScroll: true,
+
+ initComponent: function() {
+ var me = this,
+ ds = me.store;
+
+ ds.on('guaranteedrange', me.onGuaranteedRange, me);
+ me.callParent(arguments);
+ },
+
+ onGuaranteedRange: function(range, start, end) {
+ var me = this,
+ ds = me.store,
+ rs;
+ // this should never happen
+ if (range.length && me.visibleStart < range[0].index) {
+ return;
+ }
+
+ ds.loadRecords(range);
+
+ if (!me.firstLoad) {
+ if (me.rendered) {
+ me.invalidate();
+ } else {
+ me.on('afterrender', me.invalidate, me, {single: true});
+ }
+ me.firstLoad = true;
+ } else {
+ // adjust to visible
+ // only sync if there is a paging scrollbar element and it has a scroll height (meaning it's currently in the DOM)
+ if (me.scrollEl && me.scrollEl.dom && me.scrollEl.dom.scrollHeight) {
+ me.syncTo();
+ }
+ }
+ },
+
+ syncTo: function() {
+ var me = this,
+ pnl = me.getPanel(),
+ store = pnl.store,
+ scrollerElDom = this.scrollEl.dom,
+ rowOffset = me.visibleStart - store.guaranteedStart,
+ scrollBy = rowOffset * me.rowHeight,
+ scrollHeight = scrollerElDom.scrollHeight,
+ clientHeight = scrollerElDom.clientHeight,
+ scrollTop = scrollerElDom.scrollTop,
+ useMaximum;
+
+
+ // BrowserBug: clientHeight reports 0 in IE9 StrictMode
+ // Instead we are using offsetHeight and hardcoding borders
+ if (Ext.isIE9 && Ext.isStrict) {
+ clientHeight = scrollerElDom.offsetHeight + 2;
+ }
+
+ // This should always be zero or greater than zero but staying
+ // safe and less than 0 we'll scroll to the bottom.
+ useMaximum = (scrollHeight - clientHeight - scrollTop <= 0);
+ this.setViewScrollTop(scrollBy, useMaximum);
+ },
+
+ getPageData : function(){
+ var panel = this.getPanel(),
+ store = panel.store,
+ totalCount = store.getTotalCount();
+
+ return {
+ total : totalCount,
+ currentPage : store.currentPage,
+ pageCount: Math.ceil(totalCount / store.pageSize),
+ fromRecord: ((store.currentPage - 1) * store.pageSize) + 1,
+ toRecord: Math.min(store.currentPage * store.pageSize, totalCount)
+ };
+ },
+
+ onElScroll: function(e, t) {
+ var me = this,
+ panel = me.getPanel(),
+ store = panel.store,
+ pageSize = store.pageSize,
+ guaranteedStart = store.guaranteedStart,
+ guaranteedEnd = store.guaranteedEnd,
+ totalCount = store.getTotalCount(),
+ numFromEdge = Math.ceil(me.percentageFromEdge * pageSize),
+ position = t.scrollTop,
+ visibleStart = Math.floor(position / me.rowHeight),
+ view = panel.down('tableview'),
+ viewEl = view.el,
+ visibleHeight = viewEl.getHeight(),
+ visibleAhead = Math.ceil(visibleHeight / me.rowHeight),
+ visibleEnd = visibleStart + visibleAhead,
+ prevPage = Math.floor(visibleStart / pageSize),
+ nextPage = Math.floor(visibleEnd / pageSize) + 2,
+ lastPage = Math.ceil(totalCount / pageSize),
+ snap = me.snapIncrement,
+ requestStart = Math.floor(visibleStart / snap) * snap,
+ requestEnd = requestStart + pageSize - 1,
+ activePrefetch = me.activePrefetch;
+
+ me.visibleStart = visibleStart;
+ me.visibleEnd = visibleEnd;
+
+
+ me.syncScroll = true;
+ if (totalCount >= pageSize) {
+ // end of request was past what the total is, grab from the end back a pageSize
+ if (requestEnd > totalCount - 1) {
+ me.cancelLoad();
+ if (store.rangeSatisfied(totalCount - pageSize, totalCount - 1)) {
+ me.syncScroll = true;
+ }
+ store.guaranteeRange(totalCount - pageSize, totalCount - 1);
+ // Out of range, need to reset the current data set
+ } else if (visibleStart <= guaranteedStart || visibleEnd > guaranteedEnd) {
+ if (visibleStart <= guaranteedStart) {
+ // need to scroll up
+ requestStart -= snap;
+ requestEnd -= snap;
+
+ if (requestStart < 0) {
+ requestStart = 0;
+ requestEnd = pageSize;
+ }
+ }
+ if (store.rangeSatisfied(requestStart, requestEnd)) {
+ me.cancelLoad();
+ store.guaranteeRange(requestStart, requestEnd);
+ } else {
+ store.mask();
+ me.attemptLoad(requestStart, requestEnd);
+ }
+ // dont sync the scroll view immediately, sync after the range has been guaranteed
+ me.syncScroll = false;
+ } else if (activePrefetch && visibleStart < (guaranteedStart + numFromEdge) && prevPage > 0) {
+ me.syncScroll = true;
+ store.prefetchPage(prevPage);
+ } else if (activePrefetch && visibleEnd > (guaranteedEnd - numFromEdge) && nextPage < lastPage) {
+ me.syncScroll = true;
+ store.prefetchPage(nextPage);
+ }
+ }
+
+ if (me.syncScroll) {
+ me.syncTo();
+ }
+ },
+
+ getSizeCalculation: function() {
+ // Use the direct ownerCt here rather than the scrollerOwner
+ // because we are calculating widths/heights.
+ var me = this,
+ owner = me.ownerGrid,
+ view = owner.getView(),
+ store = me.store,
+ dock = me.dock,
+ elDom = me.el.dom,
+ width = 1,
+ height = 1;
+
+ if (!me.rowHeight) {
+ me.rowHeight = view.el.down(view.getItemSelector()).getHeight(false, true);
+ }
+
+ // If the Store is *locally* filtered, use the filtered count from getCount.
+ height = store[(!store.remoteFilter && store.isFiltered()) ? 'getCount' : 'getTotalCount']() * me.rowHeight;
+
+ if (isNaN(width)) {
+ width = 1;
+ }
+ if (isNaN(height)) {
+ height = 1;
+ }
+ return {
+ width: width,
+ height: height
+ };
+ },
+
+ attemptLoad: function(start, end) {
+ var me = this;
+ if (!me.loadTask) {
+ me.loadTask = Ext.create('Ext.util.DelayedTask', me.doAttemptLoad, me, []);
+ }
+ me.loadTask.delay(me.scrollToLoadBuffer, me.doAttemptLoad, me, [start, end]);
+ },
+
+ cancelLoad: function() {
+ if (this.loadTask) {
+ this.loadTask.cancel();
+ }
+ },
+
+ doAttemptLoad: function(start, end) {
+ var store = this.getPanel().store;
+ store.guaranteeRange(start, end);
+ },
+
+ setViewScrollTop: function(scrollTop, useMax) {
+ var me = this,
+ owner = me.getPanel(),
+ items = owner.query('tableview'),
+ i = 0,
+ len = items.length,
+ center,
+ centerEl,
+ calcScrollTop,
+ maxScrollTop,
+ scrollerElDom = me.el.dom;
+
+ owner.virtualScrollTop = scrollTop;
+
+ center = items[1] || items[0];
+ centerEl = center.el.dom;
+
+ maxScrollTop = ((owner.store.pageSize * me.rowHeight) - centerEl.clientHeight);
+ calcScrollTop = (scrollTop % ((owner.store.pageSize * me.rowHeight) + 1));
+ if (useMax) {
+ calcScrollTop = maxScrollTop;
+ }
+ if (calcScrollTop > maxScrollTop) {
+ //Ext.Error.raise("Calculated scrollTop was larger than maxScrollTop");
+ return;
+ // calcScrollTop = maxScrollTop;
+ }
+ for (; i < len; i++) {
+ items[i].el.dom.scrollTop = calcScrollTop;
+ }
+ }
+});
+
+/**
+ * @author Nicolas Ferrero
+ *
+ * TablePanel is the basis of both {@link Ext.tree.Panel TreePanel} and {@link Ext.grid.Panel GridPanel}.
+ *
+ * TablePanel aggregates:
+ *
+ * - a Selection Model
+ * - a View
+ * - a Store
+ * - Scrollers
+ * - Ext.grid.header.Container
+ */
+Ext.define('Ext.panel.Table', {
+ extend: 'Ext.panel.Panel',
+
+ alias: 'widget.tablepanel',
+
+ uses: [
+ 'Ext.selection.RowModel',
+ 'Ext.grid.Scroller',
+ 'Ext.grid.header.Container',
+ 'Ext.grid.Lockable'
+ ],
+
+ extraBaseCls: Ext.baseCSSPrefix + 'grid',
+ extraBodyCls: Ext.baseCSSPrefix + 'grid-body',
+
+ layout: 'fit',
+ /**
+ * @property {Boolean} hasView
+ * True to indicate that a view has been injected into the panel.
+ */
+ hasView: false,
+
+ // each panel should dictate what viewType and selType to use
+ /**
+ * @cfg {String} viewType
+ * An xtype of view to use. This is automatically set to 'gridview' by {@link Ext.grid.Panel Grid}
+ * and to 'treeview' by {@link Ext.tree.Panel Tree}.
+ */
+ viewType: null,
+
+ /**
+ * @cfg {Object} viewConfig
+ * A config object that will be applied to the grid's UI view. Any of the config options available for
+ * {@link Ext.view.Table} can be specified here. This option is ignored if {@link #view} is specified.
+ */
+
+ /**
+ * @cfg {Ext.view.Table} view
+ * The {@link Ext.view.Table} used by the grid. Use {@link #viewConfig} to just supply some config options to
+ * view (instead of creating an entire View instance).
+ */
+
+ /**
+ * @cfg {String} selType
+ * An xtype of selection model to use. Defaults to 'rowmodel'. This is used to create selection model if just
+ * a config object or nothing at all given in {@link #selModel} config.
+ */
+ selType: 'rowmodel',
+
+ /**
+ * @cfg {Ext.selection.Model/Object} selModel
+ * A {@link Ext.selection.Model selection model} instance or config object. In latter case the {@link #selType}
+ * config option determines to which type of selection model this config is applied.
+ */
+
+ /**
+ * @cfg {Boolean} multiSelect
+ * True to enable 'MULTI' selection mode on selection model. See {@link Ext.selection.Model#mode}.
+ */
+
+ /**
+ * @cfg {Boolean} simpleSelect
+ * True to enable 'SIMPLE' selection mode on selection model. See {@link Ext.selection.Model#mode}.
+ */
+
+ /**
+ * @cfg {Ext.data.Store} store (required)
+ * The {@link Ext.data.Store Store} the grid should use as its data source.
+ */
+
+ /**
+ * @cfg {Number} scrollDelta
+ * Number of pixels to scroll when scrolling with mousewheel.
+ */
+ scrollDelta: 40,
+
+ /**
+ * @cfg {String/Boolean} scroll
+ * Scrollers configuration. Valid values are 'both', 'horizontal' or 'vertical'.
+ * True implies 'both'. False implies 'none'.
+ */
+ scroll: true,
+
+ /**
+ * @cfg {Ext.grid.column.Column[]} columns
+ * An array of {@link Ext.grid.column.Column column} definition objects which define all columns that appear in this
+ * grid. Each column definition provides the header text for the column, and a definition of where the data for that
+ * column comes from.
+ */
+
+ /**
+ * @cfg {Boolean} forceFit
+ * Ttrue to force the columns to fit into the available width. Headers are first sized according to configuration,
+ * whether that be a specific width, or flex. Then they are all proportionally changed in width so that the entire
+ * content width is used.
+ */
+
+ /**
+ * @cfg {Ext.grid.feature.Feature[]} features
+ * An array of grid Features to be added to this grid. See {@link Ext.grid.feature.Feature} for usage.
+ */
+
+ /**
+ * @cfg {Boolean} [hideHeaders=false]
+ * True to hide column headers.
+ */
+
+ /**
+ * @cfg {Boolean} deferRowRender
+ * Defaults to true to enable deferred row rendering.
+ *
+ * This allows the View to execute a refresh quickly, with the expensive update of the row structure deferred so
+ * that layouts with GridPanels appear, and lay out more quickly.
+ */
+
+ deferRowRender: true,
+
+ /**
+ * @cfg {Boolean} sortableColumns
+ * False to disable column sorting via clicking the header and via the Sorting menu items.
+ */
+ sortableColumns: true,
+
+ /**
+ * @cfg {Boolean} [enableLocking=false]
+ * True to enable locking support for this grid. Alternatively, locking will also be automatically
+ * enabled if any of the columns in the column configuration contain the locked config option.
+ */
+ enableLocking: false,
+
+ verticalScrollDock: 'right',
+ verticalScrollerType: 'gridscroller',
+
+ horizontalScrollerPresentCls: Ext.baseCSSPrefix + 'horizontal-scroller-present',
+ verticalScrollerPresentCls: Ext.baseCSSPrefix + 'vertical-scroller-present',
+
+ // private property used to determine where to go down to find views
+ // this is here to support locking.
+ scrollerOwner: true,
+
+ invalidateScrollerOnRefresh: true,
+
+ /**
+ * @cfg {Boolean} enableColumnMove
+ * False to disable column dragging within this grid.
+ */
+ enableColumnMove: true,
+
+ /**
+ * @cfg {Boolean} enableColumnResize
+ * False to disable column resizing within this grid.
+ */
+ enableColumnResize: true,
+
+ /**
+ * @cfg {Boolean} enableColumnHide
+ * False to disable column hiding within this grid.
+ */
+ enableColumnHide: true,
+
+ initComponent: function() {
+
+ var me = this,
+ scroll = me.scroll,
+ vertical = false,
+ horizontal = false,
+ headerCtCfg = me.columns || me.colModel,
+ i = 0,
+ view,
+ border = me.border;
+
+ if (me.hideHeaders) {
+ border = false;
+ }
+
+ // Look up the configured Store. If none configured, use the fieldless, empty Store defined in Ext.data.Store.
+ me.store = Ext.data.StoreManager.lookup(me.store || 'ext-empty-store');
+
+ // The columns/colModel config may be either a fully instantiated HeaderContainer, or an array of Column definitions, or a config object of a HeaderContainer
+ // Either way, we extract a columns property referencing an array of Column definitions.
+ if (headerCtCfg instanceof Ext.grid.header.Container) {
+ me.headerCt = headerCtCfg;
+ me.headerCt.border = border;
+ me.columns = me.headerCt.items.items;
+ } else {
+ if (Ext.isArray(headerCtCfg)) {
+ headerCtCfg = {
+ items: headerCtCfg,
+ border: border
+ };
+ }
+ Ext.apply(headerCtCfg, {
+ forceFit: me.forceFit,
+ sortable: me.sortableColumns,
+ enableColumnMove: me.enableColumnMove,
+ enableColumnResize: me.enableColumnResize,
+ enableColumnHide: me.enableColumnHide,
+ border: border
+ });
+ me.columns = headerCtCfg.items;
+
+ // If any of the Column objects contain a locked property, and are not processed, this is a lockable TablePanel, a
+ // special view will be injected by the Ext.grid.Lockable mixin, so no processing of .
+ if (me.enableLocking || Ext.ComponentQuery.query('{locked !== undefined}{processed != true}', me.columns).length) {
+ me.self.mixin('lockable', Ext.grid.Lockable);
+ me.injectLockable();
+ }
+ }
+
+ me.addEvents(
+ /**
+ * @event reconfigure
+ * Fires after a reconfigure.
+ * @param {Ext.panel.Table} this
+ */
+ 'reconfigure',
+ /**
+ * @event viewready
+ * Fires when the grid view is available (use this for selecting a default row).
+ * @param {Ext.panel.Table} this
+ */
+ 'viewready',
+ /**
+ * @event scrollerhide
+ * Fires when a scroller is hidden.
+ * @param {Ext.grid.Scroller} scroller
+ * @param {String} orientation Orientation, can be 'vertical' or 'horizontal'
+ */
+ 'scrollerhide',
+ /**
+ * @event scrollershow
+ * Fires when a scroller is shown.
+ * @param {Ext.grid.Scroller} scroller
+ * @param {String} orientation Orientation, can be 'vertical' or 'horizontal'
+ */
+ 'scrollershow'
+ );
+
+ me.bodyCls = me.bodyCls || '';
+ me.bodyCls += (' ' + me.extraBodyCls);
+
+ me.cls = me.cls || '';
+ me.cls += (' ' + me.extraBaseCls);
+
+ // autoScroll is not a valid configuration
+ delete me.autoScroll;
+
+ // If this TablePanel is lockable (Either configured lockable, or any of the defined columns has a 'locked' property)
+ // than a special lockable view containing 2 side-by-side grids will have been injected so we do not need to set up any UI.
+ if (!me.hasView) {
+
+ // If we were not configured with a ready-made headerCt (either by direct config with a headerCt property, or by passing
+ // a HeaderContainer instance as the 'columns' property, then go ahead and create one from the config object created above.
+ if (!me.headerCt) {
+ me.headerCt = Ext.create('Ext.grid.header.Container', headerCtCfg);
+ }
+
+ // Extract the array of Column objects
+ me.columns = me.headerCt.items.items;
+
+ if (me.hideHeaders) {
+ me.headerCt.height = 0;
+ me.headerCt.border = false;
+ me.headerCt.addCls(Ext.baseCSSPrefix + 'grid-header-ct-hidden');
+ me.addCls(Ext.baseCSSPrefix + 'grid-header-hidden');
+ // IE Quirks Mode fix
+ // If hidden configuration option was used, several layout calculations will be bypassed.
+ if (Ext.isIEQuirks) {
+ me.headerCt.style = {
+ display: 'none'
+ };
+ }
+ }
+
+ // turn both on.
+ if (scroll === true || scroll === 'both') {
+ vertical = horizontal = true;
+ } else if (scroll === 'horizontal') {
+ horizontal = true;
+ } else if (scroll === 'vertical') {
+ vertical = true;
+ // All other values become 'none' or false.
+ } else {
+ me.headerCt.availableSpaceOffset = 0;
+ }
+
+ if (vertical) {
+ me.verticalScroller = Ext.ComponentManager.create(me.initVerticalScroller());
+ me.mon(me.verticalScroller, {
+ bodyscroll: me.onVerticalScroll,
+ scope: me
+ });
+ }
+
+ if (horizontal) {
+ me.horizontalScroller = Ext.ComponentManager.create(me.initHorizontalScroller());
+ me.mon(me.horizontalScroller, {
+ bodyscroll: me.onHorizontalScroll,
+ scope: me
+ });
+ }
+
+ me.headerCt.on('resize', me.onHeaderResize, me);
+ me.relayHeaderCtEvents(me.headerCt);
+ me.features = me.features || [];
+ if (!Ext.isArray(me.features)) {
+ me.features = [me.features];
+ }
+ me.dockedItems = me.dockedItems || [];
+ me.dockedItems.unshift(me.headerCt);
+ me.viewConfig = me.viewConfig || {};
+ me.viewConfig.invalidateScrollerOnRefresh = me.invalidateScrollerOnRefresh;
+
+ // AbstractDataView will look up a Store configured as an object
+ // getView converts viewConfig into a View instance
+ view = me.getView();
+
+ view.on({
+ afterrender: function () {
+ // hijack the view el's scroll method
+ view.el.scroll = Ext.Function.bind(me.elScroll, me);
+ // We use to listen to document.body wheel events, but that's a
+ // little much. We scope just to the view now.
+ me.mon(view.el, {
+ mousewheel: me.onMouseWheel,
+ scope: me
+ });
+ },
+ single: true
+ });
+ me.items = [view];
+ me.hasView = true;
+
+ me.mon(view.store, {
+ load: me.onStoreLoad,
+ scope: me
+ });
+ me.mon(view, {
+ viewReady: me.onViewReady,
+ resize: me.onViewResize,
+ refresh: {
+ fn: me.onViewRefresh,
+ scope: me,
+ buffer: 50
+ },
+ scope: me
+ });
+ this.relayEvents(view, [
+ /**
+ * @event beforeitemmousedown
+ * @alias Ext.view.View#beforeitemmousedown
+ */
+ 'beforeitemmousedown',
+ /**
+ * @event beforeitemmouseup
+ * @alias Ext.view.View#beforeitemmouseup
+ */
+ 'beforeitemmouseup',
+ /**
+ * @event beforeitemmouseenter
+ * @alias Ext.view.View#beforeitemmouseenter
+ */
+ 'beforeitemmouseenter',
+ /**
+ * @event beforeitemmouseleave
+ * @alias Ext.view.View#beforeitemmouseleave
+ */
+ 'beforeitemmouseleave',
+ /**
+ * @event beforeitemclick
+ * @alias Ext.view.View#beforeitemclick
+ */
+ 'beforeitemclick',
+ /**
+ * @event beforeitemdblclick
+ * @alias Ext.view.View#beforeitemdblclick
+ */
+ 'beforeitemdblclick',
+ /**
+ * @event beforeitemcontextmenu
+ * @alias Ext.view.View#beforeitemcontextmenu
+ */
+ 'beforeitemcontextmenu',
+ /**
+ * @event itemmousedown
+ * @alias Ext.view.View#itemmousedown
+ */
+ 'itemmousedown',
+ /**
+ * @event itemmouseup
+ * @alias Ext.view.View#itemmouseup
+ */
+ 'itemmouseup',
+ /**
+ * @event itemmouseenter
+ * @alias Ext.view.View#itemmouseenter
+ */
+ 'itemmouseenter',
+ /**
+ * @event itemmouseleave
+ * @alias Ext.view.View#itemmouseleave
+ */
+ 'itemmouseleave',
+ /**
+ * @event itemclick
+ * @alias Ext.view.View#itemclick
+ */
+ 'itemclick',
+ /**
+ * @event itemdblclick
+ * @alias Ext.view.View#itemdblclick
+ */
+ 'itemdblclick',
+ /**
+ * @event itemcontextmenu
+ * @alias Ext.view.View#itemcontextmenu
+ */
+ 'itemcontextmenu',
+ /**
+ * @event beforecontainermousedown
+ * @alias Ext.view.View#beforecontainermousedown
+ */
+ 'beforecontainermousedown',
+ /**
+ * @event beforecontainermouseup
+ * @alias Ext.view.View#beforecontainermouseup
+ */
+ 'beforecontainermouseup',
+ /**
+ * @event beforecontainermouseover
+ * @alias Ext.view.View#beforecontainermouseover
+ */
+ 'beforecontainermouseover',
+ /**
+ * @event beforecontainermouseout
+ * @alias Ext.view.View#beforecontainermouseout
+ */
+ 'beforecontainermouseout',
+ /**
+ * @event beforecontainerclick
+ * @alias Ext.view.View#beforecontainerclick
+ */
+ 'beforecontainerclick',
+ /**
+ * @event beforecontainerdblclick
+ * @alias Ext.view.View#beforecontainerdblclick
+ */
+ 'beforecontainerdblclick',
+ /**
+ * @event beforecontainercontextmenu
+ * @alias Ext.view.View#beforecontainercontextmenu
+ */
+ 'beforecontainercontextmenu',
+ /**
+ * @event containermouseup
+ * @alias Ext.view.View#containermouseup
+ */
+ 'containermouseup',
+ /**
+ * @event containermouseover
+ * @alias Ext.view.View#containermouseover
+ */
+ 'containermouseover',
+ /**
+ * @event containermouseout
+ * @alias Ext.view.View#containermouseout
+ */
+ 'containermouseout',
+ /**
+ * @event containerclick
+ * @alias Ext.view.View#containerclick
+ */
+ 'containerclick',
+ /**
+ * @event containerdblclick
+ * @alias Ext.view.View#containerdblclick
+ */
+ 'containerdblclick',
+ /**
+ * @event containercontextmenu
+ * @alias Ext.view.View#containercontextmenu
+ */
+ 'containercontextmenu',
+ /**
+ * @event selectionchange
+ * @alias Ext.selection.Model#selectionchange
+ */
+ 'selectionchange',
+ /**
+ * @event beforeselect
+ * @alias Ext.selection.RowModel#beforeselect
+ */
+ 'beforeselect',
+ /**
+ * @event select
+ * @alias Ext.selection.RowModel#select
+ */
+ 'select',
+ /**
+ * @event beforedeselect
+ * @alias Ext.selection.RowModel#beforedeselect
+ */
+ 'beforedeselect',
+ /**
+ * @event deselect
+ * @alias Ext.selection.RowModel#deselect
+ */
+ 'deselect'
+ ]);
+ }
+
+ me.callParent(arguments);
+ },
+
+ onRender: function(){
+ var vScroll = this.verticalScroller,
+ hScroll = this.horizontalScroller;
+
+ if (vScroll) {
+ vScroll.ensureDimension();
+ }
+ if (hScroll) {
+ hScroll.ensureDimension();
+ }
+ this.callParent(arguments);
+ },
+
+ // state management
+ initStateEvents: function(){
+ var events = this.stateEvents;
+ // push on stateEvents if they don't exist
+ Ext.each(['columnresize', 'columnmove', 'columnhide', 'columnshow', 'sortchange'], function(event){
+ if (Ext.Array.indexOf(events, event)) {
+ events.push(event);
+ }
+ });
+ this.callParent();
+ },
+
+ /**
+ * Returns the horizontal scroller config.
+ */
+ initHorizontalScroller: function () {
+ var me = this,
+ ret = {
+ xtype: 'gridscroller',
+ dock: 'bottom',
+ section: me,
+ store: me.store
+ };
+
+ return ret;
+ },
+
+ /**
+ * Returns the vertical scroller config.
+ */
+ initVerticalScroller: function () {
+ var me = this,
+ ret = me.verticalScroller || {};
+
+ Ext.applyIf(ret, {
+ xtype: me.verticalScrollerType,
+ dock: me.verticalScrollDock,
+ store: me.store
+ });
+
+ return ret;
+ },
+
+ relayHeaderCtEvents: function (headerCt) {
+ this.relayEvents(headerCt, [
+ /**
+ * @event columnresize
+ * @alias Ext.grid.header.Container#columnresize
+ */
+ 'columnresize',
+ /**
+ * @event columnmove
+ * @alias Ext.grid.header.Container#columnmove
+ */
+ 'columnmove',
+ /**
+ * @event columnhide
+ * @alias Ext.grid.header.Container#columnhide
+ */
+ 'columnhide',
+ /**
+ * @event columnshow
+ * @alias Ext.grid.header.Container#columnshow
+ */
+ 'columnshow',
+ /**
+ * @event sortchange
+ * @alias Ext.grid.header.Container#sortchange
+ */
+ 'sortchange'
+ ]);
+ },
+
+ getState: function(){
+ var me = this,
+ state = me.callParent(),
+ sorter = me.store.sorters.first();
+
+ state.columns = (me.headerCt || me).getColumnsState();
+
+ if (sorter) {
+ state.sort = {
+ property: sorter.property,
+ direction: sorter.direction
+ };
+ }
+
+ return state;
+ },
+
+ applyState: function(state) {
+ var me = this,
+ sorter = state.sort,
+ store = me.store,
+ columns = state.columns;
+
+ delete state.columns;
+
+ // Ensure superclass has applied *its* state.
+ // AbstractComponent saves dimensions (and anchor/flex) plus collapsed state.
+ me.callParent(arguments);
+
+ if (columns) {
+ (me.headerCt || me).applyColumnsState(columns);
+ }
+
+ if (sorter) {
+ if (store.remoteSort) {
+ store.sorters.add(Ext.create('Ext.util.Sorter', {
+ property: sorter.property,
+ direction: sorter.direction
+ }));
+ }
+ else {
+ store.sort(sorter.property, sorter.direction);
+ }
+ }
+ },
+
+ /**
+ * Returns the store associated with this Panel.
+ * @return {Ext.data.Store} The store
+ */
+ getStore: function(){
+ return this.store;
+ },
+
+ /**
+ * Gets the view for this panel.
+ * @return {Ext.view.Table}
+ */
+ getView: function() {
+ var me = this,
+ sm;
+
+ if (!me.view) {
+ sm = me.getSelectionModel();
+ me.view = me.createComponent(Ext.apply({}, me.viewConfig, {
+ deferInitialRefresh: me.deferRowRender,
+ xtype: me.viewType,
+ store: me.store,
+ headerCt: me.headerCt,
+ selModel: sm,
+ features: me.features,
+ panel: me
+ }));
+ me.mon(me.view, {
+ uievent: me.processEvent,
+ scope: me
+ });
+ sm.view = me.view;
+ me.headerCt.view = me.view;
+ me.relayEvents(me.view, ['cellclick', 'celldblclick']);
+ }
+ return me.view;
+ },
+
+ /**
+ * @private
+ * @override
+ * autoScroll is never valid for all classes which extend TablePanel.
+ */
+ setAutoScroll: Ext.emptyFn,
+
+ // This method hijacks Ext.view.Table's el scroll method.
+ // This enables us to keep the virtualized scrollbars in sync
+ // with the view. It currently does NOT support animation.
+ elScroll: function(direction, distance, animate) {
+ var me = this,
+ scroller;
+
+ if (direction === "up" || direction === "left") {
+ distance = -distance;
+ }
+
+ if (direction === "down" || direction === "up") {
+ scroller = me.getVerticalScroller();
+
+ //if the grid does not currently need a vertical scroller don't try to update it (EXTJSIV-3891)
+ if (scroller) {
+ scroller.scrollByDeltaY(distance);
+ }
+ } else {
+ scroller = me.getHorizontalScroller();
+
+ //if the grid does not currently need a horizontal scroller don't try to update it (EXTJSIV-3891)
+ if (scroller) {
+ scroller.scrollByDeltaX(distance);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Processes UI events from the view. Propagates them to whatever internal Components need to process them.
+ * @param {String} type Event type, eg 'click'
+ * @param {Ext.view.Table} view TableView Component
+ * @param {HTMLElement} cell Cell HtmlElement the event took place within
+ * @param {Number} recordIndex Index of the associated Store Model (-1 if none)
+ * @param {Number} cellIndex Cell index within the row
+ * @param {Ext.EventObject} e Original event
+ */
+ processEvent: function(type, view, cell, recordIndex, cellIndex, e) {
+ var me = this,
+ header;
+
+ if (cellIndex !== -1) {
+ header = me.headerCt.getGridColumns()[cellIndex];
+ return header.processEvent.apply(header, arguments);
+ }
+ },
+
+ /**
+ * Requests a recalculation of scrollbars and puts them in if they are needed.
+ */
+ determineScrollbars: function() {
+ // Set a flag so that afterComponentLayout does not recurse back into here.
+ if (this.determineScrollbarsRunning) {
+ return;
+ }
+ this.determineScrollbarsRunning = true;
+ var me = this,
+ view = me.view,
+ box,
+ tableEl,
+ scrollWidth,
+ clientWidth,
+ scrollHeight,
+ clientHeight,
+ verticalScroller = me.verticalScroller,
+ horizontalScroller = me.horizontalScroller,
+ curScrollbars = (verticalScroller && verticalScroller.ownerCt === me ? 1 : 0) |
+ (horizontalScroller && horizontalScroller.ownerCt === me ? 2 : 0),
+ reqScrollbars = 0; // 1 = vertical, 2 = horizontal, 3 = both
+
+ // If we are not collapsed, and the view has been rendered AND filled, then we can determine scrollbars
+ if (!me.collapsed && view && view.viewReady) {
+
+ // Calculate maximum, *scrollbarless* space which the view has available.
+ // It will be the Fit Layout's calculated size, plus the widths of any currently shown scrollbars
+ box = view.el.getSize();
+
+ clientWidth = box.width + ((curScrollbars & 1) ? verticalScroller.width : 0);
+ clientHeight = box.height + ((curScrollbars & 2) ? horizontalScroller.height : 0);
+
+ // Calculate the width of the scrolling block
+ // There will never be a horizontal scrollbar if all columns are flexed.
+
+ scrollWidth = (me.headerCt.query('[flex]').length && !me.headerCt.layout.tooNarrow) ? 0 : me.headerCt.getFullWidth();
+
+ // Calculate the height of the scrolling block
+ if (verticalScroller && verticalScroller.el) {
+ scrollHeight = verticalScroller.getSizeCalculation().height;
+ } else {
+ tableEl = view.el.child('table', true);
+ scrollHeight = tableEl ? tableEl.offsetHeight : 0;
+ }
+
+ // View is too high.
+ // Definitely need a vertical scrollbar
+ if (scrollHeight > clientHeight) {
+ reqScrollbars = 1;
+
+ // But if scrollable block width goes into the zone required by the vertical scrollbar, we'll also need a horizontal
+ if (horizontalScroller && ((clientWidth - scrollWidth) < verticalScroller.width)) {
+ reqScrollbars = 3;
+ }
+ }
+
+ // View height fits. But we stil may need a horizontal scrollbar, and this might necessitate a vertical one.
+ else {
+ // View is too wide.
+ // Definitely need a horizontal scrollbar
+ if (scrollWidth > clientWidth) {
+ reqScrollbars = 2;
+
+ // But if scrollable block height goes into the zone required by the horizontal scrollbar, we'll also need a vertical
+ if (verticalScroller && ((clientHeight - scrollHeight) < horizontalScroller.height)) {
+ reqScrollbars = 3;
+ }
+ }
+ }
+
+ // If scrollbar requirements have changed, change 'em...
+ if (reqScrollbars !== curScrollbars) {
+
+ // Suspend component layout while we add/remove the docked scrollers
+ me.suspendLayout = true;
+ if (reqScrollbars & 1) {
+ me.showVerticalScroller();
+ } else {
+ me.hideVerticalScroller();
+ }
+ if (reqScrollbars & 2) {
+ me.showHorizontalScroller();
+ } else {
+ me.hideHorizontalScroller();
+ }
+ me.suspendLayout = false;
+
+ // Lay out the Component.
+ me.doComponentLayout();
+ // Lay out me.items
+ me.getLayout().layout();
+ }
+ }
+ delete me.determineScrollbarsRunning;
+ },
+
+ onViewResize: function() {
+ this.determineScrollbars();
+ },
+
+ afterComponentLayout: function() {
+ this.callParent(arguments);
+ this.determineScrollbars();
+ this.invalidateScroller();
+ },
+
+ onHeaderResize: function() {
+ if (!this.componentLayout.layoutBusy && this.view && this.view.rendered) {
+ this.determineScrollbars();
+ this.invalidateScroller();
+ }
+ },
+
+ afterCollapse: function() {
+ var me = this;
+ if (me.verticalScroller) {
+ me.verticalScroller.saveScrollPos();
+ }
+ if (me.horizontalScroller) {
+ me.horizontalScroller.saveScrollPos();
+ }
+ me.callParent(arguments);
+ },
+
+ afterExpand: function() {
+ var me = this;
+ me.callParent(arguments);
+ if (me.verticalScroller) {
+ me.verticalScroller.restoreScrollPos();
+ }
+ if (me.horizontalScroller) {
+ me.horizontalScroller.restoreScrollPos();
+ }
+ },
+
+ /**
+ * Hides the verticalScroller and removes the horizontalScrollerPresentCls.
+ */
+ hideHorizontalScroller: function() {
+ var me = this;
+
+ if (me.horizontalScroller && me.horizontalScroller.ownerCt === me) {
+ me.verticalScroller.setReservedSpace(0);
+ me.removeDocked(me.horizontalScroller, false);
+ me.removeCls(me.horizontalScrollerPresentCls);
+ me.fireEvent('scrollerhide', me.horizontalScroller, 'horizontal');
+ }
+
+ },
+
+ /**
+ * Shows the horizontalScroller and add the horizontalScrollerPresentCls.
+ */
+ showHorizontalScroller: function() {
+ var me = this;
+
+ if (me.verticalScroller) {
+ me.verticalScroller.setReservedSpace(Ext.getScrollbarSize().height - 1);
+ }
+ if (me.horizontalScroller && me.horizontalScroller.ownerCt !== me) {
+ me.addDocked(me.horizontalScroller);
+ me.addCls(me.horizontalScrollerPresentCls);
+ me.fireEvent('scrollershow', me.horizontalScroller, 'horizontal');
+ }
+ },
+
+ /**
+ * Hides the verticalScroller and removes the verticalScrollerPresentCls.
+ */
+ hideVerticalScroller: function() {
+ var me = this;
+
+ me.setHeaderReserveOffset(false);
+ if (me.verticalScroller && me.verticalScroller.ownerCt === me) {
+ me.removeDocked(me.verticalScroller, false);
+ me.removeCls(me.verticalScrollerPresentCls);
+ me.fireEvent('scrollerhide', me.verticalScroller, 'vertical');
+ }
+ },
+
+ /**
+ * Shows the verticalScroller and adds the verticalScrollerPresentCls.
+ */
+ showVerticalScroller: function() {
+ var me = this;
+
+ me.setHeaderReserveOffset(true);
+ if (me.verticalScroller && me.verticalScroller.ownerCt !== me) {
+ me.addDocked(me.verticalScroller);
+ me.addCls(me.verticalScrollerPresentCls);
+ me.fireEvent('scrollershow', me.verticalScroller, 'vertical');
+ }
+ },
+
+ setHeaderReserveOffset: function (reserveOffset) {
+ var headerCt = this.headerCt,
+ layout = headerCt.layout;
+
+ // only trigger a layout when reserveOffset is changing
+ if (layout && layout.reserveOffset !== reserveOffset) {
+ layout.reserveOffset = reserveOffset;
+ if (!this.suspendLayout) {
+ headerCt.doLayout();
+ }
+ }
+ },
+
+ /**
+ * Invalides scrollers that are present and forces a recalculation. (Not related to showing/hiding the scrollers)
+ */
+ invalidateScroller: function() {
+ var me = this,
+ vScroll = me.verticalScroller,
+ hScroll = me.horizontalScroller;
+
+ if (vScroll) {
+ vScroll.invalidate();
+ }
+ if (hScroll) {
+ hScroll.invalidate();
+ }
+ },
+
+ // refresh the view when a header moves
+ onHeaderMove: function(headerCt, header, fromIdx, toIdx) {
+ this.view.refresh();
+ },
+
+ // Section onHeaderHide is invoked after view.
+ onHeaderHide: function(headerCt, header) {
+ this.invalidateScroller();
+ },
+
+ onHeaderShow: function(headerCt, header) {
+ this.invalidateScroller();
+ },
+
+ getVerticalScroller: function() {
+ return this.getScrollerOwner().down('gridscroller[dock=' + this.verticalScrollDock + ']');
+ },
+
+ getHorizontalScroller: function() {
+ return this.getScrollerOwner().down('gridscroller[dock=bottom]');
+ },
+
+ onMouseWheel: function(e) {
+ var me = this,
+ vertScroller = me.getVerticalScroller(),
+ horizScroller = me.getHorizontalScroller(),
+ scrollDelta = -me.scrollDelta,
+ deltas = e.getWheelDeltas(),
+ deltaX = scrollDelta * deltas.x,
+ deltaY = scrollDelta * deltas.y,
+ vertScrollerEl, horizScrollerEl,
+ vertScrollerElDom, horizScrollerElDom,
+ horizontalCanScrollLeft, horizontalCanScrollRight,
+ verticalCanScrollDown, verticalCanScrollUp;
+
+ // calculate whether or not both scrollbars can scroll right/left and up/down
+ if (horizScroller) {
+ horizScrollerEl = horizScroller.scrollEl;
+ if (horizScrollerEl) {
+ horizScrollerElDom = horizScrollerEl.dom;
+ horizontalCanScrollRight = horizScrollerElDom.scrollLeft !== horizScrollerElDom.scrollWidth - horizScrollerElDom.clientWidth;
+ horizontalCanScrollLeft = horizScrollerElDom.scrollLeft !== 0;
+ }
+ }
+ if (vertScroller) {
+ vertScrollerEl = vertScroller.scrollEl;
+ if (vertScrollerEl) {
+ vertScrollerElDom = vertScrollerEl.dom;
+ verticalCanScrollDown = vertScrollerElDom.scrollTop !== vertScrollerElDom.scrollHeight - vertScrollerElDom.clientHeight;
+ verticalCanScrollUp = vertScrollerElDom.scrollTop !== 0;
+ }
+ }
+
+ if (horizScroller) {
+ if ((deltaX < 0 && horizontalCanScrollLeft) || (deltaX > 0 && horizontalCanScrollRight)) {
+ e.stopEvent();
+ horizScroller.scrollByDeltaX(deltaX);
+ }
+ }
+ if (vertScroller) {
+ if ((deltaY < 0 && verticalCanScrollUp) || (deltaY > 0 && verticalCanScrollDown)) {
+ e.stopEvent();
+ vertScroller.scrollByDeltaY(deltaY);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Fires the TablePanel's viewready event when the view declares that its internal DOM is ready
+ */
+ onViewReady: function() {
+ var me = this;
+ me.fireEvent('viewready', me);
+ if (me.deferRowRender) {
+ me.determineScrollbars();
+ me.invalidateScroller();
+ }
+ },
+
+ /**
+ * @private
+ * Determines and invalidates scrollers on view refresh
+ */
+ onViewRefresh: function() {
+ var me = this;
+
+ // Refresh *during* render must be ignored.
+ if (!me.rendering) {
+ this.determineScrollbars();
+ if (this.invalidateScrollerOnRefresh) {
+ this.invalidateScroller();
+ }
+ }
+ },
+
+ /**
+ * Sets the scrollTop of the TablePanel.
+ * @param {Number} top
+ */
+ setScrollTop: function(top) {
+ var me = this,
+ rootCmp = me.getScrollerOwner(),
+ verticalScroller = me.getVerticalScroller();
+
+ rootCmp.virtualScrollTop = top;
+ if (verticalScroller) {
+ verticalScroller.setScrollTop(top);
+ }
+ },
+
+ getScrollerOwner: function() {
+ var rootCmp = this;
+ if (!this.scrollerOwner) {
+ rootCmp = this.up('[scrollerOwner]');
+ }
+ return rootCmp;
+ },
+
+ /**
+ * Scrolls the TablePanel by deltaY
+ * @param {Number} deltaY
+ */
+ scrollByDeltaY: function(deltaY) {
+ var verticalScroller = this.getVerticalScroller();
+
+ if (verticalScroller) {
+ verticalScroller.scrollByDeltaY(deltaY);
+ }
+ },
+
+ /**
+ * Scrolls the TablePanel by deltaX
+ * @param {Number} deltaX
+ */
+ scrollByDeltaX: function(deltaX) {
+ var horizontalScroller = this.getHorizontalScroller();
+
+ if (horizontalScroller) {
+ horizontalScroller.scrollByDeltaX(deltaX);
+ }
+ },
+
+ /**
+ * Gets left hand side marker for header resizing.
+ * @private
+ */
+ getLhsMarker: function() {
+ var me = this;
+
+ if (!me.lhsMarker) {
+ me.lhsMarker = Ext.DomHelper.append(me.el, {
+ cls: Ext.baseCSSPrefix + 'grid-resize-marker'
+ }, true);
+ }
+ return me.lhsMarker;
+ },
+
+ /**
+ * Gets right hand side marker for header resizing.
+ * @private
+ */
+ getRhsMarker: function() {
+ var me = this;
+
+ if (!me.rhsMarker) {
+ me.rhsMarker = Ext.DomHelper.append(me.el, {
+ cls: Ext.baseCSSPrefix + 'grid-resize-marker'
+ }, true);
+ }
+ return me.rhsMarker;
+ },
+
+ /**
+ * Returns the selection model being used and creates it via the configuration if it has not been created already.
+ * @return {Ext.selection.Model} selModel
+ */
+ getSelectionModel: function(){
+ if (!this.selModel) {
+ this.selModel = {};
+ }
+
+ var mode = 'SINGLE',
+ type;
+ if (this.simpleSelect) {
+ mode = 'SIMPLE';
+ } else if (this.multiSelect) {
+ mode = 'MULTI';
+ }
+
+ Ext.applyIf(this.selModel, {
+ allowDeselect: this.allowDeselect,
+ mode: mode
+ });
+
+ if (!this.selModel.events) {
+ type = this.selModel.selType || this.selType;
+ this.selModel = Ext.create('selection.' + type, this.selModel);
+ }
+
+ if (!this.selModel.hasRelaySetup) {
+ this.relayEvents(this.selModel, [
+ 'selectionchange', 'beforeselect', 'beforedeselect', 'select', 'deselect'
+ ]);
+ this.selModel.hasRelaySetup = true;
+ }
+
+ // lock the selection model if user
+ // has disabled selection
+ if (this.disableSelection) {
+ this.selModel.locked = true;
+ }
+ return this.selModel;
+ },
+
+ onVerticalScroll: function(event, target) {
+ var owner = this.getScrollerOwner(),
+ items = owner.query('tableview'),
+ i = 0,
+ len = items.length;
+
+ for (; i < len; i++) {
+ items[i].el.dom.scrollTop = target.scrollTop;
+ }
+ },
+
+ onHorizontalScroll: function(event, target) {
+ var owner = this.getScrollerOwner(),
+ items = owner.query('tableview'),
+ center = items[1] || items[0];
+
+ center.el.dom.scrollLeft = target.scrollLeft;
+ this.headerCt.el.dom.scrollLeft = target.scrollLeft;
+ },
+
+ // template method meant to be overriden
+ onStoreLoad: Ext.emptyFn,
+
+ getEditorParent: function() {
+ return this.body;
+ },
+
+ bindStore: function(store) {
+ var me = this;
+ me.store = store;
+ me.getView().bindStore(store);
+ },
+
+ beforeDestroy: function(){
+ // may be some duplication here since the horizontal and vertical
+ // scroller may be part of the docked items, but we need to clean
+ // them up in case they aren't visible.
+ Ext.destroy(this.horizontalScroller, this.verticalScroller);
+ this.callParent();
+ },
+
+ /**
+ * Reconfigures the table with a new store/columns. Either the store or the columns can be ommitted if you don't wish
+ * to change them.
+ * @param {Ext.data.Store} store (Optional) The new store.
+ * @param {Object[]} columns (Optional) An array of column configs
+ */
+ reconfigure: function(store, columns) {
+ var me = this,
+ headerCt = me.headerCt;
+
+ if (me.lockable) {
+ me.reconfigureLockable(store, columns);
+ } else {
+ if (columns) {
+ headerCt.suspendLayout = true;
+ headerCt.removeAll();
+ headerCt.add(columns);
+ }
+ if (store) {
+ store = Ext.StoreManager.lookup(store);
+ me.bindStore(store);
+ } else {
+ me.getView().refresh();
+ }
+ if (columns) {
+ headerCt.suspendLayout = false;
+ me.forceComponentLayout();
+ }
+ }
+ me.fireEvent('reconfigure', me);
+ }
+});
+/**
+ * This class encapsulates the user interface for a tabular data set.
+ * It acts as a centralized manager for controlling the various interface
+ * elements of the view. This includes handling events, such as row and cell
+ * level based DOM events. It also reacts to events from the underlying {@link Ext.selection.Model}
+ * to provide visual feedback to the user.
+ *
+ * This class does not provide ways to manipulate the underlying data of the configured
+ * {@link Ext.data.Store}.
+ *
+ * This is the base class for both {@link Ext.grid.View} and {@link Ext.tree.View} and is not
+ * to be used directly.
+ */
+Ext.define('Ext.view.Table', {
+ extend: 'Ext.view.View',
+ alias: 'widget.tableview',
+ uses: [
+ 'Ext.view.TableChunker',
+ 'Ext.util.DelayedTask',
+ 'Ext.util.MixedCollection'
+ ],
+
+ baseCls: Ext.baseCSSPrefix + 'grid-view',
+
+ // row
+ itemSelector: '.' + Ext.baseCSSPrefix + 'grid-row',
+ // cell
+ cellSelector: '.' + Ext.baseCSSPrefix + 'grid-cell',
+
+ selectedItemCls: Ext.baseCSSPrefix + 'grid-row-selected',
+ selectedCellCls: Ext.baseCSSPrefix + 'grid-cell-selected',
+ focusedItemCls: Ext.baseCSSPrefix + 'grid-row-focused',
+ overItemCls: Ext.baseCSSPrefix + 'grid-row-over',
+ altRowCls: Ext.baseCSSPrefix + 'grid-row-alt',
+ rowClsRe: /(?:^|\s*)grid-row-(first|last|alt)(?:\s+|$)/g,
+ cellRe: new RegExp('x-grid-cell-([^\\s]+) ', ''),
+
+ // cfg docs inherited
+ trackOver: true,
+
+ /**
+ * Override this function to apply custom CSS classes to rows during rendering. This function should return the
+ * CSS class name (or empty string '' for none) that will be added to the row's wrapping div. To apply multiple
+ * class names, simply return them space-delimited within the string (e.g. 'my-class another-class').
+ * Example usage:
+ *
+ * viewConfig: {
+ * getRowClass: function(record, rowIndex, rowParams, store){
+ * return record.get("valid") ? "row-valid" : "row-error";
+ * }
+ * }
+ *
+ * @param {Ext.data.Model} record The record corresponding to the current row.
+ * @param {Number} index The row index.
+ * @param {Object} rowParams **DEPRECATED.** For row body use the
+ * {@link Ext.grid.feature.RowBody#getAdditionalData getAdditionalData} method of the rowbody feature.
+ * @param {Ext.data.Store} store The store this grid is bound to
+ * @return {String} a CSS class name to add to the row.
+ * @method
+ */
+ getRowClass: null,
+
+ initComponent: function() {
+ var me = this;
+
+ me.scrollState = {};
+ me.selModel.view = me;
+ me.headerCt.view = me;
+ me.initFeatures();
+ me.tpl = '<div></div>';
+ me.callParent();
+ me.mon(me.store, {
+ load: me.onStoreLoad,
+ scope: me
+ });
+
+ // this.addEvents(
+ // /**
+ // * @event rowfocus
+ // * @param {Ext.data.Model} record
+ // * @param {HTMLElement} row
+ // * @param {Number} rowIdx
+ // */
+ // 'rowfocus'
+ // );
+ },
+
+ // scroll to top of the grid when store loads
+ onStoreLoad: function(){
+ var me = this;
+
+ if (me.invalidateScrollerOnRefresh) {
+ if (Ext.isGecko) {
+ if (!me.scrollToTopTask) {
+ me.scrollToTopTask = Ext.create('Ext.util.DelayedTask', me.scrollToTop, me);
+ }
+ me.scrollToTopTask.delay(1);
+ } else {
+ me .scrollToTop();
+ }
+ }
+ },
+
+ // scroll the view to the top
+ scrollToTop: Ext.emptyFn,
+
+ /**
+ * Add a listener to the main view element. It will be destroyed with the view.
+ * @private
+ */
+ addElListener: function(eventName, fn, scope){
+ this.mon(this, eventName, fn, scope, {
+ element: 'el'
+ });
+ },
+
+ /**
+ * Get the columns used for generating a template via TableChunker.
+ * See {@link Ext.grid.header.Container#getGridColumns}.
+ * @private
+ */
+ getGridColumns: function() {
+ return this.headerCt.getGridColumns();
+ },
+
+ /**
+ * Get a leaf level header by index regardless of what the nesting
+ * structure is.
+ * @private
+ * @param {Number} index The index
+ */
+ getHeaderAtIndex: function(index) {
+ return this.headerCt.getHeaderAtIndex(index);
+ },
+
+ /**
+ * Get the cell (td) for a particular record and column.
+ * @param {Ext.data.Model} record
+ * @param {Ext.grid.column.Column} column
+ * @private
+ */
+ getCell: function(record, column) {
+ var row = this.getNode(record);
+ return Ext.fly(row).down(column.getCellSelector());
+ },
+
+ /**
+ * Get a reference to a feature
+ * @param {String} id The id of the feature
+ * @return {Ext.grid.feature.Feature} The feature. Undefined if not found
+ */
+ getFeature: function(id) {
+ var features = this.featuresMC;
+ if (features) {
+ return features.get(id);
+ }
+ },
+
+ /**
+ * Initializes each feature and bind it to this view.
+ * @private
+ */
+ initFeatures: function() {
+ var me = this,
+ i = 0,
+ features,
+ len;
+
+ me.features = me.features || [];
+ features = me.features;
+ len = features.length;
+
+ me.featuresMC = Ext.create('Ext.util.MixedCollection');
+ for (; i < len; i++) {
+ // ensure feature hasnt already been instantiated
+ if (!features[i].isFeature) {
+ features[i] = Ext.create('feature.' + features[i].ftype, features[i]);
+ }
+ // inject a reference to view
+ features[i].view = me;
+ me.featuresMC.add(features[i]);
+ }
+ },
+
+ /**
+ * Gives features an injection point to attach events to the markup that
+ * has been created for this view.
+ * @private
+ */
+ attachEventsForFeatures: function() {
+ var features = this.features,
+ ln = features.length,
+ i = 0;
+
+ for (; i < ln; i++) {
+ if (features[i].isFeature) {
+ features[i].attachEvents();
+ }
+ }
+ },
+
+ afterRender: function() {
+ var me = this;
+
+ me.callParent();
+ me.mon(me.el, {
+ scroll: me.fireBodyScroll,
+ scope: me
+ });
+ me.el.unselectable();
+ me.attachEventsForFeatures();
+ },
+
+ fireBodyScroll: function(e, t) {
+ this.fireEvent('bodyscroll', e, t);
+ },
+
+ // TODO: Refactor headerCt dependency here to colModel
+ /**
+ * Uses the headerCt to transform data from dataIndex keys in a record to
+ * headerId keys in each header and then run them through each feature to
+ * get additional data for variables they have injected into the view template.
+ * @private
+ */
+ prepareData: function(data, idx, record) {
+ var me = this,
+ orig = me.headerCt.prepareData(data, idx, record, me, me.ownerCt),
+ features = me.features,
+ ln = features.length,
+ i = 0,
+ node, feature;
+
+ for (; i < ln; i++) {
+ feature = features[i];
+ if (feature.isFeature) {
+ Ext.apply(orig, feature.getAdditionalData(data, idx, record, orig, me));
+ }
+ }
+
+ return orig;
+ },
+
+ // TODO: Refactor headerCt dependency here to colModel
+ collectData: function(records, startIndex) {
+ var preppedRecords = this.callParent(arguments),
+ headerCt = this.headerCt,
+ fullWidth = headerCt.getFullWidth(),
+ features = this.features,
+ ln = features.length,
+ o = {
+ rows: preppedRecords,
+ fullWidth: fullWidth
+ },
+ i = 0,
+ feature,
+ j = 0,
+ jln,
+ rowParams;
+
+ jln = preppedRecords.length;
+ // process row classes, rowParams has been deprecated and has been moved
+ // to the individual features that implement the behavior.
+ if (this.getRowClass) {
+ for (; j < jln; j++) {
+ rowParams = {};
+ preppedRecords[j]['rowCls'] = this.getRowClass(records[j], j, rowParams, this.store);
+ }
+ }
+ // currently only one feature may implement collectData. This is to modify
+ // what's returned to the view before its rendered
+ for (; i < ln; i++) {
+ feature = features[i];
+ if (feature.isFeature && feature.collectData && !feature.disabled) {
+ o = feature.collectData(records, preppedRecords, startIndex, fullWidth, o);
+ break;
+ }
+ }
+ return o;
+ },
+
+ // TODO: Refactor header resizing to column resizing
+ /**
+ * When a header is resized, setWidth on the individual columns resizer class,
+ * the top level table, save/restore scroll state, generate a new template and
+ * restore focus to the grid view's element so that keyboard navigation
+ * continues to work.
+ * @private
+ */
+ onHeaderResize: function(header, w, suppressFocus) {
+ var me = this,
+ el = me.el;
+
+ if (el) {
+ me.saveScrollState();
+ // Grab the col and set the width, css
+ // class is generated in TableChunker.
+ // Select composites because there may be several chunks.
+
+ // IE6 and IE7 bug.
+ // Setting the width of the first TD does not work - ends up with a 1 pixel discrepancy.
+ // We need to increment the passed with in this case.
+ if (Ext.isIE6 || Ext.isIE7) {
+ if (header.el.hasCls(Ext.baseCSSPrefix + 'column-header-first')) {
+ w += 1;
+ }
+ }
+ el.select('.' + Ext.baseCSSPrefix + 'grid-col-resizer-'+header.id).setWidth(w);
+ el.select('.' + Ext.baseCSSPrefix + 'grid-table-resizer').setWidth(me.headerCt.getFullWidth());
+ me.restoreScrollState();
+ if (!me.ignoreTemplate) {
+ me.setNewTemplate();
+ }
+ if (!suppressFocus) {
+ me.el.focus();
+ }
+ }
+ },
+
+ /**
+ * When a header is shown restore its oldWidth if it was previously hidden.
+ * @private
+ */
+ onHeaderShow: function(headerCt, header, suppressFocus) {
+ var me = this;
+ me.ignoreTemplate = true;
+ // restore headers that were dynamically hidden
+ if (header.oldWidth) {
+ me.onHeaderResize(header, header.oldWidth, suppressFocus);
+ delete header.oldWidth;
+ // flexed headers will have a calculated size set
+ // this additional check has to do with the fact that
+ // defaults: {width: 100} will fight with a flex value
+ } else if (header.width && !header.flex) {
+ me.onHeaderResize(header, header.width, suppressFocus);
+ }
+ delete me.ignoreTemplate;
+ me.setNewTemplate();
+ },
+
+ /**
+ * When the header hides treat it as a resize to 0.
+ * @private
+ */
+ onHeaderHide: function(headerCt, header, suppressFocus) {
+ this.onHeaderResize(header, 0, suppressFocus);
+ },
+
+ /**
+ * Set a new template based on the current columns displayed in the
+ * grid.
+ * @private
+ */
+ setNewTemplate: function() {
+ var me = this,
+ columns = me.headerCt.getColumnsForTpl(true);
+
+ me.tpl = me.getTableChunker().getTableTpl({
+ columns: columns,
+ features: me.features
+ });
+ },
+
+ /**
+ * Returns the configured chunker or default of Ext.view.TableChunker
+ */
+ getTableChunker: function() {
+ return this.chunker || Ext.view.TableChunker;
+ },
+
+ /**
+ * Adds a CSS Class to a specific row.
+ * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model
+ * representing this row
+ * @param {String} cls
+ */
+ addRowCls: function(rowInfo, cls) {
+ var row = this.getNode(rowInfo);
+ if (row) {
+ Ext.fly(row).addCls(cls);
+ }
+ },
+
+ /**
+ * Removes a CSS Class from a specific row.
+ * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model
+ * representing this row
+ * @param {String} cls
+ */
+ removeRowCls: function(rowInfo, cls) {
+ var row = this.getNode(rowInfo);
+ if (row) {
+ Ext.fly(row).removeCls(cls);
+ }
+ },
+
+ // GridSelectionModel invokes onRowSelect as selection changes
+ onRowSelect : function(rowIdx) {
+ this.addRowCls(rowIdx, this.selectedItemCls);
+ },
+
+ // GridSelectionModel invokes onRowDeselect as selection changes
+ onRowDeselect : function(rowIdx) {
+ var me = this;
+
+ me.removeRowCls(rowIdx, me.selectedItemCls);
+ me.removeRowCls(rowIdx, me.focusedItemCls);
+ },
+
+ onCellSelect: function(position) {
+ var cell = this.getCellByPosition(position);
+ if (cell) {
+ cell.addCls(this.selectedCellCls);
+ }
+ },
+
+ onCellDeselect: function(position) {
+ var cell = this.getCellByPosition(position);
+ if (cell) {
+ cell.removeCls(this.selectedCellCls);
+ }
+
+ },
+
+ onCellFocus: function(position) {
+ //var cell = this.getCellByPosition(position);
+ this.focusCell(position);
+ },
+
+ getCellByPosition: function(position) {
+ var row = position.row,
+ column = position.column,
+ store = this.store,
+ node = this.getNode(row),
+ header = this.headerCt.getHeaderAtIndex(column),
+ cellSelector,
+ cell = false;
+
+ if (header && node) {
+ cellSelector = header.getCellSelector();
+ cell = Ext.fly(node).down(cellSelector);
+ }
+ return cell;
+ },
+
+ // GridSelectionModel invokes onRowFocus to 'highlight'
+ // the last row focused
+ onRowFocus: function(rowIdx, highlight, supressFocus) {
+ var me = this,
+ row = me.getNode(rowIdx);
+
+ if (highlight) {
+ me.addRowCls(rowIdx, me.focusedItemCls);
+ if (!supressFocus) {
+ me.focusRow(rowIdx);
+ }
+ //this.el.dom.setAttribute('aria-activedescendant', row.id);
+ } else {
+ me.removeRowCls(rowIdx, me.focusedItemCls);
+ }
+ },
+
+ /**
+ * Focuses a particular row and brings it into view. Will fire the rowfocus event.
+ * @param {HTMLElement/String/Number/Ext.data.Model} rowIdx
+ * An HTMLElement template node, index of a template node, the id of a template node or the
+ * record associated with the node.
+ */
+ focusRow: function(rowIdx) {
+ var me = this,
+ row = me.getNode(rowIdx),
+ el = me.el,
+ adjustment = 0,
+ panel = me.ownerCt,
+ rowRegion,
+ elRegion,
+ record;
+
+ if (row && el) {
+ elRegion = el.getRegion();
+ rowRegion = Ext.fly(row).getRegion();
+ // row is above
+ if (rowRegion.top < elRegion.top) {
+ adjustment = rowRegion.top - elRegion.top;
+ // row is below
+ } else if (rowRegion.bottom > elRegion.bottom) {
+ adjustment = rowRegion.bottom - elRegion.bottom;
+ }
+ record = me.getRecord(row);
+ rowIdx = me.store.indexOf(record);
+
+ if (adjustment) {
+ // scroll the grid itself, so that all gridview's update.
+ panel.scrollByDeltaY(adjustment);
+ }
+ me.fireEvent('rowfocus', record, row, rowIdx);
+ }
+ },
+
+ focusCell: function(position) {
+ var me = this,
+ cell = me.getCellByPosition(position),
+ el = me.el,
+ adjustmentY = 0,
+ adjustmentX = 0,
+ elRegion = el.getRegion(),
+ panel = me.ownerCt,
+ cellRegion,
+ record;
+
+ if (cell) {
+ cellRegion = cell.getRegion();
+ // cell is above
+ if (cellRegion.top < elRegion.top) {
+ adjustmentY = cellRegion.top - elRegion.top;
+ // cell is below
+ } else if (cellRegion.bottom > elRegion.bottom) {
+ adjustmentY = cellRegion.bottom - elRegion.bottom;
+ }
+
+ // cell is left
+ if (cellRegion.left < elRegion.left) {
+ adjustmentX = cellRegion.left - elRegion.left;
+ // cell is right
+ } else if (cellRegion.right > elRegion.right) {
+ adjustmentX = cellRegion.right - elRegion.right;
+ }
+
+ if (adjustmentY) {
+ // scroll the grid itself, so that all gridview's update.
+ panel.scrollByDeltaY(adjustmentY);
+ }
+ if (adjustmentX) {
+ panel.scrollByDeltaX(adjustmentX);
+ }
+ el.focus();
+ me.fireEvent('cellfocus', record, cell, position);
+ }
+ },
+
+ /**
+ * Scrolls by delta. This affects this individual view ONLY and does not
+ * synchronize across views or scrollers.
+ * @param {Number} delta
+ * @param {String} dir (optional) Valid values are scrollTop and scrollLeft. Defaults to scrollTop.
+ * @private
+ */
+ scrollByDelta: function(delta, dir) {
+ dir = dir || 'scrollTop';
+ var elDom = this.el.dom;
+ elDom[dir] = (elDom[dir] += delta);
+ },
+
+ onUpdate: function(ds, index) {
+ this.callParent(arguments);
+ },
+
+ /**
+ * Saves the scrollState in a private variable. Must be used in conjunction with restoreScrollState
+ */
+ saveScrollState: function() {
+ if (this.rendered) {
+ var dom = this.el.dom,
+ state = this.scrollState;
+
+ state.left = dom.scrollLeft;
+ state.top = dom.scrollTop;
+ }
+ },
+
+ /**
+ * Restores the scrollState.
+ * Must be used in conjunction with saveScrollState
+ * @private
+ */
+ restoreScrollState: function() {
+ if (this.rendered) {
+ var dom = this.el.dom,
+ state = this.scrollState,
+ headerEl = this.headerCt.el.dom;
+
+ headerEl.scrollLeft = dom.scrollLeft = state.left;
+ dom.scrollTop = state.top;
+ }
+ },
+
+ /**
+ * Refreshes the grid view. Saves and restores the scroll state, generates a new template, stripes rows and
+ * invalidates the scrollers.
+ */
+ refresh: function() {
+ this.setNewTemplate();
+ this.callParent(arguments);
+ },
+
+ processItemEvent: function(record, row, rowIndex, e) {
+ var me = this,
+ cell = e.getTarget(me.cellSelector, row),
+ cellIndex = cell ? cell.cellIndex : -1,
+ map = me.statics().EventMap,
+ selModel = me.getSelectionModel(),
+ type = e.type,
+ result;
+
+ if (type == 'keydown' && !cell && selModel.getCurrentPosition) {
+ // CellModel, otherwise we can't tell which cell to invoke
+ cell = me.getCellByPosition(selModel.getCurrentPosition());
+ if (cell) {
+ cell = cell.dom;
+ cellIndex = cell.cellIndex;
+ }
+ }
+
+ result = me.fireEvent('uievent', type, me, cell, rowIndex, cellIndex, e);
+
+ if (result === false || me.callParent(arguments) === false) {
+ return false;
+ }
+
+ // Don't handle cellmouseenter and cellmouseleave events for now
+ if (type == 'mouseover' || type == 'mouseout') {
+ return true;
+ }
+
+ return !(
+ // We are adding cell and feature events
+ (me['onBeforeCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
+ (me.fireEvent('beforecell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false) ||
+ (me['onCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
+ (me.fireEvent('cell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false)
+ );
+ },
+
+ processSpecialEvent: function(e) {
+ var me = this,
+ map = me.statics().EventMap,
+ features = me.features,
+ ln = features.length,
+ type = e.type,
+ i, feature, prefix, featureTarget,
+ beforeArgs, args,
+ panel = me.ownerCt;
+
+ me.callParent(arguments);
+
+ if (type == 'mouseover' || type == 'mouseout') {
+ return;
+ }
+
+ for (i = 0; i < ln; i++) {
+ feature = features[i];
+ if (feature.hasFeatureEvent) {
+ featureTarget = e.getTarget(feature.eventSelector, me.getTargetEl());
+ if (featureTarget) {
+ prefix = feature.eventPrefix;
+ // allows features to implement getFireEventArgs to change the
+ // fireEvent signature
+ beforeArgs = feature.getFireEventArgs('before' + prefix + type, me, featureTarget, e);
+ args = feature.getFireEventArgs(prefix + type, me, featureTarget, e);
+
+ if (
+ // before view event
+ (me.fireEvent.apply(me, beforeArgs) === false) ||
+ // panel grid event
+ (panel.fireEvent.apply(panel, beforeArgs) === false) ||
+ // view event
+ (me.fireEvent.apply(me, args) === false) ||
+ // panel event
+ (panel.fireEvent.apply(panel, args) === false)
+ ) {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ },
+
+ onCellMouseDown: Ext.emptyFn,
+ onCellMouseUp: Ext.emptyFn,
+ onCellClick: Ext.emptyFn,
+ onCellDblClick: Ext.emptyFn,
+ onCellContextMenu: Ext.emptyFn,
+ onCellKeyDown: Ext.emptyFn,
+ onBeforeCellMouseDown: Ext.emptyFn,
+ onBeforeCellMouseUp: Ext.emptyFn,
+ onBeforeCellClick: Ext.emptyFn,
+ onBeforeCellDblClick: Ext.emptyFn,
+ onBeforeCellContextMenu: Ext.emptyFn,
+ onBeforeCellKeyDown: Ext.emptyFn,
+
+ /**
+ * Expands a particular header to fit the max content width.
+ * This will ONLY expand, not contract.
+ * @private
+ */
+ expandToFit: function(header) {
+ if (header) {
+ var maxWidth = this.getMaxContentWidth(header);
+ delete header.flex;
+ header.setWidth(maxWidth);
+ }
+ },
+
+ /**
+ * Returns the max contentWidth of the header's text and all cells
+ * in the grid under this header.
+ * @private
+ */
+ getMaxContentWidth: function(header) {
+ var cellSelector = header.getCellInnerSelector(),
+ cells = this.el.query(cellSelector),
+ i = 0,
+ ln = cells.length,
+ maxWidth = header.el.dom.scrollWidth,
+ scrollWidth;
+
+ for (; i < ln; i++) {
+ scrollWidth = cells[i].scrollWidth;
+ if (scrollWidth > maxWidth) {
+ maxWidth = scrollWidth;
+ }
+ }
+ return maxWidth;
+ },
+
+ getPositionByEvent: function(e) {
+ var me = this,
+ cellNode = e.getTarget(me.cellSelector),
+ rowNode = e.getTarget(me.itemSelector),
+ record = me.getRecord(rowNode),
+ header = me.getHeaderByCell(cellNode);
+
+ return me.getPosition(record, header);
+ },
+
+ getHeaderByCell: function(cell) {
+ if (cell) {
+ var m = cell.className.match(this.cellRe);
+ if (m && m[1]) {
+ return Ext.getCmp(m[1]);
+ }
+ }
+ return false;
+ },
+
+ /**
+ * @param {Object} position The current row and column: an object containing the following properties:
+ *
+ * - row - The row index
+ * - column - The column index
+ *
+ * @param {String} direction 'up', 'down', 'right' and 'left'
+ * @param {Ext.EventObject} e event
+ * @param {Boolean} preventWrap Set to true to prevent wrap around to the next or previous row.
+ * @param {Function} verifierFn A function to verify the validity of the calculated position.
+ * When using this function, you must return true to allow the newPosition to be returned.
+ * @param {Object} scope Scope to run the verifierFn in
+ * @returns {Object} newPosition An object containing the following properties:
+ *
+ * - row - The row index
+ * - column - The column index
+ *
+ * @private
+ */
+ walkCells: function(pos, direction, e, preventWrap, verifierFn, scope) {
+ var me = this,
+ row = pos.row,
+ column = pos.column,
+ rowCount = me.store.getCount(),
+ firstCol = me.getFirstVisibleColumnIndex(),
+ lastCol = me.getLastVisibleColumnIndex(),
+ newPos = {row: row, column: column},
+ activeHeader = me.headerCt.getHeaderAtIndex(column);
+
+ // no active header or its currently hidden
+ if (!activeHeader || activeHeader.hidden) {
+ return false;
+ }
+
+ e = e || {};
+ direction = direction.toLowerCase();
+ switch (direction) {
+ case 'right':
+ // has the potential to wrap if its last
+ if (column === lastCol) {
+ // if bottom row and last column, deny right
+ if (preventWrap || row === rowCount - 1) {
+ return false;
+ }
+ if (!e.ctrlKey) {
+ // otherwise wrap to nextRow and firstCol
+ newPos.row = row + 1;
+ newPos.column = firstCol;
+ }
+ // go right
+ } else {
+ if (!e.ctrlKey) {
+ newPos.column = column + me.getRightGap(activeHeader);
+ } else {
+ newPos.column = lastCol;
+ }
+ }
+ break;
+
+ case 'left':
+ // has the potential to wrap
+ if (column === firstCol) {
+ // if top row and first column, deny left
+ if (preventWrap || row === 0) {
+ return false;
+ }
+ if (!e.ctrlKey) {
+ // otherwise wrap to prevRow and lastCol
+ newPos.row = row - 1;
+ newPos.column = lastCol;
+ }
+ // go left
+ } else {
+ if (!e.ctrlKey) {
+ newPos.column = column + me.getLeftGap(activeHeader);
+ } else {
+ newPos.column = firstCol;
+ }
+ }
+ break;
+
+ case 'up':
+ // if top row, deny up
+ if (row === 0) {
+ return false;
+ // go up
+ } else {
+ if (!e.ctrlKey) {
+ newPos.row = row - 1;
+ } else {
+ newPos.row = 0;
+ }
+ }
+ break;
+
+ case 'down':
+ // if bottom row, deny down
+ if (row === rowCount - 1) {
+ return false;
+ // go down
+ } else {
+ if (!e.ctrlKey) {
+ newPos.row = row + 1;
+ } else {
+ newPos.row = rowCount - 1;
+ }
+ }
+ break;
+ }
+
+ if (verifierFn && verifierFn.call(scope || window, newPos) !== true) {
+ return false;
+ } else {
+ return newPos;
+ }
+ },
+ getFirstVisibleColumnIndex: function() {
+ var headerCt = this.getHeaderCt(),
+ allColumns = headerCt.getGridColumns(),
+ visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
+ firstHeader = visHeaders[0];
+
+ return headerCt.getHeaderIndex(firstHeader);
+ },
+
+ getLastVisibleColumnIndex: function() {
+ var headerCt = this.getHeaderCt(),
+ allColumns = headerCt.getGridColumns(),
+ visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
+ lastHeader = visHeaders[visHeaders.length - 1];
+
+ return headerCt.getHeaderIndex(lastHeader);
+ },
+
+ getHeaderCt: function() {
+ return this.headerCt;
+ },
+
+ getPosition: function(record, header) {
+ var me = this,
+ store = me.store,
+ gridCols = me.headerCt.getGridColumns();
+
+ return {
+ row: store.indexOf(record),
+ column: Ext.Array.indexOf(gridCols, header)
+ };
+ },
+
+ /**
+ * Determines the 'gap' between the closest adjacent header to the right
+ * that is not hidden.
+ * @private
+ */
+ getRightGap: function(activeHeader) {
+ var headerCt = this.getHeaderCt(),
+ headers = headerCt.getGridColumns(),
+ activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
+ i = activeHeaderIdx + 1,
+ nextIdx;
+
+ for (; i <= headers.length; i++) {
+ if (!headers[i].hidden) {
+ nextIdx = i;
+ break;
+ }
+ }
+
+ return nextIdx - activeHeaderIdx;
+ },
+
+ beforeDestroy: function() {
+ if (this.rendered) {
+ this.el.removeAllListeners();
+ }
+ this.callParent(arguments);
+ },
+
+ /**
+ * Determines the 'gap' between the closest adjacent header to the left
+ * that is not hidden.
+ * @private
+ */
+ getLeftGap: function(activeHeader) {
+ var headerCt = this.getHeaderCt(),
+ headers = headerCt.getGridColumns(),
+ activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
+ i = activeHeaderIdx - 1,
+ prevIdx;
+
+ for (; i >= 0; i--) {
+ if (!headers[i].hidden) {
+ prevIdx = i;
+ break;
+ }
+ }
+
+ return prevIdx - activeHeaderIdx;
+ }
+});
+/**
+ * @class Ext.grid.View
+ * @extends Ext.view.Table
+ *
+ * The grid View class provides extra {@link Ext.grid.Panel} specific functionality to the
+ * {@link Ext.view.Table}. In general, this class is not instanced directly, instead a viewConfig
+ * option is passed to the grid:
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * // other options
+ * viewConfig: {
+ * stripeRows: false
+ * }
+ * });
+ *
+ * ## Drag Drop
+ *
+ * Drag and drop functionality can be achieved in the grid by attaching a {@link Ext.grid.plugin.DragDrop} plugin
+ * when creating the view.
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * // other options
+ * viewConfig: {
+ * plugins: {
+ * ddGroup: 'people-group',
+ * ptype: 'gridviewdragdrop',
+ * enableDrop: false
+ * }
+ * }
+ * });
+ */
+Ext.define('Ext.grid.View', {
+ extend: 'Ext.view.Table',
+ alias: 'widget.gridview',
+
+ /**
+ * @cfg {Boolean} stripeRows <tt>true</tt> to stripe the rows. Default is <tt>true</tt>.
+ * <p>This causes the CSS class <tt><b>x-grid-row-alt</b></tt> to be added to alternate rows of
+ * the grid. A default CSS rule is provided which sets a background color, but you can override this
+ * with a rule which either overrides the <b>background-color</b> style using the '!important'
+ * modifier, or which uses a CSS selector of higher specificity.</p>
+ */
+ stripeRows: true,
+
+ invalidateScrollerOnRefresh: true,
+
+ /**
+ * Scroll the GridView to the top by scrolling the scroller.
+ * @private
+ */
+ scrollToTop : function(){
+ if (this.rendered) {
+ var section = this.ownerCt,
+ verticalScroller = section.verticalScroller;
+
+ if (verticalScroller) {
+ verticalScroller.scrollToTop();
+ }
+ }
+ },
+
+ // after adding a row stripe rows from then on
+ onAdd: function(ds, records, index) {
+ this.callParent(arguments);
+ this.doStripeRows(index);
+ },
+
+ // after removing a row stripe rows from then on
+ onRemove: function(ds, records, index) {
+ this.callParent(arguments);
+ this.doStripeRows(index);
+ },
+
+ onUpdate: function(ds, record, operation) {
+ var index = ds.indexOf(record);
+ this.callParent(arguments);
+ this.doStripeRows(index, index);
+ },
+
+ /**
+ * Stripe rows from a particular row index
+ * @param {Number} startRow
+ * @param {Number} endRow (Optional) argument specifying the last row to process. By default process up to the last row.
+ * @private
+ */
+ doStripeRows: function(startRow, endRow) {
+ // ensure stripeRows configuration is turned on
+ if (this.stripeRows) {
+ var rows = this.getNodes(startRow, endRow),
+ rowsLn = rows.length,
+ i = 0,
+ row;
+
+ for (; i < rowsLn; i++) {
+ row = rows[i];
+ // Remove prior applied row classes.
+ row.className = row.className.replace(this.rowClsRe, ' ');
+ startRow++;
+ // Every odd row will get an additional cls
+ if (startRow % 2 === 0) {
+ row.className += (' ' + this.altRowCls);
+ }
+ }
+ }
+ },
+
+ refresh: function(firstPass) {
+ this.callParent(arguments);
+ this.doStripeRows(0);
+ // TODO: Remove gridpanel dependency
+ var g = this.up('gridpanel');
+ if (g && this.invalidateScrollerOnRefresh) {
+ g.invalidateScroller();
+ }
+ }
+});
+
+/**
+ * @author Aaron Conran
+ * @docauthor Ed Spencer
+ *
+ * Grids are an excellent way of showing large amounts of tabular data on the client side. Essentially a supercharged
+ * `<table>`, GridPanel makes it easy to fetch, sort and filter large amounts of data.
+ *
+ * Grids are composed of two main pieces - a {@link Ext.data.Store Store} full of data and a set of columns to render.
+ *
+ * ## Basic GridPanel
+ *
+ * @example
+ * Ext.create('Ext.data.Store', {
+ * storeId:'simpsonsStore',
+ * fields:['name', 'email', 'phone'],
+ * data:{'items':[
+ * { 'name': 'Lisa', "email":"lisa@simpsons.com", "phone":"555-111-1224" },
+ * { 'name': 'Bart', "email":"bart@simpsons.com", "phone":"555-222-1234" },
+ * { 'name': 'Homer', "email":"home@simpsons.com", "phone":"555-222-1244" },
+ * { 'name': 'Marge', "email":"marge@simpsons.com", "phone":"555-222-1254" }
+ * ]},
+ * proxy: {
+ * type: 'memory',
+ * reader: {
+ * type: 'json',
+ * root: 'items'
+ * }
+ * }
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Simpsons',
+ * store: Ext.data.StoreManager.lookup('simpsonsStore'),
+ * columns: [
+ * { header: 'Name', dataIndex: 'name' },
+ * { header: 'Email', dataIndex: 'email', flex: 1 },
+ * { header: 'Phone', dataIndex: 'phone' }
+ * ],
+ * height: 200,
+ * width: 400,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * The code above produces a simple grid with three columns. We specified a Store which will load JSON data inline.
+ * In most apps we would be placing the grid inside another container and wouldn't need to use the
+ * {@link #height}, {@link #width} and {@link #renderTo} configurations but they are included here to make it easy to get
+ * up and running.
+ *
+ * The grid we created above will contain a header bar with a title ('Simpsons'), a row of column headers directly underneath
+ * and finally the grid rows under the headers.
+ *
+ * ## Configuring columns
+ *
+ * By default, each column is sortable and will toggle between ASC and DESC sorting when you click on its header. Each
+ * column header is also reorderable by default, and each gains a drop-down menu with options to hide and show columns.
+ * It's easy to configure each column - here we use the same example as above and just modify the columns config:
+ *
+ * columns: [
+ * {
+ * header: 'Name',
+ * dataIndex: 'name',
+ * sortable: false,
+ * hideable: false,
+ * flex: 1
+ * },
+ * {
+ * header: 'Email',
+ * dataIndex: 'email',
+ * hidden: true
+ * },
+ * {
+ * header: 'Phone',
+ * dataIndex: 'phone',
+ * width: 100
+ * }
+ * ]
+ *
+ * We turned off sorting and hiding on the 'Name' column so clicking its header now has no effect. We also made the Email
+ * column hidden by default (it can be shown again by using the menu on any other column). We also set the Phone column to
+ * a fixed with of 100px and flexed the Name column, which means it takes up all remaining width after the other columns
+ * have been accounted for. See the {@link Ext.grid.column.Column column docs} for more details.
+ *
+ * ## Renderers
+ *
+ * As well as customizing columns, it's easy to alter the rendering of individual cells using renderers. A renderer is
+ * tied to a particular column and is passed the value that would be rendered into each cell in that column. For example,
+ * we could define a renderer function for the email column to turn each email address into a mailto link:
+ *
+ * columns: [
+ * {
+ * header: 'Email',
+ * dataIndex: 'email',
+ * renderer: function(value) {
+ * return Ext.String.format('<a href="mailto:{0}">{1}</a>', value, value);
+ * }
+ * }
+ * ]
+ *
+ * See the {@link Ext.grid.column.Column column docs} for more information on renderers.
+ *
+ * ## Selection Models
+ *
+ * Sometimes all you want is to render data onto the screen for viewing, but usually it's necessary to interact with or
+ * update that data. Grids use a concept called a Selection Model, which is simply a mechanism for selecting some part of
+ * the data in the grid. The two main types of Selection Model are RowSelectionModel, where entire rows are selected, and
+ * CellSelectionModel, where individual cells are selected.
+ *
+ * Grids use a Row Selection Model by default, but this is easy to customise like so:
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * selType: 'cellmodel',
+ * store: ...
+ * });
+ *
+ * Specifying the `cellmodel` changes a couple of things. Firstly, clicking on a cell now
+ * selects just that cell (using a {@link Ext.selection.RowModel rowmodel} will select the entire row), and secondly the
+ * keyboard navigation will walk from cell to cell instead of row to row. Cell-based selection models are usually used in
+ * conjunction with editing.
+ *
+ * ## Editing
+ *
+ * Grid has built-in support for in-line editing. There are two chief editing modes - cell editing and row editing. Cell
+ * editing is easy to add to your existing column setup - here we'll just modify the example above to include an editor
+ * on both the name and the email columns:
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Simpsons',
+ * store: Ext.data.StoreManager.lookup('simpsonsStore'),
+ * columns: [
+ * { header: 'Name', dataIndex: 'name', field: 'textfield' },
+ * { header: 'Email', dataIndex: 'email', flex: 1,
+ * field: {
+ * xtype: 'textfield',
+ * allowBlank: false
+ * }
+ * },
+ * { header: 'Phone', dataIndex: 'phone' }
+ * ],
+ * selType: 'cellmodel',
+ * plugins: [
+ * Ext.create('Ext.grid.plugin.CellEditing', {
+ * clicksToEdit: 1
+ * })
+ * ],
+ * height: 200,
+ * width: 400,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * This requires a little explanation. We're passing in {@link #store store} and {@link #columns columns} as normal, but
+ * this time we've also specified a {@link Ext.grid.column.Column#field field} on two of our columns. For the Name column
+ * we just want a default textfield to edit the value, so we specify 'textfield'. For the Email column we customized the
+ * editor slightly by passing allowBlank: false, which will provide inline validation.
+ *
+ * To support cell editing, we also specified that the grid should use the 'cellmodel' {@link #selType}, and created an
+ * instance of the {@link Ext.grid.plugin.CellEditing CellEditing plugin}, which we configured to activate each editor after a
+ * single click.
+ *
+ * ## Row Editing
+ *
+ * The other type of editing is row-based editing, using the RowEditor component. This enables you to edit an entire row
+ * at a time, rather than editing cell by cell. Row Editing works in exactly the same way as cell editing, all we need to
+ * do is change the plugin type to {@link Ext.grid.plugin.RowEditing}, and set the selType to 'rowmodel':
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Simpsons',
+ * store: Ext.data.StoreManager.lookup('simpsonsStore'),
+ * columns: [
+ * { header: 'Name', dataIndex: 'name', field: 'textfield' },
+ * { header: 'Email', dataIndex: 'email', flex:1,
+ * field: {
+ * xtype: 'textfield',
+ * allowBlank: false
+ * }
+ * },
+ * { header: 'Phone', dataIndex: 'phone' }
+ * ],
+ * selType: 'rowmodel',
+ * plugins: [
+ * Ext.create('Ext.grid.plugin.RowEditing', {
+ * clicksToEdit: 1
+ * })
+ * ],
+ * height: 200,
+ * width: 400,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * Again we passed some configuration to our {@link Ext.grid.plugin.RowEditing} plugin, and now when we click each row a row
+ * editor will appear and enable us to edit each of the columns we have specified an editor for.
+ *
+ * ## Sorting & Filtering
+ *
+ * Every grid is attached to a {@link Ext.data.Store Store}, which provides multi-sort and filtering capabilities. It's
+ * easy to set up a grid to be sorted from the start:
+ *
+ * var myGrid = Ext.create('Ext.grid.Panel', {
+ * store: {
+ * fields: ['name', 'email', 'phone'],
+ * sorters: ['name', 'phone']
+ * },
+ * columns: [
+ * { text: 'Name', dataIndex: 'name' },
+ * { text: 'Email', dataIndex: 'email' }
+ * ]
+ * });
+ *
+ * Sorting at run time is easily accomplished by simply clicking each column header. If you need to perform sorting on
+ * more than one field at run time it's easy to do so by adding new sorters to the store:
+ *
+ * myGrid.store.sort([
+ * { property: 'name', direction: 'ASC' },
+ * { property: 'email', direction: 'DESC' }
+ * ]);
+ *
+ * See {@link Ext.data.Store} for examples of filtering.
+ *
+ * ## Grouping
+ *
+ * Grid supports the grouping of rows by any field. For example if we had a set of employee records, we might want to
+ * group by the department that each employee works in. Here's how we might set that up:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.Store', {
+ * storeId:'employeeStore',
+ * fields:['name', 'senority', 'department'],
+ * groupField: 'department',
+ * data: {'employees':[
+ * { "name": "Michael Scott", "senority": 7, "department": "Manangement" },
+ * { "name": "Dwight Schrute", "senority": 2, "department": "Sales" },
+ * { "name": "Jim Halpert", "senority": 3, "department": "Sales" },
+ * { "name": "Kevin Malone", "senority": 4, "department": "Accounting" },
+ * { "name": "Angela Martin", "senority": 5, "department": "Accounting" }
+ * ]},
+ * proxy: {
+ * type: 'memory',
+ * reader: {
+ * type: 'json',
+ * root: 'employees'
+ * }
+ * }
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Employees',
+ * store: Ext.data.StoreManager.lookup('employeeStore'),
+ * columns: [
+ * { header: 'Name', dataIndex: 'name' },
+ * { header: 'Senority', dataIndex: 'senority' }
+ * ],
+ * features: [{ftype:'grouping'}],
+ * width: 200,
+ * height: 275,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * ## Infinite Scrolling
+ *
+ * Grid supports infinite scrolling as an alternative to using a paging toolbar. Your users can scroll through thousands
+ * of records without the performance penalties of renderering all the records on screen at once. The grid should be bound
+ * to a store with a pageSize specified.
+ *
+ * var grid = Ext.create('Ext.grid.Panel', {
+ * // Use a PagingGridScroller (this is interchangeable with a PagingToolbar)
+ * verticalScrollerType: 'paginggridscroller',
+ * // do not reset the scrollbar when the view refreshs
+ * invalidateScrollerOnRefresh: false,
+ * // infinite scrolling does not support selection
+ * disableSelection: true,
+ * // ...
+ * });
+ *
+ * ## Paging
+ *
+ * Grid supports paging through large sets of data via a PagingToolbar or PagingGridScroller (see the Infinite Scrolling section above).
+ * To leverage paging via a toolbar or scroller, you need to set a pageSize configuration on the Store.
+ *
+ * @example
+ * var itemsPerPage = 2; // set the number of items you want per page
+ *
+ * var store = Ext.create('Ext.data.Store', {
+ * id:'simpsonsStore',
+ * autoLoad: false,
+ * fields:['name', 'email', 'phone'],
+ * pageSize: itemsPerPage, // items per page
+ * proxy: {
+ * type: 'ajax',
+ * url: 'pagingstore.js', // url that will load data with respect to start and limit params
+ * reader: {
+ * type: 'json',
+ * root: 'items',
+ * totalProperty: 'total'
+ * }
+ * }
+ * });
+ *
+ * // specify segment of data you want to load using params
+ * store.load({
+ * params:{
+ * start:0,
+ * limit: itemsPerPage
+ * }
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Simpsons',
+ * store: store,
+ * columns: [
+ * {header: 'Name', dataIndex: 'name'},
+ * {header: 'Email', dataIndex: 'email', flex:1},
+ * {header: 'Phone', dataIndex: 'phone'}
+ * ],
+ * width: 400,
+ * height: 125,
+ * dockedItems: [{
+ * xtype: 'pagingtoolbar',
+ * store: store, // same store GridPanel is using
+ * dock: 'bottom',
+ * displayInfo: true
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.grid.Panel', {
+ extend: 'Ext.panel.Table',
+ requires: ['Ext.grid.View'],
+ alias: ['widget.gridpanel', 'widget.grid'],
+ alternateClassName: ['Ext.list.ListView', 'Ext.ListView', 'Ext.grid.GridPanel'],
+ viewType: 'gridview',
+
+ lockable: false,
+
+ // Required for the Lockable Mixin. These are the configurations which will be copied to the
+ // normal and locked sub tablepanels
+ normalCfgCopy: ['invalidateScrollerOnRefresh', 'verticalScroller', 'verticalScrollDock', 'verticalScrollerType', 'scroll'],
+ lockedCfgCopy: ['invalidateScrollerOnRefresh'],
+
+ /**
+ * @cfg {Boolean} [columnLines=false] Adds column line styling
+ */
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.columnLines) {
+ me.setColumnLines(me.columnLines);
+ }
+
+ me.callParent();
+ },
+
+ setColumnLines: function(show) {
+ var me = this,
+ method = (show) ? 'addClsWithUI' : 'removeClsWithUI';
+
+ me[method]('with-col-lines');
+ }
+});
+
+// Currently has the following issues:
+// - Does not handle postEditValue
+// - Fields without editors need to sync with their values in Store
+// - starting to edit another record while already editing and dirty should probably prevent it
+// - aggregating validation messages
+// - tabIndex is not managed bc we leave elements in dom, and simply move via positioning
+// - layout issues when changing sizes/width while hidden (layout bug)
+
+/**
+ * @class Ext.grid.RowEditor
+ * @extends Ext.form.Panel
+ *
+ * Internal utility class used to provide row editing functionality. For developers, they should use
+ * the RowEditing plugin to use this functionality with a grid.
+ *
+ * @ignore
+ */
+Ext.define('Ext.grid.RowEditor', {
+ extend: 'Ext.form.Panel',
+ requires: [
+ 'Ext.tip.ToolTip',
+ 'Ext.util.HashMap',
+ 'Ext.util.KeyNav'
+ ],
+
+ saveBtnText : 'Update',
+ cancelBtnText: 'Cancel',
+ errorsText: 'Errors',
+ dirtyText: 'You need to commit or cancel your changes',
+
+ lastScrollLeft: 0,
+ lastScrollTop: 0,
+
+ border: false,
+
+ // Change the hideMode to offsets so that we get accurate measurements when
+ // the roweditor is hidden for laying out things like a TriggerField.
+ hideMode: 'offsets',
+
+ initComponent: function() {
+ var me = this,
+ form;
+
+ me.cls = Ext.baseCSSPrefix + 'grid-row-editor';
+
+ me.layout = {
+ type: 'hbox',
+ align: 'middle'
+ };
+
+ // Maintain field-to-column mapping
+ // It's easy to get a field from a column, but not vice versa
+ me.columns = Ext.create('Ext.util.HashMap');
+ me.columns.getKey = function(columnHeader) {
+ var f;
+ if (columnHeader.getEditor) {
+ f = columnHeader.getEditor();
+ if (f) {
+ return f.id;
+ }
+ }
+ return columnHeader.id;
+ };
+ me.mon(me.columns, {
+ add: me.onFieldAdd,
+ remove: me.onFieldRemove,
+ replace: me.onFieldReplace,
+ scope: me
+ });
+
+ me.callParent(arguments);
+
+ if (me.fields) {
+ me.setField(me.fields);
+ delete me.fields;
+ }
+
+ form = me.getForm();
+ form.trackResetOnLoad = true;
+ },
+
+ onFieldChange: function() {
+ var me = this,
+ form = me.getForm(),
+ valid = form.isValid();
+ if (me.errorSummary && me.isVisible()) {
+ me[valid ? 'hideToolTip' : 'showToolTip']();
+ }
+ if (me.floatingButtons) {
+ me.floatingButtons.child('#update').setDisabled(!valid);
+ }
+ me.isValid = valid;
+ },
+
+ afterRender: function() {
+ var me = this,
+ plugin = me.editingPlugin;
+
+ me.callParent(arguments);
+ me.mon(me.renderTo, 'scroll', me.onCtScroll, me, { buffer: 100 });
+
+ // Prevent from bubbling click events to the grid view
+ me.mon(me.el, {
+ click: Ext.emptyFn,
+ stopPropagation: true
+ });
+
+ me.el.swallowEvent([
+ 'keypress',
+ 'keydown'
+ ]);
+
+ me.keyNav = Ext.create('Ext.util.KeyNav', me.el, {
+ enter: plugin.completeEdit,
+ esc: plugin.onEscKey,
+ scope: plugin
+ });
+
+ me.mon(plugin.view, {
+ beforerefresh: me.onBeforeViewRefresh,
+ refresh: me.onViewRefresh,
+ scope: me
+ });
+ },
+
+ onBeforeViewRefresh: function(view) {
+ var me = this,
+ viewDom = view.el.dom;
+
+ if (me.el.dom.parentNode === viewDom) {
+ viewDom.removeChild(me.el.dom);
+ }
+ },
+
+ onViewRefresh: function(view) {
+ var me = this,
+ viewDom = view.el.dom,
+ context = me.context,
+ idx;
+
+ viewDom.appendChild(me.el.dom);
+
+ // Recover our row node after a view refresh
+ if (context && (idx = context.store.indexOf(context.record)) >= 0) {
+ context.row = view.getNode(idx);
+ me.reposition();
+ if (me.tooltip && me.tooltip.isVisible()) {
+ me.tooltip.setTarget(context.row);
+ }
+ } else {
+ me.editingPlugin.cancelEdit();
+ }
+ },
+
+ onCtScroll: function(e, target) {
+ var me = this,
+ scrollTop = target.scrollTop,
+ scrollLeft = target.scrollLeft;
+
+ if (scrollTop !== me.lastScrollTop) {
+ me.lastScrollTop = scrollTop;
+ if ((me.tooltip && me.tooltip.isVisible()) || me.hiddenTip) {
+ me.repositionTip();
+ }
+ }
+ if (scrollLeft !== me.lastScrollLeft) {
+ me.lastScrollLeft = scrollLeft;
+ me.reposition();
+ }
+ },
+
+ onColumnAdd: function(column) {
+ this.setField(column);
+ },
+
+ onColumnRemove: function(column) {
+ this.columns.remove(column);
+ },
+
+ onColumnResize: function(column, width) {
+ column.getEditor().setWidth(width - 2);
+ if (this.isVisible()) {
+ this.reposition();
+ }
+ },
+
+ onColumnHide: function(column) {
+ column.getEditor().hide();
+ if (this.isVisible()) {
+ this.reposition();
+ }
+ },
+
+ onColumnShow: function(column) {
+ var field = column.getEditor();
+ field.setWidth(column.getWidth() - 2).show();
+ if (this.isVisible()) {
+ this.reposition();
+ }
+ },
+
+ onColumnMove: function(column, fromIdx, toIdx) {
+ var field = column.getEditor();
+ if (this.items.indexOf(field) != toIdx) {
+ this.move(fromIdx, toIdx);
+ }
+ },
+
+ onFieldAdd: function(map, fieldId, column) {
+ var me = this,
+ colIdx = me.editingPlugin.grid.headerCt.getHeaderIndex(column),
+ field = column.getEditor({ xtype: 'displayfield' });
+
+ me.insert(colIdx, field);
+ },
+
+ onFieldRemove: function(map, fieldId, column) {
+ var me = this,
+ field = column.getEditor(),
+ fieldEl = field.el;
+ me.remove(field, false);
+ if (fieldEl) {
+ fieldEl.remove();
+ }
+ },
+
+ onFieldReplace: function(map, fieldId, column, oldColumn) {
+ var me = this;
+ me.onFieldRemove(map, fieldId, oldColumn);
+ },
+
+ clearFields: function() {
+ var me = this,
+ map = me.columns;
+ map.each(function(fieldId) {
+ map.removeAtKey(fieldId);
+ });
+ },
+
+ getFloatingButtons: function() {
+ var me = this,
+ cssPrefix = Ext.baseCSSPrefix,
+ btnsCss = cssPrefix + 'grid-row-editor-buttons',
+ plugin = me.editingPlugin,
+ btns;
+
+ if (!me.floatingButtons) {
+ btns = me.floatingButtons = Ext.create('Ext.Container', {
+ renderTpl: [
+ '<div class="{baseCls}-ml"></div>',
+ '<div class="{baseCls}-mr"></div>',
+ '<div class="{baseCls}-bl"></div>',
+ '<div class="{baseCls}-br"></div>',
+ '<div class="{baseCls}-bc"></div>'
+ ],
+
+ renderTo: me.el,
+ baseCls: btnsCss,
+ layout: {
+ type: 'hbox',
+ align: 'middle'
+ },
+ defaults: {
+ margins: '0 1 0 1'
+ },
+ items: [{
+ itemId: 'update',
+ flex: 1,
+ xtype: 'button',
+ handler: plugin.completeEdit,
+ scope: plugin,
+ text: me.saveBtnText,
+ disabled: !me.isValid
+ }, {
+ flex: 1,
+ xtype: 'button',
+ handler: plugin.cancelEdit,
+ scope: plugin,
+ text: me.cancelBtnText
+ }]
+ });
+
+ // Prevent from bubbling click events to the grid view
+ me.mon(btns.el, {
+ // BrowserBug: Opera 11.01
+ // causes the view to scroll when a button is focused from mousedown
+ mousedown: Ext.emptyFn,
+ click: Ext.emptyFn,
+ stopEvent: true
+ });
+ }
+ return me.floatingButtons;
+ },
+
+ reposition: function(animateConfig) {
+ var me = this,
+ context = me.context,
+ row = context && Ext.get(context.row),
+ btns = me.getFloatingButtons(),
+ btnEl = btns.el,
+ grid = me.editingPlugin.grid,
+ viewEl = grid.view.el,
+ scroller = grid.verticalScroller,
+
+ // always get data from ColumnModel as its what drives
+ // the GridView's sizing
+ mainBodyWidth = grid.headerCt.getFullWidth(),
+ scrollerWidth = grid.getWidth(),
+
+ // use the minimum as the columns may not fill up the entire grid
+ // width
+ width = Math.min(mainBodyWidth, scrollerWidth),
+ scrollLeft = grid.view.el.dom.scrollLeft,
+ btnWidth = btns.getWidth(),
+ left = (width - btnWidth) / 2 + scrollLeft,
+ y, rowH, newHeight,
+
+ invalidateScroller = function() {
+ if (scroller) {
+ scroller.invalidate();
+ btnEl.scrollIntoView(viewEl, false);
+ }
+ if (animateConfig && animateConfig.callback) {
+ animateConfig.callback.call(animateConfig.scope || me);
+ }
+ };
+
+ // need to set both top/left
+ if (row && Ext.isElement(row.dom)) {
+ // Bring our row into view if necessary, so a row editor that's already
+ // visible and animated to the row will appear smooth
+ row.scrollIntoView(viewEl, false);
+
+ // Get the y position of the row relative to its top-most static parent.
+ // offsetTop will be relative to the table, and is incorrect
+ // when mixed with certain grid features (e.g., grouping).
+ y = row.getXY()[1] - 5;
+ rowH = row.getHeight();
+ newHeight = rowH + 10;
+
+ // IE doesn't set the height quite right.
+ // This isn't a border-box issue, it even happens
+ // in IE8 and IE7 quirks.
+ // TODO: Test in IE9!
+ if (Ext.isIE) {
+ newHeight += 2;
+ }
+
+ // Set editor height to match the row height
+ if (me.getHeight() != newHeight) {
+ me.setHeight(newHeight);
+ me.el.setLeft(0);
+ }
+
+ if (animateConfig) {
+ var animObj = {
+ to: {
+ y: y
+ },
+ duration: animateConfig.duration || 125,
+ listeners: {
+ afteranimate: function() {
+ invalidateScroller();
+ y = row.getXY()[1] - 5;
+ me.el.setY(y);
+ }
+ }
+ };
+ me.animate(animObj);
+ } else {
+ me.el.setY(y);
+ invalidateScroller();
+ }
+ }
+ if (me.getWidth() != mainBodyWidth) {
+ me.setWidth(mainBodyWidth);
+ }
+ btnEl.setLeft(left);
+ },
+
+ getEditor: function(fieldInfo) {
+ var me = this;
+
+ if (Ext.isNumber(fieldInfo)) {
+ // Query only form fields. This just future-proofs us in case we add
+ // other components to RowEditor later on. Don't want to mess with
+ // indices.
+ return me.query('>[isFormField]')[fieldInfo];
+ } else if (fieldInfo instanceof Ext.grid.column.Column) {
+ return fieldInfo.getEditor();
+ }
+ },
+
+ removeField: function(field) {
+ var me = this;
+
+ // Incase we pass a column instead, which is fine
+ field = me.getEditor(field);
+ me.mun(field, 'validitychange', me.onValidityChange, me);
+
+ // Remove field/column from our mapping, which will fire the event to
+ // remove the field from our container
+ me.columns.removeKey(field.id);
+ },
+
+ setField: function(column) {
+ var me = this,
+ field;
+
+ if (Ext.isArray(column)) {
+ Ext.Array.forEach(column, me.setField, me);
+ return;
+ }
+
+ // Get a default display field if necessary
+ field = column.getEditor(null, {
+ xtype: 'displayfield',
+ // Default display fields will not return values. This is done because
+ // the display field will pick up column renderers from the grid.
+ getModelData: function() {
+ return null;
+ }
+ });
+ field.margins = '0 0 0 2';
+ field.setWidth(column.getDesiredWidth() - 2);
+ me.mon(field, 'change', me.onFieldChange, me);
+
+ // Maintain mapping of fields-to-columns
+ // This will fire events that maintain our container items
+ me.columns.add(field.id, column);
+ if (column.hidden) {
+ me.onColumnHide(column);
+ }
+ if (me.isVisible() && me.context) {
+ me.renderColumnData(field, me.context.record);
+ }
+ },
+
+ loadRecord: function(record) {
+ var me = this,
+ form = me.getForm();
+ form.loadRecord(record);
+ if (form.isValid()) {
+ me.hideToolTip();
+ } else {
+ me.showToolTip();
+ }
+
+ // render display fields so they honor the column renderer/template
+ Ext.Array.forEach(me.query('>displayfield'), function(field) {
+ me.renderColumnData(field, record);
+ }, me);
+ },
+
+ renderColumnData: function(field, record) {
+ var me = this,
+ grid = me.editingPlugin.grid,
+ headerCt = grid.headerCt,
+ view = grid.view,
+ store = view.store,
+ column = me.columns.get(field.id),
+ value = record.get(column.dataIndex);
+
+ // honor our column's renderer (TemplateHeader sets renderer for us!)
+ if (column.renderer) {
+ var metaData = { tdCls: '', style: '' },
+ rowIdx = store.indexOf(record),
+ colIdx = headerCt.getHeaderIndex(column);
+
+ value = column.renderer.call(
+ column.scope || headerCt.ownerCt,
+ value,
+ metaData,
+ record,
+ rowIdx,
+ colIdx,
+ store,
+ view
+ );
+ }
+
+ field.setRawValue(value);
+ field.resetOriginalValue();
+ },
+
+ beforeEdit: function() {
+ var me = this;
+
+ if (me.isVisible() && !me.autoCancel && me.isDirty()) {
+ me.showToolTip();
+ return false;
+ }
+ },
+
+ /**
+ * Start editing the specified grid at the specified position.
+ * @param {Ext.data.Model} record The Store data record which backs the row to be edited.
+ * @param {Ext.data.Model} columnHeader The Column object defining the column to be edited.
+ */
+ startEdit: function(record, columnHeader) {
+ var me = this,
+ grid = me.editingPlugin.grid,
+ view = grid.getView(),
+ store = grid.store,
+ context = me.context = Ext.apply(me.editingPlugin.context, {
+ view: grid.getView(),
+ store: store
+ });
+
+ // make sure our row is selected before editing
+ context.grid.getSelectionModel().select(record);
+
+ // Reload the record data
+ me.loadRecord(record);
+
+ if (!me.isVisible()) {
+ me.show();
+ me.focusContextCell();
+ } else {
+ me.reposition({
+ callback: this.focusContextCell
+ });
+ }
+ },
+
+ // Focus the cell on start edit based upon the current context
+ focusContextCell: function() {
+ var field = this.getEditor(this.context.colIdx);
+ if (field && field.focus) {
+ field.focus();
+ }
+ },
+
+ cancelEdit: function() {
+ var me = this,
+ form = me.getForm();
+
+ me.hide();
+ form.clearInvalid();
+ form.reset();
+ },
+
+ completeEdit: function() {
+ var me = this,
+ form = me.getForm();
+
+ if (!form.isValid()) {
+ return;
+ }
+
+ form.updateRecord(me.context.record);
+ me.hide();
+ return true;
+ },
+
+ onShow: function() {
+ var me = this;
+ me.callParent(arguments);
+ me.reposition();
+ },
+
+ onHide: function() {
+ var me = this;
+ me.callParent(arguments);
+ me.hideToolTip();
+ me.invalidateScroller();
+ if (me.context) {
+ me.context.view.focus();
+ me.context = null;
+ }
+ },
+
+ isDirty: function() {
+ var me = this,
+ form = me.getForm();
+ return form.isDirty();
+ },
+
+ getToolTip: function() {
+ var me = this,
+ tip;
+
+ if (!me.tooltip) {
+ tip = me.tooltip = Ext.createWidget('tooltip', {
+ cls: Ext.baseCSSPrefix + 'grid-row-editor-errors',
+ title: me.errorsText,
+ autoHide: false,
+ closable: true,
+ closeAction: 'disable',
+ anchor: 'left'
+ });
+ }
+ return me.tooltip;
+ },
+
+ hideToolTip: function() {
+ var me = this,
+ tip = me.getToolTip();
+ if (tip.rendered) {
+ tip.disable();
+ }
+ me.hiddenTip = false;
+ },
+
+ showToolTip: function() {
+ var me = this,
+ tip = me.getToolTip(),
+ context = me.context,
+ row = Ext.get(context.row),
+ viewEl = context.grid.view.el;
+
+ tip.setTarget(row);
+ tip.showAt([-10000, -10000]);
+ tip.body.update(me.getErrors());
+ tip.mouseOffset = [viewEl.getWidth() - row.getWidth() + me.lastScrollLeft + 15, 0];
+ me.repositionTip();
+ tip.doLayout();
+ tip.enable();
+ },
+
+ repositionTip: function() {
+ var me = this,
+ tip = me.getToolTip(),
+ context = me.context,
+ row = Ext.get(context.row),
+ viewEl = context.grid.view.el,
+ viewHeight = viewEl.getHeight(),
+ viewTop = me.lastScrollTop,
+ viewBottom = viewTop + viewHeight,
+ rowHeight = row.getHeight(),
+ rowTop = row.dom.offsetTop,
+ rowBottom = rowTop + rowHeight;
+
+ if (rowBottom > viewTop && rowTop < viewBottom) {
+ tip.show();
+ me.hiddenTip = false;
+ } else {
+ tip.hide();
+ me.hiddenTip = true;
+ }
+ },
+
+ getErrors: function() {
+ var me = this,
+ dirtyText = !me.autoCancel && me.isDirty() ? me.dirtyText + '<br />' : '',
+ errors = [];
+
+ Ext.Array.forEach(me.query('>[isFormField]'), function(field) {
+ errors = errors.concat(
+ Ext.Array.map(field.getErrors(), function(e) {
+ return '<li>' + e + '</li>';
+ })
+ );
+ }, me);
+
+ return dirtyText + '<ul>' + errors.join('') + '</ul>';
+ },
+
+ invalidateScroller: function() {
+ var me = this,
+ context = me.context,
+ scroller = context.grid.verticalScroller;
+
+ if (scroller) {
+ scroller.invalidate();
+ }
+ }
+});
+/**
+ * @class Ext.grid.header.Container
+ * @extends Ext.container.Container
+ *
+ * Container which holds headers and is docked at the top or bottom of a TablePanel.
+ * The HeaderContainer drives resizing/moving/hiding of columns within the TableView.
+ * As headers are hidden, moved or resized the headercontainer is responsible for
+ * triggering changes within the view.
+ */
+Ext.define('Ext.grid.header.Container', {
+ extend: 'Ext.container.Container',
+ uses: [
+ 'Ext.grid.ColumnLayout',
+ 'Ext.grid.column.Column',
+ 'Ext.menu.Menu',
+ 'Ext.menu.CheckItem',
+ 'Ext.menu.Separator',
+ 'Ext.grid.plugin.HeaderResizer',
+ 'Ext.grid.plugin.HeaderReorderer'
+ ],
+ border: true,
+
+ alias: 'widget.headercontainer',
+
+ baseCls: Ext.baseCSSPrefix + 'grid-header-ct',
+ dock: 'top',
+
+ /**
+ * @cfg {Number} weight
+ * HeaderContainer overrides the default weight of 0 for all docked items to 100.
+ * This is so that it has more priority over things like toolbars.
+ */
+ weight: 100,
+ defaultType: 'gridcolumn',
+ /**
+ * @cfg {Number} defaultWidth
+ * Width of the header if no width or flex is specified. Defaults to 100.
+ */
+ defaultWidth: 100,
+
+
+ sortAscText: 'Sort Ascending',
+ sortDescText: 'Sort Descending',
+ sortClearText: 'Clear Sort',
+ columnsText: 'Columns',
+
+ lastHeaderCls: Ext.baseCSSPrefix + 'column-header-last',
+ firstHeaderCls: Ext.baseCSSPrefix + 'column-header-first',
+ headerOpenCls: Ext.baseCSSPrefix + 'column-header-open',
+
+ // private; will probably be removed by 4.0
+ triStateSort: false,
+
+ ddLock: false,
+
+ dragging: false,
+
+ /**
+ * <code>true</code> if this HeaderContainer is in fact a group header which contains sub headers.
+ * @type Boolean
+ * @property isGroupHeader
+ */
+
+ /**
+ * @cfg {Boolean} sortable
+ * Provides the default sortable state for all Headers within this HeaderContainer.
+ * Also turns on or off the menus in the HeaderContainer. Note that the menu is
+ * shared across every header and therefore turning it off will remove the menu
+ * items for every header.
+ */
+ sortable: true,
+
+ initComponent: function() {
+ var me = this;
+
+ me.headerCounter = 0;
+ me.plugins = me.plugins || [];
+
+ // TODO: Pass in configurations to turn on/off dynamic
+ // resizing and disable resizing all together
+
+ // Only set up a Resizer and Reorderer for the topmost HeaderContainer.
+ // Nested Group Headers are themselves HeaderContainers
+ if (!me.isHeader) {
+ me.resizer = Ext.create('Ext.grid.plugin.HeaderResizer');
+ me.reorderer = Ext.create('Ext.grid.plugin.HeaderReorderer');
+ if (!me.enableColumnResize) {
+ me.resizer.disable();
+ }
+ if (!me.enableColumnMove) {
+ me.reorderer.disable();
+ }
+ me.plugins.push(me.reorderer, me.resizer);
+ }
+
+ // Base headers do not need a box layout
+ if (me.isHeader && !me.items) {
+ me.layout = 'auto';
+ }
+ // HeaderContainer and Group header needs a gridcolumn layout.
+ else {
+ me.layout = {
+ type: 'gridcolumn',
+ availableSpaceOffset: me.availableSpaceOffset,
+ align: 'stretchmax',
+ resetStretch: true
+ };
+ }
+ me.defaults = me.defaults || {};
+ Ext.applyIf(me.defaults, {
+ width: me.defaultWidth,
+ triStateSort: me.triStateSort,
+ sortable: me.sortable
+ });
+ me.callParent();
+ me.addEvents(
+ /**
+ * @event columnresize
+ * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
+ * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
+ * @param {Number} width
+ */
+ 'columnresize',
+
+ /**
+ * @event headerclick
+ * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
+ * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
+ * @param {Ext.EventObject} e
+ * @param {HTMLElement} t
+ */
+ 'headerclick',
+
+ /**
+ * @event headertriggerclick
+ * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
+ * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
+ * @param {Ext.EventObject} e
+ * @param {HTMLElement} t
+ */
+ 'headertriggerclick',
+
+ /**
+ * @event columnmove
+ * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
+ * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
+ * @param {Number} fromIdx
+ * @param {Number} toIdx
+ */
+ 'columnmove',
+ /**
+ * @event columnhide
+ * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
+ * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
+ */
+ 'columnhide',
+ /**
+ * @event columnshow
+ * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
+ * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
+ */
+ 'columnshow',
+ /**
+ * @event sortchange
+ * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
+ * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
+ * @param {String} direction
+ */
+ 'sortchange',
+ /**
+ * @event menucreate
+ * Fired immediately after the column header menu is created.
+ * @param {Ext.grid.header.Container} ct This instance
+ * @param {Ext.menu.Menu} menu The Menu that was created
+ */
+ 'menucreate'
+ );
+ },
+
+ onDestroy: function() {
+ Ext.destroy(this.resizer, this.reorderer);
+ this.callParent();
+ },
+
+ applyDefaults: function(config){
+ /*
+ * Ensure header.Container defaults don't get applied to a RowNumberer
+ * if an xtype is supplied. This isn't an ideal solution however it's
+ * much more likely that a RowNumberer with no options will be created,
+ * wanting to use the defaults specified on the class as opposed to
+ * those setup on the Container.
+ */
+ if (config && !config.isComponent && config.xtype == 'rownumberer') {
+ return config;
+ }
+ return this.callParent([config]);
+ },
+
+ applyColumnsState: function(columns) {
+ if (!columns || !columns.length) {
+ return;
+ }
+
+ var me = this,
+ i = 0,
+ index,
+ col;
+
+ Ext.each(columns, function (columnState) {
+ col = me.down('gridcolumn[headerId=' + columnState.id + ']');
+ if (col) {
+ index = me.items.indexOf(col);
+ if (i !== index) {
+ me.moveHeader(index, i);
+ }
+
+ if (col.applyColumnState) {
+ col.applyColumnState(columnState);
+ }
+ ++i;
+ }
+ });
+ },
+
+ getColumnsState: function () {
+ var me = this,
+ columns = [],
+ state;
+
+ me.items.each(function (col) {
+ state = col.getColumnState && col.getColumnState();
+ if (state) {
+ columns.push(state);
+ }
+ });
+
+ return columns;
+ },
+
+ // Invalidate column cache on add
+ // We cannot refresh the View on every add because this method is called
+ // when the HeaderDropZone moves Headers around, that will also refresh the view
+ onAdd: function(c) {
+ var me = this;
+ if (!c.headerId) {
+ c.headerId = c.initialConfig.id || ('h' + (++me.headerCounter));
+ }
+ me.callParent(arguments);
+ me.purgeCache();
+ },
+
+ // Invalidate column cache on remove
+ // We cannot refresh the View on every remove because this method is called
+ // when the HeaderDropZone moves Headers around, that will also refresh the view
+ onRemove: function(c) {
+ var me = this;
+ me.callParent(arguments);
+ me.purgeCache();
+ },
+
+ afterRender: function() {
+ this.callParent();
+ var store = this.up('[store]').store,
+ sorters = store.sorters,
+ first = sorters.first(),
+ hd;
+
+ if (first) {
+ hd = this.down('gridcolumn[dataIndex=' + first.property +']');
+ if (hd) {
+ hd.setSortState(first.direction, false, true);
+ }
+ }
+ },
+
+ afterLayout: function() {
+ if (!this.isHeader) {
+ var me = this,
+ topHeaders = me.query('>gridcolumn:not([hidden])'),
+ viewEl,
+ firstHeaderEl,
+ lastHeaderEl;
+
+ me.callParent(arguments);
+
+ if (topHeaders.length) {
+ firstHeaderEl = topHeaders[0].el;
+ if (firstHeaderEl !== me.pastFirstHeaderEl) {
+ if (me.pastFirstHeaderEl) {
+ me.pastFirstHeaderEl.removeCls(me.firstHeaderCls);
+ }
+ firstHeaderEl.addCls(me.firstHeaderCls);
+ me.pastFirstHeaderEl = firstHeaderEl;
+ }
+
+ lastHeaderEl = topHeaders[topHeaders.length - 1].el;
+ if (lastHeaderEl !== me.pastLastHeaderEl) {
+ if (me.pastLastHeaderEl) {
+ me.pastLastHeaderEl.removeCls(me.lastHeaderCls);
+ }
+ lastHeaderEl.addCls(me.lastHeaderCls);
+ me.pastLastHeaderEl = lastHeaderEl;
+ }
+ }
+ }
+
+ },
+
+ onHeaderShow: function(header, preventLayout) {
+ // Pass up to the GridSection
+ var me = this,
+ gridSection = me.ownerCt,
+ menu = me.getMenu(),
+ topItems, topItemsVisible,
+ colCheckItem,
+ itemToEnable,
+ len, i;
+
+ if (menu) {
+
+ colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
+ if (colCheckItem) {
+ colCheckItem.setChecked(true, true);
+ }
+
+ // There's more than one header visible, and we've disabled some checked items... re-enable them
+ topItems = menu.query('#columnItem>menucheckitem[checked]');
+ topItemsVisible = topItems.length;
+ if ((me.getVisibleGridColumns().length > 1) && me.disabledMenuItems && me.disabledMenuItems.length) {
+ if (topItemsVisible == 1) {
+ Ext.Array.remove(me.disabledMenuItems, topItems[0]);
+ }
+ for (i = 0, len = me.disabledMenuItems.length; i < len; i++) {
+ itemToEnable = me.disabledMenuItems[i];
+ if (!itemToEnable.isDestroyed) {
+ itemToEnable[itemToEnable.menu ? 'enableCheckChange' : 'enable']();
+ }
+ }
+ if (topItemsVisible == 1) {
+ me.disabledMenuItems = topItems;
+ } else {
+ me.disabledMenuItems = [];
+ }
+ }
+ }
+
+ // Only update the grid UI when we are notified about base level Header shows;
+ // Group header shows just cause a layout of the HeaderContainer
+ if (!header.isGroupHeader) {
+ if (me.view) {
+ me.view.onHeaderShow(me, header, true);
+ }
+ if (gridSection) {
+ gridSection.onHeaderShow(me, header);
+ }
+ }
+ me.fireEvent('columnshow', me, header);
+
+ // The header's own hide suppresses cascading layouts, so lay the headers out now
+ if (preventLayout !== true) {
+ me.doLayout();
+ }
+ },
+
+ doComponentLayout: function(){
+ var me = this;
+ if (me.view && me.view.saveScrollState) {
+ me.view.saveScrollState();
+ }
+ me.callParent(arguments);
+ if (me.view && me.view.restoreScrollState) {
+ me.view.restoreScrollState();
+ }
+ },
+
+ onHeaderHide: function(header, suppressLayout) {
+ // Pass up to the GridSection
+ var me = this,
+ gridSection = me.ownerCt,
+ menu = me.getMenu(),
+ colCheckItem;
+
+ if (menu) {
+
+ // If the header was hidden programmatically, sync the Menu state
+ colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']');
+ if (colCheckItem) {
+ colCheckItem.setChecked(false, true);
+ }
+ me.setDisabledItems();
+ }
+
+ // Only update the UI when we are notified about base level Header hides;
+ if (!header.isGroupHeader) {
+ if (me.view) {
+ me.view.onHeaderHide(me, header, true);
+ }
+ if (gridSection) {
+ gridSection.onHeaderHide(me, header);
+ }
+
+ // The header's own hide suppresses cascading layouts, so lay the headers out now
+ if (!suppressLayout) {
+ me.doLayout();
+ }
+ }
+ me.fireEvent('columnhide', me, header);
+ },
+
+ setDisabledItems: function(){
+ var me = this,
+ menu = me.getMenu(),
+ i = 0,
+ len,
+ itemsToDisable,
+ itemToDisable;
+
+ // Find what to disable. If only one top level item remaining checked, we have to disable stuff.
+ itemsToDisable = menu.query('#columnItem>menucheckitem[checked]');
+ if ((itemsToDisable.length === 1)) {
+ if (!me.disabledMenuItems) {
+ me.disabledMenuItems = [];
+ }
+
+ // If down to only one column visible, also disable any descendant checkitems
+ if ((me.getVisibleGridColumns().length === 1) && itemsToDisable[0].menu) {
+ itemsToDisable = itemsToDisable.concat(itemsToDisable[0].menu.query('menucheckitem[checked]'));
+ }
+
+ len = itemsToDisable.length;
+ // Disable any further unchecking at any level.
+ for (i = 0; i < len; i++) {
+ itemToDisable = itemsToDisable[i];
+ if (!Ext.Array.contains(me.disabledMenuItems, itemToDisable)) {
+
+ // If we only want to disable check change: it might be a disabled item, so enable it prior to
+ // setting its correct disablement level.
+ itemToDisable.disabled = false;
+ itemToDisable[itemToDisable.menu ? 'disableCheckChange' : 'disable']();
+ me.disabledMenuItems.push(itemToDisable);
+ }
+ }
+ }
+ },
+
+ /**
+ * Temporarily lock the headerCt. This makes it so that clicking on headers
+ * don't trigger actions like sorting or opening of the header menu. This is
+ * done because extraneous events may be fired on the headers after interacting
+ * with a drag drop operation.
+ * @private
+ */
+ tempLock: function() {
+ this.ddLock = true;
+ Ext.Function.defer(function() {
+ this.ddLock = false;
+ }, 200, this);
+ },
+
+ onHeaderResize: function(header, w, suppressFocus) {
+ this.tempLock();
+ if (this.view && this.view.rendered) {
+ this.view.onHeaderResize(header, w, suppressFocus);
+ }
+ },
+
+ onHeaderClick: function(header, e, t) {
+ this.fireEvent("headerclick", this, header, e, t);
+ },
+
+ onHeaderTriggerClick: function(header, e, t) {
+ // generate and cache menu, provide ability to cancel/etc
+ if (this.fireEvent("headertriggerclick", this, header, e, t) !== false) {
+ this.showMenuBy(t, header);
+ }
+ },
+
+ showMenuBy: function(t, header) {
+ var menu = this.getMenu(),
+ ascItem = menu.down('#ascItem'),
+ descItem = menu.down('#descItem'),
+ sortableMth;
+
+ menu.activeHeader = menu.ownerCt = header;
+ menu.setFloatParent(header);
+ // TODO: remove coupling to Header's titleContainer el
+ header.titleContainer.addCls(this.headerOpenCls);
+
+ // enable or disable asc & desc menu items based on header being sortable
+ sortableMth = header.sortable ? 'enable' : 'disable';
+ if (ascItem) {
+ ascItem[sortableMth]();
+ }
+ if (descItem) {
+ descItem[sortableMth]();
+ }
+ menu.showBy(t);
+ },
+
+ // remove the trigger open class when the menu is hidden
+ onMenuDeactivate: function() {
+ var menu = this.getMenu();
+ // TODO: remove coupling to Header's titleContainer el
+ menu.activeHeader.titleContainer.removeCls(this.headerOpenCls);
+ },
+
+ moveHeader: function(fromIdx, toIdx) {
+
+ // An automatically expiring lock
+ this.tempLock();
+ this.onHeaderMoved(this.move(fromIdx, toIdx), fromIdx, toIdx);
+ },
+
+ purgeCache: function() {
+ var me = this;
+ // Delete column cache - column order has changed.
+ delete me.gridDataColumns;
+ delete me.hideableColumns;
+
+ // Menu changes when columns are moved. It will be recreated.
+ if (me.menu) {
+ me.menu.destroy();
+ delete me.menu;
+ }
+ },
+
+ onHeaderMoved: function(header, fromIdx, toIdx) {
+ var me = this,
+ gridSection = me.ownerCt;
+
+ if (gridSection && gridSection.onHeaderMove) {
+ gridSection.onHeaderMove(me, header, fromIdx, toIdx);
+ }
+ me.fireEvent("columnmove", me, header, fromIdx, toIdx);
+ },
+
+ /**
+ * Gets the menu (and will create it if it doesn't already exist)
+ * @private
+ */
+ getMenu: function() {
+ var me = this;
+
+ if (!me.menu) {
+ me.menu = Ext.create('Ext.menu.Menu', {
+ hideOnParentHide: false, // Persists when owning ColumnHeader is hidden
+ items: me.getMenuItems(),
+ listeners: {
+ deactivate: me.onMenuDeactivate,
+ scope: me
+ }
+ });
+ me.setDisabledItems();
+ me.fireEvent('menucreate', me, me.menu);
+ }
+ return me.menu;
+ },
+
+ /**
+ * Returns an array of menu items to be placed into the shared menu
+ * across all headers in this header container.
+ * @returns {Array} menuItems
+ */
+ getMenuItems: function() {
+ var me = this,
+ menuItems = [],
+ hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null;
+
+ if (me.sortable) {
+ menuItems = [{
+ itemId: 'ascItem',
+ text: me.sortAscText,
+ cls: Ext.baseCSSPrefix + 'hmenu-sort-asc',
+ handler: me.onSortAscClick,
+ scope: me
+ },{
+ itemId: 'descItem',
+ text: me.sortDescText,
+ cls: Ext.baseCSSPrefix + 'hmenu-sort-desc',
+ handler: me.onSortDescClick,
+ scope: me
+ }];
+ }
+ if (hideableColumns && hideableColumns.length) {
+ menuItems.push('-', {
+ itemId: 'columnItem',
+ text: me.columnsText,
+ cls: Ext.baseCSSPrefix + 'cols-icon',
+ menu: hideableColumns
+ });
+ }
+ return menuItems;
+ },
+
+ // sort asc when clicking on item in menu
+ onSortAscClick: function() {
+ var menu = this.getMenu(),
+ activeHeader = menu.activeHeader;
+
+ activeHeader.setSortState('ASC');
+ },
+
+ // sort desc when clicking on item in menu
+ onSortDescClick: function() {
+ var menu = this.getMenu(),
+ activeHeader = menu.activeHeader;
+
+ activeHeader.setSortState('DESC');
+ },
+
+ /**
+ * Returns an array of menu CheckItems corresponding to all immediate children of the passed Container which have been configured as hideable.
+ */
+ getColumnMenu: function(headerContainer) {
+ var menuItems = [],
+ i = 0,
+ item,
+ items = headerContainer.query('>gridcolumn[hideable]'),
+ itemsLn = items.length,
+ menuItem;
+
+ for (; i < itemsLn; i++) {
+ item = items[i];
+ menuItem = Ext.create('Ext.menu.CheckItem', {
+ text: item.text,
+ checked: !item.hidden,
+ hideOnClick: false,
+ headerId: item.id,
+ menu: item.isGroupHeader ? this.getColumnMenu(item) : undefined,
+ checkHandler: this.onColumnCheckChange,
+ scope: this
+ });
+ if (itemsLn === 1) {
+ menuItem.disabled = true;
+ }
+ menuItems.push(menuItem);
+
+ // If the header is ever destroyed - for instance by dragging out the last remaining sub header,
+ // then the associated menu item must also be destroyed.
+ item.on({
+ destroy: Ext.Function.bind(menuItem.destroy, menuItem)
+ });
+ }
+ return menuItems;
+ },
+
+ onColumnCheckChange: function(checkItem, checked) {
+ var header = Ext.getCmp(checkItem.headerId);
+ header[checked ? 'show' : 'hide']();
+ },
+
+ /**
+ * Get the columns used for generating a template via TableChunker.
+ * Returns an array of all columns and their
+ * - dataIndex
+ * - align
+ * - width
+ * - id
+ * - columnId - used to create an identifying CSS class
+ * - cls The tdCls configuration from the Column object
+ * @private
+ */
+ getColumnsForTpl: function(flushCache) {
+ var cols = [],
+ headers = this.getGridColumns(flushCache),
+ headersLn = headers.length,
+ i = 0,
+ header,
+ width;
+
+ for (; i < headersLn; i++) {
+ header = headers[i];
+
+ if (header.hidden || header.up('headercontainer[hidden=true]')) {
+ width = 0;
+ } else {
+ width = header.getDesiredWidth();
+ // IE6 and IE7 bug.
+ // Setting the width of the first TD does not work - ends up with a 1 pixel discrepancy.
+ // We need to increment the passed with in this case.
+ if ((i === 0) && (Ext.isIE6 || Ext.isIE7)) {
+ width += 1;
+ }
+ }
+ cols.push({
+ dataIndex: header.dataIndex,
+ align: header.align,
+ width: width,
+ id: header.id,
+ cls: header.tdCls,
+ columnId: header.getItemId()
+ });
+ }
+ return cols;
+ },
+
+ /**
+ * Returns the number of <b>grid columns</b> descended from this HeaderContainer.
+ * Group Columns are HeaderContainers. All grid columns are returned, including hidden ones.
+ */
+ getColumnCount: function() {
+ return this.getGridColumns().length;
+ },
+
+ /**
+ * Gets the full width of all columns that are visible.
+ */
+ getFullWidth: function(flushCache) {
+ var fullWidth = 0,
+ headers = this.getVisibleGridColumns(flushCache),
+ headersLn = headers.length,
+ i = 0;
+
+ for (; i < headersLn; i++) {
+ if (!isNaN(headers[i].width)) {
+ // use headers getDesiredWidth if its there
+ if (headers[i].getDesiredWidth) {
+ fullWidth += headers[i].getDesiredWidth();
+ // if injected a diff cmp use getWidth
+ } else {
+ fullWidth += headers[i].getWidth();
+ }
+ }
+ }
+ return fullWidth;
+ },
+
+ // invoked internally by a header when not using triStateSorting
+ clearOtherSortStates: function(activeHeader) {
+ var headers = this.getGridColumns(),
+ headersLn = headers.length,
+ i = 0,
+ oldSortState;
+
+ for (; i < headersLn; i++) {
+ if (headers[i] !== activeHeader) {
+ oldSortState = headers[i].sortState;
+ // unset the sortstate and dont recurse
+ headers[i].setSortState(null, true);
+ //if (!silent && oldSortState !== null) {
+ // this.fireEvent('sortchange', this, headers[i], null);
+ //}
+ }
+ }
+ },
+
+ /**
+ * Returns an array of the <b>visible</b> columns in the grid. This goes down to the lowest column header
+ * level, and does not return <i>grouped</i> headers which contain sub headers.
+ * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache.
+ * @returns {Array}
+ */
+ getVisibleGridColumns: function(refreshCache) {
+ return Ext.ComponentQuery.query(':not([hidden])', this.getGridColumns(refreshCache));
+ },
+
+ /**
+ * Returns an array of all columns which map to Store fields. This goes down to the lowest column header
+ * level, and does not return <i>grouped</i> headers which contain sub headers.
+ * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache.
+ * @returns {Array}
+ */
+ getGridColumns: function(refreshCache) {
+ var me = this,
+ result = refreshCache ? null : me.gridDataColumns;
+
+ // Not already got the column cache, so collect the base columns
+ if (!result) {
+ me.gridDataColumns = result = [];
+ me.cascade(function(c) {
+ if ((c !== me) && !c.isGroupHeader) {
+ result.push(c);
+ }
+ });
+ }
+
+ return result;
+ },
+
+ /**
+ * @private
+ * For use by column headers in determining whether there are any hideable columns when deciding whether or not
+ * the header menu should be disabled.
+ */
+ getHideableColumns: function(refreshCache) {
+ var me = this,
+ result = refreshCache ? null : me.hideableColumns;
+
+ if (!result) {
+ result = me.hideableColumns = me.query('[hideable]');
+ }
+ return result;
+ },
+
+ /**
+ * Get the index of a leaf level header regardless of what the nesting
+ * structure is.
+ */
+ getHeaderIndex: function(header) {
+ var columns = this.getGridColumns();
+ return Ext.Array.indexOf(columns, header);
+ },
+
+ /**
+ * Get a leaf level header by index regardless of what the nesting
+ * structure is.
+ */
+ getHeaderAtIndex: function(index) {
+ var columns = this.getGridColumns();
+ return columns[index];
+ },
+
+ /**
+ * Maps the record data to base it on the header id's.
+ * This correlates to the markup/template generated by
+ * TableChunker.
+ */
+ prepareData: function(data, rowIdx, record, view, panel) {
+ var obj = {},
+ headers = this.gridDataColumns || this.getGridColumns(),
+ headersLn = headers.length,
+ colIdx = 0,
+ header,
+ headerId,
+ renderer,
+ value,
+ metaData,
+ store = panel.store;
+
+ for (; colIdx < headersLn; colIdx++) {
+ metaData = {
+ tdCls: '',
+ style: ''
+ };
+ header = headers[colIdx];
+ headerId = header.id;
+ renderer = header.renderer;
+ value = data[header.dataIndex];
+
+ // When specifying a renderer as a string, it always resolves
+ // to Ext.util.Format
+ if (typeof renderer === "string") {
+ header.renderer = renderer = Ext.util.Format[renderer];
+ }
+
+ if (typeof renderer === "function") {
+ value = renderer.call(
+ header.scope || this.ownerCt,
+ value,
+ // metadata per cell passing an obj by reference so that
+ // it can be manipulated inside the renderer
+ metaData,
+ record,
+ rowIdx,
+ colIdx,
+ store,
+ view
+ );
+ }
+
+
+ obj[headerId+'-modified'] = record.isModified(header.dataIndex) ? Ext.baseCSSPrefix + 'grid-dirty-cell' : '';
+ obj[headerId+'-tdCls'] = metaData.tdCls;
+ obj[headerId+'-tdAttr'] = metaData.tdAttr;
+ obj[headerId+'-style'] = metaData.style;
+ if (value === undefined || value === null || value === '') {
+ value = ' ';
+ }
+ obj[headerId] = value;
+ }
+ return obj;
+ },
+
+ expandToFit: function(header) {
+ if (this.view) {
+ this.view.expandToFit(header);
+ }
+ }
+});
+
+/**
+ * This class specifies the definition for a column inside a {@link Ext.grid.Panel}. It encompasses
+ * both the grid header configuration as well as displaying data within the grid itself. If the
+ * {@link #columns} configuration is specified, this column will become a column group and can
+ * contain other columns inside. In general, this class will not be created directly, rather
+ * an array of column configurations will be passed to the grid:
+ *
+ * @example
+ * Ext.create('Ext.data.Store', {
+ * storeId:'employeeStore',
+ * fields:['firstname', 'lastname', 'senority', 'dep', 'hired'],
+ * data:[
+ * {firstname:"Michael", lastname:"Scott", senority:7, dep:"Manangement", hired:"01/10/2004"},
+ * {firstname:"Dwight", lastname:"Schrute", senority:2, dep:"Sales", hired:"04/01/2004"},
+ * {firstname:"Jim", lastname:"Halpert", senority:3, dep:"Sales", hired:"02/22/2006"},
+ * {firstname:"Kevin", lastname:"Malone", senority:4, dep:"Accounting", hired:"06/10/2007"},
+ * {firstname:"Angela", lastname:"Martin", senority:5, dep:"Accounting", hired:"10/21/2008"}
+ * ]
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Column Demo',
+ * store: Ext.data.StoreManager.lookup('employeeStore'),
+ * columns: [
+ * {text: 'First Name', dataIndex:'firstname'},
+ * {text: 'Last Name', dataIndex:'lastname'},
+ * {text: 'Hired Month', dataIndex:'hired', xtype:'datecolumn', format:'M'},
+ * {text: 'Department (Yrs)', xtype:'templatecolumn', tpl:'{dep} ({senority})'}
+ * ],
+ * width: 400,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * # Convenience Subclasses
+ *
+ * There are several column subclasses that provide default rendering for various data types
+ *
+ * - {@link Ext.grid.column.Action}: Renders icons that can respond to click events inline
+ * - {@link Ext.grid.column.Boolean}: Renders for boolean values
+ * - {@link Ext.grid.column.Date}: Renders for date values
+ * - {@link Ext.grid.column.Number}: Renders for numeric values
+ * - {@link Ext.grid.column.Template}: Renders a value using an {@link Ext.XTemplate} using the record data
+ *
+ * # Setting Sizes
+ *
+ * The columns are laid out by a {@link Ext.layout.container.HBox} layout, so a column can either
+ * be given an explicit width value or a flex configuration. If no width is specified the grid will
+ * automatically the size the column to 100px. For column groups, the size is calculated by measuring
+ * the width of the child columns, so a width option should not be specified in that case.
+ *
+ * # Header Options
+ *
+ * - {@link #text}: Sets the header text for the column
+ * - {@link #sortable}: Specifies whether the column can be sorted by clicking the header or using the column menu
+ * - {@link #hideable}: Specifies whether the column can be hidden using the column menu
+ * - {@link #menuDisabled}: Disables the column header menu
+ * - {@link #draggable}: Specifies whether the column header can be reordered by dragging
+ * - {@link #groupable}: Specifies whether the grid can be grouped by the column dataIndex. See also {@link Ext.grid.feature.Grouping}
+ *
+ * # Data Options
+ *
+ * - {@link #dataIndex}: The dataIndex is the field in the underlying {@link Ext.data.Store} to use as the value for the column.
+ * - {@link #renderer}: Allows the underlying store value to be transformed before being displayed in the grid
+ */
+Ext.define('Ext.grid.column.Column', {
+ extend: 'Ext.grid.header.Container',
+ alias: 'widget.gridcolumn',
+ requires: ['Ext.util.KeyNav'],
+ alternateClassName: 'Ext.grid.Column',
+
+ baseCls: Ext.baseCSSPrefix + 'column-header ' + Ext.baseCSSPrefix + 'unselectable',
+
+ // Not the standard, automatically applied overCls because we must filter out overs of child headers.
+ hoverCls: Ext.baseCSSPrefix + 'column-header-over',
+
+ handleWidth: 5,
+
+ sortState: null,
+
+ possibleSortStates: ['ASC', 'DESC'],
+
+ renderTpl:
+ '<div id="{id}-titleContainer" class="' + Ext.baseCSSPrefix + 'column-header-inner">' +
+ '<span id="{id}-textEl" class="' + Ext.baseCSSPrefix + 'column-header-text">' +
+ '{text}' +
+ '</span>' +
+ '<tpl if="!values.menuDisabled">'+
+ '<div id="{id}-triggerEl" class="' + Ext.baseCSSPrefix + 'column-header-trigger"></div>'+
+ '</tpl>' +
+ '</div>',
+
+ /**
+ * @cfg {Object[]} columns
+ * An optional array of sub-column definitions. This column becomes a group, and houses the columns defined in the
+ * `columns` config.
+ *
+ * Group columns may not be sortable. But they may be hideable and moveable. And you may move headers into and out
+ * of a group. Note that if all sub columns are dragged out of a group, the group is destroyed.
+ */
+
+ /**
+ * @cfg {String} dataIndex
+ * The name of the field in the grid's {@link Ext.data.Store}'s {@link Ext.data.Model} definition from
+ * which to draw the column's value. **Required.**
+ */
+ dataIndex: null,
+
+ /**
+ * @cfg {String} text
+ * The header text to be used as innerHTML (html tags are accepted) to display in the Grid.
+ * **Note**: to have a clickable header with no text displayed you can use the default of ` ` aka ` `.
+ */
+ text: ' ',
+
+ /**
+ * @cfg {Boolean} sortable
+ * False to disable sorting of this column. Whether local/remote sorting is used is specified in
+ * `{@link Ext.data.Store#remoteSort}`. Defaults to true.
+ */
+ sortable: true,
+
+ /**
+ * @cfg {Boolean} groupable
+ * If the grid uses a {@link Ext.grid.feature.Grouping}, this option may be used to disable the header menu
+ * item to group by the column selected. By default, the header menu group option is enabled. Set to false to
+ * disable (but still show) the group option in the header menu for the column.
+ */
+
+ /**
+ * @cfg {Boolean} fixed
+ * @deprecated.
+ * True to prevent the column from being resizable.
+ */
+
+ /**
+ * @cfg {Boolean} resizable
+ * Set to <code>false</code> to prevent the column from being resizable. Defaults to <code>true</code>
+ */
+
+ /**
+ * @cfg {Boolean} hideable
+ * False to prevent the user from hiding this column. Defaults to true.
+ */
+ hideable: true,
+
+ /**
+ * @cfg {Boolean} menuDisabled
+ * True to disable the column header menu containing sort/hide options. Defaults to false.
+ */
+ menuDisabled: false,
+
+ /**
+ * @cfg {Function} renderer
+ * A renderer is an 'interceptor' method which can be used transform data (value, appearance, etc.)
+ * before it is rendered. Example:
+ *
+ * {
+ * renderer: function(value){
+ * if (value === 1) {
+ * return '1 person';
+ * }
+ * return value + ' people';
+ * }
+ * }
+ *
+ * @cfg {Object} renderer.value The data value for the current cell
+ * @cfg {Object} renderer.metaData A collection of metadata about the current cell; can be used or modified
+ * by the renderer. Recognized properties are: tdCls, tdAttr, and style.
+ * @cfg {Ext.data.Model} renderer.record The record for the current row
+ * @cfg {Number} renderer.rowIndex The index of the current row
+ * @cfg {Number} renderer.colIndex The index of the current column
+ * @cfg {Ext.data.Store} renderer.store The data store
+ * @cfg {Ext.view.View} renderer.view The current view
+ * @cfg {String} renderer.return The HTML string to be rendered.
+ */
+ renderer: false,
+
+ /**
+ * @cfg {String} align
+ * Sets the alignment of the header and rendered columns. Defaults to 'left'.
+ */
+ align: 'left',
+
+ /**
+ * @cfg {Boolean} draggable
+ * False to disable drag-drop reordering of this column. Defaults to true.
+ */
+ draggable: true,
+
+ // Header does not use the typical ComponentDraggable class and therefore we
+ // override this with an emptyFn. It is controlled at the HeaderDragZone.
+ initDraggable: Ext.emptyFn,
+
+ /**
+ * @cfg {String} tdCls
+ * A CSS class names to apply to the table cells for this column.
+ */
+
+ /**
+ * @cfg {Object/String} editor
+ * An optional xtype or config object for a {@link Ext.form.field.Field Field} to use for editing.
+ * Only applicable if the grid is using an {@link Ext.grid.plugin.Editing Editing} plugin.
+ */
+
+ /**
+ * @cfg {Object/String} field
+ * Alias for {@link #editor}.
+ * @deprecated 4.0.5 Use {@link #editor} instead.
+ */
+
+ /**
+ * @property {Ext.Element} triggerEl
+ * Element that acts as button for column header dropdown menu.
+ */
+
+ /**
+ * @property {Ext.Element} textEl
+ * Element that contains the text in column header.
+ */
+
+ /**
+ * @private
+ * Set in this class to identify, at runtime, instances which are not instances of the
+ * HeaderContainer base class, but are in fact, the subclass: Header.
+ */
+ isHeader: true,
+
+ initComponent: function() {
+ var me = this,
+ i,
+ len,
+ item;
+
+ if (Ext.isDefined(me.header)) {
+ me.text = me.header;
+ delete me.header;
+ }
+
+ // Flexed Headers need to have a minWidth defined so that they can never be squeezed out of existence by the
+ // HeaderContainer's specialized Box layout, the ColumnLayout. The ColumnLayout's overridden calculateChildboxes
+ // method extends the available layout space to accommodate the "desiredWidth" of all the columns.
+ if (me.flex) {
+ me.minWidth = me.minWidth || Ext.grid.plugin.HeaderResizer.prototype.minColWidth;
+ }
+ // Non-flexed Headers may never be squeezed in the event of a shortfall so
+ // always set their minWidth to their current width.
+ else {
+ me.minWidth = me.width;
+ }
+
+ if (!me.triStateSort) {
+ me.possibleSortStates.length = 2;
+ }
+
+ // A group header; It contains items which are themselves Headers
+ if (Ext.isDefined(me.columns)) {
+ me.isGroupHeader = true;
+
+
+ // The headers become child items
+ me.items = me.columns;
+ delete me.columns;
+ delete me.flex;
+ me.width = 0;
+
+ // Acquire initial width from sub headers
+ for (i = 0, len = me.items.length; i < len; i++) {
+ item = me.items[i];
+ if (!item.hidden) {
+ me.width += item.width || Ext.grid.header.Container.prototype.defaultWidth;
+ }
+ }
+ me.minWidth = me.width;
+
+ me.cls = (me.cls||'') + ' ' + Ext.baseCSSPrefix + 'group-header';
+ me.sortable = false;
+ me.resizable = false;
+ me.align = 'center';
+ }
+
+ me.addChildEls('titleContainer', 'triggerEl', 'textEl');
+
+ // Initialize as a HeaderContainer
+ me.callParent(arguments);
+ },
+
+ onAdd: function(childHeader) {
+ childHeader.isSubHeader = true;
+ childHeader.addCls(Ext.baseCSSPrefix + 'group-sub-header');
+ this.callParent(arguments);
+ },
+
+ onRemove: function(childHeader) {
+ childHeader.isSubHeader = false;
+ childHeader.removeCls(Ext.baseCSSPrefix + 'group-sub-header');
+ this.callParent(arguments);
+ },
+
+ initRenderData: function() {
+ var me = this;
+
+ Ext.applyIf(me.renderData, {
+ text: me.text,
+ menuDisabled: me.menuDisabled
+ });
+ return me.callParent(arguments);
+ },
+
+ applyColumnState: function (state) {
+ var me = this,
+ defined = Ext.isDefined;
+
+ // apply any columns
+ me.applyColumnsState(state.columns);
+
+ // Only state properties which were saved should be restored.
+ // (Only user-changed properties were saved by getState)
+ if (defined(state.hidden)) {
+ me.hidden = state.hidden;
+ }
+ if (defined(state.locked)) {
+ me.locked = state.locked;
+ }
+ if (defined(state.sortable)) {
+ me.sortable = state.sortable;
+ }
+ if (defined(state.width)) {
+ delete me.flex;
+ me.width = state.width;
+ } else if (defined(state.flex)) {
+ delete me.width;
+ me.flex = state.flex;
+ }
+ },
+
+ getColumnState: function () {
+ var me = this,
+ columns = [],
+ state = {
+ id: me.headerId
+ };
+
+ me.savePropsToState(['hidden', 'sortable', 'locked', 'flex', 'width'], state);
+
+ if (me.isGroupHeader) {
+ me.items.each(function(column){
+ columns.push(column.getColumnState());
+ });
+ if (columns.length) {
+ state.columns = columns;
+ }
+ } else if (me.isSubHeader && me.ownerCt.hidden) {
+ // don't set hidden on the children so they can auto height
+ delete me.hidden;
+ }
+
+ if ('width' in state) {
+ delete state.flex; // width wins
+ }
+ return state;
+ },
+
+ /**
+ * Sets the header text for this Column.
+ * @param {String} text The header to display on this Column.
+ */
+ setText: function(text) {
+ this.text = text;
+ if (this.rendered) {
+ this.textEl.update(text);
+ }
+ },
+
+ // Find the topmost HeaderContainer: An ancestor which is NOT a Header.
+ // Group Headers are themselves HeaderContainers
+ getOwnerHeaderCt: function() {
+ return this.up(':not([isHeader])');
+ },
+
+ /**
+ * Returns the true grid column index associated with this column only if this column is a base level Column. If it
+ * is a group column, it returns `false`.
+ * @return {Number}
+ */
+ getIndex: function() {
+ return this.isGroupColumn ? false : this.getOwnerHeaderCt().getHeaderIndex(this);
+ },
+
+ onRender: function() {
+ var me = this,
+ grid = me.up('tablepanel');
+
+ // Disable the menu if there's nothing to show in the menu, ie:
+ // Column cannot be sorted, grouped or locked, and there are no grid columns which may be hidden
+ if (grid && (!me.sortable || grid.sortableColumns === false) && !me.groupable && !me.lockable && (grid.enableColumnHide === false || !me.getOwnerHeaderCt().getHideableColumns().length)) {
+ me.menuDisabled = true;
+ }
+ me.callParent(arguments);
+ },
+
+ afterRender: function() {
+ var me = this,
+ el = me.el;
+
+ me.callParent(arguments);
+
+ el.addCls(Ext.baseCSSPrefix + 'column-header-align-' + me.align).addClsOnOver(me.overCls);
+
+ me.mon(el, {
+ click: me.onElClick,
+ dblclick: me.onElDblClick,
+ scope: me
+ });
+
+ // BrowserBug: Ie8 Strict Mode, this will break the focus for this browser,
+ // must be fixed when focus management will be implemented.
+ if (!Ext.isIE8 || !Ext.isStrict) {
+ me.mon(me.getFocusEl(), {
+ focus: me.onTitleMouseOver,
+ blur: me.onTitleMouseOut,
+ scope: me
+ });
+ }
+
+ me.mon(me.titleContainer, {
+ mouseenter: me.onTitleMouseOver,
+ mouseleave: me.onTitleMouseOut,
+ scope: me
+ });
+
+ me.keyNav = Ext.create('Ext.util.KeyNav', el, {
+ enter: me.onEnterKey,
+ down: me.onDownKey,
+ scope: me
+ });
+ },
+
+ /**
+ * Sets the width of this Column.
+ * @param {Number} width New width.
+ */
+ setWidth: function(width, /* private - used internally */ doLayout) {
+ var me = this,
+ headerCt = me.ownerCt,
+ siblings,
+ len, i,
+ oldWidth = me.getWidth(),
+ groupWidth = 0,
+ sibling;
+
+ if (width !== oldWidth) {
+ me.oldWidth = oldWidth;
+
+ // Non-flexed Headers may never be squeezed in the event of a shortfall so
+ // always set the minWidth to their current width.
+ me.minWidth = me.width = width;
+
+ // Bubble size changes upwards to group headers
+ if (headerCt.isGroupHeader) {
+ siblings = headerCt.items.items;
+ len = siblings.length;
+
+ for (i = 0; i < len; i++) {
+ sibling = siblings[i];
+ if (!sibling.hidden) {
+ groupWidth += (sibling === me) ? width : sibling.getWidth();
+ }
+ }
+ headerCt.setWidth(groupWidth, doLayout);
+ } else if (doLayout !== false) {
+ // Allow the owning Container to perform the sizing
+ headerCt.doLayout();
+ }
+ }
+ },
+
+ afterComponentLayout: function(width, height) {
+ var me = this,
+ ownerHeaderCt = this.getOwnerHeaderCt();
+
+ me.callParent(arguments);
+
+ // Only changes at the base level inform the grid's HeaderContainer which will update the View
+ // Skip this if the width is null or undefined which will be the Box layout's initial pass through the child Components
+ // Skip this if it's the initial size setting in which case there is no ownerheaderCt yet - that is set afterRender
+ if (width && !me.isGroupHeader && ownerHeaderCt) {
+ ownerHeaderCt.onHeaderResize(me, width, true);
+ }
+ if (me.oldWidth && (width !== me.oldWidth)) {
+ ownerHeaderCt.fireEvent('columnresize', ownerHeaderCt, this, width);
+ }
+ delete me.oldWidth;
+ },
+
+ // private
+ // After the container has laid out and stretched, it calls this to correctly pad the inner to center the text vertically
+ // Total available header height must be passed to enable padding for inner elements to be calculated.
+ setPadding: function(headerHeight) {
+ var me = this,
+ lineHeight = Ext.util.TextMetrics.measure(me.textEl.dom, me.text).height;
+
+ // Top title containing element must stretch to match height of sibling group headers
+ if (!me.isGroupHeader) {
+ if (me.titleContainer.getHeight() < headerHeight) {
+ me.titleContainer.dom.style.height = headerHeight + 'px';
+ }
+ }
+ headerHeight = me.titleContainer.getViewSize().height;
+
+ // Vertically center the header text in potentially vertically stretched header
+ if (lineHeight) {
+ me.titleContainer.setStyle({
+ paddingTop: Math.max(((headerHeight - lineHeight) / 2), 0) + 'px'
+ });
+ }
+
+ // Only IE needs this
+ if (Ext.isIE && me.triggerEl) {
+ me.triggerEl.setHeight(headerHeight);
+ }
+ },
+
+ onDestroy: function() {
+ var me = this;
+ // force destroy on the textEl, IE reports a leak
+ Ext.destroy(me.textEl, me.keyNav);
+ delete me.keyNav;
+ me.callParent(arguments);
+ },
+
+ onTitleMouseOver: function() {
+ this.titleContainer.addCls(this.hoverCls);
+ },
+
+ onTitleMouseOut: function() {
+ this.titleContainer.removeCls(this.hoverCls);
+ },
+
+ onDownKey: function(e) {
+ if (this.triggerEl) {
+ this.onElClick(e, this.triggerEl.dom || this.el.dom);
+ }
+ },
+
+ onEnterKey: function(e) {
+ this.onElClick(e, this.el.dom);
+ },
+
+ /**
+ * @private
+ * Double click
+ * @param e
+ * @param t
+ */
+ onElDblClick: function(e, t) {
+ var me = this,
+ ownerCt = me.ownerCt;
+ if (ownerCt && Ext.Array.indexOf(ownerCt.items, me) !== 0 && me.isOnLeftEdge(e) ) {
+ ownerCt.expandToFit(me.previousSibling('gridcolumn'));
+ }
+ },
+
+ onElClick: function(e, t) {
+
+ // The grid's docked HeaderContainer.
+ var me = this,
+ ownerHeaderCt = me.getOwnerHeaderCt();
+
+ if (ownerHeaderCt && !ownerHeaderCt.ddLock) {
+ // Firefox doesn't check the current target in a within check.
+ // Therefore we check the target directly and then within (ancestors)
+ if (me.triggerEl && (e.target === me.triggerEl.dom || t === me.triggerEl.dom || e.within(me.triggerEl))) {
+ ownerHeaderCt.onHeaderTriggerClick(me, e, t);
+ // if its not on the left hand edge, sort
+ } else if (e.getKey() || (!me.isOnLeftEdge(e) && !me.isOnRightEdge(e))) {
+ me.toggleSortState();
+ ownerHeaderCt.onHeaderClick(me, e, t);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Process UI events from the view. The owning TablePanel calls this method, relaying events from the TableView
+ * @param {String} type Event type, eg 'click'
+ * @param {Ext.view.Table} view TableView Component
+ * @param {HTMLElement} cell Cell HtmlElement the event took place within
+ * @param {Number} recordIndex Index of the associated Store Model (-1 if none)
+ * @param {Number} cellIndex Cell index within the row
+ * @param {Ext.EventObject} e Original event
+ */
+ processEvent: function(type, view, cell, recordIndex, cellIndex, e) {
+ return this.fireEvent.apply(this, arguments);
+ },
+
+ toggleSortState: function() {
+ var me = this,
+ idx,
+ nextIdx;
+
+ if (me.sortable) {
+ idx = Ext.Array.indexOf(me.possibleSortStates, me.sortState);
+
+ nextIdx = (idx + 1) % me.possibleSortStates.length;
+ me.setSortState(me.possibleSortStates[nextIdx]);
+ }
+ },
+
+ doSort: function(state) {
+ var ds = this.up('tablepanel').store;
+ ds.sort({
+ property: this.getSortParam(),
+ direction: state
+ });
+ },
+
+ /**
+ * Returns the parameter to sort upon when sorting this header. By default this returns the dataIndex and will not
+ * need to be overriden in most cases.
+ * @return {String}
+ */
+ getSortParam: function() {
+ return this.dataIndex;
+ },
+
+ //setSortState: function(state, updateUI) {
+ //setSortState: function(state, doSort) {
+ setSortState: function(state, skipClear, initial) {
+ var me = this,
+ colSortClsPrefix = Ext.baseCSSPrefix + 'column-header-sort-',
+ ascCls = colSortClsPrefix + 'ASC',
+ descCls = colSortClsPrefix + 'DESC',
+ nullCls = colSortClsPrefix + 'null',
+ ownerHeaderCt = me.getOwnerHeaderCt(),
+ oldSortState = me.sortState;
+
+ if (oldSortState !== state && me.getSortParam()) {
+ me.addCls(colSortClsPrefix + state);
+ // don't trigger a sort on the first time, we just want to update the UI
+ if (state && !initial) {
+ me.doSort(state);
+ }
+ switch (state) {
+ case 'DESC':
+ me.removeCls([ascCls, nullCls]);
+ break;
+ case 'ASC':
+ me.removeCls([descCls, nullCls]);
+ break;
+ case null:
+ me.removeCls([ascCls, descCls]);
+ break;
+ }
+ if (ownerHeaderCt && !me.triStateSort && !skipClear) {
+ ownerHeaderCt.clearOtherSortStates(me);
+ }
+ me.sortState = state;
+ ownerHeaderCt.fireEvent('sortchange', ownerHeaderCt, me, state);
+ }
+ },
+
+ hide: function() {
+ var me = this,
+ items,
+ len, i,
+ lb,
+ newWidth = 0,
+ ownerHeaderCt = me.getOwnerHeaderCt();
+
+ // Hiding means setting to zero width, so cache the width
+ me.oldWidth = me.getWidth();
+
+ // Hiding a group header hides itself, and then informs the HeaderContainer about its sub headers (Suppressing header layout)
+ if (me.isGroupHeader) {
+ items = me.items.items;
+ me.callParent(arguments);
+ ownerHeaderCt.onHeaderHide(me);
+ for (i = 0, len = items.length; i < len; i++) {
+ items[i].hidden = true;
+ ownerHeaderCt.onHeaderHide(items[i], true);
+ }
+ return;
+ }
+
+ // TODO: Work with Jamie to produce a scheme where we can show/hide/resize without triggering a layout cascade
+ lb = me.ownerCt.componentLayout.layoutBusy;
+ me.ownerCt.componentLayout.layoutBusy = true;
+ me.callParent(arguments);
+ me.ownerCt.componentLayout.layoutBusy = lb;
+
+ // Notify owning HeaderContainer
+ ownerHeaderCt.onHeaderHide(me);
+
+ if (me.ownerCt.isGroupHeader) {
+ // If we've just hidden the last header in a group, then hide the group
+ items = me.ownerCt.query('>:not([hidden])');
+ if (!items.length) {
+ me.ownerCt.hide();
+ }
+ // Size the group down to accommodate fewer sub headers
+ else {
+ for (i = 0, len = items.length; i < len; i++) {
+ newWidth += items[i].getWidth();
+ }
+ me.ownerCt.minWidth = newWidth;
+ me.ownerCt.setWidth(newWidth);
+ }
+ }
+ },
+
+ show: function() {
+ var me = this,
+ ownerCt = me.ownerCt,
+ ownerCtCompLayout = ownerCt.componentLayout,
+ ownerCtCompLayoutBusy = ownerCtCompLayout.layoutBusy,
+ ownerCtLayout = ownerCt.layout,
+ ownerCtLayoutBusy = ownerCtLayout.layoutBusy,
+ items,
+ len, i,
+ item,
+ newWidth = 0;
+
+ // TODO: Work with Jamie to produce a scheme where we can show/hide/resize without triggering a layout cascade
+
+ // Suspend our owner's layouts (both component and container):
+ ownerCtCompLayout.layoutBusy = ownerCtLayout.layoutBusy = true;
+
+ me.callParent(arguments);
+
+ ownerCtCompLayout.layoutBusy = ownerCtCompLayoutBusy;
+ ownerCtLayout.layoutBusy = ownerCtLayoutBusy;
+
+ // If a sub header, ensure that the group header is visible
+ if (me.isSubHeader) {
+ if (!ownerCt.isVisible()) {
+ ownerCt.show();
+ }
+ }
+
+ // If we've just shown a group with all its sub headers hidden, then show all its sub headers
+ if (me.isGroupHeader && !me.query(':not([hidden])').length) {
+ items = me.query('>*');
+ for (i = 0, len = items.length; i < len; i++) {
+ item = items[i];
+ item.preventLayout = true;
+ item.show();
+ newWidth += item.getWidth();
+ delete item.preventLayout;
+ }
+ me.setWidth(newWidth);
+ }
+
+ // Resize the owning group to accommodate
+ if (ownerCt.isGroupHeader && me.preventLayout !== true) {
+ items = ownerCt.query('>:not([hidden])');
+ for (i = 0, len = items.length; i < len; i++) {
+ newWidth += items[i].getWidth();
+ }
+ ownerCt.minWidth = newWidth;
+ ownerCt.setWidth(newWidth);
+ }
+
+ // Notify owning HeaderContainer
+ ownerCt = me.getOwnerHeaderCt();
+ if (ownerCt) {
+ ownerCt.onHeaderShow(me, me.preventLayout);
+ }
+ },
+
+ getDesiredWidth: function() {
+ var me = this;
+ if (me.rendered && me.componentLayout && me.componentLayout.lastComponentSize) {
+ // headers always have either a width or a flex
+ // because HeaderContainer sets a defaults width
+ // therefore we can ignore the natural width
+ // we use the componentLayout's tracked width so that
+ // we can calculate the desired width when rendered
+ // but not visible because its being obscured by a layout
+ return me.componentLayout.lastComponentSize.width;
+ // Flexed but yet to be rendered this could be the case
+ // where a HeaderContainer and Headers are simply used as data
+ // structures and not rendered.
+ }
+ else if (me.flex) {
+ // this is going to be wrong, the defaultWidth
+ return me.width;
+ }
+ else {
+ return me.width;
+ }
+ },
+
+ getCellSelector: function() {
+ return '.' + Ext.baseCSSPrefix + 'grid-cell-' + this.getItemId();
+ },
+
+ getCellInnerSelector: function() {
+ return this.getCellSelector() + ' .' + Ext.baseCSSPrefix + 'grid-cell-inner';
+ },
+
+ isOnLeftEdge: function(e) {
+ return (e.getXY()[0] - this.el.getLeft() <= this.handleWidth);
+ },
+
+ isOnRightEdge: function(e) {
+ return (this.el.getRight() - e.getXY()[0] <= this.handleWidth);
+ }
+
+ // intentionally omit getEditor and setEditor definitions bc we applyIf into columns
+ // when the editing plugin is injected
+
+ /**
+ * @method getEditor
+ * Retrieves the editing field for editing associated with this header. Returns false if there is no field
+ * associated with the Header the method will return false. If the field has not been instantiated it will be
+ * created. Note: These methods only has an implementation if a Editing plugin has been enabled on the grid.
+ * @param {Object} record The {@link Ext.data.Model Model} instance being edited.
+ * @param {Object} defaultField An object representing a default field to be created
+ * @return {Ext.form.field.Field} field
+ */
+ /**
+ * @method setEditor
+ * Sets the form field to be used for editing. Note: This method only has an implementation if an Editing plugin has
+ * been enabled on the grid.
+ * @param {Object} field An object representing a field to be created. If no xtype is specified a 'textfield' is
+ * assumed.
+ */
+});
+
+/**
+ * This is a utility class that can be passed into a {@link Ext.grid.column.Column} as a column config that provides
+ * an automatic row numbering column.
+ *
+ * Usage:
+ *
+ * columns: [
+ * {xtype: 'rownumberer'},
+ * {text: "Company", flex: 1, sortable: true, dataIndex: 'company'},
+ * {text: "Price", width: 120, sortable: true, renderer: Ext.util.Format.usMoney, dataIndex: 'price'},
+ * {text: "Change", width: 120, sortable: true, dataIndex: 'change'},
+ * {text: "% Change", width: 120, sortable: true, dataIndex: 'pctChange'},
+ * {text: "Last Updated", width: 120, sortable: true, renderer: Ext.util.Format.dateRenderer('m/d/Y'), dataIndex: 'lastChange'}
+ * ]
+ *
+ */
+Ext.define('Ext.grid.RowNumberer', {
+ extend: 'Ext.grid.column.Column',
+ alias: 'widget.rownumberer',
+
+ /**
+ * @cfg {String} text
+ * Any valid text or HTML fragment to display in the header cell for the row number column.
+ */
+ text: " ",
+
+ /**
+ * @cfg {Number} width
+ * The default width in pixels of the row number column.
+ */
+ width: 23,
+
+ /**
+ * @cfg {Boolean} sortable
+ * True if the row number column is sortable.
+ * @hide
+ */
+ sortable: false,
+
+ align: 'right',
+
+ constructor : function(config){
+ this.callParent(arguments);
+ if (this.rowspan) {
+ this.renderer = Ext.Function.bind(this.renderer, this);
+ }
+ },
+
+ // private
+ resizable: false,
+ hideable: false,
+ menuDisabled: true,
+ dataIndex: '',
+ cls: Ext.baseCSSPrefix + 'row-numberer',
+ rowspan: undefined,
+
+ // private
+ renderer: function(value, metaData, record, rowIdx, colIdx, store) {
+ if (this.rowspan){
+ metaData.cellAttr = 'rowspan="'+this.rowspan+'"';
+ }
+
+ metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special';
+ return store.indexOfTotal(record) + 1;
+ }
+});
+
+/**
+ * @class Ext.view.DropZone
+ * @extends Ext.dd.DropZone
+ * @private
+ */
+Ext.define('Ext.view.DropZone', {
+ extend: 'Ext.dd.DropZone',
+
+ indicatorHtml: '<div class="x-grid-drop-indicator-left"></div><div class="x-grid-drop-indicator-right"></div>',
+ indicatorCls: 'x-grid-drop-indicator',
+
+ constructor: function(config) {
+ var me = this;
+ Ext.apply(me, config);
+
+ // Create a ddGroup unless one has been configured.
+ // User configuration of ddGroups allows users to specify which
+ // DD instances can interact with each other. Using one
+ // based on the id of the View would isolate it and mean it can only
+ // interact with a DragZone on the same View also using a generated ID.
+ if (!me.ddGroup) {
+ me.ddGroup = 'view-dd-zone-' + me.view.id;
+ }
+
+ // The DropZone's encapsulating element is the View's main element. It must be this because drop gestures
+ // may require scrolling on hover near a scrolling boundary. In Ext 4.x two DD instances may not use the
+ // same element, so a DragZone on this same View must use the View's parent element as its element.
+ me.callParent([me.view.el]);
+ },
+
+// Fire an event through the client DataView. Lock this DropZone during the event processing so that
+// its data does not become corrupted by processing mouse events.
+ fireViewEvent: function() {
+ var me = this,
+ result;
+
+ me.lock();
+ result = me.view.fireEvent.apply(me.view, arguments);
+ me.unlock();
+ return result;
+ },
+
+ getTargetFromEvent : function(e) {
+ var node = e.getTarget(this.view.getItemSelector()),
+ mouseY, nodeList, testNode, i, len, box;
+
+// Not over a row node: The content may be narrower than the View's encapsulating element, so return the closest.
+// If we fall through because the mouse is below the nodes (or there are no nodes), we'll get an onContainerOver call.
+ if (!node) {
+ mouseY = e.getPageY();
+ for (i = 0, nodeList = this.view.getNodes(), len = nodeList.length; i < len; i++) {
+ testNode = nodeList[i];
+ box = Ext.fly(testNode).getBox();
+ if (mouseY <= box.bottom) {
+ return testNode;
+ }
+ }
+ }
+ return node;
+ },
+
+ getIndicator: function() {
+ var me = this;
+
+ if (!me.indicator) {
+ me.indicator = Ext.createWidget('component', {
+ html: me.indicatorHtml,
+ cls: me.indicatorCls,
+ ownerCt: me.view,
+ floating: true,
+ shadow: false
+ });
+ }
+ return me.indicator;
+ },
+
+ getPosition: function(e, node) {
+ var y = e.getXY()[1],
+ region = Ext.fly(node).getRegion(),
+ pos;
+
+ if ((region.bottom - y) >= (region.bottom - region.top) / 2) {
+ pos = "before";
+ } else {
+ pos = "after";
+ }
+ return pos;
+ },
+
+ /**
+ * @private Determines whether the record at the specified offset from the passed record
+ * is in the drag payload.
+ * @param records
+ * @param record
+ * @param offset
+ * @returns {Boolean} True if the targeted record is in the drag payload
+ */
+ containsRecordAtOffset: function(records, record, offset) {
+ if (!record) {
+ return false;
+ }
+ var view = this.view,
+ recordIndex = view.indexOf(record),
+ nodeBefore = view.getNode(recordIndex + offset),
+ recordBefore = nodeBefore ? view.getRecord(nodeBefore) : null;
+
+ return recordBefore && Ext.Array.contains(records, recordBefore);
+ },
+
+ positionIndicator: function(node, data, e) {
+ var me = this,
+ view = me.view,
+ pos = me.getPosition(e, node),
+ overRecord = view.getRecord(node),
+ draggingRecords = data.records,
+ indicator, indicatorY;
+
+ if (!Ext.Array.contains(draggingRecords, overRecord) && (
+ pos == 'before' && !me.containsRecordAtOffset(draggingRecords, overRecord, -1) ||
+ pos == 'after' && !me.containsRecordAtOffset(draggingRecords, overRecord, 1)
+ )) {
+ me.valid = true;
+
+ if (me.overRecord != overRecord || me.currentPosition != pos) {
+
+ indicatorY = Ext.fly(node).getY() - view.el.getY() - 1;
+ if (pos == 'after') {
+ indicatorY += Ext.fly(node).getHeight();
+ }
+ me.getIndicator().setWidth(Ext.fly(view.el).getWidth()).showAt(0, indicatorY);
+
+ // Cache the overRecord and the 'before' or 'after' indicator.
+ me.overRecord = overRecord;
+ me.currentPosition = pos;
+ }
+ } else {
+ me.invalidateDrop();
+ }
+ },
+
+ invalidateDrop: function() {
+ if (this.valid) {
+ this.valid = false;
+ this.getIndicator().hide();
+ }
+ },
+
+ // The mouse is over a View node
+ onNodeOver: function(node, dragZone, e, data) {
+ var me = this;
+
+ if (!Ext.Array.contains(data.records, me.view.getRecord(node))) {
+ me.positionIndicator(node, data, e);
+ }
+ return me.valid ? me.dropAllowed : me.dropNotAllowed;
+ },
+
+ // Moved out of the DropZone without dropping.
+ // Remove drop position indicator
+ notifyOut: function(node, dragZone, e, data) {
+ var me = this;
+
+ me.callParent(arguments);
+ delete me.overRecord;
+ delete me.currentPosition;
+ if (me.indicator) {
+ me.indicator.hide();
+ }
+ },
+
+ // The mouse is past the end of all nodes (or there are no nodes)
+ onContainerOver : function(dd, e, data) {
+ var me = this,
+ view = me.view,
+ count = view.store.getCount();
+
+ // There are records, so position after the last one
+ if (count) {
+ me.positionIndicator(view.getNode(count - 1), data, e);
+ }
+
+ // No records, position the indicator at the top
+ else {
+ delete me.overRecord;
+ delete me.currentPosition;
+ me.getIndicator().setWidth(Ext.fly(view.el).getWidth()).showAt(0, 0);
+ me.valid = true;
+ }
+ return me.dropAllowed;
+ },
+
+ onContainerDrop : function(dd, e, data) {
+ return this.onNodeDrop(dd, null, e, data);
+ },
+
+ onNodeDrop: function(node, dragZone, e, data) {
+ var me = this,
+ dropped = false,
+
+ // Create a closure to perform the operation which the event handler may use.
+ // Users may now return <code>false</code> from the beforedrop handler, and perform any kind
+ // of asynchronous processing such as an Ext.Msg.confirm, or an Ajax request,
+ // and complete the drop gesture at some point in the future by calling this function.
+ processDrop = function () {
+ me.invalidateDrop();
+ me.handleNodeDrop(data, me.overRecord, me.currentPosition);
+ dropped = true;
+ me.fireViewEvent('drop', node, data, me.overRecord, me.currentPosition);
+ },
+ performOperation = false;
+
+ if (me.valid) {
+ performOperation = me.fireViewEvent('beforedrop', node, data, me.overRecord, me.currentPosition, processDrop);
+ if (performOperation !== false) {
+ // If the processDrop function was called in the event handler, do not do it again.
+ if (!dropped) {
+ processDrop();
+ }
+ }
+ }
+ return performOperation;
+ },
+
+ destroy: function(){
+ Ext.destroy(this.indicator);
+ delete this.indicator;
+ this.callParent();
+ }
+});
+
+Ext.define('Ext.grid.ViewDropZone', {
+ extend: 'Ext.view.DropZone',
+
+ indicatorHtml: '<div class="x-grid-drop-indicator-left"></div><div class="x-grid-drop-indicator-right"></div>',
+ indicatorCls: 'x-grid-drop-indicator',
+
+ handleNodeDrop : function(data, record, position) {
+ var view = this.view,
+ store = view.getStore(),
+ index, records, i, len;
+
+ // If the copy flag is set, create a copy of the Models with the same IDs
+ if (data.copy) {
+ records = data.records;
+ data.records = [];
+ for (i = 0, len = records.length; i < len; i++) {
+ data.records.push(records[i].copy(records[i].getId()));
+ }
+ } else {
+ /*
+ * Remove from the source store. We do this regardless of whether the store
+ * is the same bacsue the store currently doesn't handle moving records
+ * within the store. In the future it should be possible to do this.
+ * Here was pass the isMove parameter if we're moving to the same view.
+ */
+ data.view.store.remove(data.records, data.view === view);
+ }
+
+ index = store.indexOf(record);
+
+ // 'after', or undefined (meaning a drop at index -1 on an empty View)...
+ if (position !== 'before') {
+ index++;
+ }
+ store.insert(index, data.records);
+ view.getSelectionModel().select(data.records);
+ }
+});
+/**
+ * A Grid header type which renders an icon, or a series of icons in a grid cell, and offers a scoped click
+ * handler for each icon.
+ *
+ * @example
+ * Ext.create('Ext.data.Store', {
+ * storeId:'employeeStore',
+ * fields:['firstname', 'lastname', 'senority', 'dep', 'hired'],
+ * data:[
+ * {firstname:"Michael", lastname:"Scott"},
+ * {firstname:"Dwight", lastname:"Schrute"},
+ * {firstname:"Jim", lastname:"Halpert"},
+ * {firstname:"Kevin", lastname:"Malone"},
+ * {firstname:"Angela", lastname:"Martin"}
+ * ]
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Action Column Demo',
+ * store: Ext.data.StoreManager.lookup('employeeStore'),
+ * columns: [
+ * {text: 'First Name', dataIndex:'firstname'},
+ * {text: 'Last Name', dataIndex:'lastname'},
+ * {
+ * xtype:'actioncolumn',
+ * width:50,
+ * items: [{
+ * icon: 'extjs/examples/shared/icons/fam/cog_edit.png', // Use a URL in the icon config
+ * tooltip: 'Edit',
+ * handler: function(grid, rowIndex, colIndex) {
+ * var rec = grid.getStore().getAt(rowIndex);
+ * alert("Edit " + rec.get('firstname'));
+ * }
+ * },{
+ * icon: 'extjs/examples/restful/images/delete.png',
+ * tooltip: 'Delete',
+ * handler: function(grid, rowIndex, colIndex) {
+ * var rec = grid.getStore().getAt(rowIndex);
+ * alert("Terminate " + rec.get('firstname'));
+ * }
+ * }]
+ * }
+ * ],
+ * width: 250,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * The action column can be at any index in the columns array, and a grid can have any number of
+ * action columns.
+ */
+Ext.define('Ext.grid.column.Action', {
+ extend: 'Ext.grid.column.Column',
+ alias: ['widget.actioncolumn'],
+ alternateClassName: 'Ext.grid.ActionColumn',
+
+ /**
+ * @cfg {String} icon
+ * The URL of an image to display as the clickable element in the column. Defaults to
+ * `{@link Ext#BLANK_IMAGE_URL Ext.BLANK_IMAGE_URL}`.
+ */
+ /**
+ * @cfg {String} iconCls
+ * A CSS class to apply to the icon image. To determine the class dynamically, configure the Column with
+ * a `{@link #getClass}` function.
+ */
+ /**
+ * @cfg {Function} handler
+ * A function called when the icon is clicked.
+ * @cfg {Ext.view.Table} handler.view The owning TableView.
+ * @cfg {Number} handler.rowIndex The row index clicked on.
+ * @cfg {Number} handler.colIndex The column index clicked on.
+ * @cfg {Object} handler.item The clicked item (or this Column if multiple {@link #items} were not configured).
+ * @cfg {Event} handler.e The click event.
+ */
+ /**
+ * @cfg {Object} scope
+ * The scope (**this** reference) in which the `{@link #handler}` and `{@link #getClass}` fuctions are executed.
+ * Defaults to this Column.
+ */
+ /**
+ * @cfg {String} tooltip
+ * A tooltip message to be displayed on hover. {@link Ext.tip.QuickTipManager#init Ext.tip.QuickTipManager} must
+ * have been initialized.
+ */
+ /* @cfg {Boolean} disabled
+ * If true, the action will not respond to click events, and will be displayed semi-opaque.
+ */
+ /**
+ * @cfg {Boolean} [stopSelection=true]
+ * Prevent grid _row_ selection upon mousedown.
+ */
+ /**
+ * @cfg {Function} getClass
+ * A function which returns the CSS class to apply to the icon image.
+ *
+ * @cfg {Object} getClass.v The value of the column's configured field (if any).
+ *
+ * @cfg {Object} getClass.metadata An object in which you may set the following attributes:
+ * @cfg {String} getClass.metadata.css A CSS class name to add to the cell's TD element.
+ * @cfg {String} getClass.metadata.attr An HTML attribute definition string to apply to the data container
+ * element *within* the table cell (e.g. 'style="color:red;"').
+ *
+ * @cfg {Ext.data.Model} getClass.r The Record providing the data.
+ *
+ * @cfg {Number} getClass.rowIndex The row index..
+ *
+ * @cfg {Number} getClass.colIndex The column index.
+ *
+ * @cfg {Ext.data.Store} getClass.store The Store which is providing the data Model.
+ */
+ /**
+ * @cfg {Object[]} items
+ * An Array which may contain multiple icon definitions, each element of which may contain:
+ *
+ * @cfg {String} items.icon The url of an image to display as the clickable element in the column.
+ *
+ * @cfg {String} items.iconCls A CSS class to apply to the icon image. To determine the class dynamically,
+ * configure the item with a `getClass` function.
+ *
+ * @cfg {Function} items.getClass A function which returns the CSS class to apply to the icon image.
+ * @cfg {Object} items.getClass.v The value of the column's configured field (if any).
+ * @cfg {Object} items.getClass.metadata An object in which you may set the following attributes:
+ * @cfg {String} items.getClass.metadata.css A CSS class name to add to the cell's TD element.
+ * @cfg {String} items.getClass.metadata.attr An HTML attribute definition string to apply to the data
+ * container element _within_ the table cell (e.g. 'style="color:red;"').
+ * @cfg {Ext.data.Model} items.getClass.r The Record providing the data.
+ * @cfg {Number} items.getClass.rowIndex The row index..
+ * @cfg {Number} items.getClass.colIndex The column index.
+ * @cfg {Ext.data.Store} items.getClass.store The Store which is providing the data Model.
+ *
+ * @cfg {Function} items.handler A function called when the icon is clicked.
+ *
+ * @cfg {Object} items.scope The scope (`this` reference) in which the `handler` and `getClass` functions
+ * are executed. Fallback defaults are this Column's configured scope, then this Column.
+ *
+ * @cfg {String} items.tooltip A tooltip message to be displayed on hover.
+ * @cfg {Boolean} items.disabled If true, the action will not respond to click events, and will be displayed semi-opaque.
+ * {@link Ext.tip.QuickTipManager#init Ext.tip.QuickTipManager} must have been initialized.
+ */
+ /**
+ * @property {Array} items
+ * An array of action items copied from the configured {@link #cfg-items items} configuration. Each will have
+ * an `enable` and `disable` method added which will enable and disable the associated action, and
+ * update the displayed icon accordingly.
+ */
+ header: ' ',
+
+ actionIdRe: new RegExp(Ext.baseCSSPrefix + 'action-col-(\\d+)'),
+
+ /**
+ * @cfg {String} altText
+ * The alt text to use for the image element.
+ */
+ altText: '',
+
+ sortable: false,
+
+ constructor: function(config) {
+ var me = this,
+ cfg = Ext.apply({}, config),
+ items = cfg.items || [me],
+ l = items.length,
+ i,
+ item;
+
+ // This is a Container. Delete the items config to be reinstated after construction.
+ delete cfg.items;
+ me.callParent([cfg]);
+
+ // Items is an array property of ActionColumns
+ me.items = items;
+
+// Renderer closure iterates through items creating an <img> element for each and tagging with an identifying
+// class name x-action-col-{n}
+ me.renderer = function(v, meta) {
+// Allow a configured renderer to create initial value (And set the other values in the "metadata" argument!)
+ v = Ext.isFunction(cfg.renderer) ? cfg.renderer.apply(this, arguments)||'' : '';
+
+ meta.tdCls += ' ' + Ext.baseCSSPrefix + 'action-col-cell';
+ for (i = 0; i < l; i++) {
+ item = items[i];
+ item.disable = Ext.Function.bind(me.disableAction, me, [i]);
+ item.enable = Ext.Function.bind(me.enableAction, me, [i]);
+ v += '<img alt="' + (item.altText || me.altText) + '" src="' + (item.icon || Ext.BLANK_IMAGE_URL) +
+ '" class="' + Ext.baseCSSPrefix + 'action-col-icon ' + Ext.baseCSSPrefix + 'action-col-' + String(i) + ' ' + (item.disabled ? Ext.baseCSSPrefix + 'item-disabled' : ' ') + (item.iconCls || '') +
+ ' ' + (Ext.isFunction(item.getClass) ? item.getClass.apply(item.scope||me.scope||me, arguments) : (me.iconCls || '')) + '"' +
+ ((item.tooltip) ? ' data-qtip="' + item.tooltip + '"' : '') + ' />';
+ }
+ return v;
+ };
+ },
+
+ /**
+ * Enables this ActionColumn's action at the specified index.
+ */
+ enableAction: function(index) {
+ var me = this;
+
+ if (!index) {
+ index = 0;
+ } else if (!Ext.isNumber(index)) {
+ index = Ext.Array.indexOf(me.items, index);
+ }
+ me.items[index].disabled = false;
+ me.up('tablepanel').el.select('.' + Ext.baseCSSPrefix + 'action-col-' + index).removeCls(me.disabledCls);
+ },
+
+ /**
+ * Disables this ActionColumn's action at the specified index.
+ */
+ disableAction: function(index) {
+ var me = this;
+
+ if (!index) {
+ index = 0;
+ } else if (!Ext.isNumber(index)) {
+ index = Ext.Array.indexOf(me.items, index);
+ }
+ me.items[index].disabled = true;
+ me.up('tablepanel').el.select('.' + Ext.baseCSSPrefix + 'action-col-' + index).addCls(me.disabledCls);
+ },
+
+ destroy: function() {
+ delete this.items;
+ delete this.renderer;
+ return this.callParent(arguments);
+ },
+
+ /**
+ * @private
+ * Process and refire events routed from the GridView's processEvent method.
+ * Also fires any configured click handlers. By default, cancels the mousedown event to prevent selection.
+ * Returns the event handler's status to allow canceling of GridView's bubbling process.
+ */
+ processEvent : function(type, view, cell, recordIndex, cellIndex, e){
+ var me = this,
+ match = e.getTarget().className.match(me.actionIdRe),
+ item, fn;
+
+ if (match) {
+ item = me.items[parseInt(match[1], 10)];
+ if (item) {
+ if (type == 'click') {
+ fn = item.handler || me.handler;
+ if (fn && !item.disabled) {
+ fn.call(item.scope || me.scope || me, view, recordIndex, cellIndex, item, e);
+ }
+ } else if (type == 'mousedown' && item.stopSelection !== false) {
+ return false;
+ }
+ }
+ }
+ return me.callParent(arguments);
+ },
+
+ cascade: function(fn, scope) {
+ fn.call(scope||this, this);
+ },
+
+ // Private override because this cannot function as a Container, and it has an items property which is an Array, NOT a MixedCollection.
+ getRefItems: function() {
+ return [];
+ }
+});
+/**
+ * A Column definition class which renders boolean data fields. See the {@link Ext.grid.column.Column#xtype xtype}
+ * config option of {@link Ext.grid.column.Column} for more details.
+ *
+ * @example
+ * Ext.create('Ext.data.Store', {
+ * storeId:'sampleStore',
+ * fields:[
+ * {name: 'framework', type: 'string'},
+ * {name: 'rocks', type: 'boolean'}
+ * ],
+ * data:{'items':[
+ * { 'framework': "Ext JS 4", 'rocks': true },
+ * { 'framework': "Sencha Touch", 'rocks': true },
+ * { 'framework': "Ext GWT", 'rocks': true },
+ * { 'framework': "Other Guys", 'rocks': false }
+ * ]},
+ * proxy: {
+ * type: 'memory',
+ * reader: {
+ * type: 'json',
+ * root: 'items'
+ * }
+ * }
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Boolean Column Demo',
+ * store: Ext.data.StoreManager.lookup('sampleStore'),
+ * columns: [
+ * { text: 'Framework', dataIndex: 'framework', flex: 1 },
+ * {
+ * xtype: 'booleancolumn',
+ * text: 'Rocks',
+ * trueText: 'Yes',
+ * falseText: 'No',
+ * dataIndex: 'rocks'
+ * }
+ * ],
+ * height: 200,
+ * width: 400,
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.grid.column.Boolean', {
+ extend: 'Ext.grid.column.Column',
+ alias: ['widget.booleancolumn'],
+ alternateClassName: 'Ext.grid.BooleanColumn',
+
+ /**
+ * @cfg {String} trueText
+ * The string returned by the renderer when the column value is not falsey.
+ */
+ trueText: 'true',
+
+ /**
+ * @cfg {String} falseText
+ * The string returned by the renderer when the column value is falsey (but not undefined).
+ */
+ falseText: 'false',
+
+ /**
+ * @cfg {String} undefinedText
+ * The string returned by the renderer when the column value is undefined.
+ */
+ undefinedText: ' ',
+
+ constructor: function(cfg){
+ this.callParent(arguments);
+ var trueText = this.trueText,
+ falseText = this.falseText,
+ undefinedText = this.undefinedText;
+
+ this.renderer = function(value){
+ if(value === undefined){
+ return undefinedText;
+ }
+ if(!value || value === 'false'){
+ return falseText;
+ }
+ return trueText;
+ };
+ }
+});
+/**
+ * A Column definition class which renders a passed date according to the default locale, or a configured
+ * {@link #format}.
+ *
+ * @example
+ * Ext.create('Ext.data.Store', {
+ * storeId:'sampleStore',
+ * fields:[
+ * { name: 'symbol', type: 'string' },
+ * { name: 'date', type: 'date' },
+ * { name: 'change', type: 'number' },
+ * { name: 'volume', type: 'number' },
+ * { name: 'topday', type: 'date' }
+ * ],
+ * data:[
+ * { symbol: "msft", date: '2011/04/22', change: 2.43, volume: 61606325, topday: '04/01/2010' },
+ * { symbol: "goog", date: '2011/04/22', change: 0.81, volume: 3053782, topday: '04/11/2010' },
+ * { symbol: "apple", date: '2011/04/22', change: 1.35, volume: 24484858, topday: '04/28/2010' },
+ * { symbol: "sencha", date: '2011/04/22', change: 8.85, volume: 5556351, topday: '04/22/2010' }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Date Column Demo',
+ * store: Ext.data.StoreManager.lookup('sampleStore'),
+ * columns: [
+ * { text: 'Symbol', dataIndex: 'symbol', flex: 1 },
+ * { text: 'Date', dataIndex: 'date', xtype: 'datecolumn', format:'Y-m-d' },
+ * { text: 'Change', dataIndex: 'change', xtype: 'numbercolumn', format:'0.00' },
+ * { text: 'Volume', dataIndex: 'volume', xtype: 'numbercolumn', format:'0,000' },
+ * { text: 'Top Day', dataIndex: 'topday', xtype: 'datecolumn', format:'l' }
+ * ],
+ * height: 200,
+ * width: 450,
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.grid.column.Date', {
+ extend: 'Ext.grid.column.Column',
+ alias: ['widget.datecolumn'],
+ requires: ['Ext.Date'],
+ alternateClassName: 'Ext.grid.DateColumn',
+
+ /**
+ * @cfg {String} format
+ * A formatting string as used by {@link Ext.Date#format} to format a Date for this Column.
+ * This defaults to the default date from {@link Ext.Date#defaultFormat} which itself my be overridden
+ * in a locale file.
+ */
+
+ initComponent: function(){
+ var me = this;
+
+ me.callParent(arguments);
+ if (!me.format) {
+ me.format = Ext.Date.defaultFormat;
+ }
+ me.renderer = Ext.util.Format.dateRenderer(me.format);
+ }
+});
+/**
+ * A Column definition class which renders a numeric data field according to a {@link #format} string.
+ *
+ * @example
+ * Ext.create('Ext.data.Store', {
+ * storeId:'sampleStore',
+ * fields:[
+ * { name: 'symbol', type: 'string' },
+ * { name: 'price', type: 'number' },
+ * { name: 'change', type: 'number' },
+ * { name: 'volume', type: 'number' },
+ * ],
+ * data:[
+ * { symbol: "msft", price: 25.76, change: 2.43, volume: 61606325 },
+ * { symbol: "goog", price: 525.73, change: 0.81, volume: 3053782 },
+ * { symbol: "apple", price: 342.41, change: 1.35, volume: 24484858 },
+ * { symbol: "sencha", price: 142.08, change: 8.85, volume: 5556351 }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Number Column Demo',
+ * store: Ext.data.StoreManager.lookup('sampleStore'),
+ * columns: [
+ * { text: 'Symbol', dataIndex: 'symbol', flex: 1 },
+ * { text: 'Current Price', dataIndex: 'price', renderer: Ext.util.Format.usMoney },
+ * { text: 'Change', dataIndex: 'change', xtype: 'numbercolumn', format:'0.00' },
+ * { text: 'Volume', dataIndex: 'volume', xtype: 'numbercolumn', format:'0,000' }
+ * ],
+ * height: 200,
+ * width: 400,
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.grid.column.Number', {
+ extend: 'Ext.grid.column.Column',
+ alias: ['widget.numbercolumn'],
+ requires: ['Ext.util.Format'],
+ alternateClassName: 'Ext.grid.NumberColumn',
+
+ /**
+ * @cfg {String} format
+ * A formatting string as used by {@link Ext.util.Format#number} to format a numeric value for this Column.
+ */
+ format : '0,000.00',
+
+ constructor: function(cfg) {
+ this.callParent(arguments);
+ this.renderer = Ext.util.Format.numberRenderer(this.format);
+ }
+});
+/**
+ * A Column definition class which renders a value by processing a {@link Ext.data.Model Model}'s
+ * {@link Ext.data.Model#persistenceProperty data} using a {@link #tpl configured}
+ * {@link Ext.XTemplate XTemplate}.
+ *
+ * @example
+ * Ext.create('Ext.data.Store', {
+ * storeId:'employeeStore',
+ * fields:['firstname', 'lastname', 'senority', 'department'],
+ * groupField: 'department',
+ * data:[
+ * { firstname: "Michael", lastname: "Scott", senority: 7, department: "Manangement" },
+ * { firstname: "Dwight", lastname: "Schrute", senority: 2, department: "Sales" },
+ * { firstname: "Jim", lastname: "Halpert", senority: 3, department: "Sales" },
+ * { firstname: "Kevin", lastname: "Malone", senority: 4, department: "Accounting" },
+ * { firstname: "Angela", lastname: "Martin", senority: 5, department: "Accounting" }
+ * ]
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Column Template Demo',
+ * store: Ext.data.StoreManager.lookup('employeeStore'),
+ * columns: [
+ * { text: 'Full Name', xtype: 'templatecolumn', tpl: '{firstname} {lastname}', flex:1 },
+ * { text: 'Deparment (Yrs)', xtype: 'templatecolumn', tpl: '{department} ({senority})' }
+ * ],
+ * height: 200,
+ * width: 300,
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.grid.column.Template', {
+ extend: 'Ext.grid.column.Column',
+ alias: ['widget.templatecolumn'],
+ requires: ['Ext.XTemplate'],
+ alternateClassName: 'Ext.grid.TemplateColumn',
+
+ /**
+ * @cfg {String/Ext.XTemplate} tpl
+ * An {@link Ext.XTemplate XTemplate}, or an XTemplate *definition string* to use to process a
+ * {@link Ext.data.Model Model}'s {@link Ext.data.Model#persistenceProperty data} to produce a
+ * column's rendered value.
+ */
+
+ constructor: function(cfg){
+ var me = this,
+ tpl;
+
+ me.callParent(arguments);
+ tpl = me.tpl = (!Ext.isPrimitive(me.tpl) && me.tpl.compile) ? me.tpl : Ext.create('Ext.XTemplate', me.tpl);
+
+ me.renderer = function(value, p, record) {
+ var data = Ext.apply({}, record.data, record.getAssociatedData());
+ return tpl.apply(data);
+ };
+ }
+});
+
+/**
+ * @class Ext.grid.feature.Feature
+ * @extends Ext.util.Observable
+ *
+ * A feature is a type of plugin that is specific to the {@link Ext.grid.Panel}. It provides several
+ * hooks that allows the developer to inject additional functionality at certain points throughout the
+ * grid creation cycle. This class provides the base template methods that are available to the developer,
+ * it should be extended.
+ *
+ * There are several built in features that extend this class, for example:
+ *
+ * - {@link Ext.grid.feature.Grouping} - Shows grid rows in groups as specified by the {@link Ext.data.Store}
+ * - {@link Ext.grid.feature.RowBody} - Adds a body section for each grid row that can contain markup.
+ * - {@link Ext.grid.feature.Summary} - Adds a summary row at the bottom of the grid with aggregate totals for a column.
+ *
+ * ## Using Features
+ * A feature is added to the grid by specifying it an array of features in the configuration:
+ *
+ * var groupingFeature = Ext.create('Ext.grid.feature.Grouping');
+ * Ext.create('Ext.grid.Panel', {
+ * // other options
+ * features: [groupingFeature]
+ * });
+ *
+ * @abstract
+ */
+Ext.define('Ext.grid.feature.Feature', {
+ extend: 'Ext.util.Observable',
+ alias: 'feature.feature',
+
+ isFeature: true,
+ disabled: false,
+
+ /**
+ * @property {Boolean}
+ * Most features will expose additional events, some may not and will
+ * need to change this to false.
+ */
+ hasFeatureEvent: true,
+
+ /**
+ * @property {String}
+ * Prefix to use when firing events on the view.
+ * For example a prefix of group would expose "groupclick", "groupcontextmenu", "groupdblclick".
+ */
+ eventPrefix: null,
+
+ /**
+ * @property {String}
+ * Selector used to determine when to fire the event with the eventPrefix.
+ */
+ eventSelector: null,
+
+ /**
+ * @property {Ext.view.Table}
+ * Reference to the TableView.
+ */
+ view: null,
+
+ /**
+ * @property {Ext.grid.Panel}
+ * Reference to the grid panel
+ */
+ grid: null,
+
+ /**
+ * Most features will not modify the data returned to the view.
+ * This is limited to one feature that manipulates the data per grid view.
+ */
+ collectData: false,
+
+ getFeatureTpl: function() {
+ return '';
+ },
+
+ /**
+ * Abstract method to be overriden when a feature should add additional
+ * arguments to its event signature. By default the event will fire:
+ * - view - The underlying Ext.view.Table
+ * - featureTarget - The matched element by the defined {@link #eventSelector}
+ *
+ * The method must also return the eventName as the first index of the array
+ * to be passed to fireEvent.
+ * @template
+ */
+ getFireEventArgs: function(eventName, view, featureTarget, e) {
+ return [eventName, view, featureTarget, e];
+ },
+
+ /**
+ * Approriate place to attach events to the view, selectionmodel, headerCt, etc
+ * @template
+ */
+ attachEvents: function() {
+
+ },
+
+ getFragmentTpl: function() {
+ return;
+ },
+
+ /**
+ * Allows a feature to mutate the metaRowTpl.
+ * The array received as a single argument can be manipulated to add things
+ * on the end/begining of a particular row.
+ * @template
+ */
+ mutateMetaRowTpl: function(metaRowTplArray) {
+
+ },
+
+ /**
+ * Allows a feature to inject member methods into the metaRowTpl. This is
+ * important for embedding functionality which will become part of the proper
+ * row tpl.
+ * @template
+ */
+ getMetaRowTplFragments: function() {
+ return {};
+ },
+
+ getTableFragments: function() {
+ return {};
+ },
+
+ /**
+ * Provide additional data to the prepareData call within the grid view.
+ * @param {Object} data The data for this particular record.
+ * @param {Number} idx The row index for this record.
+ * @param {Ext.data.Model} record The record instance
+ * @param {Object} orig The original result from the prepareData call to massage.
+ * @template
+ */
+ getAdditionalData: function(data, idx, record, orig) {
+ return {};
+ },
+
+ /**
+ * Enable a feature
+ */
+ enable: function() {
+ this.disabled = false;
+ },
+
+ /**
+ * Disable a feature
+ */
+ disable: function() {
+ this.disabled = true;
+ }
+
+});
+/**
+ * @class Ext.grid.feature.AbstractSummary
+ * @extends Ext.grid.feature.Feature
+ * A small abstract class that contains the shared behaviour for any summary
+ * calculations to be used in the grid.
+ */
+Ext.define('Ext.grid.feature.AbstractSummary', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.grid.feature.Feature',
+
+ alias: 'feature.abstractsummary',
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Boolean} showSummaryRow True to show the summary row. Defaults to <tt>true</tt>.
+ */
+ showSummaryRow: true,
+
+ // @private
+ nestedIdRe: /\{\{id\}([\w\-]*)\}/g,
+
+ /**
+ * Toggle whether or not to show the summary row.
+ * @param {Boolean} visible True to show the summary row
+ */
+ toggleSummaryRow: function(visible){
+ this.showSummaryRow = !!visible;
+ },
+
+ /**
+ * Gets any fragments to be used in the tpl
+ * @private
+ * @return {Object} The fragments
+ */
+ getSummaryFragments: function(){
+ var fragments = {};
+ if (this.showSummaryRow) {
+ Ext.apply(fragments, {
+ printSummaryRow: Ext.bind(this.printSummaryRow, this)
+ });
+ }
+ return fragments;
+ },
+
+ /**
+ * Prints a summary row
+ * @private
+ * @param {Object} index The index in the template
+ * @return {String} The value of the summary row
+ */
+ printSummaryRow: function(index){
+ var inner = this.view.getTableChunker().metaRowTpl.join(''),
+ prefix = Ext.baseCSSPrefix;
+
+ inner = inner.replace(prefix + 'grid-row', prefix + 'grid-row-summary');
+ inner = inner.replace('{{id}}', '{gridSummaryValue}');
+ inner = inner.replace(this.nestedIdRe, '{id$1}');
+ inner = inner.replace('{[this.embedRowCls()]}', '{rowCls}');
+ inner = inner.replace('{[this.embedRowAttr()]}', '{rowAttr}');
+ inner = Ext.create('Ext.XTemplate', inner, {
+ firstOrLastCls: Ext.view.TableChunker.firstOrLastCls
+ });
+
+ return inner.applyTemplate({
+ columns: this.getPrintData(index)
+ });
+ },
+
+ /**
+ * Gets the value for the column from the attached data.
+ * @param {Ext.grid.column.Column} column The header
+ * @param {Object} data The current data
+ * @return {String} The value to be rendered
+ */
+ getColumnValue: function(column, summaryData){
+ var comp = Ext.getCmp(column.id),
+ value = summaryData[column.id],
+ renderer = comp.summaryRenderer;
+
+ if (renderer) {
+ value = renderer.call(
+ comp.scope || this,
+ value,
+ summaryData,
+ column.dataIndex
+ );
+ }
+ return value;
+ },
+
+ /**
+ * Get the summary data for a field.
+ * @private
+ * @param {Ext.data.Store} store The store to get the data from
+ * @param {String/Function} type The type of aggregation. If a function is specified it will
+ * be passed to the stores aggregate function.
+ * @param {String} field The field to aggregate on
+ * @param {Boolean} group True to aggregate in grouped mode
+ * @return {Number/String/Object} See the return type for the store functions.
+ */
+ getSummary: function(store, type, field, group){
+ if (type) {
+ if (Ext.isFunction(type)) {
+ return store.aggregate(type, null, group);
+ }
+
+ switch (type) {
+ case 'count':
+ return store.count(group);
+ case 'min':
+ return store.min(field, group);
+ case 'max':
+ return store.max(field, group);
+ case 'sum':
+ return store.sum(field, group);
+ case 'average':
+ return store.average(field, group);
+ default:
+ return group ? {} : '';
+
+ }
+ }
+ }
+
+});
+
+/**
+ * @class Ext.grid.feature.Chunking
+ * @extends Ext.grid.feature.Feature
+ */
+Ext.define('Ext.grid.feature.Chunking', {
+ extend: 'Ext.grid.feature.Feature',
+ alias: 'feature.chunking',
+
+ chunkSize: 20,
+ rowHeight: Ext.isIE ? 27 : 26,
+ visibleChunk: 0,
+ hasFeatureEvent: false,
+ attachEvents: function() {
+ var grid = this.view.up('gridpanel'),
+ scroller = grid.down('gridscroller[dock=right]');
+ scroller.el.on('scroll', this.onBodyScroll, this, {buffer: 300});
+ //this.view.on('bodyscroll', this.onBodyScroll, this, {buffer: 300});
+ },
+
+ onBodyScroll: function(e, t) {
+ var view = this.view,
+ top = t.scrollTop,
+ nextChunk = Math.floor(top / this.rowHeight / this.chunkSize);
+ if (nextChunk !== this.visibleChunk) {
+
+ this.visibleChunk = nextChunk;
+ view.refresh();
+ view.el.dom.scrollTop = top;
+ //BrowserBug: IE6,7,8 quirks mode takes setting scrollTop 2x.
+ view.el.dom.scrollTop = top;
+ }
+ },
+
+ collectData: function(records, preppedRecords, startIndex, fullWidth, orig) {
+ var o = {
+ fullWidth: orig.fullWidth,
+ chunks: []
+ },
+ //headerCt = this.view.headerCt,
+ //colums = headerCt.getColumnsForTpl(),
+ recordCount = orig.rows.length,
+ start = 0,
+ i = 0,
+ visibleChunk = this.visibleChunk,
+ chunk,
+ rows,
+ chunkLength;
+
+ for (; start < recordCount; start+=this.chunkSize, i++) {
+ if (start+this.chunkSize > recordCount) {
+ chunkLength = recordCount - start;
+ } else {
+ chunkLength = this.chunkSize;
+ }
+
+ if (i >= visibleChunk - 1 && i <= visibleChunk + 1) {
+ rows = orig.rows.slice(start, start+this.chunkSize);
+ } else {
+ rows = [];
+ }
+ o.chunks.push({
+ rows: rows,
+ fullWidth: fullWidth,
+ chunkHeight: chunkLength * this.rowHeight
+ });
+ }
+
+
+ return o;
+ },
+
+ getTableFragments: function() {
+ return {
+ openTableWrap: function() {
+ return '<tpl for="chunks"><div class="' + Ext.baseCSSPrefix + 'grid-chunk" style="height: {chunkHeight}px;">';
+ },
+ closeTableWrap: function() {
+ return '</div></tpl>';
+ }
+ };
+ }
+});
+
+/**
+ * @class Ext.grid.feature.Grouping
+ * @extends Ext.grid.feature.Feature
+ *
+ * This feature allows to display the grid rows aggregated into groups as specified by the {@link Ext.data.Store#groupers}
+ * specified on the Store. The group will show the title for the group name and then the appropriate records for the group
+ * underneath. The groups can also be expanded and collapsed.
+ *
+ * ## Extra Events
+ * This feature adds several extra events that will be fired on the grid to interact with the groups:
+ *
+ * - {@link #groupclick}
+ * - {@link #groupdblclick}
+ * - {@link #groupcontextmenu}
+ * - {@link #groupexpand}
+ * - {@link #groupcollapse}
+ *
+ * ## Menu Augmentation
+ * This feature adds extra options to the grid column menu to provide the user with functionality to modify the grouping.
+ * This can be disabled by setting the {@link #enableGroupingMenu} option. The option to disallow grouping from being turned off
+ * by thew user is {@link #enableNoGroups}.
+ *
+ * ## Controlling Group Text
+ * The {@link #groupHeaderTpl} is used to control the rendered title for each group. It can modified to customized
+ * the default display.
+ *
+ * ## Example Usage
+ *
+ * var groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
+ * groupHeaderTpl: 'Group: {name} ({rows.length})', //print the number of items in the group
+ * startCollapsed: true // start all groups collapsed
+ * });
+ *
+ * @ftype grouping
+ * @author Nicolas Ferrero
+ */
+Ext.define('Ext.grid.feature.Grouping', {
+ extend: 'Ext.grid.feature.Feature',
+ alias: 'feature.grouping',
+
+ eventPrefix: 'group',
+ eventSelector: '.' + Ext.baseCSSPrefix + 'grid-group-hd',
+
+ constructor: function() {
+ var me = this;
+
+ me.collapsedState = {};
+ me.callParent(arguments);
+ },
+
+ /**
+ * @event groupclick
+ * @param {Ext.view.Table} view
+ * @param {HTMLElement} node
+ * @param {String} group The name of the group
+ * @param {Ext.EventObject} e
+ */
+
+ /**
+ * @event groupdblclick
+ * @param {Ext.view.Table} view
+ * @param {HTMLElement} node
+ * @param {String} group The name of the group
+ * @param {Ext.EventObject} e
+ */
+
+ /**
+ * @event groupcontextmenu
+ * @param {Ext.view.Table} view
+ * @param {HTMLElement} node
+ * @param {String} group The name of the group
+ * @param {Ext.EventObject} e
+ */
+
+ /**
+ * @event groupcollapse
+ * @param {Ext.view.Table} view
+ * @param {HTMLElement} node
+ * @param {String} group The name of the group
+ * @param {Ext.EventObject} e
+ */
+
+ /**
+ * @event groupexpand
+ * @param {Ext.view.Table} view
+ * @param {HTMLElement} node
+ * @param {String} group The name of the group
+ * @param {Ext.EventObject} e
+ */
+
+ /**
+ * @cfg {String} groupHeaderTpl
+ * Template snippet, this cannot be an actual template. {name} will be replaced with the current group.
+ * Defaults to 'Group: {name}'
+ */
+ groupHeaderTpl: 'Group: {name}',
+
+ /**
+ * @cfg {Number} depthToIndent
+ * Number of pixels to indent per grouping level
+ */
+ depthToIndent: 17,
+
+ collapsedCls: Ext.baseCSSPrefix + 'grid-group-collapsed',
+ hdCollapsedCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsed',
+
+ /**
+ * @cfg {String} groupByText Text displayed in the grid header menu for grouping by header.
+ */
+ groupByText : 'Group By This Field',
+ /**
+ * @cfg {String} showGroupsText Text displayed in the grid header for enabling/disabling grouping.
+ */
+ showGroupsText : 'Show in Groups',
+
+ /**
+ * @cfg {Boolean} hideGroupedHeader<tt>true</tt> to hide the header that is currently grouped.
+ */
+ hideGroupedHeader : false,
+
+ /**
+ * @cfg {Boolean} startCollapsed <tt>true</tt> to start all groups collapsed
+ */
+ startCollapsed : false,
+
+ /**
+ * @cfg {Boolean} enableGroupingMenu <tt>true</tt> to enable the grouping control in the header menu
+ */
+ enableGroupingMenu : true,
+
+ /**
+ * @cfg {Boolean} enableNoGroups <tt>true</tt> to allow the user to turn off grouping
+ */
+ enableNoGroups : true,
+
+ enable: function() {
+ var me = this,
+ view = me.view,
+ store = view.store,
+ groupToggleMenuItem;
+
+ me.lastGroupField = me.getGroupField();
+
+ if (me.lastGroupIndex) {
+ store.group(me.lastGroupIndex);
+ }
+ me.callParent();
+ groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
+ groupToggleMenuItem.setChecked(true, true);
+ me.refreshIf();
+ },
+
+ disable: function() {
+ var me = this,
+ view = me.view,
+ store = view.store,
+ remote = store.remoteGroup,
+ groupToggleMenuItem,
+ lastGroup;
+
+ lastGroup = store.groupers.first();
+ if (lastGroup) {
+ me.lastGroupIndex = lastGroup.property;
+ me.block();
+ store.clearGrouping();
+ me.unblock();
+ }
+
+ me.callParent();
+ groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem');
+ groupToggleMenuItem.setChecked(true, true);
+ groupToggleMenuItem.setChecked(false, true);
+ if (!remote) {
+ view.refresh();
+ }
+ },
+
+ refreshIf: function() {
+ if (this.blockRefresh !== true) {
+ this.view.refresh();
+ }
+ },
+
+ getFeatureTpl: function(values, parent, x, xcount) {
+ var me = this;
+
+ return [
+ '<tpl if="typeof rows !== \'undefined\'">',
+ // group row tpl
+ '<tr class="' + Ext.baseCSSPrefix + 'grid-group-hd ' + (me.startCollapsed ? me.hdCollapsedCls : '') + ' {hdCollapsedCls}"><td class="' + Ext.baseCSSPrefix + 'grid-cell" colspan="' + parent.columns.length + '" {[this.indentByDepth(values)]}><div class="' + Ext.baseCSSPrefix + 'grid-cell-inner"><div class="' + Ext.baseCSSPrefix + 'grid-group-title">{collapsed}' + me.groupHeaderTpl + '</div></div></td></tr>',
+ // this is the rowbody
+ '<tr id="{viewId}-gp-{name}" class="' + Ext.baseCSSPrefix + 'grid-group-body ' + (me.startCollapsed ? me.collapsedCls : '') + ' {collapsedCls}"><td colspan="' + parent.columns.length + '">{[this.recurse(values)]}</td></tr>',
+ '</tpl>'
+ ].join('');
+ },
+
+ getFragmentTpl: function() {
+ return {
+ indentByDepth: this.indentByDepth,
+ depthToIndent: this.depthToIndent
+ };
+ },
+
+ indentByDepth: function(values) {
+ var depth = values.depth || 0;
+ return 'style="padding-left:'+ depth * this.depthToIndent + 'px;"';
+ },
+
+ // Containers holding these components are responsible for
+ // destroying them, we are just deleting references.
+ destroy: function() {
+ var me = this;
+
+ delete me.view;
+ delete me.prunedHeader;
+ },
+
+ // perhaps rename to afterViewRender
+ attachEvents: function() {
+ var me = this,
+ view = me.view;
+
+ view.on({
+ scope: me,
+ groupclick: me.onGroupClick,
+ rowfocus: me.onRowFocus
+ });
+ view.store.on('groupchange', me.onGroupChange, me);
+
+ me.pruneGroupedHeader();
+
+ if (me.enableGroupingMenu) {
+ me.injectGroupingMenu();
+ }
+ me.lastGroupField = me.getGroupField();
+ me.block();
+ me.onGroupChange();
+ me.unblock();
+ },
+
+ injectGroupingMenu: function() {
+ var me = this,
+ view = me.view,
+ headerCt = view.headerCt;
+ headerCt.showMenuBy = me.showMenuBy;
+ headerCt.getMenuItems = me.getMenuItems();
+ },
+
+ showMenuBy: function(t, header) {
+ var menu = this.getMenu(),
+ groupMenuItem = menu.down('#groupMenuItem'),
+ groupableMth = header.groupable === false ? 'disable' : 'enable';
+
+ groupMenuItem[groupableMth]();
+ Ext.grid.header.Container.prototype.showMenuBy.apply(this, arguments);
+ },
+
+ getMenuItems: function() {
+ var me = this,
+ groupByText = me.groupByText,
+ disabled = me.disabled,
+ showGroupsText = me.showGroupsText,
+ enableNoGroups = me.enableNoGroups,
+ groupMenuItemClick = Ext.Function.bind(me.onGroupMenuItemClick, me),
+ groupToggleMenuItemClick = Ext.Function.bind(me.onGroupToggleMenuItemClick, me);
+
+ // runs in the scope of headerCt
+ return function() {
+ var o = Ext.grid.header.Container.prototype.getMenuItems.call(this);
+ o.push('-', {
+ iconCls: Ext.baseCSSPrefix + 'group-by-icon',
+ itemId: 'groupMenuItem',
+ text: groupByText,
+ handler: groupMenuItemClick
+ });
+ if (enableNoGroups) {
+ o.push({
+ itemId: 'groupToggleMenuItem',
+ text: showGroupsText,
+ checked: !disabled,
+ checkHandler: groupToggleMenuItemClick
+ });
+ }
+ return o;
+ };
+ },
+
+
+ /**
+ * Group by the header the user has clicked on.
+ * @private
+ */
+ onGroupMenuItemClick: function(menuItem, e) {
+ var me = this,
+ menu = menuItem.parentMenu,
+ hdr = menu.activeHeader,
+ view = me.view,
+ store = view.store,
+ remote = store.remoteGroup;
+
+ delete me.lastGroupIndex;
+ me.block();
+ me.enable();
+ store.group(hdr.dataIndex);
+ me.pruneGroupedHeader();
+ me.unblock();
+ if (!remote) {
+ view.refresh();
+ }
+ },
+
+ block: function(){
+ this.blockRefresh = this.view.blockRefresh = true;
+ },
+
+ unblock: function(){
+ this.blockRefresh = this.view.blockRefresh = false;
+ },
+
+ /**
+ * Turn on and off grouping via the menu
+ * @private
+ */
+ onGroupToggleMenuItemClick: function(menuItem, checked) {
+ this[checked ? 'enable' : 'disable']();
+ },
+
+ /**
+ * Prunes the grouped header from the header container
+ * @private
+ */
+ pruneGroupedHeader: function() {
+ var me = this,
+ view = me.view,
+ store = view.store,
+ groupField = me.getGroupField(),
+ headerCt = view.headerCt,
+ header = headerCt.down('header[dataIndex=' + groupField + ']');
+
+ if (header) {
+ if (me.prunedHeader) {
+ me.prunedHeader.show();
+ }
+ me.prunedHeader = header;
+ header.hide();
+ }
+ },
+
+ getGroupField: function(){
+ var group = this.view.store.groupers.first();
+ if (group) {
+ return group.property;
+ }
+ return '';
+ },
+
+ /**
+ * When a row gains focus, expand the groups above it
+ * @private
+ */
+ onRowFocus: function(rowIdx) {
+ var node = this.view.getNode(rowIdx),
+ groupBd = Ext.fly(node).up('.' + this.collapsedCls);
+
+ if (groupBd) {
+ // for multiple level groups, should expand every groupBd
+ // above
+ this.expand(groupBd);
+ }
+ },
+
+ /**
+ * Expand a group by the groupBody
+ * @param {Ext.Element} groupBd
+ * @private
+ */
+ expand: function(groupBd) {
+ var me = this,
+ view = me.view,
+ grid = view.up('gridpanel'),
+ groupBdDom = Ext.getDom(groupBd);
+
+ me.collapsedState[groupBdDom.id] = false;
+
+ groupBd.removeCls(me.collapsedCls);
+ groupBd.prev().removeCls(me.hdCollapsedCls);
+
+ grid.determineScrollbars();
+ grid.invalidateScroller();
+ view.fireEvent('groupexpand');
+ },
+
+ /**
+ * Collapse a group by the groupBody
+ * @param {Ext.Element} groupBd
+ * @private
+ */
+ collapse: function(groupBd) {
+ var me = this,
+ view = me.view,
+ grid = view.up('gridpanel'),
+ groupBdDom = Ext.getDom(groupBd);
+
+ me.collapsedState[groupBdDom.id] = true;
+
+ groupBd.addCls(me.collapsedCls);
+ groupBd.prev().addCls(me.hdCollapsedCls);
+
+ grid.determineScrollbars();
+ grid.invalidateScroller();
+ view.fireEvent('groupcollapse');
+ },
+
+ onGroupChange: function(){
+ var me = this,
+ field = me.getGroupField(),
+ menuItem;
+
+ if (me.hideGroupedHeader) {
+ if (me.lastGroupField) {
+ menuItem = me.getMenuItem(me.lastGroupField);
+ if (menuItem) {
+ menuItem.setChecked(true);
+ }
+ }
+ if (field) {
+ menuItem = me.getMenuItem(field);
+ if (menuItem) {
+ menuItem.setChecked(false);
+ }
+ }
+ }
+ if (me.blockRefresh !== true) {
+ me.view.refresh();
+ }
+ me.lastGroupField = field;
+ },
+
+ /**
+ * Gets the related menu item for a dataIndex
+ * @private
+ * @return {Ext.grid.header.Container} The header
+ */
+ getMenuItem: function(dataIndex){
+ var view = this.view,
+ header = view.headerCt.down('gridcolumn[dataIndex=' + dataIndex + ']'),
+ menu = view.headerCt.getMenu();
+
+ return menu.down('menuitem[headerId='+ header.id +']');
+ },
+
+ /**
+ * Toggle between expanded/collapsed state when clicking on
+ * the group.
+ * @private
+ */
+ onGroupClick: function(view, group, idx, foo, e) {
+ var me = this,
+ toggleCls = me.toggleCls,
+ groupBd = Ext.fly(group.nextSibling, '_grouping');
+
+ if (groupBd.hasCls(me.collapsedCls)) {
+ me.expand(groupBd);
+ } else {
+ me.collapse(groupBd);
+ }
+ },
+
+ // Injects isRow and closeRow into the metaRowTpl.
+ getMetaRowTplFragments: function() {
+ return {
+ isRow: this.isRow,
+ closeRow: this.closeRow
+ };
+ },
+
+ // injected into rowtpl and wrapped around metaRowTpl
+ // becomes part of the standard tpl
+ isRow: function() {
+ return '<tpl if="typeof rows === \'undefined\'">';
+ },
+
+ // injected into rowtpl and wrapped around metaRowTpl
+ // becomes part of the standard tpl
+ closeRow: function() {
+ return '</tpl>';
+ },
+
+ // isRow and closeRow are injected via getMetaRowTplFragments
+ mutateMetaRowTpl: function(metaRowTpl) {
+ metaRowTpl.unshift('{[this.isRow()]}');
+ metaRowTpl.push('{[this.closeRow()]}');
+ },
+
+ // injects an additional style attribute via tdAttrKey with the proper
+ // amount of padding
+ getAdditionalData: function(data, idx, record, orig) {
+ var view = this.view,
+ hCt = view.headerCt,
+ col = hCt.items.getAt(0),
+ o = {},
+ tdAttrKey = col.id + '-tdAttr';
+
+ // maintain the current tdAttr that a user may ahve set.
+ o[tdAttrKey] = this.indentByDepth(data) + " " + (orig[tdAttrKey] ? orig[tdAttrKey] : '');
+ o.collapsed = 'true';
+ return o;
+ },
+
+ // return matching preppedRecords
+ getGroupRows: function(group, records, preppedRecords, fullWidth) {
+ var me = this,
+ children = group.children,
+ rows = group.rows = [],
+ view = me.view;
+ group.viewId = view.id;
+
+ Ext.Array.each(records, function(record, idx) {
+ if (Ext.Array.indexOf(children, record) != -1) {
+ rows.push(Ext.apply(preppedRecords[idx], {
+ depth: 1
+ }));
+ }
+ });
+ delete group.children;
+ group.fullWidth = fullWidth;
+ if (me.collapsedState[view.id + '-gp-' + group.name]) {
+ group.collapsedCls = me.collapsedCls;
+ group.hdCollapsedCls = me.hdCollapsedCls;
+ }
+
+ return group;
+ },
+
+ // return the data in a grouped format.
+ collectData: function(records, preppedRecords, startIndex, fullWidth, o) {
+ var me = this,
+ store = me.view.store,
+ groups;
+
+ if (!me.disabled && store.isGrouped()) {
+ groups = store.getGroups();
+ Ext.Array.each(groups, function(group, idx){
+ me.getGroupRows(group, records, preppedRecords, fullWidth);
+ }, me);
+ return {
+ rows: groups,
+ fullWidth: fullWidth
+ };
+ }
+ return o;
+ },
+
+ // adds the groupName to the groupclick, groupdblclick, groupcontextmenu
+ // events that are fired on the view. Chose not to return the actual
+ // group itself because of its expense and because developers can simply
+ // grab the group via store.getGroups(groupName)
+ getFireEventArgs: function(type, view, featureTarget, e) {
+ var returnArray = [type, view, featureTarget],
+ groupBd = Ext.fly(featureTarget.nextSibling, '_grouping'),
+ groupBdId = Ext.getDom(groupBd).id,
+ prefix = view.id + '-gp-',
+ groupName = groupBdId.substr(prefix.length);
+
+ returnArray.push(groupName, e);
+
+ return returnArray;
+ }
+});
+
+/**
+ * @class Ext.grid.feature.GroupingSummary
+ * @extends Ext.grid.feature.Grouping
+ *
+ * This feature adds an aggregate summary row at the bottom of each group that is provided
+ * by the {@link Ext.grid.feature.Grouping} feature. There are two aspects to the summary:
+ *
+ * ## Calculation
+ *
+ * The summary value needs to be calculated for each column in the grid. This is controlled
+ * by the summaryType option specified on the column. There are several built in summary types,
+ * which can be specified as a string on the column configuration. These call underlying methods
+ * on the store:
+ *
+ * - {@link Ext.data.Store#count count}
+ * - {@link Ext.data.Store#sum sum}
+ * - {@link Ext.data.Store#min min}
+ * - {@link Ext.data.Store#max max}
+ * - {@link Ext.data.Store#average average}
+ *
+ * Alternatively, the summaryType can be a function definition. If this is the case,
+ * the function is called with an array of records to calculate the summary value.
+ *
+ * ## Rendering
+ *
+ * Similar to a column, the summary also supports a summaryRenderer function. This
+ * summaryRenderer is called before displaying a value. The function is optional, if
+ * not specified the default calculated value is shown. The summaryRenderer is called with:
+ *
+ * - value {Object} - The calculated value.
+ * - summaryData {Object} - Contains all raw summary values for the row.
+ * - field {String} - The name of the field we are calculating
+ *
+ * ## Example Usage
+ *
+ * @example
+ * Ext.define('TestResult', {
+ * extend: 'Ext.data.Model',
+ * fields: ['student', 'subject', {
+ * name: 'mark',
+ * type: 'int'
+ * }]
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * width: 200,
+ * height: 240,
+ * renderTo: document.body,
+ * features: [{
+ * groupHeaderTpl: 'Subject: {name}',
+ * ftype: 'groupingsummary'
+ * }],
+ * store: {
+ * model: 'TestResult',
+ * groupField: 'subject',
+ * data: [{
+ * student: 'Student 1',
+ * subject: 'Math',
+ * mark: 84
+ * },{
+ * student: 'Student 1',
+ * subject: 'Science',
+ * mark: 72
+ * },{
+ * student: 'Student 2',
+ * subject: 'Math',
+ * mark: 96
+ * },{
+ * student: 'Student 2',
+ * subject: 'Science',
+ * mark: 68
+ * }]
+ * },
+ * columns: [{
+ * dataIndex: 'student',
+ * text: 'Name',
+ * summaryType: 'count',
+ * summaryRenderer: function(value){
+ * return Ext.String.format('{0} student{1}', value, value !== 1 ? 's' : '');
+ * }
+ * }, {
+ * dataIndex: 'mark',
+ * text: 'Mark',
+ * summaryType: 'average'
+ * }]
+ * });
+ */
+Ext.define('Ext.grid.feature.GroupingSummary', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.grid.feature.Grouping',
+
+ alias: 'feature.groupingsummary',
+
+ mixins: {
+ summary: 'Ext.grid.feature.AbstractSummary'
+ },
+
+ /* End Definitions */
+
+
+ /**
+ * Modifies the row template to include the summary row.
+ * @private
+ * @return {String} The modified template
+ */
+ getFeatureTpl: function() {
+ var tpl = this.callParent(arguments);
+
+ if (this.showSummaryRow) {
+ // lop off the end </tpl> so we can attach it
+ tpl = tpl.replace('</tpl>', '');
+ tpl += '{[this.printSummaryRow(xindex)]}</tpl>';
+ }
+ return tpl;
+ },
+
+ /**
+ * Gets any fragments needed for the template.
+ * @private
+ * @return {Object} The fragments
+ */
+ getFragmentTpl: function() {
+ var me = this,
+ fragments = me.callParent();
+
+ Ext.apply(fragments, me.getSummaryFragments());
+ if (me.showSummaryRow) {
+ // this gets called before render, so we'll setup the data here.
+ me.summaryGroups = me.view.store.getGroups();
+ me.summaryData = me.generateSummaryData();
+ }
+ return fragments;
+ },
+
+ /**
+ * Gets the data for printing a template row
+ * @private
+ * @param {Number} index The index in the template
+ * @return {Array} The template values
+ */
+ getPrintData: function(index){
+ var me = this,
+ columns = me.view.headerCt.getColumnsForTpl(),
+ i = 0,
+ length = columns.length,
+ data = [],
+ name = me.summaryGroups[index - 1].name,
+ active = me.summaryData[name],
+ column;
+
+ for (; i < length; ++i) {
+ column = columns[i];
+ column.gridSummaryValue = this.getColumnValue(column, active);
+ data.push(column);
+ }
+ return data;
+ },
+
+ /**
+ * Generates all of the summary data to be used when processing the template
+ * @private
+ * @return {Object} The summary data
+ */
+ generateSummaryData: function(){
+ var me = this,
+ data = {},
+ remoteData = {},
+ store = me.view.store,
+ groupField = this.getGroupField(),
+ reader = store.proxy.reader,
+ groups = me.summaryGroups,
+ columns = me.view.headerCt.getColumnsForTpl(),
+ remote,
+ i,
+ length,
+ fieldData,
+ root,
+ key,
+ comp;
+
+ for (i = 0, length = groups.length; i < length; ++i) {
+ data[groups[i].name] = {};
+ }
+
+ /**
+ * @cfg {String} [remoteRoot=undefined] The name of the property which contains the Array of
+ * summary objects. It allows to use server-side calculated summaries.
+ */
+ if (me.remoteRoot && reader.rawData) {
+ // reset reader root and rebuild extractors to extract summaries data
+ root = reader.root;
+ reader.root = me.remoteRoot;
+ reader.buildExtractors(true);
+ Ext.Array.each(reader.getRoot(reader.rawData), function(value) {
+ remoteData[value[groupField]] = value;
+ });
+ // restore initial reader configuration
+ reader.root = root;
+ reader.buildExtractors(true);
+ }
+
+ for (i = 0, length = columns.length; i < length; ++i) {
+ comp = Ext.getCmp(columns[i].id);
+ fieldData = me.getSummary(store, comp.summaryType, comp.dataIndex, true);
+
+ for (key in fieldData) {
+ if (fieldData.hasOwnProperty(key)) {
+ data[key][comp.id] = fieldData[key];
+ }
+ }
+
+ for (key in remoteData) {
+ if (remoteData.hasOwnProperty(key)) {
+ remote = remoteData[key][comp.dataIndex];
+ if (remote !== undefined && data[key] !== undefined) {
+ data[key][comp.id] = remote;
+ }
+ }
+ }
+ }
+ return data;
+ }
+});
+
+/**
+ * @class Ext.grid.feature.RowBody
+ * @extends Ext.grid.feature.Feature
+ *
+ * The rowbody feature enhances the grid's markup to have an additional
+ * tr -> td -> div which spans the entire width of the original row.
+ *
+ * This is useful to to associate additional information with a particular
+ * record in a grid.
+ *
+ * Rowbodies are initially hidden unless you override getAdditionalData.
+ *
+ * Will expose additional events on the gridview with the prefix of 'rowbody'.
+ * For example: 'rowbodyclick', 'rowbodydblclick', 'rowbodycontextmenu'.
+ *
+ * @ftype rowbody
+ */
+Ext.define('Ext.grid.feature.RowBody', {
+ extend: 'Ext.grid.feature.Feature',
+ alias: 'feature.rowbody',
+ rowBodyHiddenCls: Ext.baseCSSPrefix + 'grid-row-body-hidden',
+ rowBodyTrCls: Ext.baseCSSPrefix + 'grid-rowbody-tr',
+ rowBodyTdCls: Ext.baseCSSPrefix + 'grid-cell-rowbody',
+ rowBodyDivCls: Ext.baseCSSPrefix + 'grid-rowbody',
+
+ eventPrefix: 'rowbody',
+ eventSelector: '.' + Ext.baseCSSPrefix + 'grid-rowbody-tr',
+
+ getRowBody: function(values) {
+ return [
+ '<tr class="' + this.rowBodyTrCls + ' {rowBodyCls}">',
+ '<td class="' + this.rowBodyTdCls + '" colspan="{rowBodyColspan}">',
+ '<div class="' + this.rowBodyDivCls + '">{rowBody}</div>',
+ '</td>',
+ '</tr>'
+ ].join('');
+ },
+
+ // injects getRowBody into the metaRowTpl.
+ getMetaRowTplFragments: function() {
+ return {
+ getRowBody: this.getRowBody,
+ rowBodyTrCls: this.rowBodyTrCls,
+ rowBodyTdCls: this.rowBodyTdCls,
+ rowBodyDivCls: this.rowBodyDivCls
+ };
+ },
+
+ mutateMetaRowTpl: function(metaRowTpl) {
+ metaRowTpl.push('{[this.getRowBody(values)]}');
+ },
+
+ /**
+ * Provide additional data to the prepareData call within the grid view.
+ * The rowbody feature adds 3 additional variables into the grid view's template.
+ * These are rowBodyCls, rowBodyColspan, and rowBody.
+ * @param {Object} data The data for this particular record.
+ * @param {Number} idx The row index for this record.
+ * @param {Ext.data.Model} record The record instance
+ * @param {Object} orig The original result from the prepareData call to massage.
+ */
+ getAdditionalData: function(data, idx, record, orig) {
+ var headerCt = this.view.headerCt,
+ colspan = headerCt.getColumnCount();
+
+ return {
+ rowBody: "",
+ rowBodyCls: this.rowBodyCls,
+ rowBodyColspan: colspan
+ };
+ }
+});
+/**
+ * @class Ext.grid.feature.RowWrap
+ * @extends Ext.grid.feature.Feature
+ * @private
+ */
+Ext.define('Ext.grid.feature.RowWrap', {
+ extend: 'Ext.grid.feature.Feature',
+ alias: 'feature.rowwrap',
+
+ // turn off feature events.
+ hasFeatureEvent: false,
+
+ mutateMetaRowTpl: function(metaRowTpl) {
+ // Remove "x-grid-row" from the first row, note this could be wrong
+ // if some other feature unshifted things in front.
+ metaRowTpl[0] = metaRowTpl[0].replace(Ext.baseCSSPrefix + 'grid-row', '');
+ metaRowTpl[0] = metaRowTpl[0].replace("{[this.embedRowCls()]}", "");
+ // 2
+ metaRowTpl.unshift('<table class="' + Ext.baseCSSPrefix + 'grid-table ' + Ext.baseCSSPrefix + 'grid-table-resizer" style="width: {[this.embedFullWidth()]}px;">');
+ // 1
+ metaRowTpl.unshift('<tr class="' + Ext.baseCSSPrefix + 'grid-row {[this.embedRowCls()]}"><td colspan="{[this.embedColSpan()]}"><div class="' + Ext.baseCSSPrefix + 'grid-rowwrap-div">');
+
+ // 3
+ metaRowTpl.push('</table>');
+ // 4
+ metaRowTpl.push('</div></td></tr>');
+ },
+
+ embedColSpan: function() {
+ return '{colspan}';
+ },
+
+ embedFullWidth: function() {
+ return '{fullWidth}';
+ },
+
+ getAdditionalData: function(data, idx, record, orig) {
+ var headerCt = this.view.headerCt,
+ colspan = headerCt.getColumnCount(),
+ fullWidth = headerCt.getFullWidth(),
+ items = headerCt.query('gridcolumn'),
+ itemsLn = items.length,
+ i = 0,
+ o = {
+ colspan: colspan,
+ fullWidth: fullWidth
+ },
+ id,
+ tdClsKey,
+ colResizerCls;
+
+ for (; i < itemsLn; i++) {
+ id = items[i].id;
+ tdClsKey = id + '-tdCls';
+ colResizerCls = Ext.baseCSSPrefix + 'grid-col-resizer-'+id;
+ // give the inner td's the resizer class
+ // while maintaining anything a user may have injected via a custom
+ // renderer
+ o[tdClsKey] = colResizerCls + " " + (orig[tdClsKey] ? orig[tdClsKey] : '');
+ // TODO: Unhackify the initial rendering width's
+ o[id+'-tdAttr'] = " style=\"width: " + (items[i].hidden ? 0 : items[i].getDesiredWidth()) + "px;\" "/* + (i === 0 ? " rowspan=\"2\"" : "")*/;
+ if (orig[id+'-tdAttr']) {
+ o[id+'-tdAttr'] += orig[id+'-tdAttr'];
+ }
+
+ }
+
+ return o;
+ },
+
+ getMetaRowTplFragments: function() {
+ return {
+ embedFullWidth: this.embedFullWidth,
+ embedColSpan: this.embedColSpan
+ };
+ }
+
+});
+/**
+ * @class Ext.grid.feature.Summary
+ * @extends Ext.grid.feature.AbstractSummary
+ *
+ * This feature is used to place a summary row at the bottom of the grid. If using a grouping,
+ * see {@link Ext.grid.feature.GroupingSummary}. There are 2 aspects to calculating the summaries,
+ * calculation and rendering.
+ *
+ * ## Calculation
+ * The summary value needs to be calculated for each column in the grid. This is controlled
+ * by the summaryType option specified on the column. There are several built in summary types,
+ * which can be specified as a string on the column configuration. These call underlying methods
+ * on the store:
+ *
+ * - {@link Ext.data.Store#count count}
+ * - {@link Ext.data.Store#sum sum}
+ * - {@link Ext.data.Store#min min}
+ * - {@link Ext.data.Store#max max}
+ * - {@link Ext.data.Store#average average}
+ *
+ * Alternatively, the summaryType can be a function definition. If this is the case,
+ * the function is called with an array of records to calculate the summary value.
+ *
+ * ## Rendering
+ * Similar to a column, the summary also supports a summaryRenderer function. This
+ * summaryRenderer is called before displaying a value. The function is optional, if
+ * not specified the default calculated value is shown. The summaryRenderer is called with:
+ *
+ * - value {Object} - The calculated value.
+ * - summaryData {Object} - Contains all raw summary values for the row.
+ * - field {String} - The name of the field we are calculating
+ *
+ * ## Example Usage
+ *
+ * @example
+ * Ext.define('TestResult', {
+ * extend: 'Ext.data.Model',
+ * fields: ['student', {
+ * name: 'mark',
+ * type: 'int'
+ * }]
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * width: 200,
+ * height: 140,
+ * renderTo: document.body,
+ * features: [{
+ * ftype: 'summary'
+ * }],
+ * store: {
+ * model: 'TestResult',
+ * data: [{
+ * student: 'Student 1',
+ * mark: 84
+ * },{
+ * student: 'Student 2',
+ * mark: 72
+ * },{
+ * student: 'Student 3',
+ * mark: 96
+ * },{
+ * student: 'Student 4',
+ * mark: 68
+ * }]
+ * },
+ * columns: [{
+ * dataIndex: 'student',
+ * text: 'Name',
+ * summaryType: 'count',
+ * summaryRenderer: function(value, summaryData, dataIndex) {
+ * return Ext.String.format('{0} student{1}', value, value !== 1 ? 's' : '');
+ * }
+ * }, {
+ * dataIndex: 'mark',
+ * text: 'Mark',
+ * summaryType: 'average'
+ * }]
+ * });
+ */
+Ext.define('Ext.grid.feature.Summary', {
+
+ /* Begin Definitions */
+
+ extend: 'Ext.grid.feature.AbstractSummary',
+
+ alias: 'feature.summary',
+
+ /* End Definitions */
+
+ /**
+ * Gets any fragments needed for the template.
+ * @private
+ * @return {Object} The fragments
+ */
+ getFragmentTpl: function() {
+ // this gets called before render, so we'll setup the data here.
+ this.summaryData = this.generateSummaryData();
+ return this.getSummaryFragments();
+ },
+
+ /**
+ * Overrides the closeRows method on the template so we can include our own custom
+ * footer.
+ * @private
+ * @return {Object} The custom fragments
+ */
+ getTableFragments: function(){
+ if (this.showSummaryRow) {
+ return {
+ closeRows: this.closeRows
+ };
+ }
+ },
+
+ /**
+ * Provide our own custom footer for the grid.
+ * @private
+ * @return {String} The custom footer
+ */
+ closeRows: function() {
+ return '</tpl>{[this.printSummaryRow()]}';
+ },
+
+ /**
+ * Gets the data for printing a template row
+ * @private
+ * @param {Number} index The index in the template
+ * @return {Array} The template values
+ */
+ getPrintData: function(index){
+ var me = this,
+ columns = me.view.headerCt.getColumnsForTpl(),
+ i = 0,
+ length = columns.length,
+ data = [],
+ active = me.summaryData,
+ column;
+
+ for (; i < length; ++i) {
+ column = columns[i];
+ column.gridSummaryValue = this.getColumnValue(column, active);
+ data.push(column);
+ }
+ return data;
+ },
+
+ /**
+ * Generates all of the summary data to be used when processing the template
+ * @private
+ * @return {Object} The summary data
+ */
+ generateSummaryData: function(){
+ var me = this,
+ data = {},
+ store = me.view.store,
+ columns = me.view.headerCt.getColumnsForTpl(),
+ i = 0,
+ length = columns.length,
+ fieldData,
+ key,
+ comp;
+
+ for (i = 0, length = columns.length; i < length; ++i) {
+ comp = Ext.getCmp(columns[i].id);
+ data[comp.id] = me.getSummary(store, comp.summaryType, comp.dataIndex, false);
+ }
+ return data;
+ }
+});
+/**
+ * @class Ext.grid.header.DragZone
+ * @extends Ext.dd.DragZone
+ * @private
+ */
+Ext.define('Ext.grid.header.DragZone', {
+ extend: 'Ext.dd.DragZone',
+ colHeaderCls: Ext.baseCSSPrefix + 'column-header',
+ maxProxyWidth: 120,
+
+ constructor: function(headerCt) {
+ this.headerCt = headerCt;
+ this.ddGroup = this.getDDGroup();
+ this.callParent([headerCt.el]);
+ this.proxy.el.addCls(Ext.baseCSSPrefix + 'grid-col-dd');
+ },
+
+ getDDGroup: function() {
+ return 'header-dd-zone-' + this.headerCt.up('[scrollerOwner]').id;
+ },
+
+ getDragData: function(e) {
+ var header = e.getTarget('.'+this.colHeaderCls),
+ headerCmp;
+
+ if (header) {
+ headerCmp = Ext.getCmp(header.id);
+ if (!this.headerCt.dragging && headerCmp.draggable && !(headerCmp.isOnLeftEdge(e) || headerCmp.isOnRightEdge(e))) {
+ var ddel = document.createElement('div');
+ ddel.innerHTML = Ext.getCmp(header.id).text;
+ return {
+ ddel: ddel,
+ header: headerCmp
+ };
+ }
+ }
+ return false;
+ },
+
+ onBeforeDrag: function() {
+ return !(this.headerCt.dragging || this.disabled);
+ },
+
+ onInitDrag: function() {
+ this.headerCt.dragging = true;
+ this.callParent(arguments);
+ },
+
+ onDragDrop: function() {
+ this.headerCt.dragging = false;
+ this.callParent(arguments);
+ },
+
+ afterRepair: function() {
+ this.callParent();
+ this.headerCt.dragging = false;
+ },
+
+ getRepairXY: function() {
+ return this.dragData.header.el.getXY();
+ },
+
+ disable: function() {
+ this.disabled = true;
+ },
+
+ enable: function() {
+ this.disabled = false;
+ }
+});
+
+/**
+ * @class Ext.grid.header.DropZone
+ * @extends Ext.dd.DropZone
+ * @private
+ */
+Ext.define('Ext.grid.header.DropZone', {
+ extend: 'Ext.dd.DropZone',
+ colHeaderCls: Ext.baseCSSPrefix + 'column-header',
+ proxyOffsets: [-4, -9],
+
+ constructor: function(headerCt){
+ this.headerCt = headerCt;
+ this.ddGroup = this.getDDGroup();
+ this.callParent([headerCt.el]);
+ },
+
+ getDDGroup: function() {
+ return 'header-dd-zone-' + this.headerCt.up('[scrollerOwner]').id;
+ },
+
+ getTargetFromEvent : function(e){
+ return e.getTarget('.' + this.colHeaderCls);
+ },
+
+ getTopIndicator: function() {
+ if (!this.topIndicator) {
+ this.topIndicator = Ext.DomHelper.append(Ext.getBody(), {
+ cls: "col-move-top",
+ html: " "
+ }, true);
+ }
+ return this.topIndicator;
+ },
+
+ getBottomIndicator: function() {
+ if (!this.bottomIndicator) {
+ this.bottomIndicator = Ext.DomHelper.append(Ext.getBody(), {
+ cls: "col-move-bottom",
+ html: " "
+ }, true);
+ }
+ return this.bottomIndicator;
+ },
+
+ getLocation: function(e, t) {
+ var x = e.getXY()[0],
+ region = Ext.fly(t).getRegion(),
+ pos, header;
+
+ if ((region.right - x) <= (region.right - region.left) / 2) {
+ pos = "after";
+ } else {
+ pos = "before";
+ }
+ return {
+ pos: pos,
+ header: Ext.getCmp(t.id),
+ node: t
+ };
+ },
+
+ positionIndicator: function(draggedHeader, node, e){
+ var location = this.getLocation(e, node),
+ header = location.header,
+ pos = location.pos,
+ nextHd = draggedHeader.nextSibling('gridcolumn:not([hidden])'),
+ prevHd = draggedHeader.previousSibling('gridcolumn:not([hidden])'),
+ region, topIndicator, bottomIndicator, topAnchor, bottomAnchor,
+ topXY, bottomXY, headerCtEl, minX, maxX;
+
+ // Cannot drag beyond non-draggable start column
+ if (!header.draggable && header.getIndex() == 0) {
+ return false;
+ }
+
+ this.lastLocation = location;
+
+ if ((draggedHeader !== header) &&
+ ((pos === "before" && nextHd !== header) ||
+ (pos === "after" && prevHd !== header)) &&
+ !header.isDescendantOf(draggedHeader)) {
+
+ // As we move in between different DropZones that are in the same
+ // group (such as the case when in a locked grid), invalidateDrop
+ // on the other dropZones.
+ var allDropZones = Ext.dd.DragDropManager.getRelated(this),
+ ln = allDropZones.length,
+ i = 0,
+ dropZone;
+
+ for (; i < ln; i++) {
+ dropZone = allDropZones[i];
+ if (dropZone !== this && dropZone.invalidateDrop) {
+ dropZone.invalidateDrop();
+ }
+ }
+
+
+ this.valid = true;
+ topIndicator = this.getTopIndicator();
+ bottomIndicator = this.getBottomIndicator();
+ if (pos === 'before') {
+ topAnchor = 'tl';
+ bottomAnchor = 'bl';
+ } else {
+ topAnchor = 'tr';
+ bottomAnchor = 'br';
+ }
+ topXY = header.el.getAnchorXY(topAnchor);
+ bottomXY = header.el.getAnchorXY(bottomAnchor);
+
+ // constrain the indicators to the viewable section
+ headerCtEl = this.headerCt.el;
+ minX = headerCtEl.getLeft();
+ maxX = headerCtEl.getRight();
+
+ topXY[0] = Ext.Number.constrain(topXY[0], minX, maxX);
+ bottomXY[0] = Ext.Number.constrain(bottomXY[0], minX, maxX);
+
+ // adjust by offsets, this is to center the arrows so that they point
+ // at the split point
+ topXY[0] -= 4;
+ topXY[1] -= 9;
+ bottomXY[0] -= 4;
+
+ // position and show indicators
+ topIndicator.setXY(topXY);
+ bottomIndicator.setXY(bottomXY);
+ topIndicator.show();
+ bottomIndicator.show();
+ // invalidate drop operation and hide indicators
+ } else {
+ this.invalidateDrop();
+ }
+ },
+
+ invalidateDrop: function() {
+ this.valid = false;
+ this.hideIndicators();
+ },
+
+ onNodeOver: function(node, dragZone, e, data) {
+ if (data.header.el.dom !== node) {
+ this.positionIndicator(data.header, node, e);
+ }
+ return this.valid ? this.dropAllowed : this.dropNotAllowed;
+ },
+
+ hideIndicators: function() {
+ this.getTopIndicator().hide();
+ this.getBottomIndicator().hide();
+ },
+
+ onNodeOut: function() {
+ this.hideIndicators();
+ },
+
+ onNodeDrop: function(node, dragZone, e, data) {
+ if (this.valid) {
+ this.invalidateDrop();
+ var hd = data.header,
+ lastLocation = this.lastLocation,
+ fromCt = hd.ownerCt,
+ fromIdx = fromCt.items.indexOf(hd), // Container.items is a MixedCollection
+ toCt = lastLocation.header.ownerCt,
+ toIdx = toCt.items.indexOf(lastLocation.header),
+ headerCt = this.headerCt,
+ groupCt,
+ scrollerOwner;
+
+ if (lastLocation.pos === 'after') {
+ toIdx++;
+ }
+
+ // If we are dragging in between two HeaderContainers that have had the lockable
+ // mixin injected we will lock/unlock headers in between sections. Note that lockable
+ // does NOT currently support grouped headers.
+ if (fromCt !== toCt && fromCt.lockableInjected && toCt.lockableInjected && toCt.lockedCt) {
+ scrollerOwner = fromCt.up('[scrollerOwner]');
+ scrollerOwner.lock(hd, toIdx);
+ } else if (fromCt !== toCt && fromCt.lockableInjected && toCt.lockableInjected && fromCt.lockedCt) {
+ scrollerOwner = fromCt.up('[scrollerOwner]');
+ scrollerOwner.unlock(hd, toIdx);
+ } else {
+ // If dragging rightwards, then after removal, the insertion index will be one less when moving
+ // in between the same container.
+ if ((fromCt === toCt) && (toIdx > fromCt.items.indexOf(hd))) {
+ toIdx--;
+ }
+
+ // Remove dragged header from where it was without destroying it or relaying its Container
+ if (fromCt !== toCt) {
+ fromCt.suspendLayout = true;
+ fromCt.remove(hd, false);
+ fromCt.suspendLayout = false;
+ }
+
+ // Dragged the last header out of the fromCt group... The fromCt group must die
+ if (fromCt.isGroupHeader) {
+ if (!fromCt.items.getCount()) {
+ groupCt = fromCt.ownerCt;
+ groupCt.suspendLayout = true;
+ groupCt.remove(fromCt, false);
+ fromCt.el.dom.parentNode.removeChild(fromCt.el.dom);
+ groupCt.suspendLayout = false;
+ } else {
+ fromCt.minWidth = fromCt.getWidth() - hd.getWidth();
+ fromCt.setWidth(fromCt.minWidth);
+ }
+ }
+
+ // Move dragged header into its drop position
+ toCt.suspendLayout = true;
+ if (fromCt === toCt) {
+ toCt.move(fromIdx, toIdx);
+ } else {
+ toCt.insert(toIdx, hd);
+ }
+ toCt.suspendLayout = false;
+
+ // Group headers acquire the aggregate width of their child headers
+ // Therefore a child header may not flex; it must contribute a fixed width.
+ // But we restore the flex value when moving back into the main header container
+ if (toCt.isGroupHeader) {
+ hd.savedFlex = hd.flex;
+ delete hd.flex;
+ hd.width = hd.getWidth();
+ // When there was previously a flex, we need to ensure we don't count for the
+ // border twice.
+ toCt.minWidth = toCt.getWidth() + hd.getWidth() - (hd.savedFlex ? 1 : 0);
+ toCt.setWidth(toCt.minWidth);
+ } else {
+ if (hd.savedFlex) {
+ hd.flex = hd.savedFlex;
+ delete hd.width;
+ }
+ }
+
+
+ // Refresh columns cache in case we remove an emptied group column
+ headerCt.purgeCache();
+ headerCt.doLayout();
+ headerCt.onHeaderMoved(hd, fromIdx, toIdx);
+ // Emptied group header can only be destroyed after the header and grid have been refreshed
+ if (!fromCt.items.getCount()) {
+ fromCt.destroy();
+ }
+ }
+ }
+ }
+});
+
+/**
+ * This class provides an abstract grid editing plugin on selected {@link Ext.grid.column.Column columns}.
+ * The editable columns are specified by providing an {@link Ext.grid.column.Column#editor editor}
+ * in the {@link Ext.grid.column.Column column configuration}.
+ *
+ * **Note:** This class should not be used directly. See {@link Ext.grid.plugin.CellEditing} and
+ * {@link Ext.grid.plugin.RowEditing}.
+ */
+Ext.define('Ext.grid.plugin.Editing', {
+ alias: 'editing.editing',
+
+ requires: [
+ 'Ext.grid.column.Column',
+ 'Ext.util.KeyNav'
+ ],
+
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ /**
+ * @cfg {Number} clicksToEdit
+ * The number of clicks on a grid required to display the editor.
+ */
+ clicksToEdit: 2,
+
+ // private
+ defaultFieldXType: 'textfield',
+
+ // cell, row, form
+ editStyle: '',
+
+ constructor: function(config) {
+ var me = this;
+ Ext.apply(me, config);
+
+ me.addEvents(
+ // Doc'ed in separate editing plugins
+ 'beforeedit',
+
+ // Doc'ed in separate editing plugins
+ 'edit',
+
+ // Doc'ed in separate editing plugins
+ 'validateedit'
+ );
+ me.mixins.observable.constructor.call(me);
+ // TODO: Deprecated, remove in 5.0
+ me.relayEvents(me, ['afteredit'], 'after');
+ },
+
+ // private
+ init: function(grid) {
+ var me = this;
+
+ me.grid = grid;
+ me.view = grid.view;
+ me.initEvents();
+ me.mon(grid, 'reconfigure', me.onReconfigure, me);
+ me.onReconfigure();
+
+ grid.relayEvents(me, ['beforeedit', 'edit', 'validateedit']);
+ // Marks the grid as editable, so that the SelectionModel
+ // can make appropriate decisions during navigation
+ grid.isEditable = true;
+ grid.editingPlugin = grid.view.editingPlugin = me;
+ },
+
+ /**
+ * Fires after the grid is reconfigured
+ * @private
+ */
+ onReconfigure: function(){
+ this.initFieldAccessors(this.view.getGridColumns());
+ },
+
+ /**
+ * @private
+ * AbstractComponent calls destroy on all its plugins at destroy time.
+ */
+ destroy: function() {
+ var me = this,
+ grid = me.grid,
+ headerCt = grid.headerCt,
+ events = grid.events;
+
+ Ext.destroy(me.keyNav);
+ me.removeFieldAccessors(grid.getView().getGridColumns());
+
+ // Clear all listeners from all our events, clear all managed listeners we added to other Observables
+ me.clearListeners();
+
+ delete me.grid.editingPlugin;
+ delete me.grid.view.editingPlugin;
+ delete me.grid;
+ delete me.view;
+ delete me.editor;
+ delete me.keyNav;
+ },
+
+ // private
+ getEditStyle: function() {
+ return this.editStyle;
+ },
+
+ // private
+ initFieldAccessors: function(column) {
+ var me = this;
+
+ if (Ext.isArray(column)) {
+ Ext.Array.forEach(column, me.initFieldAccessors, me);
+ return;
+ }
+
+ // Augment the Header class to have a getEditor and setEditor method
+ // Important: Only if the header does not have its own implementation.
+ Ext.applyIf(column, {
+ getEditor: function(record, defaultField) {
+ return me.getColumnField(this, defaultField);
+ },
+
+ setEditor: function(field) {
+ me.setColumnField(this, field);
+ }
+ });
+ },
+
+ // private
+ removeFieldAccessors: function(column) {
+ var me = this;
+
+ if (Ext.isArray(column)) {
+ Ext.Array.forEach(column, me.removeFieldAccessors, me);
+ return;
+ }
+
+ delete column.getEditor;
+ delete column.setEditor;
+ },
+
+ // private
+ // remaps to the public API of Ext.grid.column.Column.getEditor
+ getColumnField: function(columnHeader, defaultField) {
+ var field = columnHeader.field;
+
+ if (!field && columnHeader.editor) {
+ field = columnHeader.editor;
+ delete columnHeader.editor;
+ }
+
+ if (!field && defaultField) {
+ field = defaultField;
+ }
+
+ if (field) {
+ if (Ext.isString(field)) {
+ field = { xtype: field };
+ }
+ if (Ext.isObject(field) && !field.isFormField) {
+ field = Ext.ComponentManager.create(field, this.defaultFieldXType);
+ columnHeader.field = field;
+ }
+
+ Ext.apply(field, {
+ name: columnHeader.dataIndex
+ });
+
+ return field;
+ }
+ },
+
+ // private
+ // remaps to the public API of Ext.grid.column.Column.setEditor
+ setColumnField: function(column, field) {
+ if (Ext.isObject(field) && !field.isFormField) {
+ field = Ext.ComponentManager.create(field, this.defaultFieldXType);
+ }
+ column.field = field;
+ },
+
+ // private
+ initEvents: function() {
+ var me = this;
+ me.initEditTriggers();
+ me.initCancelTriggers();
+ },
+
+ // @abstract
+ initCancelTriggers: Ext.emptyFn,
+
+ // private
+ initEditTriggers: function() {
+ var me = this,
+ view = me.view,
+ clickEvent = me.clicksToEdit === 1 ? 'click' : 'dblclick';
+
+ // Start editing
+ me.mon(view, 'cell' + clickEvent, me.startEditByClick, me);
+ view.on('render', function() {
+ me.keyNav = Ext.create('Ext.util.KeyNav', view.el, {
+ enter: me.onEnterKey,
+ esc: me.onEscKey,
+ scope: me
+ });
+ }, me, { single: true });
+ },
+
+ // private
+ onEnterKey: function(e) {
+ var me = this,
+ grid = me.grid,
+ selModel = grid.getSelectionModel(),
+ record,
+ columnHeader = grid.headerCt.getHeaderAtIndex(0);
+
+ // Calculate editing start position from SelectionModel
+ // CellSelectionModel
+ if (selModel.getCurrentPosition) {
+ pos = selModel.getCurrentPosition();
+ record = grid.store.getAt(pos.row);
+ columnHeader = grid.headerCt.getHeaderAtIndex(pos.column);
+ }
+ // RowSelectionModel
+ else {
+ record = selModel.getLastSelected();
+ }
+ me.startEdit(record, columnHeader);
+ },
+
+ // private
+ onEscKey: function(e) {
+ this.cancelEdit();
+ },
+
+ // private
+ startEditByClick: function(view, cell, colIdx, record, row, rowIdx, e) {
+ this.startEdit(record, view.getHeaderAtIndex(colIdx));
+ },
+
+ /**
+ * @private
+ * @template
+ * Template method called before editing begins.
+ * @param {Object} context The current editing context
+ * @return {Boolean} Return false to cancel the editing process
+ */
+ beforeEdit: Ext.emptyFn,
+
+ /**
+ * Starts editing the specified record, using the specified Column definition to define which field is being edited.
+ * @param {Ext.data.Model/Number} record The Store data record which backs the row to be edited, or index of the record in Store.
+ * @param {Ext.grid.column.Column/Number} columnHeader The Column object defining the column to be edited, or index of the column.
+ */
+ startEdit: function(record, columnHeader) {
+ var me = this,
+ context = me.getEditingContext(record, columnHeader);
+
+ if (me.beforeEdit(context) === false || me.fireEvent('beforeedit', context) === false || context.cancel) {
+ return false;
+ }
+
+ me.context = context;
+ me.editing = true;
+ },
+
+ /**
+ * @private
+ * Collects all information necessary for any subclasses to perform their editing functions.
+ * @param record
+ * @param columnHeader
+ * @returns {Object} The editing context based upon the passed record and column
+ */
+ getEditingContext: function(record, columnHeader) {
+ var me = this,
+ grid = me.grid,
+ store = grid.store,
+ rowIdx,
+ colIdx,
+ view = grid.getView(),
+ value;
+
+ // If they'd passed numeric row, column indices, look them up.
+ if (Ext.isNumber(record)) {
+ rowIdx = record;
+ record = store.getAt(rowIdx);
+ } else {
+ rowIdx = store.indexOf(record);
+ }
+ if (Ext.isNumber(columnHeader)) {
+ colIdx = columnHeader;
+ columnHeader = grid.headerCt.getHeaderAtIndex(colIdx);
+ } else {
+ colIdx = columnHeader.getIndex();
+ }
+
+ value = record.get(columnHeader.dataIndex);
+ return {
+ grid: grid,
+ record: record,
+ field: columnHeader.dataIndex,
+ value: value,
+ row: view.getNode(rowIdx),
+ column: columnHeader,
+ rowIdx: rowIdx,
+ colIdx: colIdx
+ };
+ },
+
+ /**
+ * Cancels any active edit that is in progress.
+ */
+ cancelEdit: function() {
+ this.editing = false;
+ },
+
+ /**
+ * Completes the edit if there is an active edit in progress.
+ */
+ completeEdit: function() {
+ var me = this;
+
+ if (me.editing && me.validateEdit()) {
+ me.fireEvent('edit', me.context);
+ }
+
+ delete me.context;
+ me.editing = false;
+ },
+
+ // @abstract
+ validateEdit: function() {
+ var me = this,
+ context = me.context;
+
+ return me.fireEvent('validateedit', me, context) !== false && !context.cancel;
+ }
+});
+
+/**
+ * The Ext.grid.plugin.CellEditing plugin injects editing at a cell level for a Grid. Only a single
+ * cell will be editable at a time. The field that will be used for the editor is defined at the
+ * {@link Ext.grid.column.Column#editor editor}. The editor can be a field instance or a field configuration.
+ *
+ * If an editor is not specified for a particular column then that cell will not be editable and it will
+ * be skipped when activated via the mouse or the keyboard.
+ *
+ * The editor may be shared for each column in the grid, or a different one may be specified for each column.
+ * An appropriate field type should be chosen to match the data structure that it will be editing. For example,
+ * to edit a date, it would be useful to specify {@link Ext.form.field.Date} as the editor.
+ *
+ * @example
+ * Ext.create('Ext.data.Store', {
+ * storeId:'simpsonsStore',
+ * fields:['name', 'email', 'phone'],
+ * data:{'items':[
+ * {"name":"Lisa", "email":"lisa@simpsons.com", "phone":"555-111-1224"},
+ * {"name":"Bart", "email":"bart@simpsons.com", "phone":"555--222-1234"},
+ * {"name":"Homer", "email":"home@simpsons.com", "phone":"555-222-1244"},
+ * {"name":"Marge", "email":"marge@simpsons.com", "phone":"555-222-1254"}
+ * ]},
+ * proxy: {
+ * type: 'memory',
+ * reader: {
+ * type: 'json',
+ * root: 'items'
+ * }
+ * }
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Simpsons',
+ * store: Ext.data.StoreManager.lookup('simpsonsStore'),
+ * columns: [
+ * {header: 'Name', dataIndex: 'name', editor: 'textfield'},
+ * {header: 'Email', dataIndex: 'email', flex:1,
+ * editor: {
+ * xtype: 'textfield',
+ * allowBlank: false
+ * }
+ * },
+ * {header: 'Phone', dataIndex: 'phone'}
+ * ],
+ * selType: 'cellmodel',
+ * plugins: [
+ * Ext.create('Ext.grid.plugin.CellEditing', {
+ * clicksToEdit: 1
+ * })
+ * ],
+ * height: 200,
+ * width: 400,
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.grid.plugin.CellEditing', {
+ alias: 'plugin.cellediting',
+ extend: 'Ext.grid.plugin.Editing',
+ requires: ['Ext.grid.CellEditor', 'Ext.util.DelayedTask'],
+
+ constructor: function() {
+ /**
+ * @event beforeedit
+ * Fires before cell editing is triggered. Return false from event handler to stop the editing.
+ *
+ * @param {Object} e An edit event with the following properties:
+ *
+ * - grid - The grid
+ * - record - The record being edited
+ * - field - The field name being edited
+ * - value - The value for the field being edited.
+ * - row - The grid table row
+ * - column - The grid {@link Ext.grid.column.Column Column} defining the column that is being edited.
+ * - rowIdx - The row index that is being edited
+ * - colIdx - The column index that is being edited
+ * - cancel - Set this to true to cancel the edit or return false from your handler.
+ */
+ /**
+ * @event edit
+ * Fires after a cell is edited. Usage example:
+ *
+ * grid.on('edit', function(editor, e) {
+ * // commit the changes right after editing finished
+ * e.record.commit();
+ * };
+ *
+ * @param {Ext.grid.plugin.Editing} editor
+ * @param {Object} e An edit event with the following properties:
+ *
+ * - grid - The grid
+ * - record - The record that was edited
+ * - field - The field name that was edited
+ * - value - The value being set
+ * - originalValue - The original value for the field, before the edit.
+ * - row - The grid table row
+ * - column - The grid {@link Ext.grid.column.Column Column} defining the column that was edited.
+ * - rowIdx - The row index that was edited
+ * - colIdx - The column index that was edited
+ */
+ /**
+ * @event validateedit
+ * Fires after a cell is edited, but before the value is set in the record. Return false from event handler to
+ * cancel the change.
+ *
+ * Usage example showing how to remove the red triangle (dirty record indicator) from some 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(editor, e) {
+ * var myTargetRow = 6;
+ *
+ * if (e.row == myTargetRow) {
+ * e.cancel = true;
+ * e.record.data[e.field] = e.value;
+ * }
+ * });
+ *
+ * @param {Ext.grid.plugin.Editing} editor
+ * @param {Object} e An edit event with the following properties:
+ *
+ * - grid - The grid
+ * - record - The record being edited
+ * - field - The field name being edited
+ * - value - The value being set
+ * - originalValue - The original value for the field, before the edit.
+ * - row - The grid table row
+ * - column - The grid {@link Ext.grid.column.Column Column} defining the column that is being edited.
+ * - rowIdx - The row index that is being edited
+ * - colIdx - The column index that is being edited
+ * - cancel - Set this to true to cancel the edit or return false from your handler.
+ */
+ this.callParent(arguments);
+ this.editors = Ext.create('Ext.util.MixedCollection', false, function(editor) {
+ return editor.editorId;
+ });
+ this.editTask = Ext.create('Ext.util.DelayedTask');
+ },
+
+ onReconfigure: function(){
+ this.editors.clear();
+ this.callParent();
+ },
+
+ /**
+ * @private
+ * AbstractComponent calls destroy on all its plugins at destroy time.
+ */
+ destroy: function() {
+ var me = this;
+ me.editTask.cancel();
+ me.editors.each(Ext.destroy, Ext);
+ me.editors.clear();
+ me.callParent(arguments);
+ },
+
+ onBodyScroll: function() {
+ var ed = this.getActiveEditor();
+ if (ed && ed.field) {
+ if (ed.field.triggerBlur) {
+ ed.field.triggerBlur();
+ } else {
+ ed.field.blur();
+ }
+ }
+ },
+
+ // private
+ // Template method called from base class's initEvents
+ initCancelTriggers: function() {
+ var me = this,
+ grid = me.grid,
+ view = grid.view;
+
+ view.addElListener('mousewheel', me.cancelEdit, me);
+ me.mon(view, 'bodyscroll', me.onBodyScroll, me);
+ me.mon(grid, {
+ columnresize: me.cancelEdit,
+ columnmove: me.cancelEdit,
+ scope: me
+ });
+ },
+
+ /**
+ * Starts editing the specified record, using the specified Column definition to define which field is being edited.
+ * @param {Ext.data.Model} record The Store data record which backs the row to be edited.
+ * @param {Ext.data.Model} columnHeader The Column object defining the column to be edited. @override
+ */
+ startEdit: function(record, columnHeader) {
+ var me = this,
+ value = record.get(columnHeader.dataIndex),
+ context = me.getEditingContext(record, columnHeader),
+ ed;
+
+ record = context.record;
+ columnHeader = context.column;
+
+ // Complete the edit now, before getting the editor's target
+ // cell DOM element. Completing the edit causes a view refresh.
+ me.completeEdit();
+
+ context.originalValue = context.value = value;
+ if (me.beforeEdit(context) === false || me.fireEvent('beforeedit', context) === false || context.cancel) {
+ return false;
+ }
+
+ // See if the field is editable for the requested record
+ if (columnHeader && !columnHeader.getEditor(record)) {
+ return false;
+ }
+
+ ed = me.getEditor(record, columnHeader);
+ if (ed) {
+ me.context = context;
+ me.setActiveEditor(ed);
+ me.setActiveRecord(record);
+ me.setActiveColumn(columnHeader);
+
+ // Defer, so we have some time between view scroll to sync up the editor
+ me.editTask.delay(15, ed.startEdit, ed, [me.getCell(record, columnHeader), value]);
+ } else {
+ // BrowserBug: WebKit & IE refuse to focus the element, rather
+ // it will focus it and then immediately focus the body. This
+ // temporary hack works for Webkit and IE6. IE7 and 8 are still
+ // broken
+ me.grid.getView().getEl(columnHeader).focus((Ext.isWebKit || Ext.isIE) ? 10 : false);
+ }
+ },
+
+ completeEdit: function() {
+ var activeEd = this.getActiveEditor();
+ if (activeEd) {
+ activeEd.completeEdit();
+ }
+ },
+
+ // internal getters/setters
+ setActiveEditor: function(ed) {
+ this.activeEditor = ed;
+ },
+
+ getActiveEditor: function() {
+ return this.activeEditor;
+ },
+
+ setActiveColumn: function(column) {
+ this.activeColumn = column;
+ },
+
+ getActiveColumn: function() {
+ return this.activeColumn;
+ },
+
+ setActiveRecord: function(record) {
+ this.activeRecord = record;
+ },
+
+ getActiveRecord: function() {
+ return this.activeRecord;
+ },
+
+ getEditor: function(record, column) {
+ var me = this,
+ editors = me.editors,
+ editorId = column.getItemId(),
+ editor = editors.getByKey(editorId);
+
+ if (editor) {
+ return editor;
+ } else {
+ editor = column.getEditor(record);
+ if (!editor) {
+ return false;
+ }
+
+ // Allow them to specify a CellEditor in the Column
+ if (!(editor instanceof Ext.grid.CellEditor)) {
+ editor = Ext.create('Ext.grid.CellEditor', {
+ editorId: editorId,
+ field: editor
+ });
+ }
+ editor.parentEl = me.grid.getEditorParent();
+ // editor.parentEl should be set here.
+ editor.on({
+ scope: me,
+ specialkey: me.onSpecialKey,
+ complete: me.onEditComplete,
+ canceledit: me.cancelEdit
+ });
+ editors.add(editor);
+ return editor;
+ }
+ },
+
+ // inherit docs
+ setColumnField: function(column, field) {
+ var ed = this.editors.getByKey(column.getItemId());
+ Ext.destroy(ed, column.field);
+ this.editors.removeAtKey(column.getItemId());
+ this.callParent(arguments);
+ },
+
+ /**
+ * Gets the cell (td) for a particular record and column.
+ * @param {Ext.data.Model} record
+ * @param {Ext.grid.column.Column} column
+ * @private
+ */
+ getCell: function(record, column) {
+ return this.grid.getView().getCell(record, column);
+ },
+
+ onSpecialKey: function(ed, field, e) {
+ var grid = this.grid,
+ sm;
+ if (e.getKey() === e.TAB) {
+ e.stopEvent();
+ sm = grid.getSelectionModel();
+ if (sm.onEditorTab) {
+ sm.onEditorTab(this, e);
+ }
+ }
+ },
+
+ onEditComplete : function(ed, value, startValue) {
+ var me = this,
+ grid = me.grid,
+ sm = grid.getSelectionModel(),
+ activeColumn = me.getActiveColumn(),
+ dataIndex;
+
+ if (activeColumn) {
+ dataIndex = activeColumn.dataIndex;
+
+ me.setActiveEditor(null);
+ me.setActiveColumn(null);
+ me.setActiveRecord(null);
+ delete sm.wasEditing;
+
+ if (!me.validateEdit()) {
+ return;
+ }
+ // Only update the record if the new value is different than the
+ // startValue, when the view refreshes its el will gain focus
+ if (value !== startValue) {
+ me.context.record.set(dataIndex, value);
+ // Restore focus back to the view's element.
+ } else {
+ grid.getView().getEl(activeColumn).focus();
+ }
+ me.context.value = value;
+ me.fireEvent('edit', me, me.context);
+ }
+ },
+
+ /**
+ * Cancels any active editing.
+ */
+ cancelEdit: function() {
+ var me = this,
+ activeEd = me.getActiveEditor(),
+ viewEl = me.grid.getView().getEl(me.getActiveColumn());
+
+ me.setActiveEditor(null);
+ me.setActiveColumn(null);
+ me.setActiveRecord(null);
+ if (activeEd) {
+ activeEd.cancelEdit();
+ viewEl.focus();
+ }
+ },
+
+ /**
+ * Starts editing by position (row/column)
+ * @param {Object} position A position with keys of row and column.
+ */
+ startEditByPosition: function(position) {
+ var me = this,
+ grid = me.grid,
+ sm = grid.getSelectionModel(),
+ editRecord = grid.store.getAt(position.row),
+ editColumnHeader = grid.headerCt.getHeaderAtIndex(position.column);
+
+ if (sm.selectByPosition) {
+ sm.selectByPosition(position);
+ }
+ me.startEdit(editRecord, editColumnHeader);
+ }
+});
+/**
+ * This plugin provides drag and/or drop functionality for a GridView.
+ *
+ * It creates a specialized instance of {@link Ext.dd.DragZone DragZone} which knows how to drag out of a {@link
+ * Ext.grid.View GridView} and loads the data object which is passed to a cooperating {@link Ext.dd.DragZone DragZone}'s
+ * methods with the following properties:
+ *
+ * - `copy` : Boolean
+ *
+ * The value of the GridView's `copy` property, or `true` if the GridView was configured with `allowCopy: true` _and_
+ * the control key was pressed when the drag operation was begun.
+ *
+ * - `view` : GridView
+ *
+ * The source GridView from which the drag originated.
+ *
+ * - `ddel` : HtmlElement
+ *
+ * The drag proxy element which moves with the mouse
+ *
+ * - `item` : HtmlElement
+ *
+ * The GridView node upon which the mousedown event was registered.
+ *
+ * - `records` : Array
+ *
+ * An Array of {@link Ext.data.Model Model}s representing the selected data being dragged from the source GridView.
+ *
+ * It also creates a specialized instance of {@link Ext.dd.DropZone} which cooperates with other DropZones which are
+ * members of the same ddGroup which processes such data objects.
+ *
+ * Adding this plugin to a view means that two new events may be fired from the client GridView, `{@link #beforedrop
+ * beforedrop}` and `{@link #drop drop}`
+ *
+ * @example
+ * Ext.create('Ext.data.Store', {
+ * storeId:'simpsonsStore',
+ * fields:['name'],
+ * data: [["Lisa"], ["Bart"], ["Homer"], ["Marge"]],
+ * proxy: {
+ * type: 'memory',
+ * reader: 'array'
+ * }
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * store: 'simpsonsStore',
+ * columns: [
+ * {header: 'Name', dataIndex: 'name', flex: true}
+ * ],
+ * viewConfig: {
+ * plugins: {
+ * ptype: 'gridviewdragdrop',
+ * dragText: 'Drag and drop to reorganize'
+ * }
+ * },
+ * height: 200,
+ * width: 400,
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.grid.plugin.DragDrop', {
+ extend: 'Ext.AbstractPlugin',
+ alias: 'plugin.gridviewdragdrop',
+
+ uses: [
+ 'Ext.view.DragZone',
+ 'Ext.grid.ViewDropZone'
+ ],
+
+ /**
+ * @event beforedrop
+ * **This event is fired through the GridView. Add listeners to the GridView object**
+ *
+ * Fired when a drop gesture has been triggered by a mouseup event in a valid drop position in the GridView.
+ *
+ * @param {HTMLElement} node The GridView node **if any** over which the mouse was positioned.
+ *
+ * Returning `false` to this event signals that the drop gesture was invalid, and if the drag proxy will animate
+ * back to the point from which the drag began.
+ *
+ * Returning `0` To this event signals that the data transfer operation should not take place, but that the gesture
+ * was valid, and that the repair operation should not take place.
+ *
+ * Any other return value continues with the data transfer operation.
+ *
+ * @param {Object} data The data object gathered at mousedown time by the cooperating {@link Ext.dd.DragZone
+ * DragZone}'s {@link Ext.dd.DragZone#getDragData getDragData} method it contains the following properties:
+ *
+ * - copy : Boolean
+ *
+ * The value of the GridView's `copy` property, or `true` if the GridView was configured with `allowCopy: true` and
+ * the control key was pressed when the drag operation was begun
+ *
+ * - view : GridView
+ *
+ * The source GridView from which the drag originated.
+ *
+ * - ddel : HtmlElement
+ *
+ * The drag proxy element which moves with the mouse
+ *
+ * - item : HtmlElement
+ *
+ * The GridView node upon which the mousedown event was registered.
+ *
+ * - records : Array
+ *
+ * An Array of {@link Ext.data.Model Model}s representing the selected data being dragged from the source GridView.
+ *
+ * @param {Ext.data.Model} overModel The Model over which the drop gesture took place.
+ *
+ * @param {String} dropPosition `"before"` or `"after"` depending on whether the mouse is above or below the midline
+ * of the node.
+ *
+ * @param {Function} dropFunction
+ *
+ * A function to call to complete the data transfer operation and either move or copy Model instances from the
+ * source View's Store to the destination View's Store.
+ *
+ * This is useful when you want to perform some kind of asynchronous processing before confirming the drop, such as
+ * an {@link Ext.window.MessageBox#confirm confirm} call, or an Ajax request.
+ *
+ * Return `0` from this event handler, and call the `dropFunction` at any time to perform the data transfer.
+ */
+
+ /**
+ * @event drop
+ * **This event is fired through the GridView. Add listeners to the GridView object** Fired when a drop operation
+ * has been completed and the data has been moved or copied.
+ *
+ * @param {HTMLElement} node The GridView node **if any** over which the mouse was positioned.
+ *
+ * @param {Object} data The data object gathered at mousedown time by the cooperating {@link Ext.dd.DragZone
+ * DragZone}'s {@link Ext.dd.DragZone#getDragData getDragData} method it contains the following properties:
+ *
+ * - copy : Boolean
+ *
+ * The value of the GridView's `copy` property, or `true` if the GridView was configured with `allowCopy: true` and
+ * the control key was pressed when the drag operation was begun
+ *
+ * - view : GridView
+ *
+ * The source GridView from which the drag originated.
+ *
+ * - ddel : HtmlElement
+ *
+ * The drag proxy element which moves with the mouse
+ *
+ * - item : HtmlElement
+ *
+ * The GridView node upon which the mousedown event was registered.
+ *
+ * - records : Array
+ *
+ * An Array of {@link Ext.data.Model Model}s representing the selected data being dragged from the source GridView.
+ *
+ * @param {Ext.data.Model} overModel The Model over which the drop gesture took place.
+ *
+ * @param {String} dropPosition `"before"` or `"after"` depending on whether the mouse is above or below the midline
+ * of the node.
+ */
+
+ dragText : '{0} selected row{1}',
+
+ /**
+ * @cfg {String} ddGroup
+ * A named drag drop group to which this object belongs. If a group is specified, then both the DragZones and
+ * DropZone used by this plugin will only interact with other drag drop objects in the same group.
+ */
+ ddGroup : "GridDD",
+
+ /**
+ * @cfg {String} dragGroup
+ * The ddGroup to which the DragZone will belong.
+ *
+ * This defines which other DropZones the DragZone will interact with. Drag/DropZones only interact with other
+ * Drag/DropZones which are members of the same ddGroup.
+ */
+
+ /**
+ * @cfg {String} dropGroup
+ * The ddGroup to which the DropZone will belong.
+ *
+ * This defines which other DragZones the DropZone will interact with. Drag/DropZones only interact with other
+ * Drag/DropZones which are members of the same ddGroup.
+ */
+
+ /**
+ * @cfg {Boolean} enableDrop
+ * False to disallow the View from accepting drop gestures.
+ */
+ enableDrop: true,
+
+ /**
+ * @cfg {Boolean} enableDrag
+ * False to disallow dragging items from the View.
+ */
+ enableDrag: true,
+
+ init : function(view) {
+ view.on('render', this.onViewRender, this, {single: true});
+ },
+
+ /**
+ * @private
+ * AbstractComponent calls destroy on all its plugins at destroy time.
+ */
+ destroy: function() {
+ Ext.destroy(this.dragZone, this.dropZone);
+ },
+
+ enable: function() {
+ var me = this;
+ if (me.dragZone) {
+ me.dragZone.unlock();
+ }
+ if (me.dropZone) {
+ me.dropZone.unlock();
+ }
+ me.callParent();
+ },
+
+ disable: function() {
+ var me = this;
+ if (me.dragZone) {
+ me.dragZone.lock();
+ }
+ if (me.dropZone) {
+ me.dropZone.lock();
+ }
+ me.callParent();
+ },
+
+ onViewRender : function(view) {
+ var me = this;
+
+ if (me.enableDrag) {
+ me.dragZone = Ext.create('Ext.view.DragZone', {
+ view: view,
+ ddGroup: me.dragGroup || me.ddGroup,
+ dragText: me.dragText
+ });
+ }
+
+ if (me.enableDrop) {
+ me.dropZone = Ext.create('Ext.grid.ViewDropZone', {
+ view: view,
+ ddGroup: me.dropGroup || me.ddGroup
+ });
+ }
+ }
+});
+/**
+ * @class Ext.grid.plugin.HeaderReorderer
+ * @extends Ext.util.Observable
+ * @private
+ */
+Ext.define('Ext.grid.plugin.HeaderReorderer', {
+ extend: 'Ext.util.Observable',
+ requires: ['Ext.grid.header.DragZone', 'Ext.grid.header.DropZone'],
+ alias: 'plugin.gridheaderreorderer',
+
+ init: function(headerCt) {
+ this.headerCt = headerCt;
+ headerCt.on('render', this.onHeaderCtRender, this);
+ },
+
+ /**
+ * @private
+ * AbstractComponent calls destroy on all its plugins at destroy time.
+ */
+ destroy: function() {
+ Ext.destroy(this.dragZone, this.dropZone);
+ },
+
+ onHeaderCtRender: function() {
+ this.dragZone = Ext.create('Ext.grid.header.DragZone', this.headerCt);
+ this.dropZone = Ext.create('Ext.grid.header.DropZone', this.headerCt);
+ if (this.disabled) {
+ this.dragZone.disable();
+ }
+ },
+
+ enable: function() {
+ this.disabled = false;
+ if (this.dragZone) {
+ this.dragZone.enable();
+ }
+ },
+
+ disable: function() {
+ this.disabled = true;
+ if (this.dragZone) {
+ this.dragZone.disable();
+ }
+ }
+});
+/**
+ * @class Ext.grid.plugin.HeaderResizer
+ * @extends Ext.util.Observable
+ *
+ * Plugin to add header resizing functionality to a HeaderContainer.
+ * Always resizing header to the left of the splitter you are resizing.
+ */
+Ext.define('Ext.grid.plugin.HeaderResizer', {
+ extend: 'Ext.util.Observable',
+ requires: ['Ext.dd.DragTracker', 'Ext.util.Region'],
+ alias: 'plugin.gridheaderresizer',
+
+ disabled: false,
+
+ /**
+ * @cfg {Boolean} dynamic
+ * Set to true to resize on the fly rather than using a proxy marker. Defaults to false.
+ */
+ configs: {
+ dynamic: true
+ },
+
+ colHeaderCls: Ext.baseCSSPrefix + 'column-header',
+
+ minColWidth: 40,
+ maxColWidth: 1000,
+ wResizeCursor: 'col-resize',
+ eResizeCursor: 'col-resize',
+ // not using w and e resize bc we are only ever resizing one
+ // column
+ //wResizeCursor: Ext.isWebKit ? 'w-resize' : 'col-resize',
+ //eResizeCursor: Ext.isWebKit ? 'e-resize' : 'col-resize',
+
+ init: function(headerCt) {
+ this.headerCt = headerCt;
+ headerCt.on('render', this.afterHeaderRender, this, {single: true});
+ },
+
+ /**
+ * @private
+ * AbstractComponent calls destroy on all its plugins at destroy time.
+ */
+ destroy: function() {
+ if (this.tracker) {
+ this.tracker.destroy();
+ }
+ },
+
+ afterHeaderRender: function() {
+ var headerCt = this.headerCt,
+ el = headerCt.el;
+
+ headerCt.mon(el, 'mousemove', this.onHeaderCtMouseMove, this);
+
+ this.tracker = Ext.create('Ext.dd.DragTracker', {
+ disabled: this.disabled,
+ onBeforeStart: Ext.Function.bind(this.onBeforeStart, this),
+ onStart: Ext.Function.bind(this.onStart, this),
+ onDrag: Ext.Function.bind(this.onDrag, this),
+ onEnd: Ext.Function.bind(this.onEnd, this),
+ tolerance: 3,
+ autoStart: 300,
+ el: el
+ });
+ },
+
+ // As we mouse over individual headers, change the cursor to indicate
+ // that resizing is available, and cache the resize target header for use
+ // if/when they mousedown.
+ onHeaderCtMouseMove: function(e, t) {
+ if (this.headerCt.dragging) {
+ if (this.activeHd) {
+ this.activeHd.el.dom.style.cursor = '';
+ delete this.activeHd;
+ }
+ } else {
+ var headerEl = e.getTarget('.' + this.colHeaderCls, 3, true),
+ overHeader, resizeHeader;
+
+ if (headerEl){
+ overHeader = Ext.getCmp(headerEl.id);
+
+ // On left edge, go back to the previous non-hidden header.
+ if (overHeader.isOnLeftEdge(e)) {
+ resizeHeader = overHeader.previousNode('gridcolumn:not([hidden])');
+
+ }
+ // Else, if on the right edge, we're resizing the column we are over
+ else if (overHeader.isOnRightEdge(e)) {
+ resizeHeader = overHeader;
+ }
+ // Between the edges: we are not resizing
+ else {
+ resizeHeader = null;
+ }
+
+ // We *are* resizing
+ if (resizeHeader) {
+ // If we're attempting to resize a group header, that cannot be resized,
+ // so find its last visible leaf header; Group headers are sized
+ // by the size of their child headers.
+ if (resizeHeader.isGroupHeader) {
+ resizeHeader = resizeHeader.down(':not([isGroupHeader]):not([hidden]):last');
+ }
+
+ // Check if the header is resizable. Continue checking the old "fixed" property, bug also
+ // check whether the resizablwe property is set to false.
+ if (resizeHeader && !(resizeHeader.fixed || (resizeHeader.resizable === false) || this.disabled)) {
+ this.activeHd = resizeHeader;
+ overHeader.el.dom.style.cursor = this.eResizeCursor;
+ }
+ // reset
+ } else {
+ overHeader.el.dom.style.cursor = '';
+ delete this.activeHd;
+ }
+ }
+ }
+ },
+
+ // only start when there is an activeHd
+ onBeforeStart : function(e){
+ var t = e.getTarget();
+ // cache the activeHd because it will be cleared.
+ this.dragHd = this.activeHd;
+
+ if (!!this.dragHd && !Ext.fly(t).hasCls('x-column-header-trigger') && !this.headerCt.dragging) {
+ //this.headerCt.dragging = true;
+ this.tracker.constrainTo = this.getConstrainRegion();
+ return true;
+ } else {
+ this.headerCt.dragging = false;
+ return false;
+ }
+ },
+
+ // get the region to constrain to, takes into account max and min col widths
+ getConstrainRegion: function() {
+ var dragHdEl = this.dragHd.el,
+ region = Ext.util.Region.getRegion(dragHdEl);
+
+ return region.adjust(
+ 0,
+ this.maxColWidth - dragHdEl.getWidth(),
+ 0,
+ this.minColWidth
+ );
+ },
+
+ // initialize the left and right hand side markers around
+ // the header that we are resizing
+ onStart: function(e){
+ var me = this,
+ dragHd = me.dragHd,
+ dragHdEl = dragHd.el,
+ width = dragHdEl.getWidth(),
+ headerCt = me.headerCt,
+ t = e.getTarget();
+
+ if (me.dragHd && !Ext.fly(t).hasCls('x-column-header-trigger')) {
+ headerCt.dragging = true;
+ }
+
+ me.origWidth = width;
+
+ // setup marker proxies
+ if (!me.dynamic) {
+ var xy = dragHdEl.getXY(),
+ gridSection = headerCt.up('[scrollerOwner]'),
+ dragHct = me.dragHd.up(':not([isGroupHeader])'),
+ firstSection = dragHct.up(),
+ lhsMarker = gridSection.getLhsMarker(),
+ rhsMarker = gridSection.getRhsMarker(),
+ el = rhsMarker.parent(),
+ offsetLeft = el.getLeft(true),
+ offsetTop = el.getTop(true),
+ topLeft = el.translatePoints(xy),
+ markerHeight = firstSection.body.getHeight() + headerCt.getHeight(),
+ top = topLeft.top - offsetTop;
+
+ lhsMarker.setTop(top);
+ rhsMarker.setTop(top);
+ lhsMarker.setHeight(markerHeight);
+ rhsMarker.setHeight(markerHeight);
+ lhsMarker.setLeft(topLeft.left - offsetLeft);
+ rhsMarker.setLeft(topLeft.left + width - offsetLeft);
+ }
+ },
+
+ // synchronize the rhsMarker with the mouse movement
+ onDrag: function(e){
+ if (!this.dynamic) {
+ var xy = this.tracker.getXY('point'),
+ gridSection = this.headerCt.up('[scrollerOwner]'),
+ rhsMarker = gridSection.getRhsMarker(),
+ el = rhsMarker.parent(),
+ topLeft = el.translatePoints(xy),
+ offsetLeft = el.getLeft(true);
+
+ rhsMarker.setLeft(topLeft.left - offsetLeft);
+ // Resize as user interacts
+ } else {
+ this.doResize();
+ }
+ },
+
+ onEnd: function(e){
+ this.headerCt.dragging = false;
+ if (this.dragHd) {
+ if (!this.dynamic) {
+ var dragHd = this.dragHd,
+ gridSection = this.headerCt.up('[scrollerOwner]'),
+ lhsMarker = gridSection.getLhsMarker(),
+ rhsMarker = gridSection.getRhsMarker(),
+ currWidth = dragHd.getWidth(),
+ offset = this.tracker.getOffset('point'),
+ offscreen = -9999;
+
+ // hide markers
+ lhsMarker.setLeft(offscreen);
+ rhsMarker.setLeft(offscreen);
+ }
+ this.doResize();
+ }
+ },
+
+ doResize: function() {
+ if (this.dragHd) {
+ var dragHd = this.dragHd,
+ nextHd,
+ offset = this.tracker.getOffset('point');
+
+ // resize the dragHd
+ if (dragHd.flex) {
+ delete dragHd.flex;
+ }
+
+ this.headerCt.suspendLayout = true;
+ dragHd.setWidth(this.origWidth + offset[0], false);
+
+ // In the case of forceFit, change the following Header width.
+ // Then apply the two width changes by laying out the owning HeaderContainer
+ // If HeaderContainer is configured forceFit, inhibit upstream layout notification, so that
+ // we can also shrink the following Header by an equal amount, and *then* inform the upstream layout.
+ if (this.headerCt.forceFit) {
+ nextHd = dragHd.nextNode('gridcolumn:not([hidden]):not([isGroupHeader])');
+ if (nextHd) {
+ delete nextHd.flex;
+ nextHd.setWidth(nextHd.getWidth() - offset[0], false);
+ }
+ }
+ this.headerCt.suspendLayout = false;
+ this.headerCt.doComponentLayout(this.headerCt.getFullWidth());
+ }
+ },
+
+ disable: function() {
+ this.disabled = true;
+ if (this.tracker) {
+ this.tracker.disable();
+ }
+ },
+
+ enable: function() {
+ this.disabled = false;
+ if (this.tracker) {
+ this.tracker.enable();
+ }
+ }
+});
+/**
+ * The Ext.grid.plugin.RowEditing plugin injects editing at a row level for a Grid. When editing begins,
+ * a small floating dialog will be shown for the appropriate row. Each editable column will show a field
+ * for editing. There is a button to save or cancel all changes for the edit.
+ *
+ * The field that will be used for the editor is defined at the
+ * {@link Ext.grid.column.Column#editor editor}. The editor can be a field instance or a field configuration.
+ * If an editor is not specified for a particular column then that column won't be editable and the value of
+ * the column will be displayed.
+ *
+ * The editor may be shared for each column in the grid, or a different one may be specified for each column.
+ * An appropriate field type should be chosen to match the data structure that it will be editing. For example,
+ * to edit a date, it would be useful to specify {@link Ext.form.field.Date} as the editor.
+ *
+ * @example
+ * Ext.create('Ext.data.Store', {
+ * storeId:'simpsonsStore',
+ * fields:['name', 'email', 'phone'],
+ * data: [
+ * {"name":"Lisa", "email":"lisa@simpsons.com", "phone":"555-111-1224"},
+ * {"name":"Bart", "email":"bart@simpsons.com", "phone":"555--222-1234"},
+ * {"name":"Homer", "email":"home@simpsons.com", "phone":"555-222-1244"},
+ * {"name":"Marge", "email":"marge@simpsons.com", "phone":"555-222-1254"}
+ * ]
+ * });
+ *
+ * Ext.create('Ext.grid.Panel', {
+ * title: 'Simpsons',
+ * store: Ext.data.StoreManager.lookup('simpsonsStore'),
+ * columns: [
+ * {header: 'Name', dataIndex: 'name', editor: 'textfield'},
+ * {header: 'Email', dataIndex: 'email', flex:1,
+ * editor: {
+ * xtype: 'textfield',
+ * allowBlank: false
+ * }
+ * },
+ * {header: 'Phone', dataIndex: 'phone'}
+ * ],
+ * selType: 'rowmodel',
+ * plugins: [
+ * Ext.create('Ext.grid.plugin.RowEditing', {
+ * clicksToEdit: 1
+ * })
+ * ],
+ * height: 200,
+ * width: 400,
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.grid.plugin.RowEditing', {
+ extend: 'Ext.grid.plugin.Editing',
+ alias: 'plugin.rowediting',
+
+ requires: [
+ 'Ext.grid.RowEditor'
+ ],
+
+ editStyle: 'row',
+
+ /**
+ * @cfg {Boolean} autoCancel
+ * True to automatically cancel any pending changes when the row editor begins editing a new row.
+ * False to force the user to explicitly cancel the pending changes. Defaults to true.
+ */
+ autoCancel: true,
+
+ /**
+ * @cfg {Number} clicksToMoveEditor
+ * The number of clicks to move the row editor to a new row while it is visible and actively editing another row.
+ * This will default to the same value as {@link Ext.grid.plugin.Editing#clicksToEdit clicksToEdit}.
+ */
+
+ /**
+ * @cfg {Boolean} errorSummary
+ * True to show a {@link Ext.tip.ToolTip tooltip} that summarizes all validation errors present
+ * in the row editor. Set to false to prevent the tooltip from showing. Defaults to true.
+ */
+ errorSummary: true,
+
+ /**
+ * @event beforeedit
+ * Fires before row editing is triggered.
+ *
+ * @param {Ext.grid.plugin.Editing} editor
+ * @param {Object} e An edit event with the following properties:
+ *
+ * - grid - The grid this editor is on
+ * - view - The grid view
+ * - store - The grid store
+ * - record - The record being edited
+ * - row - The grid table row
+ * - column - The grid {@link Ext.grid.column.Column Column} defining the column that initiated the edit
+ * - rowIdx - The row index that is being edited
+ * - colIdx - The column index that initiated the edit
+ * - cancel - Set this to true to cancel the edit or return false from your handler.
+ */
+
+ /**
+ * @event canceledit
+ * Fires when the user has started editing a row but then cancelled the edit
+ * @param {Object} grid The grid
+ */
+
+ /**
+ * @event edit
+ * Fires after a row is edited. Usage example:
+ *
+ * grid.on('edit', function(editor, e) {
+ * // commit the changes right after editing finished
+ * e.record.commit();
+ * };
+ *
+ * @param {Ext.grid.plugin.Editing} editor
+ * @param {Object} e An edit event with the following properties:
+ *
+ * - grid - The grid this editor is on
+ * - view - The grid view
+ * - store - The grid store
+ * - record - The record being edited
+ * - row - The grid table row
+ * - column - The grid {@link Ext.grid.column.Column Column} defining the column that initiated the edit
+ * - rowIdx - The row index that is being edited
+ * - colIdx - The column index that initiated the edit
+ */
+ /**
+ * @event validateedit
+ * Fires after a cell is edited, but before the value is set in the record. Return false to cancel the change. The
+ * edit event object has the following properties
+ *
+ * Usage example showing how to remove the red triangle (dirty record indicator) from some 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(editor, e) {
+ * var myTargetRow = 6;
+ *
+ * if (e.rowIdx == myTargetRow) {
+ * e.cancel = true;
+ * e.record.data[e.field] = e.value;
+ * }
+ * });
+ *
+ * @param {Ext.grid.plugin.Editing} editor
+ * @param {Object} e An edit event with the following properties:
+ *
+ * - grid - The grid this editor is on
+ * - view - The grid view
+ * - store - The grid store
+ * - record - The record being edited
+ * - row - The grid table row
+ * - column - The grid {@link Ext.grid.column.Column Column} defining the column that initiated the edit
+ * - rowIdx - The row index that is being edited
+ * - colIdx - The column index that initiated the edit
+ * - cancel - Set this to true to cancel the edit or return false from your handler.
+ */
+
+ constructor: function() {
+ var me = this;
+ me.callParent(arguments);
+
+ if (!me.clicksToMoveEditor) {
+ me.clicksToMoveEditor = me.clicksToEdit;
+ }
+
+ me.autoCancel = !!me.autoCancel;
+ },
+
+ /**
+ * @private
+ * AbstractComponent calls destroy on all its plugins at destroy time.
+ */
+ destroy: function() {
+ var me = this;
+ Ext.destroy(me.editor);
+ me.callParent(arguments);
+ },
+
+ /**
+ * Starts editing the specified record, using the specified Column definition to define which field is being edited.
+ * @param {Ext.data.Model} record The Store data record which backs the row to be edited.
+ * @param {Ext.data.Model} columnHeader The Column object defining the column to be edited. @override
+ */
+ startEdit: function(record, columnHeader) {
+ var me = this,
+ editor = me.getEditor();
+
+ if (me.callParent(arguments) === false) {
+ return false;
+ }
+
+ // Fire off our editor
+ if (editor.beforeEdit() !== false) {
+ editor.startEdit(me.context.record, me.context.column);
+ }
+ },
+
+ // private
+ cancelEdit: function() {
+ var me = this;
+
+ if (me.editing) {
+ me.getEditor().cancelEdit();
+ me.callParent(arguments);
+
+ me.fireEvent('canceledit', me.context);
+ }
+ },
+
+ // private
+ completeEdit: function() {
+ var me = this;
+
+ if (me.editing && me.validateEdit()) {
+ me.editing = false;
+ me.fireEvent('edit', me.context);
+ }
+ },
+
+ // private
+ validateEdit: function() {
+ var me = this,
+ editor = me.editor,
+ context = me.context,
+ record = context.record,
+ newValues = {},
+ originalValues = {},
+ name;
+
+ editor.items.each(function(item) {
+ name = item.name;
+
+ newValues[name] = item.getValue();
+ originalValues[name] = record.get(name);
+ });
+
+ Ext.apply(context, {
+ newValues : newValues,
+ originalValues : originalValues
+ });
+
+ return me.callParent(arguments) && me.getEditor().completeEdit();
+ },
+
+ // private
+ getEditor: function() {
+ var me = this;
+
+ if (!me.editor) {
+ me.editor = me.initEditor();
+ }
+ return me.editor;
+ },
+
+ // private
+ initEditor: function() {
+ var me = this,
+ grid = me.grid,
+ view = me.view,
+ headerCt = grid.headerCt;
+
+ return Ext.create('Ext.grid.RowEditor', {
+ autoCancel: me.autoCancel,
+ errorSummary: me.errorSummary,
+ fields: headerCt.getGridColumns(),
+ hidden: true,
+
+ // keep a reference..
+ editingPlugin: me,
+ renderTo: view.el
+ });
+ },
+
+ // private
+ initEditTriggers: function() {
+ var me = this,
+ grid = me.grid,
+ view = me.view,
+ headerCt = grid.headerCt,
+ moveEditorEvent = me.clicksToMoveEditor === 1 ? 'click' : 'dblclick';
+
+ me.callParent(arguments);
+
+ if (me.clicksToMoveEditor !== me.clicksToEdit) {
+ me.mon(view, 'cell' + moveEditorEvent, me.moveEditorByClick, me);
+ }
+
+ view.on('render', function() {
+ // Column events
+ me.mon(headerCt, {
+ add: me.onColumnAdd,
+ remove: me.onColumnRemove,
+ columnresize: me.onColumnResize,
+ columnhide: me.onColumnHide,
+ columnshow: me.onColumnShow,
+ columnmove: me.onColumnMove,
+ scope: me
+ });
+ }, me, { single: true });
+ },
+
+ startEditByClick: function() {
+ var me = this;
+ if (!me.editing || me.clicksToMoveEditor === me.clicksToEdit) {
+ me.callParent(arguments);
+ }
+ },
+
+ moveEditorByClick: function() {
+ var me = this;
+ if (me.editing) {
+ me.superclass.startEditByClick.apply(me, arguments);
+ }
+ },
+
+ // private
+ onColumnAdd: function(ct, column) {
+ if (column.isHeader) {
+ var me = this,
+ editor;
+
+ me.initFieldAccessors(column);
+ editor = me.getEditor();
+
+ if (editor && editor.onColumnAdd) {
+ editor.onColumnAdd(column);
+ }
+ }
+ },
+
+ // private
+ onColumnRemove: function(ct, column) {
+ if (column.isHeader) {
+ var me = this,
+ editor = me.getEditor();
+
+ if (editor && editor.onColumnRemove) {
+ editor.onColumnRemove(column);
+ }
+ me.removeFieldAccessors(column);
+ }
+ },
+
+ // private
+ onColumnResize: function(ct, column, width) {
+ if (column.isHeader) {
+ var me = this,
+ editor = me.getEditor();
+
+ if (editor && editor.onColumnResize) {
+ editor.onColumnResize(column, width);
+ }
+ }
+ },
+
+ // private
+ onColumnHide: function(ct, column) {
+ // no isHeader check here since its already a columnhide event.
+ var me = this,
+ editor = me.getEditor();
+
+ if (editor && editor.onColumnHide) {
+ editor.onColumnHide(column);
+ }
+ },
+
+ // private
+ onColumnShow: function(ct, column) {
+ // no isHeader check here since its already a columnshow event.
+ var me = this,
+ editor = me.getEditor();
+
+ if (editor && editor.onColumnShow) {
+ editor.onColumnShow(column);
+ }
+ },
+
+ // private
+ onColumnMove: function(ct, column, fromIdx, toIdx) {
+ // no isHeader check here since its already a columnmove event.
+ var me = this,
+ editor = me.getEditor();
+
+ if (editor && editor.onColumnMove) {
+ editor.onColumnMove(column, fromIdx, toIdx);
+ }
+ },
+
+ // private
+ setColumnField: function(column, field) {
+ var me = this;
+ me.callParent(arguments);
+ me.getEditor().setField(column.field, column);
+ }
+});
+
+/**
+ * @class Ext.grid.property.Grid
+ * @extends Ext.grid.Panel
+ *
+ * A specialized grid implementation intended to mimic the traditional property grid as typically seen in
+ * development IDEs. Each row in the grid represents a property of some object, and the data is stored
+ * as a set of name/value pairs in {@link Ext.grid.property.Property Properties}. Example usage:
+ *
+ * @example
+ * Ext.create('Ext.grid.property.Grid', {
+ * title: 'Properties Grid',
+ * width: 300,
+ * renderTo: Ext.getBody(),
+ * source: {
+ * "(name)": "My Object",
+ * "Created": Ext.Date.parse('10/15/2006', 'm/d/Y'),
+ * "Available": false,
+ * "Version": .01,
+ * "Description": "A test object"
+ * }
+ * });
+ */
+Ext.define('Ext.grid.property.Grid', {
+
+ extend: 'Ext.grid.Panel',
+
+ alias: 'widget.propertygrid',
+
+ alternateClassName: 'Ext.grid.PropertyGrid',
+
+ uses: [
+ 'Ext.grid.plugin.CellEditing',
+ 'Ext.grid.property.Store',
+ 'Ext.grid.property.HeaderContainer',
+ 'Ext.XTemplate',
+ 'Ext.grid.CellEditor',
+ 'Ext.form.field.Date',
+ 'Ext.form.field.Text',
+ 'Ext.form.field.Number'
+ ],
+
+ /**
+ * @cfg {Object} propertyNames An object containing custom property name/display name pairs.
+ * If specified, the display name will be shown in the name column instead of the property name.
+ */
+
+ /**
+ * @cfg {Object} source A data object to use as the data source of the grid (see {@link #setSource} for details).
+ */
+
+ /**
+ * @cfg {Object} customEditors An object containing name/value pairs of custom editor type definitions that allow
+ * the grid to support additional types of editable fields. By default, the grid supports strongly-typed editing
+ * of strings, dates, numbers and booleans using built-in form editors, but any custom type can be supported and
+ * associated with a custom input control by specifying a custom editor. The name of the editor
+ * type should correspond with the name of the property that will use the editor. Example usage:
+ * <pre><code>
+var grid = new Ext.grid.property.Grid({
+
+ // Custom editors for certain property names
+ customEditors: {
+ evtStart: Ext.create('Ext.form.TimeField' {selectOnFocus:true})
+ },
+
+ // Displayed name for property names in the source
+ propertyNames: {
+ evtStart: 'Start Time'
+ },
+
+ // Data object containing properties to edit
+ source: {
+ evtStart: '10:00 AM'
+ }
+});
+</code></pre>
+ */
+
+ /**
+ * @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:
+ * <pre><code>
+var grid = Ext.create('Ext.grid.property.Grid', {
+ customRenderers: {
+ Available: function(v){
+ if (v) {
+ return '<span style="color: green;">Yes</span>';
+ } else {
+ return '<span style="color: red;">No</span>';
+ }
+ }
+ },
+ source: {
+ Available: true
+ }
+});
+</code></pre>
+ */
+
+ /**
+ * @cfg {String} valueField
+ * Optional. The name of the field from the property store to use as the value field name. Defaults to <code>'value'</code>
+ * This may be useful if you do not configure the property Grid from an object, but use your own store configuration.
+ */
+ valueField: 'value',
+
+ /**
+ * @cfg {String} nameField
+ * Optional. The name of the field from the property store to use as the property field name. Defaults to <code>'name'</code>
+ * This may be useful if you do not configure the property Grid from an object, but use your own store configuration.
+ */
+ nameField: 'name',
+
+ /**
+ * @cfg {Number} nameColumnWidth
+ * Optional. Specify the width for the name column. The value column will take any remaining space. Defaults to <tt>115</tt>.
+ */
+
+ // private config overrides
+ enableColumnMove: false,
+ columnLines: true,
+ stripeRows: false,
+ trackMouseOver: false,
+ clicksToEdit: 1,
+ enableHdMenu: false,
+
+ // private
+ initComponent : function(){
+ var me = this;
+
+ me.addCls(Ext.baseCSSPrefix + 'property-grid');
+ me.plugins = me.plugins || [];
+
+ // Enable cell editing. Inject a custom startEdit which always edits column 1 regardless of which column was clicked.
+ me.plugins.push(Ext.create('Ext.grid.plugin.CellEditing', {
+ clicksToEdit: me.clicksToEdit,
+
+ // Inject a startEdit which always edits the value column
+ startEdit: function(record, column) {
+ // Maintainer: Do not change this 'this' to 'me'! It is the CellEditing object's own scope.
+ return this.self.prototype.startEdit.call(this, record, me.headerCt.child('#' + me.valueField));
+ }
+ }));
+
+ me.selModel = {
+ selType: 'cellmodel',
+ onCellSelect: function(position) {
+ if (position.column != 1) {
+ position.column = 1;
+ }
+ return this.self.prototype.onCellSelect.call(this, position);
+ }
+ };
+ me.customRenderers = me.customRenderers || {};
+ me.customEditors = me.customEditors || {};
+
+ // Create a property.Store from the source object unless configured with a store
+ if (!me.store) {
+ me.propStore = me.store = Ext.create('Ext.grid.property.Store', me, me.source);
+ }
+
+ me.store.sort('name', 'ASC');
+ me.columns = Ext.create('Ext.grid.property.HeaderContainer', me, me.store);
+
+ me.addEvents(
+ /**
+ * @event beforepropertychange
+ * Fires before a property value changes. Handlers can return false to cancel the property change
+ * (this will internally call {@link Ext.data.Model#reject} on the property's record).
+ * @param {Object} source The source data object for the grid (corresponds to the same object passed in
+ * as the {@link #source} config property).
+ * @param {String} recordId The record's id in the data store
+ * @param {Object} value The current edited property value
+ * @param {Object} oldValue The original property value prior to editing
+ */
+ 'beforepropertychange',
+ /**
+ * @event propertychange
+ * Fires after a property value has changed.
+ * @param {Object} source The source data object for the grid (corresponds to the same object passed in
+ * as the {@link #source} config property).
+ * @param {String} recordId The record's id in the data store
+ * @param {Object} value The current edited property value
+ * @param {Object} oldValue The original property value prior to editing
+ */
+ 'propertychange'
+ );
+ me.callParent();
+
+ // Inject a custom implementation of walkCells which only goes up or down
+ me.getView().walkCells = this.walkCells;
+
+ // Set up our default editor set for the 4 atomic data types
+ me.editors = {
+ 'date' : Ext.create('Ext.grid.CellEditor', { field: Ext.create('Ext.form.field.Date', {selectOnFocus: true})}),
+ 'string' : Ext.create('Ext.grid.CellEditor', { field: Ext.create('Ext.form.field.Text', {selectOnFocus: true})}),
+ 'number' : Ext.create('Ext.grid.CellEditor', { field: Ext.create('Ext.form.field.Number', {selectOnFocus: true})}),
+ 'boolean' : Ext.create('Ext.grid.CellEditor', { field: Ext.create('Ext.form.field.ComboBox', {
+ editable: false,
+ store: [[ true, me.headerCt.trueText ], [false, me.headerCt.falseText ]]
+ })})
+ };
+
+ // Track changes to the data so we can fire our events.
+ me.store.on('update', me.onUpdate, me);
+ },
+
+ // private
+ onUpdate : function(store, record, operation) {
+ var me = this,
+ v, oldValue;
+
+ if (operation == Ext.data.Model.EDIT) {
+ v = record.get(me.valueField);
+ oldValue = record.modified.value;
+ if (me.fireEvent('beforepropertychange', me.source, record.getId(), v, oldValue) !== false) {
+ if (me.source) {
+ me.source[record.getId()] = v;
+ }
+ record.commit();
+ me.fireEvent('propertychange', me.source, record.getId(), v, oldValue);
+ } else {
+ record.reject();
+ }
+ }
+ },
+
+ // Custom implementation of walkCells which only goes up and down.
+ walkCells: function(pos, direction, e, preventWrap, verifierFn, scope) {
+ if (direction == 'left') {
+ direction = 'up';
+ } else if (direction == 'right') {
+ direction = 'down';
+ }
+ pos = Ext.view.Table.prototype.walkCells.call(this, pos, direction, e, preventWrap, verifierFn, scope);
+ if (!pos.column) {
+ pos.column = 1;
+ }
+ return pos;
+ },
+
+ // private
+ // returns the correct editor type for the property type, or a custom one keyed by the property name
+ getCellEditor : function(record, column) {
+ var me = this,
+ propName = record.get(me.nameField),
+ val = record.get(me.valueField),
+ editor = me.customEditors[propName];
+
+ // A custom editor was found. If not already wrapped with a CellEditor, wrap it, and stash it back
+ // If it's not even a Field, just a config object, instantiate it before wrapping it.
+ if (editor) {
+ if (!(editor instanceof Ext.grid.CellEditor)) {
+ if (!(editor instanceof Ext.form.field.Base)) {
+ editor = Ext.ComponentManager.create(editor, 'textfield');
+ }
+ editor = me.customEditors[propName] = Ext.create('Ext.grid.CellEditor', { field: editor });
+ }
+ } else if (Ext.isDate(val)) {
+ editor = me.editors.date;
+ } else if (Ext.isNumber(val)) {
+ editor = me.editors.number;
+ } else if (Ext.isBoolean(val)) {
+ editor = me.editors['boolean'];
+ } else {
+ editor = me.editors.string;
+ }
+
+ // Give the editor a unique ID because the CellEditing plugin caches them
+ editor.editorId = propName;
+ return editor;
+ },
+
+ beforeDestroy: function() {
+ var me = this;
+ me.callParent();
+ me.destroyEditors(me.editors);
+ me.destroyEditors(me.customEditors);
+ delete me.source;
+ },
+
+ destroyEditors: function (editors) {
+ for (var ed in editors) {
+ if (editors.hasOwnProperty(ed)) {
+ Ext.destroy(editors[ed]);
+ }
+ }
+ },
+
+ /**
+ * Sets the source data object containing the property data. The data object can contain one or more name/value
+ * pairs representing all of the properties of an object to display in the grid, and this data will automatically
+ * be loaded into the grid's {@link #store}. The values should be supplied in the proper data type if needed,
+ * otherwise string type will be assumed. If the grid already contains data, this method will replace any
+ * existing data. See also the {@link #source} config value. Example usage:
+ * <pre><code>
+grid.setSource({
+ "(name)": "My Object",
+ "Created": Ext.Date.parse('10/15/2006', 'm/d/Y'), // date type
+ "Available": false, // boolean type
+ "Version": .01, // decimal type
+ "Description": "A test object"
+});
+</code></pre>
+ * @param {Object} source The data object
+ */
+ setSource: function(source) {
+ this.source = source;
+ this.propStore.setSource(source);
+ },
+
+ /**
+ * Gets the source data object containing the property data. See {@link #setSource} for details regarding the
+ * format of the data object.
+ * @return {Object} The data object
+ */
+ getSource: function() {
+ return this.propStore.getSource();
+ },
+
+ /**
+ * Sets the value of a property.
+ * @param {String} prop The name of the property to set
+ * @param {Object} value The value to test
+ * @param {Boolean} create (Optional) True to create the property if it doesn't already exist. Defaults to <tt>false</tt>.
+ */
+ 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
+ */
+});
+/**
+ * @class Ext.grid.property.HeaderContainer
+ * @extends Ext.grid.header.Container
+ * A custom HeaderContainer for the {@link Ext.grid.property.Grid}. Generally it should not need to be used directly.
+ */
+Ext.define('Ext.grid.property.HeaderContainer', {
+
+ extend: 'Ext.grid.header.Container',
+
+ alternateClassName: 'Ext.grid.PropertyColumnModel',
+
+ nameWidth: 115,
+
+ // private - strings used for locale support
+ nameText : 'Name',
+ valueText : 'Value',
+ dateFormat : 'm/j/Y',
+ trueText: 'true',
+ falseText: 'false',
+
+ // private
+ nameColumnCls: Ext.baseCSSPrefix + 'grid-property-name',
+
+ /**
+ * Creates new HeaderContainer.
+ * @param {Ext.grid.property.Grid} grid The grid this store will be bound to
+ * @param {Object} source The source data config object
+ */
+ constructor : function(grid, store) {
+ var me = this;
+
+ me.grid = grid;
+ me.store = store;
+ me.callParent([{
+ items: [{
+ header: me.nameText,
+ width: grid.nameColumnWidth || me.nameWidth,
+ sortable: true,
+ dataIndex: grid.nameField,
+ renderer: Ext.Function.bind(me.renderProp, me),
+ itemId: grid.nameField,
+ menuDisabled :true,
+ tdCls: me.nameColumnCls
+ }, {
+ header: me.valueText,
+ renderer: Ext.Function.bind(me.renderCell, me),
+ getEditor: Ext.Function.bind(me.getCellEditor, me),
+ flex: 1,
+ fixed: true,
+ dataIndex: grid.valueField,
+ itemId: grid.valueField,
+ menuDisabled: true
+ }]
+ }]);
+ },
+
+ getCellEditor: function(record){
+ return this.grid.getCellEditor(record, this);
+ },
+
+ // private
+ // Render a property name cell
+ renderProp : function(v) {
+ return this.getPropertyName(v);
+ },
+
+ // private
+ // Render a property value cell
+ renderCell : function(val, meta, rec) {
+ var me = this,
+ renderer = me.grid.customRenderers[rec.get(me.grid.nameField)],
+ result = val;
+
+ if (renderer) {
+ return renderer.apply(me, arguments);
+ }
+ if (Ext.isDate(val)) {
+ result = me.renderDate(val);
+ } else if (Ext.isBoolean(val)) {
+ result = me.renderBool(val);
+ }
+ return Ext.util.Format.htmlEncode(result);
+ },
+
+ // private
+ renderDate : Ext.util.Format.date,
+
+ // private
+ renderBool : function(bVal) {
+ return this[bVal ? 'trueText' : 'falseText'];
+ },
+
+ // private
+ // Renders custom property names instead of raw names if defined in the Grid
+ getPropertyName : function(name) {
+ var pn = this.grid.propertyNames;
+ return pn && pn[name] ? pn[name] : name;
+ }
+});
+/**
+ * @class Ext.grid.property.Property
+ * A specific {@link Ext.data.Model} type that represents a name/value pair and is made to work with the
+ * {@link Ext.grid.property.Grid}. Typically, Properties do not need to be created directly as they can be
+ * created implicitly by simply using the appropriate data configs either via the {@link Ext.grid.property.Grid#source}
+ * config property or by calling {@link Ext.grid.property.Grid#setSource}. However, if the need arises, these records
+ * can also be created explicitly as shown below. Example usage:
+ * <pre><code>
+var rec = new Ext.grid.property.Property({
+ name: 'birthday',
+ value: Ext.Date.parse('17/06/1962', 'd/m/Y')
+});
+// Add record to an already populated grid
+grid.store.addSorted(rec);
+</code></pre>
+ * @constructor
+ * @param {Object} config A data object in the format:<pre><code>
+{
+ name: [name],
+ value: [value]
+}</code></pre>
+ * The specified value's type
+ * will be read automatically by the grid to determine the type of editor to use when displaying it.
+ */
+Ext.define('Ext.grid.property.Property', {
+ extend: 'Ext.data.Model',
+
+ alternateClassName: 'Ext.PropGridProperty',
+
+ fields: [{
+ name: 'name',
+ type: 'string'
+ }, {
+ name: 'value'
+ }],
+ idProperty: 'name'
+});
+/**
+ * @class Ext.grid.property.Store
+ * @extends Ext.data.Store
+ * A custom {@link Ext.data.Store} for the {@link Ext.grid.property.Grid}. This class handles the mapping
+ * between the custom data source objects supported by the grid and the {@link Ext.grid.property.Property} format
+ * used by the {@link Ext.data.Store} base class.
+ */
+Ext.define('Ext.grid.property.Store', {
+
+ extend: 'Ext.data.Store',
+
+ alternateClassName: 'Ext.grid.PropertyStore',
+
+ uses: ['Ext.data.reader.Reader', 'Ext.data.proxy.Proxy', 'Ext.data.ResultSet', 'Ext.grid.property.Property'],
+
+ /**
+ * Creates new property store.
+ * @param {Ext.grid.Panel} grid The grid this store will be bound to
+ * @param {Object} source The source data config object
+ */
+ constructor : function(grid, source){
+ var me = this;
+
+ me.grid = grid;
+ me.source = source;
+ me.callParent([{
+ data: source,
+ model: Ext.grid.property.Property,
+ proxy: me.getProxy()
+ }]);
+ },
+
+ // Return a singleton, customized Proxy object which configures itself with a custom Reader
+ getProxy: function() {
+ if (!this.proxy) {
+ Ext.grid.property.Store.prototype.proxy = Ext.create('Ext.data.proxy.Memory', {
+ model: Ext.grid.property.Property,
+ reader: this.getReader()
+ });
+ }
+ return this.proxy;
+ },
+
+ // Return a singleton, customized Reader object which reads Ext.grid.property.Property records from an object.
+ getReader: function() {
+ if (!this.reader) {
+ Ext.grid.property.Store.prototype.reader = Ext.create('Ext.data.reader.Reader', {
+ model: Ext.grid.property.Property,
+
+ buildExtractors: Ext.emptyFn,
+
+ read: function(dataObject) {
+ return this.readRecords(dataObject);
+ },
+
+ readRecords: function(dataObject) {
+ var val,
+ propName,
+ result = {
+ records: [],
+ success: true
+ };
+
+ for (propName in dataObject) {
+ if (dataObject.hasOwnProperty(propName)) {
+ val = dataObject[propName];
+ if (this.isEditableValue(val)) {
+ result.records.push(new Ext.grid.property.Property({
+ name: propName,
+ value: val
+ }, propName));
+ }
+ }
+ }
+ result.total = result.count = result.records.length;
+ return Ext.create('Ext.data.ResultSet', result);
+ },
+
+ // private
+ isEditableValue: function(val){
+ return Ext.isPrimitive(val) || Ext.isDate(val);
+ }
+ });
+ }
+ return this.reader;
+ },
+
+ // protected - should only be called by the grid. Use grid.setSource instead.
+ setSource : function(dataObject) {
+ var me = this;
+
+ me.source = dataObject;
+ me.suspendEvents();
+ me.removeAll();
+ me.proxy.data = dataObject;
+ me.load();
+ me.resumeEvents();
+ me.fireEvent('datachanged', me);
+ },
+
+ // private
+ getProperty : function(row) {
+ return Ext.isNumber(row) ? this.getAt(row) : this.getById(row);
+ },
+
+ // private
+ setValue : function(prop, value, create){
+ var me = this,
+ rec = me.getRec(prop);
+
+ if (rec) {
+ rec.set('value', value);
+ me.source[prop] = value;
+ } else if (create) {
+ // only create if specified.
+ me.source[prop] = value;
+ rec = new Ext.grid.property.Property({name: prop, value: value}, prop);
+ me.add(rec);
+ }
+ },
+
+ // private
+ remove : function(prop) {
+ var rec = this.getRec(prop);
+ if (rec) {
+ this.callParent([rec]);
+ delete this.source[prop];
+ }
+ },
+
+ // private
+ getRec : function(prop) {
+ return this.getById(prop);
+ },
+
+ // protected - should only be called by the grid. Use grid.getSource instead.
+ getSource : function() {
+ return this.source;
+ }
+});
+/**
+ * Component layout for components which maintain an inner body element which must be resized to synchronize with the
+ * Component size.
+ * @class Ext.layout.component.Body
+ * @extends Ext.layout.component.Component
+ * @private
+ */
+
+Ext.define('Ext.layout.component.Body', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.body'],
+
+ extend: 'Ext.layout.component.Component',
+
+ uses: ['Ext.layout.container.Container'],
+
+ /* End Definitions */
+
+ type: 'body',
+
+ onLayout: function(width, height) {
+ var me = this,
+ owner = me.owner;
+
+ // Size the Component's encapsulating element according to the dimensions
+ me.setTargetSize(width, height);
+
+ // Size the Component's body element according to the content box of the encapsulating element
+ me.setBodySize.apply(me, arguments);
+
+ // We need to bind to the owner whenever we do not have a user set height or width.
+ if (owner && owner.layout && owner.layout.isLayout) {
+ if (!Ext.isNumber(owner.height) || !Ext.isNumber(owner.width)) {
+ owner.layout.bindToOwnerCtComponent = true;
+ }
+ else {
+ owner.layout.bindToOwnerCtComponent = false;
+ }
+ }
+
+ me.callParent(arguments);
+ },
+
+ /**
+ * @private
+ * <p>Sizes the Component's body element to fit exactly within the content box of the Component's encapsulating element.<p>
+ */
+ setBodySize: function(width, height) {
+ var me = this,
+ owner = me.owner,
+ frameSize = owner.frameSize,
+ isNumber = Ext.isNumber;
+
+ if (isNumber(width)) {
+ width -= owner.el.getFrameWidth('lr') - frameSize.left - frameSize.right;
+ }
+ if (isNumber(height)) {
+ height -= owner.el.getFrameWidth('tb') - frameSize.top - frameSize.bottom;
+ }
+
+ me.setElementSize(owner.body, width, height);
+ }
+});
+/**
+ * Component layout for Ext.form.FieldSet components
+ * @class Ext.layout.component.FieldSet
+ * @extends Ext.layout.component.Body
+ * @private
+ */
+Ext.define('Ext.layout.component.FieldSet', {
+ extend: 'Ext.layout.component.Body',
+ alias: ['layout.fieldset'],
+
+ type: 'fieldset',
+
+ doContainerLayout: function() {
+ // Prevent layout/rendering of children if the fieldset is collapsed
+ if (!this.owner.collapsed) {
+ this.callParent();
+ }
+ }
+});
+/**
+ * Component layout for tabs
+ * @class Ext.layout.component.Tab
+ * @extends Ext.layout.component.Button
+ * @private
+ */
+Ext.define('Ext.layout.component.Tab', {
+
+ alias: ['layout.tab'],
+
+ extend: 'Ext.layout.component.Button',
+
+ //type: 'button',
+
+ beforeLayout: function() {
+ var me = this, dirty = me.lastClosable !== me.owner.closable;
+
+ if (dirty) {
+ delete me.adjWidth;
+ }
+
+ return this.callParent(arguments) || dirty;
+ },
+
+ onLayout: function () {
+ var me = this;
+
+ me.callParent(arguments);
+
+ me.lastClosable = me.owner.closable;
+ }
+});
+/**
+ * @private
+ * @class Ext.layout.component.field.File
+ * @extends Ext.layout.component.field.Field
+ * Layout class for {@link Ext.form.field.File} fields. Adjusts the input field size to accommodate
+ * the file picker trigger button.
+ * @private
+ */
+
+Ext.define('Ext.layout.component.field.File', {
+ alias: ['layout.filefield'],
+ extend: 'Ext.layout.component.field.Field',
+
+ type: 'filefield',
+
+ sizeBodyContents: function(width, height) {
+ var me = this,
+ owner = me.owner;
+
+ if (!owner.buttonOnly) {
+ // Decrease the field's width by the width of the button and the configured buttonMargin.
+ // Both the text field and the button are floated left in CSS so they'll stack up side by side.
+ me.setElementSize(owner.inputEl, Ext.isNumber(width) ? width - owner.button.getWidth() - owner.buttonMargin : width);
+ }
+ }
+});
+/**
+ * @class Ext.layout.component.field.Slider
+ * @extends Ext.layout.component.field.Field
+ * @private
+ */
+
+Ext.define('Ext.layout.component.field.Slider', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.sliderfield'],
+
+ extend: 'Ext.layout.component.field.Field',
+
+ /* End Definitions */
+
+ type: 'sliderfield',
+
+ sizeBodyContents: function(width, height) {
+ var owner = this.owner,
+ thumbs = owner.thumbs,
+ length = thumbs.length,
+ inputEl = owner.inputEl,
+ innerEl = owner.innerEl,
+ endEl = owner.endEl,
+ i = 0;
+
+ /*
+ * If we happen to be animating during a resize, the position of the thumb will likely be off
+ * when the animation stops. As such, just stop any animations before syncing the thumbs.
+ */
+ for(; i < length; ++i) {
+ thumbs[i].el.stopAnimation();
+ }
+
+ if (owner.vertical) {
+ inputEl.setHeight(height);
+ innerEl.setHeight(Ext.isNumber(height) ? height - inputEl.getPadding('t') - endEl.getPadding('b') : height);
+ }
+ else {
+ inputEl.setWidth(width);
+ innerEl.setWidth(Ext.isNumber(width) ? width - inputEl.getPadding('l') - endEl.getPadding('r') : width);
+ }
+ owner.syncThumbs();
+ }
+});
+
+/**
+ * @class Ext.layout.container.Absolute
+ * @extends Ext.layout.container.Anchor
+ *
+ * This is a layout that inherits the anchoring of {@link Ext.layout.container.Anchor} and adds the
+ * ability for x/y positioning using the standard x and y component config options.
+ *
+ * This class is intended to be extended or created via the {@link Ext.container.Container#layout layout}
+ * configuration property. See {@link Ext.container.Container#layout} for additional details.
+ *
+ * @example
+ * Ext.create('Ext.form.Panel', {
+ * title: 'Absolute Layout',
+ * width: 300,
+ * height: 275,
+ * layout:'absolute',
+ * layoutConfig: {
+ * // layout-specific configs go here
+ * //itemCls: 'x-abs-layout-item',
+ * },
+ * url:'save-form.php',
+ * defaultType: 'textfield',
+ * items: [{
+ * x: 10,
+ * y: 10,
+ * xtype:'label',
+ * text: 'Send To:'
+ * },{
+ * x: 80,
+ * y: 10,
+ * name: 'to',
+ * anchor:'90%' // anchor width by percentage
+ * },{
+ * x: 10,
+ * y: 40,
+ * xtype:'label',
+ * text: 'Subject:'
+ * },{
+ * x: 80,
+ * y: 40,
+ * name: 'subject',
+ * anchor: '90%' // anchor width by percentage
+ * },{
+ * x:0,
+ * y: 80,
+ * xtype: 'textareafield',
+ * name: 'msg',
+ * anchor: '100% 100%' // anchor width and height
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.layout.container.Absolute', {
+
+ /* Begin Definitions */
+
+ alias: 'layout.absolute',
+ extend: 'Ext.layout.container.Anchor',
+ alternateClassName: 'Ext.layout.AbsoluteLayout',
+
+ /* End Definitions */
+
+ itemCls: Ext.baseCSSPrefix + 'abs-layout-item',
+
+ type: 'absolute',
+
+ onLayout: function() {
+ var me = this,
+ target = me.getTarget(),
+ targetIsBody = target.dom === document.body;
+
+ // Do not set position: relative; when the absolute layout target is the body
+ if (!targetIsBody) {
+ target.position();
+ }
+ me.paddingLeft = target.getPadding('l');
+ me.paddingTop = target.getPadding('t');
+ me.callParent(arguments);
+ },
+
+ // private
+ adjustWidthAnchor: function(value, comp) {
+ //return value ? value - comp.getPosition(true)[0] + this.paddingLeft: value;
+ return value ? value - comp.getPosition(true)[0] : value;
+ },
+
+ // private
+ adjustHeightAnchor: function(value, comp) {
+ //return value ? value - comp.getPosition(true)[1] + this.paddingTop: value;
+ return value ? value - comp.getPosition(true)[1] : value;
+ }
+});
+/**
+ * @class Ext.layout.container.Accordion
+ * @extends Ext.layout.container.VBox
+ *
+ * This is a layout that manages multiple Panels in an expandable accordion style such that only
+ * **one Panel can be expanded at any given time**. Each Panel has built-in support for expanding and collapsing.
+ *
+ * Note: Only Ext Panels and all subclasses of Ext.panel.Panel may be used in an accordion layout Container.
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Accordion Layout',
+ * width: 300,
+ * height: 300,
+ * layout:'accordion',
+ * defaults: {
+ * // applied to each contained panel
+ * bodyStyle: 'padding:15px'
+ * },
+ * layoutConfig: {
+ * // layout-specific configs go here
+ * titleCollapse: false,
+ * animate: true,
+ * activeOnTop: true
+ * },
+ * items: [{
+ * title: 'Panel 1',
+ * html: 'Panel content!'
+ * },{
+ * title: 'Panel 2',
+ * html: 'Panel content!'
+ * },{
+ * title: 'Panel 3',
+ * html: 'Panel content!'
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.layout.container.Accordion', {
+ extend: 'Ext.layout.container.VBox',
+ alias: ['layout.accordion'],
+ alternateClassName: 'Ext.layout.AccordionLayout',
+
+ itemCls: Ext.baseCSSPrefix + 'box-item ' + Ext.baseCSSPrefix + 'accordion-item',
+
+ align: 'stretch',
+
+ /**
+ * @cfg {Boolean} fill
+ * True to adjust the active item's height to fill the available space in the container, false to use the
+ * item's current height, or auto height if not explicitly set.
+ */
+ fill : true,
+
+ /**
+ * @cfg {Boolean} autoWidth
+ * Child Panels have their width actively managed to fit within the accordion's width.
+ * @deprecated This config is ignored in ExtJS 4
+ */
+ autoWidth : true,
+
+ /**
+ * @cfg {Boolean} titleCollapse
+ * True to allow expand/collapse of each contained panel by clicking anywhere on the title bar, false to allow
+ * expand/collapse only when the toggle tool button is clicked. When set to false,
+ * {@link #hideCollapseTool} should be false also.
+ */
+ titleCollapse : true,
+
+ /**
+ * @cfg {Boolean} hideCollapseTool
+ * True to hide the contained Panels' collapse/expand toggle buttons, false to display them.
+ * When set to true, {@link #titleCollapse} is automatically set to <code>true</code>.
+ */
+ hideCollapseTool : false,
+
+ /**
+ * @cfg {Boolean} collapseFirst
+ * True to make sure the collapse/expand toggle button always renders first (to the left of) any other tools
+ * in the contained Panels' title bars, false to render it last.
+ */
+ collapseFirst : false,
+
+ /**
+ * @cfg {Boolean} animate
+ * True to slide the contained panels open and closed during expand/collapse using animation, false to open and
+ * close directly with no animation. Note: The layout performs animated collapsing
+ * and expanding, <i>not</i> the child Panels.
+ */
+ animate : true,
+ /**
+ * @cfg {Boolean} activeOnTop
+ * Only valid when {@link #multi} is `false` and {@link #animate} is `false`.
+ *
+ * True to swap the position of each panel as it is expanded so that it becomes the first item in the container,
+ * false to keep the panels in the rendered order.
+ */
+ activeOnTop : false,
+ /**
+ * @cfg {Boolean} multi
+ * Set to <code>true</code> to enable multiple accordion items to be open at once.
+ */
+ multi: false,
+
+ constructor: function() {
+ var me = this;
+
+ me.callParent(arguments);
+
+ // animate flag must be false during initial render phase so we don't get animations.
+ me.initialAnimate = me.animate;
+ me.animate = false;
+
+ // Child Panels are not absolutely positioned if we are not filling, so use a different itemCls.
+ if (me.fill === false) {
+ me.itemCls = Ext.baseCSSPrefix + 'accordion-item';
+ }
+ },
+
+ // Cannot lay out a fitting accordion before we have been allocated a height.
+ // So during render phase, layout will not be performed.
+ beforeLayout: function() {
+ var me = this;
+
+ me.callParent(arguments);
+ if (me.fill) {
+ if (!(me.owner.el.dom.style.height || me.getLayoutTargetSize().height)) {
+ return false;
+ }
+ } else {
+ me.owner.componentLayout.monitorChildren = false;
+ me.autoSize = true;
+ me.owner.setAutoScroll(true);
+ }
+ },
+
+ renderItems : function(items, target) {
+ var me = this,
+ ln = items.length,
+ i = 0,
+ comp,
+ targetSize = me.getLayoutTargetSize(),
+ renderedPanels = [];
+
+ for (; i < ln; i++) {
+ comp = items[i];
+ if (!comp.rendered) {
+ renderedPanels.push(comp);
+
+ // Set up initial properties for Panels in an accordion.
+ if (me.collapseFirst) {
+ comp.collapseFirst = me.collapseFirst;
+ }
+ if (me.hideCollapseTool) {
+ comp.hideCollapseTool = me.hideCollapseTool;
+ comp.titleCollapse = true;
+ }
+ else if (me.titleCollapse) {
+ comp.titleCollapse = me.titleCollapse;
+ }
+
+ delete comp.hideHeader;
+ comp.collapsible = true;
+ comp.title = comp.title || ' ';
+
+ // Set initial sizes
+ comp.width = targetSize.width;
+ if (me.fill) {
+ delete comp.height;
+ delete comp.flex;
+
+ // If there is an expanded item, all others must be rendered collapsed.
+ if (me.expandedItem !== undefined) {
+ comp.collapsed = true;
+ }
+ // Otherwise expand the first item with collapsed explicitly configured as false
+ else if (comp.hasOwnProperty('collapsed') && comp.collapsed === false) {
+ comp.flex = 1;
+ me.expandedItem = i;
+ } else {
+ comp.collapsed = true;
+ }
+ // If we are fitting, then intercept expand/collapse requests.
+ me.owner.mon(comp, {
+ show: me.onComponentShow,
+ beforeexpand: me.onComponentExpand,
+ beforecollapse: me.onComponentCollapse,
+ scope: me
+ });
+ } else {
+ delete comp.flex;
+ comp.animCollapse = me.initialAnimate;
+ comp.autoHeight = true;
+ comp.autoScroll = false;
+ }
+ comp.border = comp.collapsed;
+ }
+ }
+
+ // If no collapsed:false Panels found, make the first one expanded.
+ if (ln && me.expandedItem === undefined) {
+ me.expandedItem = 0;
+ comp = items[0];
+ comp.collapsed = comp.border = false;
+ if (me.fill) {
+ comp.flex = 1;
+ }
+ }
+
+ // Render all Panels.
+ me.callParent(arguments);
+
+ // Postprocess rendered Panels.
+ ln = renderedPanels.length;
+ for (i = 0; i < ln; i++) {
+ comp = renderedPanels[i];
+
+ // Delete the dimension property so that our align: 'stretch' processing manages the width from here
+ delete comp.width;
+
+ comp.header.addCls(Ext.baseCSSPrefix + 'accordion-hd');
+ comp.body.addCls(Ext.baseCSSPrefix + 'accordion-body');
+ }
+ },
+
+ onLayout: function() {
+ var me = this;
+
+
+ if (me.fill) {
+ me.callParent(arguments);
+ } else {
+ var targetSize = me.getLayoutTargetSize(),
+ items = me.getVisibleItems(),
+ len = items.length,
+ i = 0, comp;
+
+ for (; i < len; i++) {
+ comp = items[i];
+ if (comp.collapsed) {
+ items[i].setWidth(targetSize.width);
+ } else {
+ items[i].setSize(null, null);
+ }
+ }
+ }
+ me.updatePanelClasses();
+
+ return me;
+ },
+
+ updatePanelClasses: function() {
+ var children = this.getLayoutItems(),
+ ln = children.length,
+ siblingCollapsed = true,
+ i, child;
+
+ for (i = 0; i < ln; i++) {
+ child = children[i];
+
+ // Fix for EXTJSIV-3724. Windows only.
+ // Collapsing the Psnel's el to a size which only allows a single hesder to be visible, scrolls the header out of view.
+ if (Ext.isWindows) {
+ child.el.dom.scrollTop = 0;
+ }
+
+ if (siblingCollapsed) {
+ child.header.removeCls(Ext.baseCSSPrefix + 'accordion-hd-sibling-expanded');
+ }
+ else {
+ child.header.addCls(Ext.baseCSSPrefix + 'accordion-hd-sibling-expanded');
+ }
+
+ if (i + 1 == ln && child.collapsed) {
+ child.header.addCls(Ext.baseCSSPrefix + 'accordion-hd-last-collapsed');
+ }
+ else {
+ child.header.removeCls(Ext.baseCSSPrefix + 'accordion-hd-last-collapsed');
+ }
+ siblingCollapsed = child.collapsed;
+ }
+ },
+
+ animCallback: function(){
+ Ext.Array.forEach(this.toCollapse, function(comp){
+ comp.fireEvent('collapse', comp);
+ });
+
+ Ext.Array.forEach(this.toExpand, function(comp){
+ comp.fireEvent('expand', comp);
+ });
+ },
+
+ setupEvents: function(){
+ this.toCollapse = [];
+ this.toExpand = [];
+ },
+
+ // When a Component expands, adjust the heights of the other Components to be just enough to accommodate
+ // their headers.
+ // The expanded Component receives the only flex value, and so gets all remaining space.
+ onComponentExpand: function(toExpand) {
+ var me = this,
+ it = me.owner.items.items,
+ len = it.length,
+ i = 0,
+ comp;
+
+ me.setupEvents();
+ for (; i < len; i++) {
+ comp = it[i];
+ if (comp === toExpand && comp.collapsed) {
+ me.setExpanded(comp);
+ } else if (!me.multi && (comp.rendered && comp.header.rendered && comp !== toExpand && !comp.collapsed)) {
+ me.setCollapsed(comp);
+ }
+ }
+
+ me.animate = me.initialAnimate;
+ if (me.activeOnTop) {
+ // insert will trigger a layout
+ me.owner.insert(0, toExpand);
+ } else {
+ me.layout();
+ }
+ me.animate = false;
+ return false;
+ },
+
+ onComponentCollapse: function(comp) {
+ var me = this,
+ toExpand = comp.next() || comp.prev(),
+ expanded = me.multi ? me.owner.query('>panel:not([collapsed])') : [];
+
+ me.setupEvents();
+ // If we are allowing multi, and the "toCollapse" component is NOT the only expanded Component,
+ // then ask the box layout to collapse it to its header.
+ if (me.multi) {
+ me.setCollapsed(comp);
+
+ // If the collapsing Panel is the only expanded one, expand the following Component.
+ // All this is handling fill: true, so there must be at least one expanded,
+ if (expanded.length === 1 && expanded[0] === comp) {
+ me.setExpanded(toExpand);
+ }
+
+ me.animate = me.initialAnimate;
+ me.layout();
+ me.animate = false;
+ }
+ // Not allowing multi: expand the next sibling if possible, prev sibling if we collapsed the last
+ else if (toExpand) {
+ me.onComponentExpand(toExpand);
+ }
+ return false;
+ },
+
+ onComponentShow: function(comp) {
+ // Showing a Component means that you want to see it, so expand it.
+ this.onComponentExpand(comp);
+ },
+
+ setCollapsed: function(comp) {
+ var otherDocks = comp.getDockedItems(),
+ dockItem,
+ len = otherDocks.length,
+ i = 0;
+
+ // Hide all docked items except the header
+ comp.hiddenDocked = [];
+ for (; i < len; i++) {
+ dockItem = otherDocks[i];
+ if ((dockItem !== comp.header) && !dockItem.hidden) {
+ dockItem.hidden = true;
+ comp.hiddenDocked.push(dockItem);
+ }
+ }
+ comp.addCls(comp.collapsedCls);
+ comp.header.addCls(comp.collapsedHeaderCls);
+ comp.height = comp.header.getHeight();
+ comp.el.setHeight(comp.height);
+ comp.collapsed = true;
+ delete comp.flex;
+ if (this.initialAnimate) {
+ this.toCollapse.push(comp);
+ } else {
+ comp.fireEvent('collapse', comp);
+ }
+ if (comp.collapseTool) {
+ comp.collapseTool.setType('expand-' + comp.getOppositeDirection(comp.collapseDirection));
+ }
+ },
+
+ setExpanded: function(comp) {
+ var otherDocks = comp.hiddenDocked,
+ len = otherDocks ? otherDocks.length : 0,
+ i = 0;
+
+ // Show temporarily hidden docked items
+ for (; i < len; i++) {
+ otherDocks[i].show();
+ }
+
+ // If it was an initial native collapse which hides the body
+ if (!comp.body.isVisible()) {
+ comp.body.show();
+ }
+ delete comp.collapsed;
+ delete comp.height;
+ delete comp.componentLayout.lastComponentSize;
+ comp.suspendLayout = false;
+ comp.flex = 1;
+ comp.removeCls(comp.collapsedCls);
+ comp.header.removeCls(comp.collapsedHeaderCls);
+ if (this.initialAnimate) {
+ this.toExpand.push(comp);
+ } else {
+ comp.fireEvent('expand', comp);
+ }
+ if (comp.collapseTool) {
+ comp.collapseTool.setType('collapse-' + comp.collapseDirection);
+ }
+ comp.setAutoScroll(comp.initialConfig.autoScroll);
+ }
+});
+/**
+ * This class functions between siblings of a {@link Ext.layout.container.VBox VBox} or {@link Ext.layout.container.HBox HBox}
+ * layout to resize both immediate siblings.
+ *
+ * By default it will set the size of both siblings. <b>One</b> of the siblings may be configured with
+ * `{@link Ext.Component#maintainFlex maintainFlex}: true` which will cause it not to receive a new size explicitly, but to be resized
+ * by the layout.
+ *
+ * A Splitter may be configured to show a centered mini-collapse tool orientated to collapse the {@link #collapseTarget}.
+ * The Splitter will then call that sibling Panel's {@link Ext.panel.Panel#collapse collapse} or {@link Ext.panel.Panel#expand expand} method
+ * to perform the appropriate operation (depending on the sibling collapse state). To create the mini-collapse tool but take care
+ * of collapsing yourself, configure the splitter with <code>{@link #performCollapse} false</code>.
+ */
+Ext.define('Ext.resizer.Splitter', {
+ extend: 'Ext.Component',
+ requires: ['Ext.XTemplate'],
+ uses: ['Ext.resizer.SplitterTracker'],
+ alias: 'widget.splitter',
+
+ renderTpl: [
+ '<tpl if="collapsible===true">',
+ '<div id="{id}-collapseEl" class="', Ext.baseCSSPrefix, 'collapse-el ',
+ Ext.baseCSSPrefix, 'layout-split-{collapseDir}"> </div>',
+ '</tpl>'
+ ],
+
+ baseCls: Ext.baseCSSPrefix + 'splitter',
+ collapsedClsInternal: Ext.baseCSSPrefix + 'splitter-collapsed',
+
+ /**
+ * @cfg {Boolean} collapsible
+ * <code>true</code> to show a mini-collapse tool in the Splitter to toggle expand and collapse on the {@link #collapseTarget} Panel.
+ * Defaults to the {@link Ext.panel.Panel#collapsible collapsible} setting of the Panel.
+ */
+ collapsible: false,
+
+ /**
+ * @cfg {Boolean} performCollapse
+ * <p>Set to <code>false</code> to prevent this Splitter's mini-collapse tool from managing the collapse
+ * state of the {@link #collapseTarget}.</p>
+ */
+
+ /**
+ * @cfg {Boolean} collapseOnDblClick
+ * <code>true</code> to enable dblclick to toggle expand and collapse on the {@link #collapseTarget} Panel.
+ */
+ collapseOnDblClick: true,
+
+ /**
+ * @cfg {Number} defaultSplitMin
+ * Provides a default minimum width or height for the two components
+ * that the splitter is between.
+ */
+ defaultSplitMin: 40,
+
+ /**
+ * @cfg {Number} defaultSplitMax
+ * Provides a default maximum width or height for the two components
+ * that the splitter is between.
+ */
+ defaultSplitMax: 1000,
+
+ /**
+ * @cfg {String} collapsedCls
+ * A class to add to the splitter when it is collapsed. See {@link #collapsible}.
+ */
+
+ width: 5,
+ height: 5,
+
+ /**
+ * @cfg {String/Ext.panel.Panel} collapseTarget
+ * <p>A string describing the relative position of the immediate sibling Panel to collapse. May be 'prev' or 'next' (Defaults to 'next')</p>
+ * <p>Or the immediate sibling Panel to collapse.</p>
+ * <p>The orientation of the mini-collapse tool will be inferred from this setting.</p>
+ * <p><b>Note that only Panels may be collapsed.</b></p>
+ */
+ collapseTarget: 'next',
+
+ /**
+ * @property orientation
+ * @type String
+ * Orientation of this Splitter. <code>'vertical'</code> when used in an hbox layout, <code>'horizontal'</code>
+ * when used in a vbox layout.
+ */
+
+ onRender: function() {
+ var me = this,
+ target = me.getCollapseTarget(),
+ collapseDir = me.getCollapseDirection();
+
+ Ext.applyIf(me.renderData, {
+ collapseDir: collapseDir,
+ collapsible: me.collapsible || target.collapsible
+ });
+
+ me.addChildEls('collapseEl');
+
+ this.callParent(arguments);
+
+ // Add listeners on the mini-collapse tool unless performCollapse is set to false
+ if (me.performCollapse !== false) {
+ if (me.renderData.collapsible) {
+ me.mon(me.collapseEl, 'click', me.toggleTargetCmp, me);
+ }
+ if (me.collapseOnDblClick) {
+ me.mon(me.el, 'dblclick', me.toggleTargetCmp, me);
+ }
+ }
+
+ // Ensure the mini collapse icon is set to the correct direction when the target is collapsed/expanded by any means
+ me.mon(target, 'collapse', me.onTargetCollapse, me);
+ me.mon(target, 'expand', me.onTargetExpand, me);
+
+ me.el.addCls(me.baseCls + '-' + me.orientation);
+ me.el.unselectable();
+
+ me.tracker = Ext.create('Ext.resizer.SplitterTracker', {
+ el: me.el
+ });
+
+ // Relay the most important events to our owner (could open wider later):
+ me.relayEvents(me.tracker, [ 'beforedragstart', 'dragstart', 'dragend' ]);
+ },
+
+ getCollapseDirection: function() {
+ var me = this,
+ idx,
+ type = me.ownerCt.layout.type;
+
+ // Avoid duplication of string tests.
+ // Create a two bit truth table of the configuration of the Splitter:
+ // Collapse Target | orientation
+ // 0 0 = next, horizontal
+ // 0 1 = next, vertical
+ // 1 0 = prev, horizontal
+ // 1 1 = prev, vertical
+ if (me.collapseTarget.isComponent) {
+ idx = Number(me.ownerCt.items.indexOf(me.collapseTarget) == me.ownerCt.items.indexOf(me) - 1) << 1 | Number(type == 'hbox');
+ } else {
+ idx = Number(me.collapseTarget == 'prev') << 1 | Number(type == 'hbox');
+ }
+
+ // Read the data out the truth table
+ me.orientation = ['horizontal', 'vertical'][idx & 1];
+ return ['bottom', 'right', 'top', 'left'][idx];
+ },
+
+ getCollapseTarget: function() {
+ var me = this;
+
+ return me.collapseTarget.isComponent ? me.collapseTarget : me.collapseTarget == 'prev' ? me.previousSibling() : me.nextSibling();
+ },
+
+ onTargetCollapse: function(target) {
+ this.el.addCls([this.collapsedClsInternal, this.collapsedCls]);
+ },
+
+ onTargetExpand: function(target) {
+ this.el.removeCls([this.collapsedClsInternal, this.collapsedCls]);
+ },
+
+ toggleTargetCmp: function(e, t) {
+ var cmp = this.getCollapseTarget();
+
+ if (cmp.isVisible()) {
+ // restore
+ if (cmp.collapsed) {
+ cmp.expand(cmp.animCollapse);
+ // collapse
+ } else {
+ cmp.collapse(this.renderData.collapseDir, cmp.animCollapse);
+ }
+ }
+ },
+
+ /*
+ * Work around IE bug. %age margins do not get recalculated on element resize unless repaint called.
+ */
+ setSize: function() {
+ var me = this;
+ me.callParent(arguments);
+ if (Ext.isIE) {
+ me.el.repaint();
+ }
+ }
+});
+
+/**
+ * This is a multi-pane, application-oriented UI layout style that supports multiple nested panels, automatic bars
+ * between regions and built-in {@link Ext.panel.Panel#collapsible expanding and collapsing} of regions.
+ *
+ * This class is intended to be extended or created via the `layout:'border'` {@link Ext.container.Container#layout}
+ * config, and should generally not need to be created directly via the new keyword.
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * width: 500,
+ * height: 400,
+ * title: 'Border Layout',
+ * layout: 'border',
+ * items: [{
+ * title: 'South Region is resizable',
+ * region: 'south', // position for region
+ * xtype: 'panel',
+ * height: 100,
+ * split: true, // enable resizing
+ * margins: '0 5 5 5'
+ * },{
+ * // xtype: 'panel' implied by default
+ * title: 'West Region is collapsible',
+ * region:'west',
+ * xtype: 'panel',
+ * margins: '5 0 0 5',
+ * width: 200,
+ * collapsible: true, // make collapsible
+ * id: 'west-region-container',
+ * layout: 'fit'
+ * },{
+ * title: 'Center Region',
+ * region: 'center', // center region is required, no width/height specified
+ * xtype: 'panel',
+ * layout: 'fit',
+ * margins: '5 5 0 0'
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * # Notes
+ *
+ * - Any Container using the Border layout **must** have a child item with `region:'center'`.
+ * The child item in the center region will always be resized to fill the remaining space
+ * not used by the other regions in the layout.
+ *
+ * - Any child items with a region of `west` or `east` may be configured with either an initial
+ * `width`, or a {@link Ext.layout.container.Box#flex} value, or an initial percentage width
+ * **string** (Which is simply divided by 100 and used as a flex value).
+ * The 'center' region has a flex value of `1`.
+ *
+ * - Any child items with a region of `north` or `south` may be configured with either an initial
+ * `height`, or a {@link Ext.layout.container.Box#flex} value, or an initial percentage height
+ * **string** (Which is simply divided by 100 and used as a flex value).
+ * The 'center' region has a flex value of `1`.
+ *
+ * - The regions of a BorderLayout are **fixed at render time** and thereafter, its child
+ * Components may not be removed or added**. To add/remove Components within a BorderLayout,
+ * have them wrapped by an additional Container which is directly managed by the BorderLayout.
+ * If the region is to be collapsible, the Container used directly by the BorderLayout manager
+ * should be a Panel. In the following example a Container (an Ext.panel.Panel) is added to
+ * the west region:
+ *
+ * wrc = {@link Ext#getCmp Ext.getCmp}('west-region-container');
+ * wrc.{@link Ext.container.Container#removeAll removeAll}();
+ * wrc.{@link Ext.container.Container#add add}({
+ * title: 'Added Panel',
+ * html: 'Some content'
+ * });
+ *
+ * - **There is no BorderLayout.Region class in ExtJS 4.0+**
+ */
+Ext.define('Ext.layout.container.Border', {
+
+ alias: ['layout.border'],
+ extend: 'Ext.layout.container.Container',
+ requires: ['Ext.resizer.Splitter', 'Ext.container.Container', 'Ext.fx.Anim'],
+ alternateClassName: 'Ext.layout.BorderLayout',
+
+ targetCls: Ext.baseCSSPrefix + 'border-layout-ct',
+
+ itemCls: Ext.baseCSSPrefix + 'border-item',
+
+ bindToOwnerCtContainer: true,
+
+ percentageRe: /(\d+)%/,
+
+ slideDirection: {
+ north: 't',
+ south: 'b',
+ west: 'l',
+ east: 'r'
+ },
+
+ constructor: function(config) {
+ this.initialConfig = config;
+ this.callParent(arguments);
+ },
+
+ onLayout: function() {
+ var me = this;
+ if (!me.borderLayoutInitialized) {
+ me.initializeBorderLayout();
+ }
+
+ // Delegate this operation to the shadow "V" or "H" box layout, and then down to any embedded layout.
+ me.fixHeightConstraints();
+ me.shadowLayout.onLayout();
+ if (me.embeddedContainer) {
+ me.embeddedContainer.layout.onLayout();
+ }
+
+ // If the panel was originally configured with collapsed: true, it will have
+ // been initialized with a "borderCollapse" flag: Collapse it now before the first layout.
+ if (!me.initialCollapsedComplete) {
+ Ext.iterate(me.regions, function(name, region){
+ if (region.borderCollapse) {
+ me.onBeforeRegionCollapse(region, region.collapseDirection, false, 0);
+ }
+ });
+ me.initialCollapsedComplete = true;
+ }
+ },
+
+ isValidParent : function(item, target, position) {
+ if (!this.borderLayoutInitialized) {
+ this.initializeBorderLayout();
+ }
+
+ // Delegate this operation to the shadow "V" or "H" box layout.
+ return this.shadowLayout.isValidParent(item, target, position);
+ },
+
+ beforeLayout: function() {
+ if (!this.borderLayoutInitialized) {
+ this.initializeBorderLayout();
+ }
+
+ // Delegate this operation to the shadow "V" or "H" box layout.
+ this.shadowLayout.beforeLayout();
+
+ // note: don't call base because that does a renderItems again
+ },
+
+ renderItems: function(items, target) {
+ },
+
+ renderItem: function(item) {
+ },
+
+ renderChildren: function() {
+ if (!this.borderLayoutInitialized) {
+ this.initializeBorderLayout();
+ }
+
+ this.shadowLayout.renderChildren();
+ },
+
+ /*
+ * Gathers items for a layout operation. Injected into child Box layouts through configuration.
+ * We must not include child items which are floated over the layout (are primed with a slide out animation)
+ */
+ getVisibleItems: function() {
+ return Ext.ComponentQuery.query(':not([slideOutAnim])', this.callParent(arguments));
+ },
+
+ initializeBorderLayout: function() {
+ var me = this,
+ i = 0,
+ items = me.getLayoutItems(),
+ ln = items.length,
+ regions = (me.regions = {}),
+ vBoxItems = [],
+ hBoxItems = [],
+ horizontalFlex = 0,
+ verticalFlex = 0,
+ comp, percentage;
+
+ // Map of Splitters for each region
+ me.splitters = {};
+
+ // Map of regions
+ for (; i < ln; i++) {
+ comp = items[i];
+ regions[comp.region] = comp;
+
+ // Intercept collapsing to implement showing an alternate Component as a collapsed placeholder
+ if (comp.region != 'center' && comp.collapsible && comp.collapseMode != 'header') {
+
+ // This layout intercepts any initial collapsed state. Panel must not do this itself.
+ comp.borderCollapse = comp.collapsed;
+ comp.collapsed = false;
+
+ comp.on({
+ beforecollapse: me.onBeforeRegionCollapse,
+ beforeexpand: me.onBeforeRegionExpand,
+ destroy: me.onRegionDestroy,
+ scope: me
+ });
+ me.setupState(comp);
+ }
+ }
+ comp = regions.center;
+ if (!comp.flex) {
+ comp.flex = 1;
+ }
+ delete comp.width;
+ comp.maintainFlex = true;
+
+ // Begin the VBox and HBox item list.
+ comp = regions.west;
+ if (comp) {
+ comp.collapseDirection = Ext.Component.DIRECTION_LEFT;
+ hBoxItems.push(comp);
+ if (comp.split) {
+ hBoxItems.push(me.splitters.west = me.createSplitter(comp));
+ }
+ percentage = Ext.isString(comp.width) && comp.width.match(me.percentageRe);
+ if (percentage) {
+ horizontalFlex += (comp.flex = parseInt(percentage[1], 10) / 100);
+ delete comp.width;
+ }
+ }
+ comp = regions.north;
+ if (comp) {
+ comp.collapseDirection = Ext.Component.DIRECTION_TOP;
+ vBoxItems.push(comp);
+ if (comp.split) {
+ vBoxItems.push(me.splitters.north = me.createSplitter(comp));
+ }
+ percentage = Ext.isString(comp.height) && comp.height.match(me.percentageRe);
+ if (percentage) {
+ verticalFlex += (comp.flex = parseInt(percentage[1], 10) / 100);
+ delete comp.height;
+ }
+ }
+
+ // Decide into which Collection the center region goes.
+ if (regions.north || regions.south) {
+ if (regions.east || regions.west) {
+
+ // Create the embedded center. Mark it with the region: 'center' property so that it can be identified as the center.
+ vBoxItems.push(me.embeddedContainer = Ext.create('Ext.container.Container', {
+ xtype: 'container',
+ region: 'center',
+ id: me.owner.id + '-embedded-center',
+ cls: Ext.baseCSSPrefix + 'border-item',
+ flex: regions.center.flex,
+ maintainFlex: true,
+ layout: {
+ type: 'hbox',
+ align: 'stretch',
+ getVisibleItems: me.getVisibleItems
+ }
+ }));
+ hBoxItems.push(regions.center);
+ }
+ // No east or west: the original center goes straight into the vbox
+ else {
+ vBoxItems.push(regions.center);
+ }
+ }
+ // If we have no north or south, then the center is part of the HBox items
+ else {
+ hBoxItems.push(regions.center);
+ }
+
+ // Finish off the VBox and HBox item list.
+ comp = regions.south;
+ if (comp) {
+ comp.collapseDirection = Ext.Component.DIRECTION_BOTTOM;
+ if (comp.split) {
+ vBoxItems.push(me.splitters.south = me.createSplitter(comp));
+ }
+ percentage = Ext.isString(comp.height) && comp.height.match(me.percentageRe);
+ if (percentage) {
+ verticalFlex += (comp.flex = parseInt(percentage[1], 10) / 100);
+ delete comp.height;
+ }
+ vBoxItems.push(comp);
+ }
+ comp = regions.east;
+ if (comp) {
+ comp.collapseDirection = Ext.Component.DIRECTION_RIGHT;
+ if (comp.split) {
+ hBoxItems.push(me.splitters.east = me.createSplitter(comp));
+ }
+ percentage = Ext.isString(comp.width) && comp.width.match(me.percentageRe);
+ if (percentage) {
+ horizontalFlex += (comp.flex = parseInt(percentage[1], 10) / 100);
+ delete comp.width;
+ }
+ hBoxItems.push(comp);
+ }
+
+ // Create the injected "items" collections for the Containers.
+ // If we have north or south, then the shadow Container will be a VBox.
+ // If there are also east or west regions, its center will be a shadow HBox.
+ // If there are *only* east or west regions, then the shadow layout will be an HBox (or Fit).
+ if (regions.north || regions.south) {
+
+ me.shadowContainer = Ext.create('Ext.container.Container', {
+ ownerCt: me.owner,
+ el: me.getTarget(),
+ layout: Ext.applyIf({
+ type: 'vbox',
+ align: 'stretch',
+ getVisibleItems: me.getVisibleItems
+ }, me.initialConfig)
+ });
+ me.createItems(me.shadowContainer, vBoxItems);
+
+ // Allow the Splitters to orientate themselves
+ if (me.splitters.north) {
+ me.splitters.north.ownerCt = me.shadowContainer;
+ }
+ if (me.splitters.south) {
+ me.splitters.south.ownerCt = me.shadowContainer;
+ }
+
+ // Inject items into the HBox Container if there is one - if there was an east or west.
+ if (me.embeddedContainer) {
+ me.embeddedContainer.ownerCt = me.shadowContainer;
+ me.createItems(me.embeddedContainer, hBoxItems);
+
+ // Allow the Splitters to orientate themselves
+ if (me.splitters.east) {
+ me.splitters.east.ownerCt = me.embeddedContainer;
+ }
+ if (me.splitters.west) {
+ me.splitters.west.ownerCt = me.embeddedContainer;
+ }
+
+ // These spliiters need to be constrained by components one-level below
+ // the component in their vobx. We update the min/maxHeight on the helper
+ // (embeddedContainer) prior to starting the split/drag. This has to be
+ // done on-the-fly to allow min/maxHeight of the E/C/W regions to be set
+ // dynamically.
+ Ext.each([me.splitters.north, me.splitters.south], function (splitter) {
+ if (splitter) {
+ splitter.on('beforedragstart', me.fixHeightConstraints, me);
+ }
+ });
+
+ // The east or west region wanted a percentage
+ if (horizontalFlex) {
+ regions.center.flex -= horizontalFlex;
+ }
+ // The north or south region wanted a percentage
+ if (verticalFlex) {
+ me.embeddedContainer.flex -= verticalFlex;
+ }
+ } else {
+ // The north or south region wanted a percentage
+ if (verticalFlex) {
+ regions.center.flex -= verticalFlex;
+ }
+ }
+ }
+ // If we have no north or south, then there's only one Container, and it's
+ // an HBox, or, if only a center region was specified, a Fit.
+ else {
+ me.shadowContainer = Ext.create('Ext.container.Container', {
+ ownerCt: me.owner,
+ el: me.getTarget(),
+ layout: Ext.applyIf({
+ type: (hBoxItems.length == 1) ? 'fit' : 'hbox',
+ align: 'stretch'
+ }, me.initialConfig)
+ });
+ me.createItems(me.shadowContainer, hBoxItems);
+
+ // Allow the Splitters to orientate themselves
+ if (me.splitters.east) {
+ me.splitters.east.ownerCt = me.shadowContainer;
+ }
+ if (me.splitters.west) {
+ me.splitters.west.ownerCt = me.shadowContainer;
+ }
+
+ // The east or west region wanted a percentage
+ if (horizontalFlex) {
+ regions.center.flex -= verticalFlex;
+ }
+ }
+
+ // Create upward links from the region Components to their shadow ownerCts
+ for (i = 0, items = me.shadowContainer.items.items, ln = items.length; i < ln; i++) {
+ items[i].shadowOwnerCt = me.shadowContainer;
+ }
+ if (me.embeddedContainer) {
+ for (i = 0, items = me.embeddedContainer.items.items, ln = items.length; i < ln; i++) {
+ items[i].shadowOwnerCt = me.embeddedContainer;
+ }
+ }
+
+ // This is the layout that we delegate all operations to
+ me.shadowLayout = me.shadowContainer.getLayout();
+
+ me.borderLayoutInitialized = true;
+ },
+
+ setupState: function(comp){
+ var getState = comp.getState;
+ comp.getState = function(){
+ // call the original getState
+ var state = getState.call(comp) || {},
+ region = comp.region;
+
+ state.collapsed = !!comp.collapsed;
+ if (region == 'west' || region == 'east') {
+ state.width = comp.getWidth();
+ } else {
+ state.height = comp.getHeight();
+ }
+ return state;
+ };
+ comp.addStateEvents(['collapse', 'expand', 'resize']);
+ },
+
+ /**
+ * Create the items collection for our shadow/embedded containers
+ * @private
+ */
+ createItems: function(container, items){
+ // Have to inject an items Collection *after* construction.
+ // The child items of the shadow layout must retain their original, user-defined ownerCt
+ delete container.items;
+ container.initItems();
+ container.items.addAll(items);
+ },
+
+ // Private
+ // Create a splitter for a child of the layout.
+ createSplitter: function(comp) {
+ var me = this,
+ interceptCollapse = (comp.collapseMode != 'header'),
+ resizer;
+
+ resizer = Ext.create('Ext.resizer.Splitter', {
+ hidden: !!comp.hidden,
+ collapseTarget: comp,
+ performCollapse: !interceptCollapse,
+ listeners: interceptCollapse ? {
+ click: {
+ fn: Ext.Function.bind(me.onSplitterCollapseClick, me, [comp]),
+ element: 'collapseEl'
+ }
+ } : null
+ });
+
+ // Mini collapse means that the splitter is the placeholder Component
+ if (comp.collapseMode == 'mini') {
+ comp.placeholder = resizer;
+ resizer.collapsedCls = comp.collapsedCls;
+ }
+
+ // Arrange to hide/show a region's associated splitter when the region is hidden/shown
+ comp.on({
+ hide: me.onRegionVisibilityChange,
+ show: me.onRegionVisibilityChange,
+ scope: me
+ });
+ return resizer;
+ },
+
+ // Private
+ // Propagates the min/maxHeight values from the inner hbox items to its container.
+ fixHeightConstraints: function () {
+ var me = this,
+ ct = me.embeddedContainer,
+ maxHeight = 1e99, minHeight = -1;
+
+ if (!ct) {
+ return;
+ }
+
+ ct.items.each(function (item) {
+ if (Ext.isNumber(item.maxHeight)) {
+ maxHeight = Math.max(maxHeight, item.maxHeight);
+ }
+ if (Ext.isNumber(item.minHeight)) {
+ minHeight = Math.max(minHeight, item.minHeight);
+ }
+ });
+
+ ct.maxHeight = maxHeight;
+ ct.minHeight = minHeight;
+ },
+
+ // Hide/show a region's associated splitter when the region is hidden/shown
+ onRegionVisibilityChange: function(comp){
+ this.splitters[comp.region][comp.hidden ? 'hide' : 'show']();
+ this.layout();
+ },
+
+ // Called when a splitter mini-collapse tool is clicked on.
+ // The listener is only added if this layout is controlling collapsing,
+ // not if the component's collapseMode is 'mini' or 'header'.
+ onSplitterCollapseClick: function(comp) {
+ if (comp.collapsed) {
+ this.onPlaceHolderToolClick(null, null, null, {client: comp});
+ } else {
+ comp.collapse();
+ }
+ },
+
+ /**
+ * Return the {@link Ext.panel.Panel#placeholder placeholder} Component to which the passed child Panel of the
+ * layout will collapse. By default, this will be a {@link Ext.panel.Header Header} component (Docked to the
+ * appropriate border). See {@link Ext.panel.Panel#placeholder placeholder}. config to customize this.
+ *
+ * **Note that this will be a fully instantiated Component, but will only be _rendered_ when the Panel is first
+ * collapsed.**
+ * @param {Ext.panel.Panel} panel The child Panel of the layout for which to return the {@link
+ * Ext.panel.Panel#placeholder placeholder}.
+ * @return {Ext.Component} The Panel's {@link Ext.panel.Panel#placeholder placeholder} unless the {@link
+ * Ext.panel.Panel#collapseMode collapseMode} is `'header'`, in which case _undefined_ is returned.
+ */
+ getPlaceholder: function(comp) {
+ var me = this,
+ placeholder = comp.placeholder,
+ shadowContainer = comp.shadowOwnerCt,
+ shadowLayout = shadowContainer.layout,
+ oppositeDirection = Ext.panel.Panel.prototype.getOppositeDirection(comp.collapseDirection),
+ horiz = (comp.region == 'north' || comp.region == 'south');
+
+ // No placeholder if the collapse mode is not the Border layout default
+ if (comp.collapseMode == 'header') {
+ return;
+ }
+
+ // Provide a replacement Container with an expand tool
+ if (!placeholder) {
+ if (comp.collapseMode == 'mini') {
+ placeholder = Ext.create('Ext.resizer.Splitter', {
+ id: 'collapse-placeholder-' + comp.id,
+ collapseTarget: comp,
+ performCollapse: false,
+ listeners: {
+ click: {
+ fn: Ext.Function.bind(me.onSplitterCollapseClick, me, [comp]),
+ element: 'collapseEl'
+ }
+ }
+ });
+ placeholder.addCls(placeholder.collapsedCls);
+ } else {
+ placeholder = {
+ id: 'collapse-placeholder-' + comp.id,
+ margins: comp.initialConfig.margins || Ext.getClass(comp).prototype.margins,
+ xtype: 'header',
+ orientation: horiz ? 'horizontal' : 'vertical',
+ title: comp.title,
+ textCls: comp.headerTextCls,
+ iconCls: comp.iconCls,
+ baseCls: comp.baseCls + '-header',
+ ui: comp.ui,
+ indicateDrag: comp.draggable,
+ cls: Ext.baseCSSPrefix + 'region-collapsed-placeholder ' + Ext.baseCSSPrefix + 'region-collapsed-' + comp.collapseDirection + '-placeholder ' + comp.collapsedCls,
+ listeners: comp.floatable ? {
+ click: {
+ fn: function(e) {
+ me.floatCollapsedPanel(e, comp);
+ },
+ element: 'el'
+ }
+ } : null
+ };
+ // Hack for IE6/7/IEQuirks's inability to display an inline-block
+ if ((Ext.isIE6 || Ext.isIE7 || (Ext.isIEQuirks)) && !horiz) {
+ placeholder.width = 25;
+ }
+ if (!comp.hideCollapseTool) {
+ placeholder[horiz ? 'tools' : 'items'] = [{
+ xtype: 'tool',
+ client: comp,
+ type: 'expand-' + oppositeDirection,
+ handler: me.onPlaceHolderToolClick,
+ scope: me
+ }];
+ }
+ }
+ placeholder = me.owner.createComponent(placeholder);
+ if (comp.isXType('panel')) {
+ comp.on({
+ titlechange: me.onRegionTitleChange,
+ iconchange: me.onRegionIconChange,
+ scope: me
+ });
+ }
+ }
+
+ // The collapsed Component holds a reference to its placeholder and vice versa
+ comp.placeholder = placeholder;
+ placeholder.comp = comp;
+
+ return placeholder;
+ },
+
+ /**
+ * @private
+ * Update the placeholder title when panel title has been set or changed.
+ */
+ onRegionTitleChange: function(comp, newTitle) {
+ comp.placeholder.setTitle(newTitle);
+ },
+
+ /**
+ * @private
+ * Update the placeholder iconCls when panel iconCls has been set or changed.
+ */
+ onRegionIconChange: function(comp, newIconCls) {
+ comp.placeholder.setIconCls(newIconCls);
+ },
+
+ /**
+ * @private
+ * Calculates the size and positioning of the passed child item. Must be present because Panel's expand,
+ * when configured with a flex, calls this method on its ownerCt's layout.
+ * @param {Ext.Component} child The child Component to calculate the box for
+ * @return {Object} Object containing box measurements for the child. Properties are left,top,width,height.
+ */
+ calculateChildBox: function(comp) {
+ var me = this;
+ if (me.shadowContainer.items.contains(comp)) {
+ return me.shadowContainer.layout.calculateChildBox(comp);
+ }
+ else if (me.embeddedContainer && me.embeddedContainer.items.contains(comp)) {
+ return me.embeddedContainer.layout.calculateChildBox(comp);
+ }
+ },
+
+ /**
+ * @private
+ * Intercepts the Panel's own collapse event and perform's substitution of the Panel
+ * with a placeholder Header orientated in the appropriate dimension.
+ * @param comp The Panel being collapsed.
+ * @param direction
+ * @param animate
+ * @returns {Boolean} false to inhibit the Panel from performing its own collapse.
+ */
+ onBeforeRegionCollapse: function(comp, direction, animate) {
+ if (comp.collapsedChangingLayout) {
+ return false;
+ }
+ comp.collapsedChangingLayout = true;
+ var me = this,
+ compEl = comp.el,
+ width,
+ miniCollapse = comp.collapseMode == 'mini',
+ shadowContainer = comp.shadowOwnerCt,
+ shadowLayout = shadowContainer.layout,
+ placeholder = comp.placeholder,
+ sl = me.owner.suspendLayout,
+ scsl = shadowContainer.suspendLayout,
+ isNorthOrWest = (comp.region == 'north' || comp.region == 'west'); // Flag to keep the placeholder non-adjacent to any Splitter
+
+ // Do not trigger a layout during transition to collapsed Component
+ me.owner.suspendLayout = true;
+ shadowContainer.suspendLayout = true;
+
+ // Prevent upward notifications from downstream layouts
+ shadowLayout.layoutBusy = true;
+ if (shadowContainer.componentLayout) {
+ shadowContainer.componentLayout.layoutBusy = true;
+ }
+ me.shadowContainer.layout.layoutBusy = true;
+ me.layoutBusy = true;
+ me.owner.componentLayout.layoutBusy = true;
+
+ // Provide a replacement Container with an expand tool
+ if (!placeholder) {
+ placeholder = me.getPlaceholder(comp);
+ }
+
+ // placeholder already in place; show it.
+ if (placeholder.shadowOwnerCt === shadowContainer) {
+ placeholder.show();
+ }
+ // Insert the collapsed placeholder Component into the appropriate Box layout shadow Container
+ // It must go next to its client Component, but non-adjacent to the splitter so splitter can find its collapse client.
+ // Inject an ownerCt value pointing to the owner, border layout Container as the user will expect.
+ else {
+ shadowContainer.insert(shadowContainer.items.indexOf(comp) + (isNorthOrWest ? 0 : 1), placeholder);
+ placeholder.shadowOwnerCt = shadowContainer;
+ placeholder.ownerCt = me.owner;
+ }
+
+ // Flag the collapsing Component as hidden and show the placeholder.
+ // This causes the shadow Box layout's calculateChildBoxes to calculate the correct new arrangement.
+ // We hide or slideOut the Component's element
+ comp.hidden = true;
+
+ if (!placeholder.rendered) {
+ shadowLayout.renderItem(placeholder, shadowLayout.innerCt);
+
+ // The inserted placeholder does not have the proper size, so copy the width
+ // for N/S or the height for E/W from the component. This fixes EXTJSIV-1562
+ // without recursive layouts. This is only an issue initially. After this time,
+ // placeholder will have the correct width/height set by the layout (which has
+ // already happened when we get here initially).
+ if (comp.region == 'north' || comp.region == 'south') {
+ placeholder.setCalculatedSize(comp.getWidth());
+ } else {
+ placeholder.setCalculatedSize(undefined, comp.getHeight());
+ }
+ }
+
+ // Jobs to be done after the collapse has been done
+ function afterCollapse() {
+ // Reinstate automatic laying out.
+ me.owner.suspendLayout = sl;
+ shadowContainer.suspendLayout = scsl;
+ delete shadowLayout.layoutBusy;
+ if (shadowContainer.componentLayout) {
+ delete shadowContainer.componentLayout.layoutBusy;
+ }
+ delete me.shadowContainer.layout.layoutBusy;
+ delete me.layoutBusy;
+ delete me.owner.componentLayout.layoutBusy;
+ delete comp.collapsedChangingLayout;
+
+ // Fire the collapse event: The Panel has in fact been collapsed, but by substitution of an alternative Component
+ comp.collapsed = true;
+ comp.fireEvent('collapse', comp);
+ }
+
+ /*
+ * Set everything to the new positions. Note that we
+ * only want to animate the collapse if it wasn't configured
+ * initially with collapsed: true
+ */
+ if (comp.animCollapse && me.initialCollapsedComplete) {
+ shadowLayout.layout();
+ compEl.dom.style.zIndex = 100;
+
+ // If we're mini-collapsing, the placholder is a Splitter. We don't want it to "bounce in"
+ if (!miniCollapse) {
+ placeholder.el.hide();
+ }
+ compEl.slideOut(me.slideDirection[comp.region], {
+ duration: Ext.Number.from(comp.animCollapse, Ext.fx.Anim.prototype.duration),
+ listeners: {
+ afteranimate: function() {
+ compEl.show().setLeftTop(-10000, -10000);
+ compEl.dom.style.zIndex = '';
+
+ // If we're mini-collapsing, the placholder is a Splitter. We don't want it to "bounce in"
+ if (!miniCollapse) {
+ placeholder.el.slideIn(me.slideDirection[comp.region], {
+ easing: 'linear',
+ duration: 100
+ });
+ }
+ afterCollapse();
+ }
+ }
+ });
+ } else {
+ compEl.setLeftTop(-10000, -10000);
+ shadowLayout.layout();
+ afterCollapse();
+ }
+
+ return false;
+ },
+
+ // Hijack the expand operation to remove the placeholder and slide the region back in.
+ onBeforeRegionExpand: function(comp, animate) {
+ // We don't check for comp.collapsedChangingLayout here because onPlaceHolderToolClick does it
+ this.onPlaceHolderToolClick(null, null, null, {client: comp, shouldFireBeforeexpand: false});
+ return false;
+ },
+
+ // Called when the collapsed placeholder is clicked to reinstate a "collapsed" (in reality hidden) Panel.
+ onPlaceHolderToolClick: function(e, target, owner, tool) {
+ var me = this,
+ comp = tool.client,
+
+ // Hide the placeholder unless it was the Component's preexisting splitter
+ hidePlaceholder = (comp.collapseMode != 'mini') || !comp.split,
+ compEl = comp.el,
+ toCompBox,
+ placeholder = comp.placeholder,
+ placeholderEl = placeholder.el,
+ shadowContainer = comp.shadowOwnerCt,
+ shadowLayout = shadowContainer.layout,
+ curSize,
+ sl = me.owner.suspendLayout,
+ scsl = shadowContainer.suspendLayout,
+ isFloating;
+
+ if (comp.collapsedChangingLayout) {
+ return false;
+ }
+ if (tool.shouldFireBeforeexpand !== false && comp.fireEvent('beforeexpand', comp, true) === false) {
+ return false;
+ }
+ comp.collapsedChangingLayout = true;
+ // If the slide in is still going, stop it.
+ // This will either leave the Component in its fully floated state (which is processed below)
+ // or in its collapsed state. Either way, we expand it..
+ if (comp.getActiveAnimation()) {
+ comp.stopAnimation();
+ }
+
+ // If the Component is fully floated when they click the placeholder Tool,
+ // it will be primed with a slide out animation object... so delete that
+ // and remove the mouseout listeners
+ if (comp.slideOutAnim) {
+ // Remove mouse leave monitors
+ compEl.un(comp.panelMouseMon);
+ placeholderEl.un(comp.placeholderMouseMon);
+
+ delete comp.slideOutAnim;
+ delete comp.panelMouseMon;
+ delete comp.placeholderMouseMon;
+
+ // If the Panel was floated and primed with a slideOut animation, we don't want to animate its layout operation.
+ isFloating = true;
+ }
+
+ // Do not trigger a layout during transition to expanded Component
+ me.owner.suspendLayout = true;
+ shadowContainer.suspendLayout = true;
+
+ // Prevent upward notifications from downstream layouts
+ shadowLayout.layoutBusy = true;
+ if (shadowContainer.componentLayout) {
+ shadowContainer.componentLayout.layoutBusy = true;
+ }
+ me.shadowContainer.layout.layoutBusy = true;
+ me.layoutBusy = true;
+ me.owner.componentLayout.layoutBusy = true;
+
+ // Unset the hidden and collapsed flags set in onBeforeRegionCollapse. The shadowLayout will now take it into account
+ // Find where the shadow Box layout plans to put the expanding Component.
+ comp.hidden = false;
+ comp.collapsed = false;
+ if (hidePlaceholder) {
+ placeholder.hidden = true;
+ }
+ toCompBox = shadowLayout.calculateChildBox(comp);
+
+ // Show the collapse tool in case it was hidden by the slide-in
+ if (comp.collapseTool) {
+ comp.collapseTool.show();
+ }
+
+ // If we're going to animate, we need to hide the component before moving it back into position
+ if (comp.animCollapse && !isFloating) {
+ compEl.setStyle('visibility', 'hidden');
+ }
+ compEl.setLeftTop(toCompBox.left, toCompBox.top);
+
+ // Equalize the size of the expanding Component prior to animation
+ // in case the layout area has changed size during the time it was collapsed.
+ curSize = comp.getSize();
+ if (curSize.height != toCompBox.height || curSize.width != toCompBox.width) {
+ me.setItemSize(comp, toCompBox.width, toCompBox.height);
+ }
+
+ // Jobs to be done after the expand has been done
+ function afterExpand() {
+ // Reinstate automatic laying out.
+ me.owner.suspendLayout = sl;
+ shadowContainer.suspendLayout = scsl;
+ delete shadowLayout.layoutBusy;
+ if (shadowContainer.componentLayout) {
+ delete shadowContainer.componentLayout.layoutBusy;
+ }
+ delete me.shadowContainer.layout.layoutBusy;
+ delete me.layoutBusy;
+ delete me.owner.componentLayout.layoutBusy;
+ delete comp.collapsedChangingLayout;
+
+ // In case it was floated out and they clicked the re-expand tool
+ comp.removeCls(Ext.baseCSSPrefix + 'border-region-slide-in');
+
+ // Fire the expand event: The Panel has in fact been expanded, but by removal of an alternative Component
+ comp.fireEvent('expand', comp);
+ }
+
+ // Hide the placeholder
+ if (hidePlaceholder) {
+ placeholder.el.hide();
+ }
+
+ // Slide the expanding Component to its new position.
+ // When that is done, layout the layout.
+ if (comp.animCollapse && !isFloating) {
+ compEl.dom.style.zIndex = 100;
+ compEl.slideIn(me.slideDirection[comp.region], {
+ duration: Ext.Number.from(comp.animCollapse, Ext.fx.Anim.prototype.duration),
+ listeners: {
+ afteranimate: function() {
+ compEl.dom.style.zIndex = '';
+ comp.hidden = false;
+ shadowLayout.onLayout();
+ afterExpand();
+ }
+ }
+ });
+ } else {
+ shadowLayout.onLayout();
+ afterExpand();
+ }
+ },
+
+ floatCollapsedPanel: function(e, comp) {
+
+ if (comp.floatable === false) {
+ return;
+ }
+
+ var me = this,
+ compEl = comp.el,
+ placeholder = comp.placeholder,
+ placeholderEl = placeholder.el,
+ shadowContainer = comp.shadowOwnerCt,
+ shadowLayout = shadowContainer.layout,
+ placeholderBox = shadowLayout.getChildBox(placeholder),
+ scsl = shadowContainer.suspendLayout,
+ curSize, toCompBox, compAnim;
+
+ // Ignore clicks on tools.
+ if (e.getTarget('.' + Ext.baseCSSPrefix + 'tool')) {
+ return;
+ }
+
+ // It's *being* animated, ignore the click.
+ // Possible future enhancement: Stop and *reverse* the current active Fx.
+ if (compEl.getActiveAnimation()) {
+ return;
+ }
+
+ // If the Component is already fully floated when they click the placeholder,
+ // it will be primed with a slide out animation object... so slide it out.
+ if (comp.slideOutAnim) {
+ me.slideOutFloatedComponent(comp);
+ return;
+ }
+
+ // Function to be called when the mouse leaves the floated Panel
+ // Slide out when the mouse leaves the region bounded by the slid Component and its placeholder.
+ function onMouseLeaveFloated(e) {
+ var slideRegion = compEl.getRegion().union(placeholderEl.getRegion()).adjust(1, -1, -1, 1);
+
+ // If mouse is not within slide Region, slide it out
+ if (!slideRegion.contains(e.getPoint())) {
+ me.slideOutFloatedComponent(comp);
+ }
+ }
+
+ // Monitor for mouseouting of the placeholder. Hide it if they exit for half a second or more
+ comp.placeholderMouseMon = placeholderEl.monitorMouseLeave(500, onMouseLeaveFloated);
+
+ // Do not trigger a layout during slide out of the Component
+ shadowContainer.suspendLayout = true;
+
+ // Prevent upward notifications from downstream layouts
+ me.layoutBusy = true;
+ me.owner.componentLayout.layoutBusy = true;
+
+ // The collapse tool is hidden while slid.
+ // It is re-shown on expand.
+ if (comp.collapseTool) {
+ comp.collapseTool.hide();
+ }
+
+ // Set flags so that the layout will calculate the boxes for what we want
+ comp.hidden = false;
+ comp.collapsed = false;
+ placeholder.hidden = true;
+
+ // Recalculate new arrangement of the Component being floated.
+ toCompBox = shadowLayout.calculateChildBox(comp);
+ placeholder.hidden = false;
+
+ // Component to appear just after the placeholder, whatever "after" means in the context of the shadow Box layout.
+ if (comp.region == 'north' || comp.region == 'west') {
+ toCompBox[shadowLayout.parallelBefore] += placeholderBox[shadowLayout.parallelPrefix] - 1;
+ } else {
+ toCompBox[shadowLayout.parallelBefore] -= (placeholderBox[shadowLayout.parallelPrefix] - 1);
+ }
+ compEl.setStyle('visibility', 'hidden');
+ compEl.setLeftTop(toCompBox.left, toCompBox.top);
+
+ // Equalize the size of the expanding Component prior to animation
+ // in case the layout area has changed size during the time it was collapsed.
+ curSize = comp.getSize();
+ if (curSize.height != toCompBox.height || curSize.width != toCompBox.width) {
+ me.setItemSize(comp, toCompBox.width, toCompBox.height);
+ }
+
+ // This animation slides the collapsed Component's el out to just beyond its placeholder
+ compAnim = {
+ listeners: {
+ afteranimate: function() {
+ shadowContainer.suspendLayout = scsl;
+ delete me.layoutBusy;
+ delete me.owner.componentLayout.layoutBusy;
+
+ // Prime the Component with an Anim config object to slide it back out
+ compAnim.listeners = {
+ afterAnimate: function() {
+ compEl.show().removeCls(Ext.baseCSSPrefix + 'border-region-slide-in').setLeftTop(-10000, -10000);
+
+ // Reinstate the correct, current state after slide out animation finishes
+ comp.hidden = true;
+ comp.collapsed = true;
+ delete comp.slideOutAnim;
+ delete comp.panelMouseMon;
+ delete comp.placeholderMouseMon;
+ }
+ };
+ comp.slideOutAnim = compAnim;
+ }
+ },
+ duration: 500
+ };
+
+ // Give the element the correct class which places it at a high z-index
+ compEl.addCls(Ext.baseCSSPrefix + 'border-region-slide-in');
+
+ // Begin the slide in
+ compEl.slideIn(me.slideDirection[comp.region], compAnim);
+
+ // Monitor for mouseouting of the slid area. Hide it if they exit for half a second or more
+ comp.panelMouseMon = compEl.monitorMouseLeave(500, onMouseLeaveFloated);
+
+ },
+
+ slideOutFloatedComponent: function(comp) {
+ var compEl = comp.el,
+ slideOutAnim;
+
+ // Remove mouse leave monitors
+ compEl.un(comp.panelMouseMon);
+ comp.placeholder.el.un(comp.placeholderMouseMon);
+
+ // Slide the Component out
+ compEl.slideOut(this.slideDirection[comp.region], comp.slideOutAnim);
+
+ delete comp.slideOutAnim;
+ delete comp.panelMouseMon;
+ delete comp.placeholderMouseMon;
+ },
+
+ /*
+ * @private
+ * Ensure any collapsed placeholder Component is destroyed along with its region.
+ * Can't do this in onDestroy because they may remove a Component and use it elsewhere.
+ */
+ onRegionDestroy: function(comp) {
+ var placeholder = comp.placeholder;
+ if (placeholder) {
+ delete placeholder.ownerCt;
+ placeholder.destroy();
+ }
+ },
+
+ /*
+ * @private
+ * Ensure any shadow Containers are destroyed.
+ * Ensure we don't keep references to Components.
+ */
+ onDestroy: function() {
+ var me = this,
+ shadowContainer = me.shadowContainer,
+ embeddedContainer = me.embeddedContainer;
+
+ if (shadowContainer) {
+ delete shadowContainer.ownerCt;
+ Ext.destroy(shadowContainer);
+ }
+
+ if (embeddedContainer) {
+ delete embeddedContainer.ownerCt;
+ Ext.destroy(embeddedContainer);
+ }
+ delete me.regions;
+ delete me.splitters;
+ delete me.shadowContainer;
+ delete me.embeddedContainer;
+ me.callParent(arguments);
+ }
+});
+
+/**
+ * This layout manages multiple child Components, each fitted to the Container, where only a single child Component can be
+ * visible at any given time. This layout style is most commonly used for wizards, tab implementations, etc.
+ * This class is intended to be extended or created via the layout:'card' {@link Ext.container.Container#layout} config,
+ * and should generally not need to be created directly via the new keyword.
+ *
+ * The CardLayout's focal method is {@link #setActiveItem}. Since only one panel is displayed at a time,
+ * the only way to move from one Component to the next is by calling setActiveItem, passing the next panel to display
+ * (or its id or index). The layout itself does not provide a user interface for handling this navigation,
+ * so that functionality must be provided by the developer.
+ *
+ * To change the active card of a container, call the setActiveItem method of its layout:
+ *
+ * Ext.create('Ext.panel.Panel', {
+ * layout: 'card',
+ * items: [
+ * { html: 'Card 1' },
+ * { html: 'Card 2' }
+ * ],
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * p.getLayout().setActiveItem(1);
+ *
+ * In the following example, a simplistic wizard setup is demonstrated. A button bar is added
+ * to the footer of the containing panel to provide navigation buttons. The buttons will be handled by a
+ * common navigation routine. Note that other uses of a CardLayout (like a tab control) would require a
+ * completely different implementation. For serious implementations, a better approach would be to extend
+ * CardLayout to provide the custom functionality needed.
+ *
+ * @example
+ * var navigate = function(panel, direction){
+ * // This routine could contain business logic required to manage the navigation steps.
+ * // It would call setActiveItem as needed, manage navigation button state, handle any
+ * // branching logic that might be required, handle alternate actions like cancellation
+ * // or finalization, etc. A complete wizard implementation could get pretty
+ * // sophisticated depending on the complexity required, and should probably be
+ * // done as a subclass of CardLayout in a real-world implementation.
+ * var layout = panel.getLayout();
+ * layout[direction]();
+ * Ext.getCmp('move-prev').setDisabled(!layout.getPrev());
+ * Ext.getCmp('move-next').setDisabled(!layout.getNext());
+ * };
+ *
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Example Wizard',
+ * width: 300,
+ * height: 200,
+ * layout: 'card',
+ * bodyStyle: 'padding:15px',
+ * defaults: {
+ * // applied to each contained panel
+ * border: false
+ * },
+ * // just an example of one possible navigation scheme, using buttons
+ * bbar: [
+ * {
+ * id: 'move-prev',
+ * text: 'Back',
+ * handler: function(btn) {
+ * navigate(btn.up("panel"), "prev");
+ * },
+ * disabled: true
+ * },
+ * '->', // greedy spacer so that the buttons are aligned to each side
+ * {
+ * id: 'move-next',
+ * text: 'Next',
+ * handler: function(btn) {
+ * navigate(btn.up("panel"), "next");
+ * }
+ * }
+ * ],
+ * // the panels (or "cards") within the layout
+ * items: [{
+ * id: 'card-0',
+ * html: '<h1>Welcome to the Wizard!</h1><p>Step 1 of 3</p>'
+ * },{
+ * id: 'card-1',
+ * html: '<p>Step 2 of 3</p>'
+ * },{
+ * id: 'card-2',
+ * html: '<h1>Congratulations!</h1><p>Step 3 of 3 - Complete</p>'
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.layout.container.Card', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.card'],
+ alternateClassName: 'Ext.layout.CardLayout',
+
+ extend: 'Ext.layout.container.AbstractCard',
+
+ /* End Definitions */
+
+ /**
+ * Makes the given card active.
+ *
+ * var card1 = Ext.create('Ext.panel.Panel', {itemId: 'card-1'});
+ * var card2 = Ext.create('Ext.panel.Panel', {itemId: 'card-2'});
+ * var panel = Ext.create('Ext.panel.Panel', {
+ * layout: 'card',
+ * activeItem: 0,
+ * items: [card1, card2]
+ * });
+ * // These are all equivalent
+ * panel.getLayout().setActiveItem(card2);
+ * panel.getLayout().setActiveItem('card-2');
+ * panel.getLayout().setActiveItem(1);
+ *
+ * @param {Ext.Component/Number/String} newCard The component, component {@link Ext.Component#id id},
+ * {@link Ext.Component#itemId itemId}, or index of component.
+ * @return {Ext.Component} the activated component or false when nothing activated.
+ * False is returned also when trying to activate an already active card.
+ */
+ setActiveItem: function(newCard) {
+ var me = this,
+ owner = me.owner,
+ oldCard = me.activeItem,
+ newIndex;
+
+ newCard = me.parseActiveItem(newCard);
+ newIndex = owner.items.indexOf(newCard);
+
+ // If the card is not a child of the owner, then add it
+ if (newIndex == -1) {
+ newIndex = owner.items.items.length;
+ owner.add(newCard);
+ }
+
+ // Is this a valid, different card?
+ if (newCard && oldCard != newCard) {
+ // If the card has not been rendered yet, now is the time to do so.
+ if (!newCard.rendered) {
+ me.renderItem(newCard, me.getRenderTarget(), owner.items.length);
+ me.configureItem(newCard, 0);
+ }
+
+ me.activeItem = newCard;
+
+ // Fire the beforeactivate and beforedeactivate events on the cards
+ if (newCard.fireEvent('beforeactivate', newCard, oldCard) === false) {
+ return false;
+ }
+ if (oldCard && oldCard.fireEvent('beforedeactivate', oldCard, newCard) === false) {
+ return false;
+ }
+
+ // If the card hasnt been sized yet, do it now
+ if (me.sizeAllCards) {
+ // onLayout calls setItemBox
+ me.onLayout();
+ }
+ else {
+ me.setItemBox(newCard, me.getTargetBox());
+ }
+
+ me.owner.suspendLayout = true;
+
+ if (oldCard) {
+ if (me.hideInactive) {
+ oldCard.hide();
+ }
+ oldCard.fireEvent('deactivate', oldCard, newCard);
+ }
+
+ // Make sure the new card is shown
+ me.owner.suspendLayout = false;
+ if (newCard.hidden) {
+ newCard.show();
+ } else {
+ me.onLayout();
+ }
+
+ newCard.fireEvent('activate', newCard, oldCard);
+
+ return newCard;
+ }
+ return false;
+ },
+
+ configureItem: function(item) {
+ // Card layout only controls dimensions which IT has controlled.
+ // That calculation has to be determined at run time by examining the ownerCt's isFixedWidth()/isFixedHeight() methods
+ item.layoutManagedHeight = 0;
+ item.layoutManagedWidth = 0;
+
+ this.callParent(arguments);
+ }});
+/**
+ * This is the layout style of choice for creating structural layouts in a multi-column format where the width of each
+ * column can be specified as a percentage or fixed width, but the height is allowed to vary based on the content. This
+ * class is intended to be extended or created via the layout:'column' {@link Ext.container.Container#layout} config,
+ * and should generally not need to be created directly via the new keyword.
+ *
+ * ColumnLayout does not have any direct config options (other than inherited ones), but it does support a specific
+ * config property of `columnWidth` that can be included in the config of any panel added to it. The layout will use
+ * the columnWidth (if present) or width of each panel during layout to determine how to size each panel. If width or
+ * columnWidth is not specified for a given panel, its width will default to the panel's width (or auto).
+ *
+ * The width property is always evaluated as pixels, and must be a number greater than or equal to 1. The columnWidth
+ * property is always evaluated as a percentage, and must be a decimal value greater than 0 and less than 1 (e.g., .25).
+ *
+ * The basic rules for specifying column widths are pretty simple. The logic makes two passes through the set of
+ * contained panels. During the first layout pass, all panels that either have a fixed width or none specified (auto)
+ * are skipped, but their widths are subtracted from the overall container width.
+ *
+ * During the second pass, all panels with columnWidths are assigned pixel widths in proportion to their percentages
+ * based on the total **remaining** container width. In other words, percentage width panels are designed to fill
+ * the space left over by all the fixed-width and/or auto-width panels. Because of this, while you can specify any
+ * number of columns with different percentages, the columnWidths must always add up to 1 (or 100%) when added
+ * together, otherwise your layout may not render as expected.
+ *
+ * @example
+ * // All columns are percentages -- they must add up to 1
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Column Layout - Percentage Only',
+ * width: 350,
+ * height: 250,
+ * layout:'column',
+ * items: [{
+ * title: 'Column 1',
+ * columnWidth: .25
+ * },{
+ * title: 'Column 2',
+ * columnWidth: .55
+ * },{
+ * title: 'Column 3',
+ * columnWidth: .20
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * // Mix of width and columnWidth -- all columnWidth values must add up
+ * // to 1. The first column will take up exactly 120px, and the last two
+ * // columns will fill the remaining container width.
+ *
+ * Ext.create('Ext.Panel', {
+ * title: 'Column Layout - Mixed',
+ * width: 350,
+ * height: 250,
+ * layout:'column',
+ * items: [{
+ * title: 'Column 1',
+ * width: 120
+ * },{
+ * title: 'Column 2',
+ * columnWidth: .7
+ * },{
+ * title: 'Column 3',
+ * columnWidth: .3
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.layout.container.Column', {
+
+ extend: 'Ext.layout.container.Auto',
+ alias: ['layout.column'],
+ alternateClassName: 'Ext.layout.ColumnLayout',
+
+ type: 'column',
+
+ itemCls: Ext.baseCSSPrefix + 'column',
+
+ targetCls: Ext.baseCSSPrefix + 'column-layout-ct',
+
+ scrollOffset: 0,
+
+ bindToOwnerCtComponent: false,
+
+ getRenderTarget : function() {
+ if (!this.innerCt) {
+
+ // the innerCt prevents wrapping and shuffling while
+ // the container is resizing
+ this.innerCt = this.getTarget().createChild({
+ cls: Ext.baseCSSPrefix + 'column-inner'
+ });
+
+ // Column layout uses natural HTML flow to arrange the child items.
+ // To ensure that all browsers (I'm looking at you IE!) add the bottom margin of the last child to the
+ // containing element height, we create a zero-sized element with style clear:both to force a "new line"
+ this.clearEl = this.innerCt.createChild({
+ cls: Ext.baseCSSPrefix + 'clear',
+ role: 'presentation'
+ });
+ }
+ return this.innerCt;
+ },
+
+ // private
+ onLayout : function() {
+ var me = this,
+ target = me.getTarget(),
+ items = me.getLayoutItems(),
+ len = items.length,
+ item,
+ i,
+ parallelMargins = [],
+ itemParallelMargins,
+ size,
+ availableWidth,
+ columnWidth;
+
+ size = me.getLayoutTargetSize();
+ if (size.width < len * 10) { // Don't lay out in impossibly small target (probably display:none, or initial, unsized Container)
+ return;
+ }
+
+ // On the first pass, for all except IE6-7, we lay out the items with no scrollbars visible using style overflow: hidden.
+ // If, after the layout, it is detected that there is vertical overflow,
+ // we will recurse back through here. Do not adjust overflow style at that time.
+ if (me.adjustmentPass) {
+ if (Ext.isIE6 || Ext.isIE7 || Ext.isIEQuirks) {
+ size.width = me.adjustedWidth;
+ }
+ } else {
+ i = target.getStyle('overflow');
+ if (i && i != 'hidden') {
+ me.autoScroll = true;
+ if (!(Ext.isIE6 || Ext.isIE7 || Ext.isIEQuirks)) {
+ target.setStyle('overflow', 'hidden');
+ size = me.getLayoutTargetSize();
+ }
+ }
+ }
+
+ availableWidth = size.width - me.scrollOffset;
+ me.innerCt.setWidth(availableWidth);
+
+ // some columns can be percentages while others are fixed
+ // so we need to make 2 passes
+ for (i = 0; i < len; i++) {
+ item = items[i];
+ itemParallelMargins = parallelMargins[i] = item.getEl().getMargin('lr');
+ if (!item.columnWidth) {
+ availableWidth -= (item.getWidth() + itemParallelMargins);
+ }
+ }
+
+ availableWidth = availableWidth < 0 ? 0 : availableWidth;
+ for (i = 0; i < len; i++) {
+ item = items[i];
+ if (item.columnWidth) {
+ columnWidth = Math.floor(item.columnWidth * availableWidth) - parallelMargins[i];
+ me.setItemSize(item, columnWidth, item.height);
+ } else {
+ me.layoutItem(item);
+ }
+ }
+
+ // After the first pass on an autoScroll layout, restore the overflow settings if it had been changed (only changed for non-IE6)
+ if (!me.adjustmentPass && me.autoScroll) {
+
+ // If there's a vertical overflow, relay with scrollbars
+ target.setStyle('overflow', 'auto');
+ me.adjustmentPass = (target.dom.scrollHeight > size.height);
+ if (Ext.isIE6 || Ext.isIE7 || Ext.isIEQuirks) {
+ me.adjustedWidth = size.width - Ext.getScrollBarWidth();
+ } else {
+ target.setStyle('overflow', 'auto');
+ }
+
+ // If the layout caused height overflow, recurse back and recalculate (with overflow setting restored on non-IE6)
+ if (me.adjustmentPass) {
+ me.onLayout();
+ }
+ }
+ delete me.adjustmentPass;
+ },
+
+ configureItem: function(item) {
+ this.callParent(arguments);
+
+ if (item.columnWidth) {
+ item.layoutManagedWidth = 1;
+ }
+ }
+});
+/**
+ * This layout allows you to easily render content into an HTML table. The total number of columns can be specified, and
+ * rowspan and colspan can be used to create complex layouts within the table. This class is intended to be extended or
+ * created via the `layout: {type: 'table'}` {@link Ext.container.Container#layout} config, and should generally not
+ * need to be created directly via the new keyword.
+ *
+ * Note that when creating a layout via config, the layout-specific config properties must be passed in via the {@link
+ * Ext.container.Container#layout} object which will then be applied internally to the layout. In the case of
+ * TableLayout, the only valid layout config properties are {@link #columns} and {@link #tableAttrs}. However, the items
+ * added to a TableLayout can supply the following table-specific config properties:
+ *
+ * - **rowspan** Applied to the table cell containing the item.
+ * - **colspan** Applied to the table cell containing the item.
+ * - **cellId** An id applied to the table cell containing the item.
+ * - **cellCls** A CSS class name added to the table cell containing the item.
+ *
+ * The basic concept of building up a TableLayout is conceptually very similar to building up a standard HTML table. You
+ * simply add each panel (or "cell") that you want to include along with any span attributes specified as the special
+ * config properties of rowspan and colspan which work exactly like their HTML counterparts. Rather than explicitly
+ * creating and nesting rows and columns as you would in HTML, you simply specify the total column count in the
+ * layoutConfig and start adding panels in their natural order from left to right, top to bottom. The layout will
+ * automatically figure out, based on the column count, rowspans and colspans, how to position each panel within the
+ * table. Just like with HTML tables, your rowspans and colspans must add up correctly in your overall layout or you'll
+ * end up with missing and/or extra cells! Example usage:
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Table Layout',
+ * width: 300,
+ * height: 150,
+ * layout: {
+ * type: 'table',
+ * // The total column count must be specified here
+ * columns: 3
+ * },
+ * defaults: {
+ * // applied to each contained panel
+ * bodyStyle: 'padding:20px'
+ * },
+ * items: [{
+ * html: 'Cell A content',
+ * rowspan: 2
+ * },{
+ * html: 'Cell B content',
+ * colspan: 2
+ * },{
+ * html: 'Cell C content',
+ * cellCls: 'highlight'
+ * },{
+ * html: 'Cell D content'
+ * }],
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.layout.container.Table', {
+
+ /* Begin Definitions */
+
+ alias: ['layout.table'],
+ extend: 'Ext.layout.container.Auto',
+ alternateClassName: 'Ext.layout.TableLayout',
+
+ /* End Definitions */
+
+ /**
+ * @cfg {Number} columns
+ * The total number of columns to create in the table for this layout. If not specified, all Components added to
+ * this layout will be rendered into a single row using one column per Component.
+ */
+
+ // private
+ monitorResize:false,
+
+ type: 'table',
+
+ // Table layout is a self-sizing layout. When an item of for example, a dock layout, the Panel must expand to accommodate
+ // a table layout. See in particular AbstractDock::onLayout for use of this flag.
+ autoSize: true,
+
+ clearEl: true, // Base class will not create it if already truthy. Not needed in tables.
+
+ targetCls: Ext.baseCSSPrefix + 'table-layout-ct',
+ tableCls: Ext.baseCSSPrefix + 'table-layout',
+ cellCls: Ext.baseCSSPrefix + 'table-layout-cell',
+
+ /**
+ * @cfg {Object} tableAttrs
+ * An object containing properties which are added to the {@link Ext.DomHelper DomHelper} specification used to
+ * create the layout's `<table>` element. Example:
+ *
+ * {
+ * xtype: 'panel',
+ * layout: {
+ * type: 'table',
+ * columns: 3,
+ * tableAttrs: {
+ * style: {
+ * width: '100%'
+ * }
+ * }
+ * }
+ * }
+ */
+ tableAttrs:null,
+
+ /**
+ * @cfg {Object} trAttrs
+ * An object containing properties which are added to the {@link Ext.DomHelper DomHelper} specification used to
+ * create the layout's <tr> elements.
+ */
+
+ /**
+ * @cfg {Object} tdAttrs
+ * An object containing properties which are added to the {@link Ext.DomHelper DomHelper} specification used to
+ * create the layout's <td> elements.
+ */
+
+ /**
+ * @private
+ * Iterates over all passed items, ensuring they are rendered in a cell in the proper
+ * location in the table structure.
+ */
+ renderItems: function(items) {
+ var tbody = this.getTable().tBodies[0],
+ rows = tbody.rows,
+ i = 0,
+ len = items.length,
+ cells, curCell, rowIdx, cellIdx, item, trEl, tdEl, itemCt;
+
+ // Calculate the correct cell structure for the current items
+ cells = this.calculateCells(items);
+
+ // Loop over each cell and compare to the current cells in the table, inserting/
+ // removing/moving cells as needed, and making sure each item is rendered into
+ // the correct cell.
+ for (; i < len; i++) {
+ curCell = cells[i];
+ rowIdx = curCell.rowIdx;
+ cellIdx = curCell.cellIdx;
+ item = items[i];
+
+ // If no row present, create and insert one
+ trEl = rows[rowIdx];
+ if (!trEl) {
+ trEl = tbody.insertRow(rowIdx);
+ if (this.trAttrs) {
+ trEl.set(this.trAttrs);
+ }
+ }
+
+ // If no cell present, create and insert one
+ itemCt = tdEl = Ext.get(trEl.cells[cellIdx] || trEl.insertCell(cellIdx));
+ if (this.needsDivWrap()) { //create wrapper div if needed - see docs below
+ itemCt = tdEl.first() || tdEl.createChild({tag: 'div'});
+ itemCt.setWidth(null);
+ }
+
+ // Render or move the component into the cell
+ if (!item.rendered) {
+ this.renderItem(item, itemCt, 0);
+ }
+ else if (!this.isValidParent(item, itemCt, 0)) {
+ this.moveItem(item, itemCt, 0);
+ }
+
+ // Set the cell properties
+ if (this.tdAttrs) {
+ tdEl.set(this.tdAttrs);
+ }
+ tdEl.set({
+ colSpan: item.colspan || 1,
+ rowSpan: item.rowspan || 1,
+ id: item.cellId || '',
+ cls: this.cellCls + ' ' + (item.cellCls || '')
+ });
+
+ // If at the end of a row, remove any extra cells
+ if (!cells[i + 1] || cells[i + 1].rowIdx !== rowIdx) {
+ cellIdx++;
+ while (trEl.cells[cellIdx]) {
+ trEl.deleteCell(cellIdx);
+ }
+ }
+ }
+
+ // Delete any extra rows
+ rowIdx++;
+ while (tbody.rows[rowIdx]) {
+ tbody.deleteRow(rowIdx);
+ }
+ },
+
+ afterLayout: function() {
+ this.callParent();
+
+ if (this.needsDivWrap()) {
+ // set wrapper div width to match layed out item - see docs below
+ Ext.Array.forEach(this.getLayoutItems(), function(item) {
+ Ext.fly(item.el.dom.parentNode).setWidth(item.getWidth());
+ });
+ }
+ },
+
+ /**
+ * @private
+ * Determine the row and cell indexes for each component, taking into consideration
+ * the number of columns and each item's configured colspan/rowspan values.
+ * @param {Array} items The layout components
+ * @return {Object[]} List of row and cell indexes for each of the components
+ */
+ calculateCells: function(items) {
+ var cells = [],
+ rowIdx = 0,
+ colIdx = 0,
+ cellIdx = 0,
+ totalCols = this.columns || Infinity,
+ rowspans = [], //rolling list of active rowspans for each column
+ i = 0, j,
+ len = items.length,
+ item;
+
+ for (; i < len; i++) {
+ item = items[i];
+
+ // Find the first available row/col slot not taken up by a spanning cell
+ while (colIdx >= totalCols || rowspans[colIdx] > 0) {
+ if (colIdx >= totalCols) {
+ // move down to next row
+ colIdx = 0;
+ cellIdx = 0;
+ rowIdx++;
+
+ // decrement all rowspans
+ for (j = 0; j < totalCols; j++) {
+ if (rowspans[j] > 0) {
+ rowspans[j]--;
+ }
+ }
+ } else {
+ colIdx++;
+ }
+ }
+
+ // Add the cell info to the list
+ cells.push({
+ rowIdx: rowIdx,
+ cellIdx: cellIdx
+ });
+
+ // Increment
+ for (j = item.colspan || 1; j; --j) {
+ rowspans[colIdx] = item.rowspan || 1;
+ ++colIdx;
+ }
+ ++cellIdx;
+ }
+
+ return cells;
+ },
+
+ /**
+ * @private
+ * Return the layout's table element, creating it if necessary.
+ */
+ getTable: function() {
+ var table = this.table;
+ if (!table) {
+ table = this.table = this.getTarget().createChild(
+ Ext.apply({
+ tag: 'table',
+ role: 'presentation',
+ cls: this.tableCls,
+ cellspacing: 0, //TODO should this be specified or should CSS handle it?
+ cn: {tag: 'tbody'}
+ }, this.tableAttrs),
+ null, true
+ );
+ }
+ return table;
+ },
+
+ /**
+ * @private
+ * Opera 10.5 has a bug where if a table cell's child has box-sizing:border-box and padding, it
+ * will include that padding in the size of the cell, making it always larger than the
+ * shrink-wrapped size of its contents. To get around this we have to wrap the contents in a div
+ * and then set that div's width to match the item rendered within it afterLayout. This method
+ * determines whether we need the wrapper div; it currently does a straight UA sniff as this bug
+ * seems isolated to just Opera 10.5, but feature detection could be added here if needed.
+ */
+ needsDivWrap: function() {
+ return Ext.isOpera10_5;
+ }
+});
+/**
+ * A base class for all menu items that require menu-related functionality such as click handling,
+ * sub-menus, icons, etc.
+ *
+ * @example
+ * Ext.create('Ext.menu.Menu', {
+ * width: 100,
+ * height: 100,
+ * floating: false, // usually you want this set to True (default)
+ * renderTo: Ext.getBody(), // usually rendered by it's containing component
+ * items: [{
+ * text: 'icon item',
+ * iconCls: 'add16'
+ * },{
+ * text: 'text item'
+ * },{
+ * text: 'plain item',
+ * plain: true
+ * }]
+ * });
+ */
+Ext.define('Ext.menu.Item', {
+ extend: 'Ext.Component',
+ alias: 'widget.menuitem',
+ alternateClassName: 'Ext.menu.TextItem',
+
+ /**
+ * @property {Boolean} activated
+ * Whether or not this item is currently activated
+ */
+
+ /**
+ * @property {Ext.menu.Menu} parentMenu
+ * The parent Menu of this item.
+ */
+
+ /**
+ * @cfg {String} activeCls
+ * The CSS class added to the menu item when the item is activated (focused/mouseover).
+ * Defaults to `Ext.baseCSSPrefix + 'menu-item-active'`.
+ */
+ activeCls: Ext.baseCSSPrefix + 'menu-item-active',
+
+ /**
+ * @cfg {String} ariaRole @hide
+ */
+ ariaRole: 'menuitem',
+
+ /**
+ * @cfg {Boolean} canActivate
+ * Whether or not this menu item can be activated when focused/mouseovered. Defaults to `true`.
+ */
+ canActivate: true,
+
+ /**
+ * @cfg {Number} clickHideDelay
+ * The delay in milliseconds to wait before hiding the menu after clicking the menu item.
+ * This only has an effect when `hideOnClick: true`. Defaults to `1`.
+ */
+ clickHideDelay: 1,
+
+ /**
+ * @cfg {Boolean} destroyMenu
+ * Whether or not to destroy any associated sub-menu when this item is destroyed. Defaults to `true`.
+ */
+ destroyMenu: true,
+
+ /**
+ * @cfg {String} disabledCls
+ * The CSS class added to the menu item when the item is disabled.
+ * Defaults to `Ext.baseCSSPrefix + 'menu-item-disabled'`.
+ */
+ disabledCls: Ext.baseCSSPrefix + 'menu-item-disabled',
+
+ /**
+ * @cfg {String} href
+ * The href attribute to use for the underlying anchor link. Defaults to `#`.
+ * @markdown
+ */
+
+ /**
+ * @cfg {String} hrefTarget
+ * The target attribute to use for the underlying anchor link. Defaults to `undefined`.
+ * @markdown
+ */
+
+ /**
+ * @cfg {Boolean} hideOnClick
+ * Whether to not to hide the owning menu when this item is clicked. Defaults to `true`.
+ * @markdown
+ */
+ hideOnClick: true,
+
+ /**
+ * @cfg {String} icon
+ * The path to an icon to display in this item. Defaults to `Ext.BLANK_IMAGE_URL`.
+ * @markdown
+ */
+
+ /**
+ * @cfg {String} iconCls
+ * A CSS class that specifies a `background-image` to use as the icon for this item. Defaults to `undefined`.
+ * @markdown
+ */
+
+ isMenuItem: true,
+
+ /**
+ * @cfg {Mixed} menu
+ * Either an instance of {@link Ext.menu.Menu} or a config object for an {@link Ext.menu.Menu}
+ * which will act as a sub-menu to this item.
+ * @markdown
+ * @property {Ext.menu.Menu} menu The sub-menu associated with this item, if one was configured.
+ */
+
+ /**
+ * @cfg {String} menuAlign
+ * The default {@link Ext.Element#getAlignToXY Ext.Element.getAlignToXY} anchor position value for this
+ * item's sub-menu relative to this item's position. Defaults to `'tl-tr?'`.
+ * @markdown
+ */
+ menuAlign: 'tl-tr?',
+
+ /**
+ * @cfg {Number} menuExpandDelay
+ * The delay in milliseconds before this item's sub-menu expands after this item is moused over. Defaults to `200`.
+ * @markdown
+ */
+ menuExpandDelay: 200,
+
+ /**
+ * @cfg {Number} menuHideDelay
+ * The delay in milliseconds before this item's sub-menu hides after this item is moused out. Defaults to `200`.
+ * @markdown
+ */
+ menuHideDelay: 200,
+
+ /**
+ * @cfg {Boolean} plain
+ * Whether or not this item is plain text/html with no icon or visual activation. Defaults to `false`.
+ * @markdown
+ */
+
+ renderTpl: [
+ '<tpl if="plain">',
+ '{text}',
+ '</tpl>',
+ '<tpl if="!plain">',
+ '<a id="{id}-itemEl" class="' + Ext.baseCSSPrefix + 'menu-item-link" href="{href}" <tpl if="hrefTarget">target="{hrefTarget}"</tpl> hidefocus="true" unselectable="on">',
+ '<img id="{id}-iconEl" src="{icon}" class="' + Ext.baseCSSPrefix + 'menu-item-icon {iconCls}" />',
+ '<span id="{id}-textEl" class="' + Ext.baseCSSPrefix + 'menu-item-text" <tpl if="menu">style="margin-right: 17px;"</tpl> >{text}</span>',
+ '<tpl if="menu">',
+ '<img id="{id}-arrowEl" src="{blank}" class="' + Ext.baseCSSPrefix + 'menu-item-arrow" />',
+ '</tpl>',
+ '</a>',
+ '</tpl>'
+ ],
+
+ maskOnDisable: false,
+
+ /**
+ * @cfg {String} text
+ * The text/html to display in this item. Defaults to `undefined`.
+ * @markdown
+ */
+
+ activate: function() {
+ var me = this;
+
+ if (!me.activated && me.canActivate && me.rendered && !me.isDisabled() && me.isVisible()) {
+ me.el.addCls(me.activeCls);
+ me.focus();
+ me.activated = true;
+ me.fireEvent('activate', me);
+ }
+ },
+
+ blur: function() {
+ this.$focused = false;
+ this.callParent(arguments);
+ },
+
+ deactivate: function() {
+ var me = this;
+
+ if (me.activated) {
+ me.el.removeCls(me.activeCls);
+ me.blur();
+ me.hideMenu();
+ me.activated = false;
+ me.fireEvent('deactivate', me);
+ }
+ },
+
+ deferExpandMenu: function() {
+ var me = this;
+
+ if (!me.menu.rendered || !me.menu.isVisible()) {
+ me.parentMenu.activeChild = me.menu;
+ me.menu.parentItem = me;
+ me.menu.parentMenu = me.menu.ownerCt = me.parentMenu;
+ me.menu.showBy(me, me.menuAlign);
+ }
+ },
+
+ deferHideMenu: function() {
+ if (this.menu.isVisible()) {
+ this.menu.hide();
+ }
+ },
+
+ deferHideParentMenus: function() {
+ Ext.menu.Manager.hideAll();
+ },
+
+ expandMenu: function(delay) {
+ var me = this;
+
+ if (me.menu) {
+ clearTimeout(me.hideMenuTimer);
+ if (delay === 0) {
+ me.deferExpandMenu();
+ } else {
+ me.expandMenuTimer = Ext.defer(me.deferExpandMenu, Ext.isNumber(delay) ? delay : me.menuExpandDelay, me);
+ }
+ }
+ },
+
+ focus: function() {
+ this.$focused = true;
+ this.callParent(arguments);
+ },
+
+ getRefItems: function(deep){
+ var menu = this.menu,
+ items;
+
+ if (menu) {
+ items = menu.getRefItems(deep);
+ items.unshift(menu);
+ }
+ return items || [];
+ },
+
+ hideMenu: function(delay) {
+ var me = this;
+
+ if (me.menu) {
+ clearTimeout(me.expandMenuTimer);
+ me.hideMenuTimer = Ext.defer(me.deferHideMenu, Ext.isNumber(delay) ? delay : me.menuHideDelay, me);
+ }
+ },
+
+ initComponent: function() {
+ var me = this,
+ prefix = Ext.baseCSSPrefix,
+ cls = [prefix + 'menu-item'];
+
+ me.addEvents(
+ /**
+ * @event activate
+ * Fires when this item is activated
+ * @param {Ext.menu.Item} item The activated item
+ */
+ 'activate',
+
+ /**
+ * @event click
+ * Fires when this item is clicked
+ * @param {Ext.menu.Item} item The item that was clicked
+ * @param {Ext.EventObject} e The underyling {@link Ext.EventObject}.
+ */
+ 'click',
+
+ /**
+ * @event deactivate
+ * Fires when this tiem is deactivated
+ * @param {Ext.menu.Item} item The deactivated item
+ */
+ 'deactivate'
+ );
+
+ if (me.plain) {
+ cls.push(prefix + 'menu-item-plain');
+ }
+
+ if (me.cls) {
+ cls.push(me.cls);
+ }
+
+ me.cls = cls.join(' ');
+
+ if (me.menu) {
+ me.menu = Ext.menu.Manager.get(me.menu);
+ }
+
+ me.callParent(arguments);
+ },
+
+ onClick: function(e) {
+ var me = this;
+
+ if (!me.href) {
+ e.stopEvent();
+ }
+
+ if (me.disabled) {
+ return;
+ }
+
+ if (me.hideOnClick) {
+ me.deferHideParentMenusTimer = Ext.defer(me.deferHideParentMenus, me.clickHideDelay, me);
+ }
+
+ Ext.callback(me.handler, me.scope || me, [me, e]);
+ me.fireEvent('click', me, e);
+
+ if (!me.hideOnClick) {
+ me.focus();
+ }
+ },
+
+ onDestroy: function() {
+ var me = this;
+
+ clearTimeout(me.expandMenuTimer);
+ clearTimeout(me.hideMenuTimer);
+ clearTimeout(me.deferHideParentMenusTimer);
+
+ if (me.menu) {
+ delete me.menu.parentItem;
+ delete me.menu.parentMenu;
+ delete me.menu.ownerCt;
+ if (me.destroyMenu !== false) {
+ me.menu.destroy();
+ }
+ }
+ me.callParent(arguments);
+ },
+
+ onRender: function(ct, pos) {
+ var me = this,
+ blank = Ext.BLANK_IMAGE_URL;
+
+ Ext.applyIf(me.renderData, {
+ href: me.href || '#',
+ hrefTarget: me.hrefTarget,
+ icon: me.icon || blank,
+ iconCls: me.iconCls + (me.checkChangeDisabled ? ' ' + me.disabledCls : ''),
+ menu: Ext.isDefined(me.menu),
+ plain: me.plain,
+ text: me.text,
+ blank: blank
+ });
+
+ me.addChildEls('itemEl', 'iconEl', 'textEl', 'arrowEl');
+
+ me.callParent(arguments);
+ },
+
+ /**
+ * Sets the {@link #click} handler of this item
+ * @param {Function} fn The handler function
+ * @param {Object} scope (optional) The scope of the handler function
+ */
+ setHandler: function(fn, scope) {
+ this.handler = fn || null;
+ this.scope = scope;
+ },
+
+ /**
+ * Sets the {@link #iconCls} of this item
+ * @param {String} iconCls The CSS class to set to {@link #iconCls}
+ */
+ setIconCls: function(iconCls) {
+ var me = this;
+
+ if (me.iconEl) {
+ if (me.iconCls) {
+ me.iconEl.removeCls(me.iconCls);
+ }
+
+ if (iconCls) {
+ me.iconEl.addCls(iconCls);
+ }
+ }
+
+ me.iconCls = iconCls;
+ },
+
+ /**
+ * Sets the {@link #text} of this item
+ * @param {String} text The {@link #text}
+ */
+ setText: function(text) {
+ var me = this,
+ el = me.textEl || me.el;
+
+ me.text = text;
+
+ if (me.rendered) {
+ el.update(text || '');
+ // cannot just call doComponentLayout due to stretchmax
+ me.ownerCt.redoComponentLayout();
+ }
+ }
+});
+
+/**
+ * A menu item that contains a togglable checkbox by default, but that can also be a part of a radio group.
+ *
+ * @example
+ * Ext.create('Ext.menu.Menu', {
+ * width: 100,
+ * height: 110,
+ * floating: false, // usually you want this set to True (default)
+ * renderTo: Ext.getBody(), // usually rendered by it's containing component
+ * items: [{
+ * xtype: 'menucheckitem',
+ * text: 'select all'
+ * },{
+ * xtype: 'menucheckitem',
+ * text: 'select specific',
+ * },{
+ * iconCls: 'add16',
+ * text: 'icon item'
+ * },{
+ * text: 'regular item'
+ * }]
+ * });
+ */
+Ext.define('Ext.menu.CheckItem', {
+ extend: 'Ext.menu.Item',
+ alias: 'widget.menucheckitem',
+
+ /**
+ * @cfg {String} checkedCls
+ * The CSS class used by {@link #cls} to show the checked state.
+ * Defaults to `Ext.baseCSSPrefix + 'menu-item-checked'`.
+ */
+ checkedCls: Ext.baseCSSPrefix + 'menu-item-checked',
+ /**
+ * @cfg {String} uncheckedCls
+ * The CSS class used by {@link #cls} to show the unchecked state.
+ * Defaults to `Ext.baseCSSPrefix + 'menu-item-unchecked'`.
+ */
+ uncheckedCls: Ext.baseCSSPrefix + 'menu-item-unchecked',
+ /**
+ * @cfg {String} groupCls
+ * The CSS class applied to this item's icon image to denote being a part of a radio group.
+ * Defaults to `Ext.baseCSSClass + 'menu-group-icon'`.
+ * Any specified {@link #iconCls} overrides this.
+ */
+ groupCls: Ext.baseCSSPrefix + 'menu-group-icon',
+
+ /**
+ * @cfg {Boolean} hideOnClick
+ * Whether to not to hide the owning menu when this item is clicked.
+ * Defaults to `false` for checkbox items, and to `true` for radio group items.
+ */
+ hideOnClick: false,
+
+ afterRender: function() {
+ var me = this;
+ this.callParent();
+ me.checked = !me.checked;
+ me.setChecked(!me.checked, true);
+ },
+
+ initComponent: function() {
+ var me = this;
+ me.addEvents(
+ /**
+ * @event beforecheckchange
+ * Fires before a change event. Return false to cancel.
+ * @param {Ext.menu.CheckItem} this
+ * @param {Boolean} checked
+ */
+ 'beforecheckchange',
+
+ /**
+ * @event checkchange
+ * Fires after a change event.
+ * @param {Ext.menu.CheckItem} this
+ * @param {Boolean} checked
+ */
+ 'checkchange'
+ );
+
+ me.callParent(arguments);
+
+ Ext.menu.Manager.registerCheckable(me);
+
+ if (me.group) {
+ if (!me.iconCls) {
+ me.iconCls = me.groupCls;
+ }
+ if (me.initialConfig.hideOnClick !== false) {
+ me.hideOnClick = true;
+ }
+ }
+ },
+
+ /**
+ * Disables just the checkbox functionality of this menu Item. If this menu item has a submenu, that submenu
+ * will still be accessible
+ */
+ disableCheckChange: function() {
+ var me = this;
+
+ if (me.iconEl) {
+ me.iconEl.addCls(me.disabledCls);
+ }
+ me.checkChangeDisabled = true;
+ },
+
+ /**
+ * Reenables the checkbox functionality of this menu item after having been disabled by {@link #disableCheckChange}
+ */
+ enableCheckChange: function() {
+ var me = this;
+
+ me.iconEl.removeCls(me.disabledCls);
+ me.checkChangeDisabled = false;
+ },
+
+ onClick: function(e) {
+ var me = this;
+ if(!me.disabled && !me.checkChangeDisabled && !(me.checked && me.group)) {
+ me.setChecked(!me.checked);
+ }
+ this.callParent([e]);
+ },
+
+ onDestroy: function() {
+ Ext.menu.Manager.unregisterCheckable(this);
+ this.callParent(arguments);
+ },
+
+ /**
+ * Sets the checked state of the item
+ * @param {Boolean} checked True to check, false to uncheck
+ * @param {Boolean} suppressEvents (optional) True to prevent firing the checkchange events. Defaults to `false`.
+ */
+ setChecked: function(checked, suppressEvents) {
+ var me = this;
+ if (me.checked !== checked && (suppressEvents || me.fireEvent('beforecheckchange', me, checked) !== false)) {
+ if (me.el) {
+ me.el[checked ? 'addCls' : 'removeCls'](me.checkedCls)[!checked ? 'addCls' : 'removeCls'](me.uncheckedCls);
+ }
+ me.checked = checked;
+ Ext.menu.Manager.onCheckChange(me, checked);
+ if (!suppressEvents) {
+ Ext.callback(me.checkHandler, me.scope, [me, checked]);
+ me.fireEvent('checkchange', me, checked);
+ }
+ }
+ }
+});
+
+/**
+ * @class Ext.menu.KeyNav
+ * @private
+ */
+Ext.define('Ext.menu.KeyNav', {
+ extend: 'Ext.util.KeyNav',
+
+ requires: ['Ext.FocusManager'],
+
+ constructor: function(menu) {
+ var me = this;
+
+ me.menu = menu;
+ me.callParent([menu.el, {
+ down: me.down,
+ enter: me.enter,
+ esc: me.escape,
+ left: me.left,
+ right: me.right,
+ space: me.enter,
+ tab: me.tab,
+ up: me.up
+ }]);
+ },
+
+ down: function(e) {
+ var me = this,
+ fi = me.menu.focusedItem;
+
+ if (fi && e.getKey() == Ext.EventObject.DOWN && me.isWhitelisted(fi)) {
+ return true;
+ }
+ me.focusNextItem(1);
+ },
+
+ enter: function(e) {
+ var menu = this.menu,
+ focused = menu.focusedItem;
+
+ if (menu.activeItem) {
+ menu.onClick(e);
+ } else if (focused && focused.isFormField) {
+ // prevent stopEvent being called
+ return true;
+ }
+ },
+
+ escape: function(e) {
+ Ext.menu.Manager.hideAll();
+ },
+
+ focusNextItem: function(step) {
+ var menu = this.menu,
+ items = menu.items,
+ focusedItem = menu.focusedItem,
+ startIdx = focusedItem ? items.indexOf(focusedItem) : -1,
+ idx = startIdx + step;
+
+ while (idx != startIdx) {
+ if (idx < 0) {
+ idx = items.length - 1;
+ } else if (idx >= items.length) {
+ idx = 0;
+ }
+
+ var item = items.getAt(idx);
+ if (menu.canActivateItem(item)) {
+ menu.setActiveItem(item);
+ break;
+ }
+ idx += step;
+ }
+ },
+
+ isWhitelisted: function(item) {
+ return Ext.FocusManager.isWhitelisted(item);
+ },
+
+ left: function(e) {
+ var menu = this.menu,
+ fi = menu.focusedItem,
+ ai = menu.activeItem;
+
+ if (fi && this.isWhitelisted(fi)) {
+ return true;
+ }
+
+ menu.hide();
+ if (menu.parentMenu) {
+ menu.parentMenu.focus();
+ }
+ },
+
+ right: function(e) {
+ var menu = this.menu,
+ fi = menu.focusedItem,
+ ai = menu.activeItem,
+ am;
+
+ if (fi && this.isWhitelisted(fi)) {
+ return true;
+ }
+
+ if (ai) {
+ am = menu.activeItem.menu;
+ if (am) {
+ ai.expandMenu(0);
+ Ext.defer(function() {
+ am.setActiveItem(am.items.getAt(0));
+ }, 25);
+ }
+ }
+ },
+
+ tab: function(e) {
+ var me = this;
+
+ if (e.shiftKey) {
+ me.up(e);
+ } else {
+ me.down(e);
+ }
+ },
+
+ up: function(e) {
+ var me = this,
+ fi = me.menu.focusedItem;
+
+ if (fi && e.getKey() == Ext.EventObject.UP && me.isWhitelisted(fi)) {
+ return true;
+ }
+ me.focusNextItem(-1);
+ }
+});
+/**
+ * Adds a separator bar to a menu, used to divide logical groups of menu items. Generally you will
+ * add one of these by using "-" in your call to add() or in your items config rather than creating one directly.
+ *
+ * @example
+ * Ext.create('Ext.menu.Menu', {
+ * width: 100,
+ * height: 100,
+ * floating: false, // usually you want this set to True (default)
+ * renderTo: Ext.getBody(), // usually rendered by it's containing component
+ * items: [{
+ * text: 'icon item',
+ * iconCls: 'add16'
+ * },{
+ * xtype: 'menuseparator'
+ * },{
+ * text: 'seperator above',
+ * },{
+ * text: 'regular item',
+ * }]
+ * });
+ */
+Ext.define('Ext.menu.Separator', {
+ extend: 'Ext.menu.Item',
+ alias: 'widget.menuseparator',
+
+ /**
+ * @cfg {String} activeCls @hide
+ */
+
+ /**
+ * @cfg {Boolean} canActivate @hide
+ */
+ canActivate: false,
+
+ /**
+ * @cfg {Boolean} clickHideDelay @hide
+ */
+
+ /**
+ * @cfg {Boolean} destroyMenu @hide
+ */
+
+ /**
+ * @cfg {Boolean} disabledCls @hide
+ */
+
+ focusable: false,
+
+ /**
+ * @cfg {String} href @hide
+ */
+
+ /**
+ * @cfg {String} hrefTarget @hide
+ */
+
+ /**
+ * @cfg {Boolean} hideOnClick @hide
+ */
+ hideOnClick: false,
+
+ /**
+ * @cfg {String} icon @hide
+ */
+
+ /**
+ * @cfg {String} iconCls @hide
+ */
+
+ /**
+ * @cfg {Object} menu @hide
+ */
+
+ /**
+ * @cfg {String} menuAlign @hide
+ */
+
+ /**
+ * @cfg {Number} menuExpandDelay @hide
+ */
+
+ /**
+ * @cfg {Number} menuHideDelay @hide
+ */
+
+ /**
+ * @cfg {Boolean} plain @hide
+ */
+ plain: true,
+
+ /**
+ * @cfg {String} separatorCls
+ * The CSS class used by the separator item to show the incised line.
+ * Defaults to `Ext.baseCSSPrefix + 'menu-item-separator'`.
+ */
+ separatorCls: Ext.baseCSSPrefix + 'menu-item-separator',
+
+ /**
+ * @cfg {String} text @hide
+ */
+ text: ' ',
+
+ onRender: function(ct, pos) {
+ var me = this,
+ sepCls = me.separatorCls;
+
+ me.cls += ' ' + sepCls;
+
+ me.callParent(arguments);
+ }
+});
+/**
+ * A menu object. This is the container to which you may add {@link Ext.menu.Item menu items}.
+ *
+ * Menus may contain either {@link Ext.menu.Item menu items}, or general {@link Ext.Component Components}.
+ * Menus may also contain {@link Ext.panel.AbstractPanel#dockedItems docked items} because it extends {@link Ext.panel.Panel}.
+ *
+ * To make a contained general {@link Ext.Component Component} line up with other {@link Ext.menu.Item menu items},
+ * specify `{@link Ext.menu.Item#plain plain}: true`. This reserves a space for an icon, and indents the Component
+ * in line with the other menu items.
+ *
+ * By default, Menus are absolutely positioned, floating Components. By configuring a Menu with `{@link #floating}: false`,
+ * a Menu may be used as a child of a {@link Ext.container.Container Container}.
+ *
+ * @example
+ * Ext.create('Ext.menu.Menu', {
+ * width: 100,
+ * height: 100,
+ * margin: '0 0 10 0',
+ * floating: false, // usually you want this set to True (default)
+ * renderTo: Ext.getBody(), // usually rendered by it's containing component
+ * items: [{
+ * text: 'regular item 1'
+ * },{
+ * text: 'regular item 2'
+ * },{
+ * text: 'regular item 3'
+ * }]
+ * });
+ *
+ * Ext.create('Ext.menu.Menu', {
+ * width: 100,
+ * height: 100,
+ * plain: true,
+ * floating: false, // usually you want this set to True (default)
+ * renderTo: Ext.getBody(), // usually rendered by it's containing component
+ * items: [{
+ * text: 'plain item 1'
+ * },{
+ * text: 'plain item 2'
+ * },{
+ * text: 'plain item 3'
+ * }]
+ * });
+ */
+Ext.define('Ext.menu.Menu', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.menu',
+ requires: [
+ 'Ext.layout.container.Fit',
+ 'Ext.layout.container.VBox',
+ 'Ext.menu.CheckItem',
+ 'Ext.menu.Item',
+ 'Ext.menu.KeyNav',
+ 'Ext.menu.Manager',
+ 'Ext.menu.Separator'
+ ],
+
+ /**
+ * @property {Ext.menu.Menu} parentMenu
+ * The parent Menu of this Menu.
+ */
+
+ /**
+ * @cfg {Boolean} allowOtherMenus
+ * True to allow multiple menus to be displayed at the same time.
+ */
+ allowOtherMenus: false,
+
+ /**
+ * @cfg {String} ariaRole @hide
+ */
+ ariaRole: 'menu',
+
+ /**
+ * @cfg {Boolean} autoRender @hide
+ * floating is true, so autoRender always happens
+ */
+
+ /**
+ * @cfg {String} defaultAlign
+ * The default {@link Ext.Element#getAlignToXY Ext.Element#getAlignToXY} anchor position value for this menu
+ * relative to its element of origin.
+ */
+ defaultAlign: 'tl-bl?',
+
+ /**
+ * @cfg {Boolean} floating
+ * A Menu configured as `floating: true` (the default) will be rendered as an absolutely positioned,
+ * {@link Ext.Component#floating floating} {@link Ext.Component Component}. If configured as `floating: false`, the Menu may be
+ * used as a child item of another {@link Ext.container.Container Container}.
+ */
+ floating: true,
+
+ /**
+ * @cfg {Boolean} @hide
+ * Menus are constrained to the document body by default
+ */
+ constrain: true,
+
+ /**
+ * @cfg {Boolean} [hidden=undefined]
+ * True to initially render the Menu as hidden, requiring to be shown manually.
+ *
+ * Defaults to `true` when `floating: true`, and defaults to `false` when `floating: false`.
+ */
+ hidden: true,
+
+ hideMode: 'visibility',
+
+ /**
+ * @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.
+ */
+ ignoreParentClicks: false,
+
+ isMenu: true,
+
+ /**
+ * @cfg {String/Object} layout @hide
+ */
+
+ /**
+ * @cfg {Boolean} showSeparator
+ * True to show the icon separator.
+ */
+ showSeparator : true,
+
+ /**
+ * @cfg {Number} minWidth
+ * The minimum width of the Menu.
+ */
+ minWidth: 120,
+
+ /**
+ * @cfg {Boolean} [plain=false]
+ * True to remove the incised line down the left side of the menu and to not indent general Component items.
+ */
+
+ initComponent: function() {
+ var me = this,
+ prefix = Ext.baseCSSPrefix,
+ cls = [prefix + 'menu'],
+ bodyCls = me.bodyCls ? [me.bodyCls] : [];
+
+ me.addEvents(
+ /**
+ * @event click
+ * Fires when this menu is clicked
+ * @param {Ext.menu.Menu} menu The menu which has been clicked
+ * @param {Ext.Component} item The menu item that was clicked. `undefined` if not applicable.
+ * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}.
+ */
+ 'click',
+
+ /**
+ * @event mouseenter
+ * Fires when the mouse enters this menu
+ * @param {Ext.menu.Menu} menu The menu
+ * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
+ */
+ 'mouseenter',
+
+ /**
+ * @event mouseleave
+ * Fires when the mouse leaves this menu
+ * @param {Ext.menu.Menu} menu The menu
+ * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
+ */
+ 'mouseleave',
+
+ /**
+ * @event mouseover
+ * Fires when the mouse is hovering over this menu
+ * @param {Ext.menu.Menu} menu The menu
+ * @param {Ext.Component} item The menu item that the mouse is over. `undefined` if not applicable.
+ * @param {Ext.EventObject} e The underlying {@link Ext.EventObject}
+ */
+ 'mouseover'
+ );
+
+ Ext.menu.Manager.register(me);
+
+ // Menu classes
+ if (me.plain) {
+ cls.push(prefix + 'menu-plain');
+ }
+ me.cls = cls.join(' ');
+
+ // Menu body classes
+ bodyCls.unshift(prefix + 'menu-body');
+ me.bodyCls = bodyCls.join(' ');
+
+ // Internal vbox layout, with scrolling overflow
+ // Placed in initComponent (rather than prototype) in order to support dynamic layout/scroller
+ // options if we wish to allow for such configurations on the Menu.
+ // e.g., scrolling speed, vbox align stretch, etc.
+ me.layout = {
+ type: 'vbox',
+ align: 'stretchmax',
+ autoSize: true,
+ clearInnerCtOnLayout: true,
+ overflowHandler: 'Scroller'
+ };
+
+ // hidden defaults to false if floating is configured as false
+ if (me.floating === false && me.initialConfig.hidden !== true) {
+ me.hidden = false;
+ }
+
+ me.callParent(arguments);
+
+ me.on('beforeshow', function() {
+ var hasItems = !!me.items.length;
+ // FIXME: When a menu has its show cancelled because of no items, it
+ // gets a visibility: hidden applied to it (instead of the default display: none)
+ // Not sure why, but we remove this style when we want to show again.
+ if (hasItems && me.rendered) {
+ me.el.setStyle('visibility', null);
+ }
+ return hasItems;
+ });
+ },
+
+ afterRender: function(ct) {
+ var me = this,
+ prefix = Ext.baseCSSPrefix,
+ space = ' ';
+
+ me.callParent(arguments);
+
+ // TODO: Move this to a subTemplate When we support them in the future
+ if (me.showSeparator) {
+ me.iconSepEl = me.layout.getRenderTarget().insertFirst({
+ cls: prefix + 'menu-icon-separator',
+ html: space
+ });
+ }
+
+ me.focusEl = me.el.createChild({
+ cls: prefix + 'menu-focus',
+ tabIndex: '-1',
+ html: space
+ });
+
+ me.mon(me.el, {
+ click: me.onClick,
+ mouseover: me.onMouseOver,
+ scope: me
+ });
+ me.mouseMonitor = me.el.monitorMouseLeave(100, me.onMouseLeave, me);
+
+ if (me.showSeparator && ((!Ext.isStrict && Ext.isIE) || Ext.isIE6)) {
+ me.iconSepEl.setHeight(me.el.getHeight());
+ }
+
+ me.keyNav = Ext.create('Ext.menu.KeyNav', me);
+ },
+
+ afterLayout: function() {
+ var me = this;
+ me.callParent(arguments);
+
+ // For IE6 & IE quirks, we have to resize the el and body since position: absolute
+ // floating elements inherit their parent's width, making them the width of
+ // document.body instead of the width of their contents.
+ // This includes left/right dock items.
+ if ((!Ext.isStrict && Ext.isIE) || Ext.isIE6) {
+ var innerCt = me.layout.getRenderTarget(),
+ innerCtWidth = 0,
+ dis = me.dockedItems,
+ l = dis.length,
+ i = 0,
+ di, clone, newWidth;
+
+ innerCtWidth = innerCt.getWidth();
+
+ newWidth = innerCtWidth + me.body.getBorderWidth('lr') + me.body.getPadding('lr');
+
+ // First set the body to the new width
+ me.body.setWidth(newWidth);
+
+ // Now we calculate additional width (docked items) and set the el's width
+ for (; i < l, di = dis.getAt(i); i++) {
+ if (di.dock == 'left' || di.dock == 'right') {
+ newWidth += di.getWidth();
+ }
+ }
+ me.el.setWidth(newWidth);
+ }
+ },
+
+ getBubbleTarget: function(){
+ return this.parentMenu || this.callParent();
+ },
+
+ /**
+ * Returns whether a menu item can be activated or not.
+ * @return {Boolean}
+ */
+ canActivateItem: function(item) {
+ return item && !item.isDisabled() && item.isVisible() && (item.canActivate || item.getXTypes().indexOf('menuitem') < 0);
+ },
+
+ /**
+ * Deactivates the current active item on the menu, if one exists.
+ */
+ deactivateActiveItem: function() {
+ var me = this;
+
+ if (me.activeItem) {
+ me.activeItem.deactivate();
+ if (!me.activeItem.activated) {
+ delete me.activeItem;
+ }
+ }
+
+ // only blur if focusedItem is not a filter
+ if (me.focusedItem && !me.filtered) {
+ me.focusedItem.blur();
+ if (!me.focusedItem.$focused) {
+ delete me.focusedItem;
+ }
+ }
+ },
+
+ clearStretch: function () {
+ // the vbox/stretchmax will set the el sizes and subsequent layouts will not
+ // reconsider them unless we clear the dimensions on the el's here:
+ if (this.rendered) {
+ this.items.each(function (item) {
+ // each menuItem component needs to layout again, so clear its cache
+ if (item.componentLayout) {
+ delete item.componentLayout.lastComponentSize;
+ }
+ if (item.el) {
+ item.el.setWidth(null);
+ }
+ });
+ }
+ },
+
+ onAdd: function () {
+ var me = this;
+
+ me.clearStretch();
+ me.callParent(arguments);
+
+ if (Ext.isIE6 || Ext.isIE7) {
+ // TODO - why does this need to be done (and not ok to do now)?
+ Ext.Function.defer(me.doComponentLayout, 10, me);
+ }
+ },
+
+ onRemove: function () {
+ this.clearStretch();
+ this.callParent(arguments);
+
+ },
+
+ redoComponentLayout: function () {
+ if (this.rendered) {
+ this.clearStretch();
+ this.doComponentLayout();
+ }
+ },
+
+ // inherit docs
+ getFocusEl: function() {
+ return this.focusEl;
+ },
+
+ // inherit docs
+ hide: function() {
+ this.deactivateActiveItem();
+ this.callParent(arguments);
+ },
+
+ // private
+ getItemFromEvent: function(e) {
+ return this.getChildByElement(e.getTarget());
+ },
+
+ lookupComponent: function(cmp) {
+ var me = this;
+
+ if (Ext.isString(cmp)) {
+ cmp = me.lookupItemFromString(cmp);
+ } else if (Ext.isObject(cmp)) {
+ cmp = me.lookupItemFromObject(cmp);
+ }
+
+ // Apply our minWidth to all of our child components so it's accounted
+ // for in our VBox layout
+ cmp.minWidth = cmp.minWidth || me.minWidth;
+
+ return cmp;
+ },
+
+ // private
+ lookupItemFromObject: function(cmp) {
+ var me = this,
+ prefix = Ext.baseCSSPrefix,
+ cls,
+ intercept;
+
+ if (!cmp.isComponent) {
+ if (!cmp.xtype) {
+ cmp = Ext.create('Ext.menu.' + (Ext.isBoolean(cmp.checked) ? 'Check': '') + 'Item', cmp);
+ } else {
+ cmp = Ext.ComponentManager.create(cmp, cmp.xtype);
+ }
+ }
+
+ if (cmp.isMenuItem) {
+ cmp.parentMenu = me;
+ }
+
+ if (!cmp.isMenuItem && !cmp.dock) {
+ cls = [prefix + 'menu-item', prefix + 'menu-item-cmp'];
+ intercept = Ext.Function.createInterceptor;
+
+ // Wrap focus/blur to control component focus
+ cmp.focus = intercept(cmp.focus, function() {
+ this.$focused = true;
+ }, cmp);
+ cmp.blur = intercept(cmp.blur, function() {
+ this.$focused = false;
+ }, cmp);
+
+ if (!me.plain && (cmp.indent === true || cmp.iconCls === 'no-icon')) {
+ cls.push(prefix + 'menu-item-indent');
+ }
+
+ if (cmp.rendered) {
+ cmp.el.addCls(cls);
+ } else {
+ cmp.cls = (cmp.cls ? cmp.cls : '') + ' ' + cls.join(' ');
+ }
+ cmp.isMenuItem = true;
+ }
+ return cmp;
+ },
+
+ // private
+ lookupItemFromString: function(cmp) {
+ return (cmp == 'separator' || cmp == '-') ?
+ Ext.createWidget('menuseparator')
+ : Ext.createWidget('menuitem', {
+ canActivate: false,
+ hideOnClick: false,
+ plain: true,
+ text: cmp
+ });
+ },
+
+ onClick: function(e) {
+ var me = this,
+ item;
+
+ if (me.disabled) {
+ e.stopEvent();
+ return;
+ }
+
+ if ((e.getTarget() == me.focusEl.dom) || e.within(me.layout.getRenderTarget())) {
+ item = me.getItemFromEvent(e) || me.activeItem;
+
+ if (item) {
+ if (item.getXTypes().indexOf('menuitem') >= 0) {
+ if (!item.menu || !me.ignoreParentClicks) {
+ item.onClick(e);
+ } else {
+ e.stopEvent();
+ }
+ }
+ }
+ me.fireEvent('click', me, item, e);
+ }
+ },
+
+ onDestroy: function() {
+ var me = this;
+
+ Ext.menu.Manager.unregister(me);
+ if (me.rendered) {
+ me.el.un(me.mouseMonitor);
+ me.keyNav.destroy();
+ delete me.keyNav;
+ }
+ me.callParent(arguments);
+ },
+
+ onMouseLeave: function(e) {
+ var me = this;
+
+ me.deactivateActiveItem();
+
+ if (me.disabled) {
+ return;
+ }
+
+ me.fireEvent('mouseleave', me, e);
+ },
+
+ onMouseOver: function(e) {
+ var me = this,
+ fromEl = e.getRelatedTarget(),
+ mouseEnter = !me.el.contains(fromEl),
+ item = me.getItemFromEvent(e);
+
+ if (mouseEnter && me.parentMenu) {
+ me.parentMenu.setActiveItem(me.parentItem);
+ me.parentMenu.mouseMonitor.mouseenter();
+ }
+
+ if (me.disabled) {
+ return;
+ }
+
+ if (item) {
+ me.setActiveItem(item);
+ if (item.activated && item.expandMenu) {
+ item.expandMenu();
+ }
+ }
+ if (mouseEnter) {
+ me.fireEvent('mouseenter', me, e);
+ }
+ me.fireEvent('mouseover', me, item, e);
+ },
+
+ setActiveItem: function(item) {
+ var me = this;
+
+ if (item && (item != me.activeItem && item != me.focusedItem)) {
+ me.deactivateActiveItem();
+ if (me.canActivateItem(item)) {
+ if (item.activate) {
+ item.activate();
+ if (item.activated) {
+ me.activeItem = item;
+ me.focusedItem = item;
+ me.focus();
+ }
+ } else {
+ item.focus();
+ me.focusedItem = item;
+ }
+ }
+ item.el.scrollIntoView(me.layout.getRenderTarget());
+ }
+ },
+
+ /**
+ * Shows the floating menu by the specified {@link Ext.Component Component} or {@link Ext.Element Element}.
+ * @param {Ext.Component/Ext.Element} component The {@link Ext.Component} or {@link Ext.Element} to show the menu by.
+ * @param {String} position (optional) Alignment position as used by {@link Ext.Element#getAlignToXY}.
+ * Defaults to `{@link #defaultAlign}`.
+ * @param {Number[]} offsets (optional) Alignment offsets as used by {@link Ext.Element#getAlignToXY}. Defaults to `undefined`.
+ * @return {Ext.menu.Menu} This Menu.
+ */
+ showBy: function(cmp, pos, off) {
+ var me = this,
+ xy,
+ region;
+
+ if (me.floating && cmp) {
+ me.layout.autoSize = true;
+
+ // show off-screen first so that we can calc position without causing a visual jump
+ me.doAutoRender();
+ delete me.needsLayout;
+
+ // Component or Element
+ cmp = cmp.el || cmp;
+
+ // Convert absolute to floatParent-relative coordinates if necessary.
+ xy = me.el.getAlignToXY(cmp, pos || me.defaultAlign, off);
+ if (me.floatParent) {
+ region = me.floatParent.getTargetEl().getViewRegion();
+ xy[0] -= region.x;
+ xy[1] -= region.y;
+ }
+ me.showAt(xy);
+ }
+ return me;
+ },
+
+ doConstrain : function() {
+ var me = this,
+ y = me.el.getY(),
+ max, full,
+ vector,
+ returnY = y, normalY, parentEl, scrollTop, viewHeight;
+
+ delete me.height;
+ me.setSize();
+ full = me.getHeight();
+ if (me.floating) {
+ //if our reset css is scoped, there will be a x-reset wrapper on this menu which we need to skip
+ parentEl = Ext.fly(me.el.getScopeParent());
+ 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 = me.maxHeight ? me.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 = me.getHeight();
+ }
+ // Always respect maxHeight
+ if (me.maxHeight){
+ max = Math.min(me.maxHeight, max);
+ }
+ if (full > max && max > 0){
+ me.layout.autoSize = false;
+ me.setHeight(max);
+ if (me.showSeparator){
+ me.iconSepEl.setHeight(me.layout.getRenderTarget().dom.scrollHeight);
+ }
+ }
+ vector = me.getConstrainVector(me.el.getScopeParent());
+ if (vector) {
+ me.setPosition(me.getPosition()[0] + vector[0]);
+ }
+ me.el.setY(returnY);
+ }
+});
+
+/**
+ * A menu containing a Ext.picker.Color Component.
+ *
+ * Notes:
+ *
+ * - Although not listed here, the **constructor** for this class accepts all of the
+ * configuration options of {@link Ext.picker.Color}.
+ * - If subclassing ColorMenu, any configuration options for the ColorPicker must be
+ * applied to the **initialConfig** property of the ColorMenu. Applying
+ * {@link Ext.picker.Color ColorPicker} configuration settings to `this` will **not**
+ * affect the ColorPicker's configuration.
+ *
+ * Example:
+ *
+ * @example
+ * var colorPicker = Ext.create('Ext.menu.ColorPicker', {
+ * value: '000000'
+ * });
+ *
+ * Ext.create('Ext.menu.Menu', {
+ * width: 100,
+ * height: 90,
+ * floating: false, // usually you want this set to True (default)
+ * renderTo: Ext.getBody(), // usually rendered by it's containing component
+ * items: [{
+ * text: 'choose a color',
+ * menu: colorPicker
+ * },{
+ * iconCls: 'add16',
+ * text: 'icon item'
+ * },{
+ * text: 'regular item'
+ * }]
+ * });
+ */
+ Ext.define('Ext.menu.ColorPicker', {
+ extend: 'Ext.menu.Menu',
+
+ alias: 'widget.colormenu',
+
+ requires: [
+ 'Ext.picker.Color'
+ ],
+
+ /**
+ * @cfg {Boolean} hideOnClick
+ * False to continue showing the menu after a date is selected.
+ */
+ hideOnClick : true,
+
+ /**
+ * @cfg {String} pickerId
+ * An id to assign to the underlying color picker.
+ */
+ pickerId : null,
+
+ /**
+ * @cfg {Number} maxHeight
+ * @hide
+ */
+
+ /**
+ * @property {Ext.picker.Color} picker
+ * The {@link Ext.picker.Color} instance for this ColorMenu
+ */
+
+ /**
+ * @event click
+ * @hide
+ */
+
+ /**
+ * @event itemclick
+ * @hide
+ */
+
+ initComponent : function(){
+ var me = this,
+ cfg = Ext.apply({}, me.initialConfig);
+
+ // Ensure we don't get duplicate listeners
+ delete cfg.listeners;
+ Ext.apply(me, {
+ plain: true,
+ showSeparator: false,
+ items: Ext.applyIf({
+ cls: Ext.baseCSSPrefix + 'menu-color-item',
+ id: me.pickerId,
+ xtype: 'colorpicker'
+ }, cfg)
+ });
+
+ me.callParent(arguments);
+
+ me.picker = me.down('colorpicker');
+
+ /**
+ * @event select
+ * @alias Ext.picker.Color#select
+ */
+ me.relayEvents(me.picker, ['select']);
+
+ if (me.hideOnClick) {
+ me.on('select', me.hidePickerOnSelect, me);
+ }
+ },
+
+ /**
+ * Hides picker on select if hideOnClick is true
+ * @private
+ */
+ hidePickerOnSelect: function() {
+ Ext.menu.Manager.hideAll();
+ }
+ });
+/**
+ * A menu containing an Ext.picker.Date Component.
+ *
+ * Notes:
+ *
+ * - Although not listed here, the **constructor** for this class accepts all of the
+ * configuration options of **{@link Ext.picker.Date}**.
+ * - If subclassing DateMenu, any configuration options for the DatePicker must be applied
+ * to the **initialConfig** property of the DateMenu. Applying {@link Ext.picker.Date Date Picker}
+ * configuration settings to **this** will **not** affect the Date Picker's configuration.
+ *
+ * Example:
+ *
+ * @example
+ * var dateMenu = Ext.create('Ext.menu.DatePicker', {
+ * handler: function(dp, date){
+ * Ext.Msg.alert('Date Selected', 'You selected ' + Ext.Date.format(date, 'M j, Y'));
+ * }
+ * });
+ *
+ * Ext.create('Ext.menu.Menu', {
+ * width: 100,
+ * height: 90,
+ * floating: false, // usually you want this set to True (default)
+ * renderTo: Ext.getBody(), // usually rendered by it's containing component
+ * items: [{
+ * text: 'choose a date',
+ * menu: dateMenu
+ * },{
+ * iconCls: 'add16',
+ * text: 'icon item'
+ * },{
+ * text: 'regular item'
+ * }]
+ * });
+ */
+ Ext.define('Ext.menu.DatePicker', {
+ extend: 'Ext.menu.Menu',
+
+ alias: 'widget.datemenu',
+
+ requires: [
+ 'Ext.picker.Date'
+ ],
+
+ /**
+ * @cfg {Boolean} hideOnClick
+ * False to continue showing the menu after a date is selected.
+ */
+ hideOnClick : true,
+
+ /**
+ * @cfg {String} pickerId
+ * An id to assign to the underlying date picker.
+ */
+ pickerId : null,
+
+ /**
+ * @cfg {Number} maxHeight
+ * @hide
+ */
+
+ /**
+ * @property {Ext.picker.Date} picker
+ * The {@link Ext.picker.Date} instance for this DateMenu
+ */
+
+ /**
+ * @event click
+ * @hide
+ */
+
+ /**
+ * @event itemclick
+ * @hide
+ */
+
+ initComponent : function(){
+ var me = this;
+
+ Ext.apply(me, {
+ showSeparator: false,
+ plain: true,
+ border: false,
+ bodyPadding: 0, // remove the body padding from the datepicker menu item so it looks like 3.3
+ items: Ext.applyIf({
+ cls: Ext.baseCSSPrefix + 'menu-date-item',
+ id: me.pickerId,
+ xtype: 'datepicker'
+ }, me.initialConfig)
+ });
+
+ me.callParent(arguments);
+
+ me.picker = me.down('datepicker');
+ /**
+ * @event select
+ * @alias Ext.picker.Date#select
+ */
+ me.relayEvents(me.picker, ['select']);
+
+ if (me.hideOnClick) {
+ me.on('select', me.hidePickerOnSelect, me);
+ }
+ },
+
+ hidePickerOnSelect: function() {
+ Ext.menu.Manager.hideAll();
+ }
+ });
+/**
+ * This class is used to display small visual icons in the header of a panel. There are a set of
+ * 25 icons that can be specified by using the {@link #type} config. The {@link #handler} config
+ * can be used to provide a function that will respond to any click events. In general, this class
+ * will not be instantiated directly, rather it will be created by specifying the {@link Ext.panel.Panel#tools}
+ * configuration on the Panel itself.
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * width: 200,
+ * height: 200,
+ * renderTo: document.body,
+ * title: 'A Panel',
+ * tools: [{
+ * type: 'help',
+ * handler: function(){
+ * // show help here
+ * }
+ * }, {
+ * itemId: 'refresh',
+ * type: 'refresh',
+ * hidden: true,
+ * handler: function(){
+ * // do refresh
+ * }
+ * }, {
+ * type: 'search',
+ * handler: function(event, target, owner, tool){
+ * // do search
+ * owner.child('#refresh').show();
+ * }
+ * }]
+ * });
+ */
+Ext.define('Ext.panel.Tool', {
+ extend: 'Ext.Component',
+ requires: ['Ext.tip.QuickTipManager'],
+ alias: 'widget.tool',
+
+ baseCls: Ext.baseCSSPrefix + 'tool',
+ disabledCls: Ext.baseCSSPrefix + 'tool-disabled',
+ toolPressedCls: Ext.baseCSSPrefix + 'tool-pressed',
+ toolOverCls: Ext.baseCSSPrefix + 'tool-over',
+ ariaRole: 'button',
+ renderTpl: ['<img id="{id}-toolEl" src="{blank}" class="{baseCls}-{type}" role="presentation"/>'],
+
+ /**
+ * @cfg {Function} handler
+ * A function to execute when the tool is clicked. Arguments passed are:
+ *
+ * - **event** : Ext.EventObject - The click event.
+ * - **toolEl** : Ext.Element - The tool Element.
+ * - **owner** : Ext.panel.Header - The host panel header.
+ * - **tool** : Ext.panel.Tool - The tool object
+ */
+
+ /**
+ * @cfg {Object} scope
+ * The scope to execute the {@link #handler} function. Defaults to the tool.
+ */
+
+ /**
+ * @cfg {String} type
+ * The type of tool to render. The following types are available:
+ *
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-close"></span> close
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-minimize"></span> minimize
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-maximize"></span> maximize
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-restore"></span> restore
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-toggle"></span> toggle
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-gear"></span> gear
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-prev"></span> prev
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-next"></span> next
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-pin"></span> pin
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-unpin"></span> unpin
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-right"></span> right
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-left"></span> left
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-down"></span> down
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-up"></span> up
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-refresh"></span> refresh
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-plus"></span> plus
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-minus"></span> minus
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-search"></span> search
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-save"></span> save
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-help"></span> help
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-print"></span> print
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-expand"></span> expand
+ * - <span class="x-tool"><img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" class="x-tool-collapse"></span> collapse
+ */
+
+ /**
+ * @cfg {String/Object} tooltip
+ * The tooltip for the tool - can be a string to be used as innerHTML (html tags are accepted) or QuickTips config
+ * object
+ */
+
+ /**
+ * @cfg {String} tooltipType
+ * The type of tooltip to use. Either 'qtip' (default) for QuickTips or 'title' for title attribute.
+ */
+ tooltipType: 'qtip',
+
+ /**
+ * @cfg {Boolean} stopEvent
+ * Specify as false to allow click event to propagate.
+ */
+ stopEvent: true,
+
+ initComponent: function() {
+ var me = this;
+ me.addEvents(
+ /**
+ * @event click
+ * Fires when the tool is clicked
+ * @param {Ext.panel.Tool} this
+ * @param {Ext.EventObject} e The event object
+ */
+ 'click'
+ );
+
+
+ me.type = me.type || me.id;
+
+ Ext.applyIf(me.renderData, {
+ baseCls: me.baseCls,
+ blank: Ext.BLANK_IMAGE_URL,
+ type: me.type
+ });
+
+ me.addChildEls('toolEl');
+
+ // alias qtip, should use tooltip since it's what we have in the docs
+ me.tooltip = me.tooltip || me.qtip;
+ me.callParent();
+ },
+
+ // inherit docs
+ afterRender: function() {
+ var me = this,
+ attr;
+
+ me.callParent(arguments);
+ if (me.tooltip) {
+ if (Ext.isObject(me.tooltip)) {
+ Ext.tip.QuickTipManager.register(Ext.apply({
+ target: me.id
+ }, me.tooltip));
+ }
+ else {
+ attr = me.tooltipType == 'qtip' ? 'data-qtip' : 'title';
+ me.toolEl.dom.setAttribute(attr, me.tooltip);
+ }
+ }
+
+ me.mon(me.toolEl, {
+ click: me.onClick,
+ mousedown: me.onMouseDown,
+ mouseover: me.onMouseOver,
+ mouseout: me.onMouseOut,
+ scope: me
+ });
+ },
+
+ /**
+ * Sets the type of the tool. Allows the icon to be changed.
+ * @param {String} type The new type. See the {@link #type} config.
+ * @return {Ext.panel.Tool} this
+ */
+ setType: function(type) {
+ var me = this;
+
+ me.type = type;
+ if (me.rendered) {
+ me.toolEl.dom.className = me.baseCls + '-' + type;
+ }
+ return me;
+ },
+
+ /**
+ * Binds this tool to a component.
+ * @private
+ * @param {Ext.Component} component The component
+ */
+ bindTo: function(component) {
+ this.owner = component;
+ },
+
+ /**
+ * Called when the tool element is clicked
+ * @private
+ * @param {Ext.EventObject} e
+ * @param {HTMLElement} target The target element
+ */
+ onClick: function(e, target) {
+ var me = this,
+ owner;
+
+ if (me.disabled) {
+ return false;
+ }
+ owner = me.owner || me.ownerCt;
+
+ //remove the pressed + over class
+ me.el.removeCls(me.toolPressedCls);
+ me.el.removeCls(me.toolOverCls);
+
+ if (me.stopEvent !== false) {
+ e.stopEvent();
+ }
+
+ Ext.callback(me.handler, me.scope || me, [e, target, owner, me]);
+ me.fireEvent('click', me, e);
+ return true;
+ },
+
+ // inherit docs
+ onDestroy: function(){
+ if (Ext.isObject(this.tooltip)) {
+ Ext.tip.QuickTipManager.unregister(this.id);
+ }
+ this.callParent();
+ },
+
+ /**
+ * Called when the user presses their mouse button down on a tool
+ * Adds the press class ({@link #toolPressedCls})
+ * @private
+ */
+ onMouseDown: function() {
+ if (this.disabled) {
+ return false;
+ }
+
+ this.el.addCls(this.toolPressedCls);
+ },
+
+ /**
+ * Called when the user rolls over a tool
+ * Adds the over class ({@link #toolOverCls})
+ * @private
+ */
+ onMouseOver: function() {
+ if (this.disabled) {
+ return false;
+ }
+ this.el.addCls(this.toolOverCls);
+ },
+
+ /**
+ * Called when the user rolls out from a tool.
+ * Removes the over class ({@link #toolOverCls})
+ * @private
+ */
+ onMouseOut: function() {
+ this.el.removeCls(this.toolOverCls);
+ }
+});
+/**
+ * @class Ext.resizer.Handle
+ * @extends Ext.Component
+ *
+ * Provides a handle for 9-point resizing of Elements or Components.
+ */
+Ext.define('Ext.resizer.Handle', {
+ extend: 'Ext.Component',
+ handleCls: '',
+ baseHandleCls: Ext.baseCSSPrefix + 'resizable-handle',
+ // Ext.resizer.Resizer.prototype.possiblePositions define the regions
+ // which will be passed in as a region configuration.
+ region: '',
+
+ onRender: function() {
+ this.addCls(
+ this.baseHandleCls,
+ this.baseHandleCls + '-' + this.region,
+ this.handleCls
+ );
+ this.callParent(arguments);
+ this.el.unselectable();
+ }
+});
+
+/**
+ * Applies drag handles to an element or component to make it resizable. The drag handles are inserted into the element
+ * (or component's element) and positioned absolute.
+ *
+ * Textarea and img elements will be wrapped with an additional div because these elements do not support child nodes.
+ * The original element can be accessed through the originalTarget property.
+ *
+ * Here is the list of valid resize handles:
+ *
+ * Value Description
+ * ------ -------------------
+ * 'n' north
+ * 's' south
+ * 'e' east
+ * 'w' west
+ * 'nw' northwest
+ * 'sw' southwest
+ * 'se' southeast
+ * 'ne' northeast
+ * 'all' all
+ *
+ * {@img Ext.resizer.Resizer/Ext.resizer.Resizer.png Ext.resizer.Resizer component}
+ *
+ * Here's an example showing the creation of a typical Resizer:
+ *
+ * Ext.create('Ext.resizer.Resizer', {
+ * el: 'elToResize',
+ * handles: 'all',
+ * minWidth: 200,
+ * minHeight: 100,
+ * maxWidth: 500,
+ * maxHeight: 400,
+ * pinned: true
+ * });
+ */
+Ext.define('Ext.resizer.Resizer', {
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+ uses: ['Ext.resizer.ResizeTracker', 'Ext.Component'],
+
+ alternateClassName: 'Ext.Resizable',
+
+ handleCls: Ext.baseCSSPrefix + 'resizable-handle',
+ pinnedCls: Ext.baseCSSPrefix + 'resizable-pinned',
+ overCls: Ext.baseCSSPrefix + 'resizable-over',
+ wrapCls: Ext.baseCSSPrefix + 'resizable-wrap',
+
+ /**
+ * @cfg {Boolean} dynamic
+ * Specify as true to update the {@link #target} (Element or {@link Ext.Component Component}) dynamically during
+ * dragging. This is `true` by default, but the {@link Ext.Component Component} class passes `false` when it is
+ * configured as {@link Ext.Component#resizable}.
+ *
+ * If specified as `false`, a proxy element is displayed during the resize operation, and the {@link #target} is
+ * updated on mouseup.
+ */
+ dynamic: true,
+
+ /**
+ * @cfg {String} handles
+ * String consisting of the resize handles to display. Defaults to 's e se' for Elements and fixed position
+ * Components. Defaults to 8 point resizing for floating Components (such as Windows). Specify either `'all'` or any
+ * of `'n s e w ne nw se sw'`.
+ */
+ handles: 's e se',
+
+ /**
+ * @cfg {Number} height
+ * Optional. The height to set target to in pixels
+ */
+ height : null,
+
+ /**
+ * @cfg {Number} width
+ * Optional. The width to set the target to in pixels
+ */
+ width : null,
+
+ /**
+ * @cfg {Number} heightIncrement
+ * The increment to snap the height resize in pixels.
+ */
+ heightIncrement : 0,
+
+ /**
+ * @cfg {Number} widthIncrement
+ * The increment to snap the width resize in pixels.
+ */
+ widthIncrement : 0,
+
+ /**
+ * @cfg {Number} minHeight
+ * The minimum height for the element
+ */
+ minHeight : 20,
+
+ /**
+ * @cfg {Number} minWidth
+ * The minimum width for the element
+ */
+ minWidth : 20,
+
+ /**
+ * @cfg {Number} maxHeight
+ * The maximum height for the element
+ */
+ maxHeight : 10000,
+
+ /**
+ * @cfg {Number} maxWidth
+ * The maximum width for the element
+ */
+ maxWidth : 10000,
+
+ /**
+ * @cfg {Boolean} pinned
+ * True to ensure that the resize handles are always visible, false indicates resizing by cursor changes only
+ */
+ pinned: false,
+
+ /**
+ * @cfg {Boolean} preserveRatio
+ * True to preserve the original ratio between height and width during resize
+ */
+ preserveRatio: false,
+
+ /**
+ * @cfg {Boolean} transparent
+ * True for transparent handles. This is only applied at config time.
+ */
+ transparent: false,
+
+ /**
+ * @cfg {Ext.Element/Ext.util.Region} constrainTo
+ * An element, or a {@link Ext.util.Region Region} into which the resize operation must be constrained.
+ */
+
+ possiblePositions: {
+ n: 'north',
+ s: 'south',
+ e: 'east',
+ w: 'west',
+ se: 'southeast',
+ sw: 'southwest',
+ nw: 'northwest',
+ ne: 'northeast'
+ },
+
+ /**
+ * @cfg {Ext.Element/Ext.Component} target
+ * The Element or Component to resize.
+ */
+
+ /**
+ * @property {Ext.Element} el
+ * Outer element for resizing behavior.
+ */
+
+ constructor: function(config) {
+ var me = this,
+ target,
+ tag,
+ handles = me.handles,
+ handleCls,
+ possibles,
+ len,
+ i = 0,
+ pos;
+
+ this.addEvents(
+ /**
+ * @event beforeresize
+ * Fired before resize is allowed. Return false to cancel resize.
+ * @param {Ext.resizer.Resizer} this
+ * @param {Number} width The start width
+ * @param {Number} height The start height
+ * @param {Ext.EventObject} e The mousedown event
+ */
+ 'beforeresize',
+ /**
+ * @event resizedrag
+ * Fires during resizing. Return false to cancel resize.
+ * @param {Ext.resizer.Resizer} this
+ * @param {Number} width The new width
+ * @param {Number} height The new height
+ * @param {Ext.EventObject} e The mousedown event
+ */
+ 'resizedrag',
+ /**
+ * @event resize
+ * Fired after a resize.
+ * @param {Ext.resizer.Resizer} this
+ * @param {Number} width The new width
+ * @param {Number} height The new height
+ * @param {Ext.EventObject} e The mouseup event
+ */
+ 'resize'
+ );
+
+ if (Ext.isString(config) || Ext.isElement(config) || config.dom) {
+ target = config;
+ config = arguments[1] || {};
+ config.target = target;
+ }
+ // will apply config to this
+ me.mixins.observable.constructor.call(me, config);
+
+ // If target is a Component, ensure that we pull the element out.
+ // Resizer must examine the underlying Element.
+ target = me.target;
+ if (target) {
+ if (target.isComponent) {
+ me.el = target.getEl();
+ if (target.minWidth) {
+ me.minWidth = target.minWidth;
+ }
+ if (target.minHeight) {
+ me.minHeight = target.minHeight;
+ }
+ if (target.maxWidth) {
+ me.maxWidth = target.maxWidth;
+ }
+ if (target.maxHeight) {
+ me.maxHeight = target.maxHeight;
+ }
+ if (target.floating) {
+ if (!this.hasOwnProperty('handles')) {
+ this.handles = 'n ne e se s sw w nw';
+ }
+ }
+ } else {
+ me.el = me.target = Ext.get(target);
+ }
+ }
+ // Backwards compatibility with Ext3.x's Resizable which used el as a config.
+ else {
+ me.target = me.el = Ext.get(me.el);
+ }
+
+ // Tags like textarea and img cannot
+ // have children and therefore must
+ // be wrapped
+ tag = me.el.dom.tagName;
+ if (tag == 'TEXTAREA' || tag == 'IMG') {
+ /**
+ * @property {Ext.Element/Ext.Component} originalTarget
+ * Reference to the original resize target if the element of the original resize target was an IMG or a
+ * TEXTAREA which must be wrapped in a DIV.
+ */
+ me.originalTarget = me.target;
+ me.target = me.el = me.el.wrap({
+ cls: me.wrapCls,
+ id: me.el.id + '-rzwrap'
+ });
+
+ // Transfer originalTarget's positioning/sizing
+ me.el.setPositioning(me.originalTarget.getPositioning());
+ me.originalTarget.clearPositioning();
+ var box = me.originalTarget.getBox();
+ me.el.setBox(box);
+ }
+
+ // Position the element, this enables us to absolute position
+ // the handles within this.el
+ me.el.position();
+ if (me.pinned) {
+ me.el.addCls(me.pinnedCls);
+ }
+
+ /**
+ * @property {Ext.resizer.ResizeTracker} resizeTracker
+ */
+ me.resizeTracker = Ext.create('Ext.resizer.ResizeTracker', {
+ disabled: me.disabled,
+ target: me.target,
+ constrainTo: me.constrainTo,
+ overCls: me.overCls,
+ throttle: me.throttle,
+ originalTarget: me.originalTarget,
+ delegate: '.' + me.handleCls,
+ dynamic: me.dynamic,
+ preserveRatio: me.preserveRatio,
+ heightIncrement: me.heightIncrement,
+ widthIncrement: me.widthIncrement,
+ minHeight: me.minHeight,
+ maxHeight: me.maxHeight,
+ minWidth: me.minWidth,
+ maxWidth: me.maxWidth
+ });
+
+ // Relay the ResizeTracker's superclass events as our own resize events
+ me.resizeTracker.on('mousedown', me.onBeforeResize, me);
+ me.resizeTracker.on('drag', me.onResize, me);
+ me.resizeTracker.on('dragend', me.onResizeEnd, me);
+
+ if (me.handles == 'all') {
+ me.handles = 'n s e w ne nw se sw';
+ }
+
+ handles = me.handles = me.handles.split(/ |\s*?[,;]\s*?/);
+ possibles = me.possiblePositions;
+ len = handles.length;
+ handleCls = me.handleCls + ' ' + (this.target.isComponent ? (me.target.baseCls + '-handle ') : '') + me.handleCls + '-';
+
+ for(; i < len; i++){
+ // if specified and possible, create
+ if (handles[i] && possibles[handles[i]]) {
+ pos = possibles[handles[i]];
+ // store a reference in this.east, this.west, etc
+
+ me[pos] = Ext.create('Ext.Component', {
+ owner: this,
+ region: pos,
+ cls: handleCls + pos,
+ renderTo: me.el
+ });
+ me[pos].el.unselectable();
+ if (me.transparent) {
+ me[pos].el.setOpacity(0);
+ }
+ }
+ }
+
+ // Constrain within configured maxima
+ if (Ext.isNumber(me.width)) {
+ me.width = Ext.Number.constrain(me.width, me.minWidth, me.maxWidth);
+ }
+ if (Ext.isNumber(me.height)) {
+ me.height = Ext.Number.constrain(me.height, me.minHeight, me.maxHeight);
+ }
+
+ // Size the element
+ if (me.width != null || me.height != null) {
+ if (me.originalTarget) {
+ me.originalTarget.setWidth(me.width);
+ me.originalTarget.setHeight(me.height);
+ }
+ me.resizeTo(me.width, me.height);
+ }
+
+ me.forceHandlesHeight();
+ },
+
+ disable: function() {
+ this.resizeTracker.disable();
+ },
+
+ enable: function() {
+ this.resizeTracker.enable();
+ },
+
+ /**
+ * @private Relay the Tracker's mousedown event as beforeresize
+ * @param tracker The Resizer
+ * @param e The Event
+ */
+ onBeforeResize: function(tracker, e) {
+ var b = this.target.getBox();
+ return this.fireEvent('beforeresize', this, b.width, b.height, e);
+ },
+
+ /**
+ * @private Relay the Tracker's drag event as resizedrag
+ * @param tracker The Resizer
+ * @param e The Event
+ */
+ onResize: function(tracker, e) {
+ var me = this,
+ b = me.target.getBox();
+ me.forceHandlesHeight();
+ return me.fireEvent('resizedrag', me, b.width, b.height, e);
+ },
+
+ /**
+ * @private Relay the Tracker's dragend event as resize
+ * @param tracker The Resizer
+ * @param e The Event
+ */
+ onResizeEnd: function(tracker, e) {
+ var me = this,
+ b = me.target.getBox();
+ me.forceHandlesHeight();
+ return me.fireEvent('resize', me, b.width, b.height, e);
+ },
+
+ /**
+ * Perform a manual resize and fires the 'resize' event.
+ * @param {Number} width
+ * @param {Number} height
+ */
+ resizeTo : function(width, height){
+ this.target.setSize(width, height);
+ this.fireEvent('resize', this, width, height, null);
+ },
+
+ /**
+ * Returns the element that was configured with the el or target config property. If a component was configured with
+ * the target property then this will return the element of this component.
+ *
+ * Textarea and img elements will be wrapped with an additional div because these elements do not support child
+ * nodes. The original element can be accessed through the originalTarget property.
+ * @return {Ext.Element} element
+ */
+ getEl : function() {
+ return this.el;
+ },
+
+ /**
+ * Returns the element or component that was configured with the target config property.
+ *
+ * Textarea and img elements will be wrapped with an additional div because these elements do not support child
+ * nodes. The original element can be accessed through the originalTarget property.
+ * @return {Ext.Element/Ext.Component}
+ */
+ getTarget: function() {
+ return this.target;
+ },
+
+ destroy: function() {
+ var h;
+ for (var i = 0, l = this.handles.length; i < l; i++) {
+ h = this[this.possiblePositions[this.handles[i]]];
+ delete h.owner;
+ Ext.destroy(h);
+ }
+ },
+
+ /**
+ * @private
+ * Fix IE6 handle height issue.
+ */
+ forceHandlesHeight : function() {
+ var me = this,
+ handle;
+ if (Ext.isIE6) {
+ handle = me.east;
+ if (handle) {
+ handle.setHeight(me.el.getHeight());
+ }
+ handle = me.west;
+ if (handle) {
+ handle.setHeight(me.el.getHeight());
+ }
+ me.el.repaint();
+ }
+ }
+});
+
+/**
+ * @class Ext.resizer.ResizeTracker
+ * @extends Ext.dd.DragTracker
+ * Private utility class for Ext.resizer.Resizer.
+ * @private
+ */
+Ext.define('Ext.resizer.ResizeTracker', {
+ extend: 'Ext.dd.DragTracker',
+ dynamic: true,
+ preserveRatio: false,
+
+ // Default to no constraint
+ constrainTo: null,
+
+ proxyCls: Ext.baseCSSPrefix + 'resizable-proxy',
+
+ constructor: function(config) {
+ var me = this;
+
+ if (!config.el) {
+ if (config.target.isComponent) {
+ me.el = config.target.getEl();
+ } else {
+ me.el = config.target;
+ }
+ }
+ this.callParent(arguments);
+
+ // Ensure that if we are preserving aspect ratio, the largest minimum is honoured
+ if (me.preserveRatio && me.minWidth && me.minHeight) {
+ var widthRatio = me.minWidth / me.el.getWidth(),
+ heightRatio = me.minHeight / me.el.getHeight();
+
+ // largest ratio of minimum:size must be preserved.
+ // So if a 400x200 pixel image has
+ // minWidth: 50, maxWidth: 50, the maxWidth will be 400 * (50/200)... that is 100
+ if (heightRatio > widthRatio) {
+ me.minWidth = me.el.getWidth() * heightRatio;
+ } else {
+ me.minHeight = me.el.getHeight() * widthRatio;
+ }
+ }
+
+ // If configured as throttled, create an instance version of resize which calls
+ // a throttled function to perform the resize operation.
+ if (me.throttle) {
+ var throttledResizeFn = Ext.Function.createThrottled(function() {
+ Ext.resizer.ResizeTracker.prototype.resize.apply(me, arguments);
+ }, me.throttle);
+
+ me.resize = function(box, direction, atEnd) {
+ if (atEnd) {
+ Ext.resizer.ResizeTracker.prototype.resize.apply(me, arguments);
+ } else {
+ throttledResizeFn.apply(null, arguments);
+ }
+ };
+ }
+ },
+
+ onBeforeStart: function(e) {
+ // record the startBox
+ this.startBox = this.el.getBox();
+ },
+
+ /**
+ * @private
+ * Returns the object that will be resized on every mousemove event.
+ * If dynamic is false, this will be a proxy, otherwise it will be our actual target.
+ */
+ getDynamicTarget: function() {
+ var me = this,
+ target = me.target;
+
+ if (me.dynamic) {
+ return target;
+ } else if (!me.proxy) {
+ me.proxy = me.createProxy(target);
+ }
+ me.proxy.show();
+ return me.proxy;
+ },
+
+ /**
+ * Create a proxy for this resizer
+ * @param {Ext.Component/Ext.Element} target The target
+ * @return {Ext.Element} A proxy element
+ */
+ createProxy: function(target){
+ var proxy,
+ cls = this.proxyCls,
+ renderTo;
+
+ if (target.isComponent) {
+ proxy = target.getProxy().addCls(cls);
+ } else {
+ renderTo = Ext.getBody();
+ if (Ext.scopeResetCSS) {
+ renderTo = Ext.getBody().createChild({
+ cls: Ext.baseCSSPrefix + 'reset'
+ });
+ }
+ proxy = target.createProxy({
+ tag: 'div',
+ cls: cls,
+ id: target.id + '-rzproxy'
+ }, renderTo);
+ }
+ proxy.removeCls(Ext.baseCSSPrefix + 'proxy-el');
+ return proxy;
+ },
+
+ onStart: function(e) {
+ // returns the Ext.ResizeHandle that the user started dragging
+ this.activeResizeHandle = Ext.getCmp(this.getDragTarget().id);
+
+ // If we are using a proxy, ensure it is sized.
+ if (!this.dynamic) {
+ this.resize(this.startBox, {
+ horizontal: 'none',
+ vertical: 'none'
+ });
+ }
+ },
+
+ onDrag: function(e) {
+ // dynamic resizing, update dimensions during resize
+ if (this.dynamic || this.proxy) {
+ this.updateDimensions(e);
+ }
+ },
+
+ updateDimensions: function(e, atEnd) {
+ var me = this,
+ region = me.activeResizeHandle.region,
+ offset = me.getOffset(me.constrainTo ? 'dragTarget' : null),
+ box = me.startBox,
+ ratio,
+ widthAdjust = 0,
+ heightAdjust = 0,
+ snappedWidth,
+ snappedHeight,
+ adjustX = 0,
+ adjustY = 0,
+ dragRatio,
+ horizDir = offset[0] < 0 ? 'right' : 'left',
+ vertDir = offset[1] < 0 ? 'down' : 'up',
+ oppositeCorner,
+ axis; // 1 = x, 2 = y, 3 = x and y.
+
+ switch (region) {
+ case 'south':
+ heightAdjust = offset[1];
+ axis = 2;
+ break;
+ case 'north':
+ heightAdjust = -offset[1];
+ adjustY = -heightAdjust;
+ axis = 2;
+ break;
+ case 'east':
+ widthAdjust = offset[0];
+ axis = 1;
+ break;
+ case 'west':
+ widthAdjust = -offset[0];
+ adjustX = -widthAdjust;
+ axis = 1;
+ break;
+ case 'northeast':
+ heightAdjust = -offset[1];
+ adjustY = -heightAdjust;
+ widthAdjust = offset[0];
+ oppositeCorner = [box.x, box.y + box.height];
+ axis = 3;
+ break;
+ case 'southeast':
+ heightAdjust = offset[1];
+ widthAdjust = offset[0];
+ oppositeCorner = [box.x, box.y];
+ axis = 3;
+ break;
+ case 'southwest':
+ widthAdjust = -offset[0];
+ adjustX = -widthAdjust;
+ heightAdjust = offset[1];
+ oppositeCorner = [box.x + box.width, box.y];
+ axis = 3;
+ break;
+ case 'northwest':
+ heightAdjust = -offset[1];
+ adjustY = -heightAdjust;
+ widthAdjust = -offset[0];
+ adjustX = -widthAdjust;
+ oppositeCorner = [box.x + box.width, box.y + box.height];
+ axis = 3;
+ break;
+ }
+
+ var newBox = {
+ width: box.width + widthAdjust,
+ height: box.height + heightAdjust,
+ x: box.x + adjustX,
+ y: box.y + adjustY
+ };
+
+ // Snap value between stops according to configured increments
+ snappedWidth = Ext.Number.snap(newBox.width, me.widthIncrement);
+ snappedHeight = Ext.Number.snap(newBox.height, me.heightIncrement);
+ if (snappedWidth != newBox.width || snappedHeight != newBox.height){
+ switch (region) {
+ case 'northeast':
+ newBox.y -= snappedHeight - newBox.height;
+ break;
+ case 'north':
+ newBox.y -= snappedHeight - newBox.height;
+ break;
+ case 'southwest':
+ newBox.x -= snappedWidth - newBox.width;
+ break;
+ case 'west':
+ newBox.x -= snappedWidth - newBox.width;
+ break;
+ case 'northwest':
+ newBox.x -= snappedWidth - newBox.width;
+ newBox.y -= snappedHeight - newBox.height;
+ }
+ newBox.width = snappedWidth;
+ newBox.height = snappedHeight;
+ }
+
+ // out of bounds
+ if (newBox.width < me.minWidth || newBox.width > me.maxWidth) {
+ newBox.width = Ext.Number.constrain(newBox.width, me.minWidth, me.maxWidth);
+
+ // Re-adjust the X position if we were dragging the west side
+ if (adjustX) {
+ newBox.x = box.x + (box.width - newBox.width);
+ }
+ } else {
+ me.lastX = newBox.x;
+ }
+ if (newBox.height < me.minHeight || newBox.height > me.maxHeight) {
+ newBox.height = Ext.Number.constrain(newBox.height, me.minHeight, me.maxHeight);
+
+ // Re-adjust the Y position if we were dragging the north side
+ if (adjustY) {
+ newBox.y = box.y + (box.height - newBox.height);
+ }
+ } else {
+ me.lastY = newBox.y;
+ }
+
+ // If this is configured to preserve the aspect ratio, or they are dragging using the shift key
+ if (me.preserveRatio || e.shiftKey) {
+ var newHeight,
+ newWidth;
+
+ ratio = me.startBox.width / me.startBox.height;
+
+ // Calculate aspect ratio constrained values.
+ newHeight = Math.min(Math.max(me.minHeight, newBox.width / ratio), me.maxHeight);
+ newWidth = Math.min(Math.max(me.minWidth, newBox.height * ratio), me.maxWidth);
+
+ // X axis: width-only change, height must obey
+ if (axis == 1) {
+ newBox.height = newHeight;
+ }
+
+ // Y axis: height-only change, width must obey
+ else if (axis == 2) {
+ newBox.width = newWidth;
+ }
+
+ // Corner drag.
+ else {
+ // Drag ratio is the ratio of the mouse point from the opposite corner.
+ // Basically what edge we are dragging, a horizontal edge or a vertical edge.
+ dragRatio = Math.abs(oppositeCorner[0] - this.lastXY[0]) / Math.abs(oppositeCorner[1] - this.lastXY[1]);
+
+ // If drag ratio > aspect ratio then width is dominant and height must obey
+ if (dragRatio > ratio) {
+ newBox.height = newHeight;
+ } else {
+ newBox.width = newWidth;
+ }
+
+ // Handle dragging start coordinates
+ if (region == 'northeast') {
+ newBox.y = box.y - (newBox.height - box.height);
+ } else if (region == 'northwest') {
+ newBox.y = box.y - (newBox.height - box.height);
+ newBox.x = box.x - (newBox.width - box.width);
+ } else if (region == 'southwest') {
+ newBox.x = box.x - (newBox.width - box.width);
+ }
+ }
+ }
+
+ if (heightAdjust === 0) {
+ vertDir = 'none';
+ }
+ if (widthAdjust === 0) {
+ horizDir = 'none';
+ }
+ me.resize(newBox, {
+ horizontal: horizDir,
+ vertical: vertDir
+ }, atEnd);
+ },
+
+ getResizeTarget: function(atEnd) {
+ return atEnd ? this.target : this.getDynamicTarget();
+ },
+
+ resize: function(box, direction, atEnd) {
+ var target = this.getResizeTarget(atEnd);
+ if (target.isComponent) {
+ if (target.floating) {
+ target.setPagePosition(box.x, box.y);
+ }
+ target.setSize(box.width, box.height);
+ } else {
+ target.setBox(box);
+ // update the originalTarget if this was wrapped.
+ if (this.originalTarget) {
+ this.originalTarget.setBox(box);
+ }
+ }
+ },
+
+ onEnd: function(e) {
+ this.updateDimensions(e, true);
+ if (this.proxy) {
+ this.proxy.hide();
+ }
+ }
+});
+
+/**
+ * @class Ext.resizer.SplitterTracker
+ * @extends Ext.dd.DragTracker
+ * Private utility class for Ext.Splitter.
+ * @private
+ */
+Ext.define('Ext.resizer.SplitterTracker', {
+ extend: 'Ext.dd.DragTracker',
+ requires: ['Ext.util.Region'],
+ enabled: true,
+
+ overlayCls: Ext.baseCSSPrefix + 'resizable-overlay',
+
+ getPrevCmp: function() {
+ var splitter = this.getSplitter();
+ return splitter.previousSibling();
+ },
+
+ getNextCmp: function() {
+ var splitter = this.getSplitter();
+ return splitter.nextSibling();
+ },
+
+ // ensure the tracker is enabled, store boxes of previous and next
+ // components and calculate the constrain region
+ onBeforeStart: function(e) {
+ var me = this,
+ prevCmp = me.getPrevCmp(),
+ nextCmp = me.getNextCmp(),
+ collapseEl = me.getSplitter().collapseEl,
+ overlay;
+
+ if (collapseEl && (e.getTarget() === me.getSplitter().collapseEl.dom)) {
+ return false;
+ }
+
+ // SplitterTracker is disabled if any of its adjacents are collapsed.
+ if (nextCmp.collapsed || prevCmp.collapsed) {
+ return false;
+ }
+
+ overlay = me.overlay = Ext.getBody().createChild({
+ cls: me.overlayCls,
+ html: ' '
+ });
+ overlay.unselectable();
+ overlay.setSize(Ext.Element.getViewWidth(true), Ext.Element.getViewHeight(true));
+ overlay.show();
+
+ // store boxes of previous and next
+ me.prevBox = prevCmp.getEl().getBox();
+ me.nextBox = nextCmp.getEl().getBox();
+ me.constrainTo = me.calculateConstrainRegion();
+ },
+
+ // We move the splitter el. Add the proxy class.
+ onStart: function(e) {
+ var splitter = this.getSplitter();
+ splitter.addCls(splitter.baseCls + '-active');
+ },
+
+ // calculate the constrain Region in which the splitter el may be moved.
+ calculateConstrainRegion: function() {
+ var me = this,
+ splitter = me.getSplitter(),
+ splitWidth = splitter.getWidth(),
+ defaultMin = splitter.defaultSplitMin,
+ orient = splitter.orientation,
+ prevBox = me.prevBox,
+ prevCmp = me.getPrevCmp(),
+ nextBox = me.nextBox,
+ nextCmp = me.getNextCmp(),
+ // prev and nextConstrainRegions are the maximumBoxes minus the
+ // minimumBoxes. The result is always the intersection
+ // of these two boxes.
+ prevConstrainRegion, nextConstrainRegion;
+
+ // vertical splitters, so resizing left to right
+ if (orient === 'vertical') {
+
+ // Region constructor accepts (top, right, bottom, left)
+ // anchored/calculated from the left
+ prevConstrainRegion = Ext.create('Ext.util.Region',
+ prevBox.y,
+ // Right boundary is x + maxWidth if there IS a maxWidth.
+ // Otherwise it is calculated based upon the minWidth of the next Component
+ (prevCmp.maxWidth ? prevBox.x + prevCmp.maxWidth : nextBox.right - (nextCmp.minWidth || defaultMin)) + splitWidth,
+ prevBox.bottom,
+ prevBox.x + (prevCmp.minWidth || defaultMin)
+ );
+ // anchored/calculated from the right
+ nextConstrainRegion = Ext.create('Ext.util.Region',
+ nextBox.y,
+ nextBox.right - (nextCmp.minWidth || defaultMin),
+ nextBox.bottom,
+ // Left boundary is right - maxWidth if there IS a maxWidth.
+ // Otherwise it is calculated based upon the minWidth of the previous Component
+ (nextCmp.maxWidth ? nextBox.right - nextCmp.maxWidth : prevBox.x + (prevBox.minWidth || defaultMin)) - splitWidth
+ );
+ } else {
+ // anchored/calculated from the top
+ prevConstrainRegion = Ext.create('Ext.util.Region',
+ prevBox.y + (prevCmp.minHeight || defaultMin),
+ prevBox.right,
+ // Bottom boundary is y + maxHeight if there IS a maxHeight.
+ // Otherwise it is calculated based upon the minWidth of the next Component
+ (prevCmp.maxHeight ? prevBox.y + prevCmp.maxHeight : nextBox.bottom - (nextCmp.minHeight || defaultMin)) + splitWidth,
+ prevBox.x
+ );
+ // anchored/calculated from the bottom
+ nextConstrainRegion = Ext.create('Ext.util.Region',
+ // Top boundary is bottom - maxHeight if there IS a maxHeight.
+ // Otherwise it is calculated based upon the minHeight of the previous Component
+ (nextCmp.maxHeight ? nextBox.bottom - nextCmp.maxHeight : prevBox.y + (prevCmp.minHeight || defaultMin)) - splitWidth,
+ nextBox.right,
+ nextBox.bottom - (nextCmp.minHeight || defaultMin),
+ nextBox.x
+ );
+ }
+
+ // intersection of the two regions to provide region draggable
+ return prevConstrainRegion.intersect(nextConstrainRegion);
+ },
+
+ // Performs the actual resizing of the previous and next components
+ performResize: function(e) {
+ var me = this,
+ offset = me.getOffset('dragTarget'),
+ splitter = me.getSplitter(),
+ orient = splitter.orientation,
+ prevCmp = me.getPrevCmp(),
+ nextCmp = me.getNextCmp(),
+ owner = splitter.ownerCt,
+ layout = owner.getLayout();
+
+ // Inhibit automatic container layout caused by setSize calls below.
+ owner.suspendLayout = true;
+
+ if (orient === 'vertical') {
+ if (prevCmp) {
+ if (!prevCmp.maintainFlex) {
+ delete prevCmp.flex;
+ prevCmp.setSize(me.prevBox.width + offset[0], prevCmp.getHeight());
+ }
+ }
+ if (nextCmp) {
+ if (!nextCmp.maintainFlex) {
+ delete nextCmp.flex;
+ nextCmp.setSize(me.nextBox.width - offset[0], nextCmp.getHeight());
+ }
+ }
+ // verticals
+ } else {
+ if (prevCmp) {
+ if (!prevCmp.maintainFlex) {
+ delete prevCmp.flex;
+ prevCmp.setSize(prevCmp.getWidth(), me.prevBox.height + offset[1]);
+ }
+ }
+ if (nextCmp) {
+ if (!nextCmp.maintainFlex) {
+ delete nextCmp.flex;
+ nextCmp.setSize(prevCmp.getWidth(), me.nextBox.height - offset[1]);
+ }
+ }
+ }
+ delete owner.suspendLayout;
+ layout.onLayout();
+ },
+
+ // Cleans up the overlay (if we have one) and calls the base. This cannot be done in
+ // onEnd, because onEnd is only called if a drag is detected but the overlay is created
+ // regardless (by onBeforeStart).
+ endDrag: function () {
+ var me = this;
+
+ if (me.overlay) {
+ me.overlay.remove();
+ delete me.overlay;
+ }
+
+ me.callParent(arguments); // this calls onEnd
+ },
+
+ // perform the resize and remove the proxy class from the splitter el
+ onEnd: function(e) {
+ var me = this,
+ splitter = me.getSplitter();
+
+ splitter.removeCls(splitter.baseCls + '-active');
+ me.performResize();
+ },
+
+ // Track the proxy and set the proper XY coordinates
+ // while constraining the drag
+ onDrag: function(e) {
+ var me = this,
+ offset = me.getOffset('dragTarget'),
+ splitter = me.getSplitter(),
+ splitEl = splitter.getEl(),
+ orient = splitter.orientation;
+
+ if (orient === "vertical") {
+ splitEl.setX(me.startRegion.left + offset[0]);
+ } else {
+ splitEl.setY(me.startRegion.top + offset[1]);
+ }
+ },
+
+ getSplitter: function() {
+ return Ext.getCmp(this.getDragCt().id);
+ }
+});
+/**
+ * @class Ext.selection.CellModel
+ * @extends Ext.selection.Model
+ */
+Ext.define('Ext.selection.CellModel', {
+ extend: 'Ext.selection.Model',
+ alias: 'selection.cellmodel',
+ requires: ['Ext.util.KeyNav'],
+
+ /**
+ * @cfg {Boolean} enableKeyNav
+ * Turns on/off keyboard navigation within the grid.
+ */
+ enableKeyNav: true,
+
+ /**
+ * @cfg {Boolean} preventWrap
+ * Set this configuration to true to prevent wrapping around of selection as
+ * a user navigates to the first or last column.
+ */
+ preventWrap: false,
+
+ constructor: function(){
+ this.addEvents(
+ /**
+ * @event deselect
+ * Fired after a cell is deselected
+ * @param {Ext.selection.CellModel} this
+ * @param {Ext.data.Model} record The record of the deselected cell
+ * @param {Number} row The row index deselected
+ * @param {Number} column The column index deselected
+ */
+ 'deselect',
+
+ /**
+ * @event select
+ * Fired after a cell is selected
+ * @param {Ext.selection.CellModel} this
+ * @param {Ext.data.Model} record The record of the selected cell
+ * @param {Number} row The row index selected
+ * @param {Number} column The column index selected
+ */
+ 'select'
+ );
+ this.callParent(arguments);
+ },
+
+ bindComponent: function(view) {
+ var me = this;
+ me.primaryView = view;
+ me.views = me.views || [];
+ me.views.push(view);
+ me.bind(view.getStore(), true);
+
+ view.on({
+ cellmousedown: me.onMouseDown,
+ refresh: me.onViewRefresh,
+ scope: me
+ });
+
+ if (me.enableKeyNav) {
+ me.initKeyNav(view);
+ }
+ },
+
+ initKeyNav: function(view) {
+ var me = this;
+
+ if (!view.rendered) {
+ view.on('render', Ext.Function.bind(me.initKeyNav, me, [view], 0), me, {single: true});
+ return;
+ }
+
+ view.el.set({
+ tabIndex: -1
+ });
+
+ // view.el has tabIndex -1 to allow for
+ // keyboard events to be passed to it.
+ me.keyNav = Ext.create('Ext.util.KeyNav', view.el, {
+ up: me.onKeyUp,
+ down: me.onKeyDown,
+ right: me.onKeyRight,
+ left: me.onKeyLeft,
+ tab: me.onKeyTab,
+ scope: me
+ });
+ },
+
+ getHeaderCt: function() {
+ return this.primaryView.headerCt;
+ },
+
+ onKeyUp: function(e, t) {
+ this.move('up', e);
+ },
+
+ onKeyDown: function(e, t) {
+ this.move('down', e);
+ },
+
+ onKeyLeft: function(e, t) {
+ this.move('left', e);
+ },
+
+ onKeyRight: function(e, t) {
+ this.move('right', e);
+ },
+
+ move: function(dir, e) {
+ var me = this,
+ pos = me.primaryView.walkCells(me.getCurrentPosition(), dir, e, me.preventWrap);
+ if (pos) {
+ me.setCurrentPosition(pos);
+ }
+ return pos;
+ },
+
+ /**
+ * Returns the current position in the format {row: row, column: column}
+ */
+ getCurrentPosition: function() {
+ return this.position;
+ },
+
+ /**
+ * Sets the current position
+ * @param {Object} position The position to set.
+ */
+ setCurrentPosition: function(pos) {
+ var me = this;
+
+ if (me.position) {
+ me.onCellDeselect(me.position);
+ }
+ if (pos) {
+ me.onCellSelect(pos);
+ }
+ me.position = pos;
+ },
+
+ /**
+ * Set the current position based on where the user clicks.
+ * @private
+ */
+ onMouseDown: function(view, cell, cellIndex, record, row, rowIndex, e) {
+ this.setCurrentPosition({
+ row: rowIndex,
+ column: cellIndex
+ });
+ },
+
+ // notify the view that the cell has been selected to update the ui
+ // appropriately and bring the cell into focus
+ onCellSelect: function(position) {
+ var me = this,
+ store = me.view.getStore(),
+ record = store.getAt(position.row);
+
+ me.doSelect(record);
+ me.primaryView.onCellSelect(position);
+ // TODO: Remove temporary cellFocus call here.
+ me.primaryView.onCellFocus(position);
+ me.fireEvent('select', me, record, position.row, position.column);
+ },
+
+ // notify view that the cell has been deselected to update the ui
+ // appropriately
+ onCellDeselect: function(position) {
+ var me = this,
+ store = me.view.getStore(),
+ record = store.getAt(position.row);
+
+ me.doDeselect(record);
+ me.primaryView.onCellDeselect(position);
+ me.fireEvent('deselect', me, record, position.row, position.column);
+ },
+
+ onKeyTab: function(e, t) {
+ var me = this,
+ direction = e.shiftKey ? 'left' : 'right',
+ editingPlugin = me.view.editingPlugin,
+ position = me.move(direction, e);
+
+ if (editingPlugin && position && me.wasEditing) {
+ editingPlugin.startEditByPosition(position);
+ }
+ delete me.wasEditing;
+ },
+
+ onEditorTab: function(editingPlugin, e) {
+ var me = this,
+ direction = e.shiftKey ? 'left' : 'right',
+ position = me.move(direction, e);
+
+ if (position) {
+ editingPlugin.startEditByPosition(position);
+ me.wasEditing = true;
+ }
+ },
+
+ refresh: function() {
+ var pos = this.getCurrentPosition();
+ if (pos) {
+ this.onCellSelect(pos);
+ }
+ },
+
+ onViewRefresh: function() {
+ var pos = this.getCurrentPosition();
+ if (pos) {
+ this.onCellDeselect(pos);
+ this.setCurrentPosition(null);
+ }
+ },
+
+ selectByPosition: function(position) {
+ this.setCurrentPosition(position);
+ }
+});
+/**
+ * @class Ext.selection.RowModel
+ * @extends Ext.selection.Model
+ */
+Ext.define('Ext.selection.RowModel', {
+ extend: 'Ext.selection.Model',
+ alias: 'selection.rowmodel',
+ requires: ['Ext.util.KeyNav'],
+
+ /**
+ * @private
+ * Number of pixels to scroll to the left/right when pressing
+ * left/right keys.
+ */
+ deltaScroll: 5,
+
+ /**
+ * @cfg {Boolean} enableKeyNav
+ *
+ * Turns on/off keyboard navigation within the grid.
+ */
+ enableKeyNav: true,
+
+ /**
+ * @cfg {Boolean} [ignoreRightMouseSelection=true]
+ * True to ignore selections that are made when using the right mouse button if there are
+ * records that are already selected. If no records are selected, selection will continue
+ * as normal
+ */
+ ignoreRightMouseSelection: true,
+
+ constructor: function(){
+ this.addEvents(
+ /**
+ * @event beforedeselect
+ * Fired before a record is deselected. If any listener returns false, the
+ * deselection is cancelled.
+ * @param {Ext.selection.RowModel} this
+ * @param {Ext.data.Model} record The deselected record
+ * @param {Number} index The row index deselected
+ */
+ 'beforedeselect',
+
+ /**
+ * @event beforeselect
+ * Fired before a record is selected. If any listener returns false, the
+ * selection is cancelled.
+ * @param {Ext.selection.RowModel} this
+ * @param {Ext.data.Model} record The selected record
+ * @param {Number} index The row index selected
+ */
+ 'beforeselect',
+
+ /**
+ * @event deselect
+ * Fired after a record is deselected
+ * @param {Ext.selection.RowModel} this
+ * @param {Ext.data.Model} record The deselected record
+ * @param {Number} index The row index deselected
+ */
+ 'deselect',
+
+ /**
+ * @event select
+ * Fired after a record is selected
+ * @param {Ext.selection.RowModel} this
+ * @param {Ext.data.Model} record The selected record
+ * @param {Number} index The row index selected
+ */
+ 'select'
+ );
+ this.callParent(arguments);
+ },
+
+ bindComponent: function(view) {
+ var me = this;
+
+ me.views = me.views || [];
+ me.views.push(view);
+ me.bind(view.getStore(), true);
+
+ view.on({
+ itemmousedown: me.onRowMouseDown,
+ scope: me
+ });
+
+ if (me.enableKeyNav) {
+ me.initKeyNav(view);
+ }
+ },
+
+ initKeyNav: function(view) {
+ var me = this;
+
+ if (!view.rendered) {
+ view.on('render', Ext.Function.bind(me.initKeyNav, me, [view], 0), me, {single: true});
+ return;
+ }
+
+ view.el.set({
+ tabIndex: -1
+ });
+
+ // view.el has tabIndex -1 to allow for
+ // keyboard events to be passed to it.
+ me.keyNav = new Ext.util.KeyNav(view.el, {
+ up: me.onKeyUp,
+ down: me.onKeyDown,
+ right: me.onKeyRight,
+ left: me.onKeyLeft,
+ pageDown: me.onKeyPageDown,
+ pageUp: me.onKeyPageUp,
+ home: me.onKeyHome,
+ end: me.onKeyEnd,
+ scope: me
+ });
+ view.el.on(Ext.EventManager.getKeyEvent(), me.onKeyPress, me);
+ },
+
+ // Returns the number of rows currently visible on the screen or
+ // false if there were no rows. This assumes that all rows are
+ // of the same height and the first view is accurate.
+ getRowsVisible: function() {
+ var rowsVisible = false,
+ view = this.views[0],
+ row = view.getNode(0),
+ rowHeight, gridViewHeight;
+
+ if (row) {
+ rowHeight = Ext.fly(row).getHeight();
+ gridViewHeight = view.el.getHeight();
+ rowsVisible = Math.floor(gridViewHeight / rowHeight);
+ }
+
+ return rowsVisible;
+ },
+
+ // go to last visible record in grid.
+ onKeyEnd: function(e, t) {
+ var me = this,
+ last = me.store.getAt(me.store.getCount() - 1);
+
+ if (last) {
+ if (e.shiftKey) {
+ me.selectRange(last, me.lastFocused || 0);
+ me.setLastFocused(last);
+ } else if (e.ctrlKey) {
+ me.setLastFocused(last);
+ } else {
+ me.doSelect(last);
+ }
+ }
+ },
+
+ // go to first visible record in grid.
+ onKeyHome: function(e, t) {
+ var me = this,
+ first = me.store.getAt(0);
+
+ if (first) {
+ if (e.shiftKey) {
+ me.selectRange(first, me.lastFocused || 0);
+ me.setLastFocused(first);
+ } else if (e.ctrlKey) {
+ me.setLastFocused(first);
+ } else {
+ me.doSelect(first, false);
+ }
+ }
+ },
+
+ // Go one page up from the lastFocused record in the grid.
+ onKeyPageUp: function(e, t) {
+ var me = this,
+ rowsVisible = me.getRowsVisible(),
+ selIdx,
+ prevIdx,
+ prevRecord,
+ currRec;
+
+ if (rowsVisible) {
+ selIdx = me.lastFocused ? me.store.indexOf(me.lastFocused) : 0;
+ prevIdx = selIdx - rowsVisible;
+ if (prevIdx < 0) {
+ prevIdx = 0;
+ }
+ prevRecord = me.store.getAt(prevIdx);
+ if (e.shiftKey) {
+ currRec = me.store.getAt(selIdx);
+ me.selectRange(prevRecord, currRec, e.ctrlKey, 'up');
+ me.setLastFocused(prevRecord);
+ } else if (e.ctrlKey) {
+ e.preventDefault();
+ me.setLastFocused(prevRecord);
+ } else {
+ me.doSelect(prevRecord);
+ }
+
+ }
+ },
+
+ // Go one page down from the lastFocused record in the grid.
+ onKeyPageDown: function(e, t) {
+ var me = this,
+ rowsVisible = me.getRowsVisible(),
+ selIdx,
+ nextIdx,
+ nextRecord,
+ currRec;
+
+ if (rowsVisible) {
+ selIdx = me.lastFocused ? me.store.indexOf(me.lastFocused) : 0;
+ nextIdx = selIdx + rowsVisible;
+ if (nextIdx >= me.store.getCount()) {
+ nextIdx = me.store.getCount() - 1;
+ }
+ nextRecord = me.store.getAt(nextIdx);
+ if (e.shiftKey) {
+ currRec = me.store.getAt(selIdx);
+ me.selectRange(nextRecord, currRec, e.ctrlKey, 'down');
+ me.setLastFocused(nextRecord);
+ } else if (e.ctrlKey) {
+ // some browsers, this means go thru browser tabs
+ // attempt to stop.
+ e.preventDefault();
+ me.setLastFocused(nextRecord);
+ } else {
+ me.doSelect(nextRecord);
+ }
+ }
+ },
+
+ // Select/Deselect based on pressing Spacebar.
+ // Assumes a SIMPLE selectionmode style
+ onKeyPress: function(e, t) {
+ if (e.getKey() === e.SPACE) {
+ e.stopEvent();
+ var me = this,
+ record = me.lastFocused;
+
+ if (record) {
+ if (me.isSelected(record)) {
+ me.doDeselect(record, false);
+ } else {
+ me.doSelect(record, true);
+ }
+ }
+ }
+ },
+
+ // Navigate one record up. This could be a selection or
+ // could be simply focusing a record for discontiguous
+ // selection. Provides bounds checking.
+ onKeyUp: function(e, t) {
+ var me = this,
+ view = me.views[0],
+ idx = me.store.indexOf(me.lastFocused),
+ record;
+
+ if (idx > 0) {
+ // needs to be the filtered count as thats what
+ // will be visible.
+ record = me.store.getAt(idx - 1);
+ if (e.shiftKey && me.lastFocused) {
+ if (me.isSelected(me.lastFocused) && me.isSelected(record)) {
+ me.doDeselect(me.lastFocused, true);
+ me.setLastFocused(record);
+ } else if (!me.isSelected(me.lastFocused)) {
+ me.doSelect(me.lastFocused, true);
+ me.doSelect(record, true);
+ } else {
+ me.doSelect(record, true);
+ }
+ } else if (e.ctrlKey) {
+ me.setLastFocused(record);
+ } else {
+ me.doSelect(record);
+ //view.focusRow(idx - 1);
+ }
+ }
+ // There was no lastFocused record, and the user has pressed up
+ // Ignore??
+ //else if (this.selected.getCount() == 0) {
+ //
+ // this.doSelect(record);
+ // //view.focusRow(idx - 1);
+ //}
+ },
+
+ // Navigate one record down. This could be a selection or
+ // could be simply focusing a record for discontiguous
+ // selection. Provides bounds checking.
+ onKeyDown: function(e, t) {
+ var me = this,
+ view = me.views[0],
+ idx = me.store.indexOf(me.lastFocused),
+ record;
+
+ // needs to be the filtered count as thats what
+ // will be visible.
+ if (idx + 1 < me.store.getCount()) {
+ record = me.store.getAt(idx + 1);
+ if (me.selected.getCount() === 0) {
+ me.doSelect(record);
+ //view.focusRow(idx + 1);
+ } else if (e.shiftKey && me.lastFocused) {
+ if (me.isSelected(me.lastFocused) && me.isSelected(record)) {
+ me.doDeselect(me.lastFocused, true);
+ me.setLastFocused(record);
+ } else if (!me.isSelected(me.lastFocused)) {
+ me.doSelect(me.lastFocused, true);
+ me.doSelect(record, true);
+ } else {
+ me.doSelect(record, true);
+ }
+ } else if (e.ctrlKey) {
+ me.setLastFocused(record);
+ } else {
+ me.doSelect(record);
+ //view.focusRow(idx + 1);
+ }
+ }
+ },
+
+ scrollByDeltaX: function(delta) {
+ var view = this.views[0],
+ section = view.up(),
+ hScroll = section.horizontalScroller;
+
+ if (hScroll) {
+ hScroll.scrollByDeltaX(delta);
+ }
+ },
+
+ onKeyLeft: function(e, t) {
+ this.scrollByDeltaX(-this.deltaScroll);
+ },
+
+ onKeyRight: function(e, t) {
+ this.scrollByDeltaX(this.deltaScroll);
+ },
+
+ // Select the record with the event included so that
+ // we can take into account ctrlKey, shiftKey, etc
+ onRowMouseDown: function(view, record, item, index, e) {
+ view.el.focus();
+ if (!this.allowRightMouseSelection(e)) {
+ return;
+ }
+ this.selectWithEvent(record, e);
+ },
+
+ /**
+ * Checks whether a selection should proceed based on the ignoreRightMouseSelection
+ * option.
+ * @private
+ * @param {Ext.EventObject} e The event
+ * @return {Boolean} False if the selection should not proceed
+ */
+ allowRightMouseSelection: function(e) {
+ var disallow = this.ignoreRightMouseSelection && e.button !== 0;
+ if (disallow) {
+ disallow = this.hasSelection();
+ }
+ return !disallow;
+ },
+
+ // Allow the GridView to update the UI by
+ // adding/removing a CSS class from the row.
+ onSelectChange: function(record, isSelected, suppressEvent, commitFn) {
+ var me = this,
+ views = me.views,
+ viewsLn = views.length,
+ store = me.store,
+ rowIdx = store.indexOf(record),
+ eventName = isSelected ? 'select' : 'deselect',
+ i = 0;
+
+ if ((suppressEvent || me.fireEvent('before' + eventName, me, record, rowIdx)) !== false &&
+ commitFn() !== false) {
+
+ for (; i < viewsLn; i++) {
+ if (isSelected) {
+ views[i].onRowSelect(rowIdx, suppressEvent);
+ } else {
+ views[i].onRowDeselect(rowIdx, suppressEvent);
+ }
+ }
+
+ if (!suppressEvent) {
+ me.fireEvent(eventName, me, record, rowIdx);
+ }
+ }
+ },
+
+ // Provide indication of what row was last focused via
+ // the gridview.
+ onLastFocusChanged: function(oldFocused, newFocused, supressFocus) {
+ var views = this.views,
+ viewsLn = views.length,
+ store = this.store,
+ rowIdx,
+ i = 0;
+
+ if (oldFocused) {
+ rowIdx = store.indexOf(oldFocused);
+ if (rowIdx != -1) {
+ for (; i < viewsLn; i++) {
+ views[i].onRowFocus(rowIdx, false);
+ }
+ }
+ }
+
+ if (newFocused) {
+ rowIdx = store.indexOf(newFocused);
+ if (rowIdx != -1) {
+ for (i = 0; i < viewsLn; i++) {
+ views[i].onRowFocus(rowIdx, true, supressFocus);
+ }
+ }
+ }
+ },
+
+ onEditorTab: function(editingPlugin, e) {
+ var me = this,
+ view = me.views[0],
+ record = editingPlugin.getActiveRecord(),
+ header = editingPlugin.getActiveColumn(),
+ position = view.getPosition(record, header),
+ direction = e.shiftKey ? 'left' : 'right',
+ newPosition = view.walkCells(position, direction, e, this.preventWrap);
+
+ if (newPosition) {
+ editingPlugin.startEditByPosition(newPosition);
+ }
+ },
+
+ selectByPosition: function(position) {
+ var record = this.store.getAt(position.row);
+ this.select(record);
+ }
+});
+/**
+ * @class Ext.selection.CheckboxModel
+ * @extends Ext.selection.RowModel
+ *
+ * A selection model that renders a column of checkboxes that can be toggled to
+ * select or deselect rows. The default mode for this selection model is MULTI.
+ *
+ * The selection model will inject a header for the checkboxes in the first view
+ * and according to the 'injectCheckbox' configuration.
+ */
+Ext.define('Ext.selection.CheckboxModel', {
+ alias: 'selection.checkboxmodel',
+ extend: 'Ext.selection.RowModel',
+
+ /**
+ * @cfg {String} mode
+ * Modes of selection.
+ * Valid values are SINGLE, SIMPLE, and MULTI. Defaults to 'MULTI'
+ */
+ mode: 'MULTI',
+
+ /**
+ * @cfg {Number/Boolean/String} injectCheckbox
+ * Instructs the SelectionModel whether or not to inject the checkbox header
+ * automatically or not. (Note: By not placing the checkbox in manually, the
+ * grid view will need to be rendered 2x on initial render.)
+ * Supported values are a Number index, false and the strings 'first' and 'last'.
+ */
+ injectCheckbox: 0,
+
+ /**
+ * @cfg {Boolean} checkOnly <tt>true</tt> if rows can only be selected by clicking on the
+ * checkbox column.
+ */
+ checkOnly: false,
+
+ headerWidth: 24,
+
+ // private
+ checkerOnCls: Ext.baseCSSPrefix + 'grid-hd-checker-on',
+
+ bindComponent: function(view) {
+ var me = this;
+
+ me.sortable = false;
+ me.callParent(arguments);
+ if (!me.hasLockedHeader() || view.headerCt.lockedCt) {
+ // if we have a locked header, only hook up to the first
+ view.headerCt.on('headerclick', me.onHeaderClick, me);
+ me.addCheckbox(true);
+ me.mon(view.ownerCt, 'reconfigure', me.addCheckbox, me);
+ }
+ },
+
+ hasLockedHeader: function(){
+ var hasLocked = false;
+ Ext.each(this.views, function(view){
+ if (view.headerCt.lockedCt) {
+ hasLocked = true;
+ return false;
+ }
+ });
+ return hasLocked;
+ },
+
+ /**
+ * Add the header checkbox to the header row
+ * @private
+ * @param {Boolean} initial True if we're binding for the first time.
+ */
+ addCheckbox: function(initial){
+ var me = this,
+ checkbox = me.injectCheckbox,
+ view = me.views[0],
+ headerCt = view.headerCt;
+
+ if (checkbox !== false) {
+ if (checkbox == 'first') {
+ checkbox = 0;
+ } else if (checkbox == 'last') {
+ checkbox = headerCt.getColumnCount();
+ }
+ headerCt.add(checkbox, me.getHeaderConfig());
+ }
+
+ if (initial !== true) {
+ view.refresh();
+ }
+ },
+
+ /**
+ * Toggle the ui header between checked and unchecked state.
+ * @param {Boolean} isChecked
+ * @private
+ */
+ toggleUiHeader: function(isChecked) {
+ var view = this.views[0],
+ headerCt = view.headerCt,
+ checkHd = headerCt.child('gridcolumn[isCheckerHd]');
+
+ if (checkHd) {
+ if (isChecked) {
+ checkHd.el.addCls(this.checkerOnCls);
+ } else {
+ checkHd.el.removeCls(this.checkerOnCls);
+ }
+ }
+ },
+
+ /**
+ * Toggle between selecting all and deselecting all when clicking on
+ * a checkbox header.
+ */
+ onHeaderClick: function(headerCt, header, e) {
+ if (header.isCheckerHd) {
+ e.stopEvent();
+ var isChecked = header.el.hasCls(Ext.baseCSSPrefix + 'grid-hd-checker-on');
+ if (isChecked) {
+ // We have to supress the event or it will scrollTo the change
+ this.deselectAll(true);
+ } else {
+ // We have to supress the event or it will scrollTo the change
+ this.selectAll(true);
+ }
+ }
+ },
+
+ /**
+ * Retrieve a configuration to be used in a HeaderContainer.
+ * This should be used when injectCheckbox is set to false.
+ */
+ getHeaderConfig: function() {
+ var me = this;
+
+ return {
+ isCheckerHd: true,
+ text : ' ',
+ width: me.headerWidth,
+ sortable: false,
+ draggable: false,
+ resizable: false,
+ hideable: false,
+ menuDisabled: true,
+ dataIndex: '',
+ cls: Ext.baseCSSPrefix + 'column-header-checkbox ',
+ renderer: Ext.Function.bind(me.renderer, me),
+ locked: me.hasLockedHeader()
+ };
+ },
+
+ /**
+ * Generates the HTML to be rendered in the injected checkbox column for each row.
+ * Creates the standard checkbox markup by default; can be overridden to provide custom rendering.
+ * See {@link Ext.grid.column.Column#renderer} for description of allowed parameters.
+ */
+ renderer: function(value, metaData, record, rowIndex, colIndex, store, view) {
+ metaData.tdCls = Ext.baseCSSPrefix + 'grid-cell-special';
+ return '<div class="' + Ext.baseCSSPrefix + 'grid-row-checker"> </div>';
+ },
+
+ // override
+ onRowMouseDown: function(view, record, item, index, e) {
+ view.el.focus();
+ var me = this,
+ checker = e.getTarget('.' + Ext.baseCSSPrefix + 'grid-row-checker');
+
+ if (!me.allowRightMouseSelection(e)) {
+ return;
+ }
+
+ // checkOnly set, but we didn't click on a checker.
+ if (me.checkOnly && !checker) {
+ return;
+ }
+
+ if (checker) {
+ var mode = me.getSelectionMode();
+ // dont change the mode if its single otherwise
+ // we would get multiple selection
+ if (mode !== 'SINGLE') {
+ me.setSelectionMode('SIMPLE');
+ }
+ me.selectWithEvent(record, e);
+ me.setSelectionMode(mode);
+ } else {
+ me.selectWithEvent(record, e);
+ }
+ },
+
+ /**
+ * Synchronize header checker value as selection changes.
+ * @private
+ */
+ onSelectChange: function() {
+ this.callParent(arguments);
+
+ // check to see if all records are selected
+ var hdSelectStatus = this.selected.getCount() === this.store.getCount();
+ this.toggleUiHeader(hdSelectStatus);
+ }
+});
+
+/**
+ * @class Ext.selection.TreeModel
+ * @extends Ext.selection.RowModel
+ *
+ * Adds custom behavior for left/right keyboard navigation for use with a tree.
+ * Depends on the view having an expand and collapse method which accepts a
+ * record.
+ *
+ * @private
+ */
+Ext.define('Ext.selection.TreeModel', {
+ extend: 'Ext.selection.RowModel',
+ alias: 'selection.treemodel',
+
+ // typically selection models prune records from the selection
+ // model when they are removed, because the TreeView constantly
+ // adds/removes records as they are expanded/collapsed
+ pruneRemoved: false,
+
+ onKeyRight: function(e, t) {
+ var focused = this.getLastFocused(),
+ view = this.view;
+
+ if (focused) {
+ // tree node is already expanded, go down instead
+ // this handles both the case where we navigate to firstChild and if
+ // there are no children to the nextSibling
+ if (focused.isExpanded()) {
+ this.onKeyDown(e, t);
+ // if its not a leaf node, expand it
+ } else if (!focused.isLeaf()) {
+ view.expand(focused);
+ }
+ }
+ },
+
+ onKeyLeft: function(e, t) {
+ var focused = this.getLastFocused(),
+ view = this.view,
+ viewSm = view.getSelectionModel(),
+ parentNode, parentRecord;
+
+ if (focused) {
+ parentNode = focused.parentNode;
+ // if focused node is already expanded, collapse it
+ if (focused.isExpanded()) {
+ view.collapse(focused);
+ // has a parentNode and its not root
+ // TODO: this needs to cover the case where the root isVisible
+ } else if (parentNode && !parentNode.isRoot()) {
+ // Select a range of records when doing multiple selection.
+ if (e.shiftKey) {
+ viewSm.selectRange(parentNode, focused, e.ctrlKey, 'up');
+ viewSm.setLastFocused(parentNode);
+ // just move focus, not selection
+ } else if (e.ctrlKey) {
+ viewSm.setLastFocused(parentNode);
+ // select it
+ } else {
+ viewSm.select(parentNode);
+ }
+ }
+ }
+ },
+
+ onKeyPress: function(e, t) {
+ var key = e.getKey(),
+ selected,
+ checked;
+
+ if (key === e.SPACE || key === e.ENTER) {
+ e.stopEvent();
+ selected = this.getLastSelected();
+ if (selected) {
+ this.view.onCheckChange(selected);
+ }
+ } else {
+ this.callParent(arguments);
+ }
+ }
+});
+
+/**
+ * @class Ext.slider.Thumb
+ * @extends Ext.Base
+ * @private
+ * Represents a single thumb element on a Slider. This would not usually be created manually and would instead
+ * be created internally by an {@link Ext.slider.Multi Multi slider}.
+ */
+Ext.define('Ext.slider.Thumb', {
+ requires: ['Ext.dd.DragTracker', 'Ext.util.Format'],
+ /**
+ * @private
+ * @property {Number} topThumbZIndex
+ * The number used internally to set the z index of the top thumb (see promoteThumb for details)
+ */
+ topZIndex: 10000,
+
+ /**
+ * @cfg {Ext.slider.MultiSlider} slider (required)
+ * The Slider to render to.
+ */
+
+ /**
+ * Creates new slider thumb.
+ * @param {Object} config (optional) Config object.
+ */
+ constructor: function(config) {
+ var me = this;
+
+ /**
+ * @property {Ext.slider.MultiSlider} slider
+ * The slider this thumb is contained within
+ */
+ Ext.apply(me, config || {}, {
+ cls: Ext.baseCSSPrefix + 'slider-thumb',
+
+ /**
+ * @cfg {Boolean} constrain True to constrain the thumb so that it cannot overlap its siblings
+ */
+ constrain: false
+ });
+ me.callParent([config]);
+
+ if (me.slider.vertical) {
+ Ext.apply(me, Ext.slider.Thumb.Vertical);
+ }
+ },
+
+ /**
+ * Renders the thumb into a slider
+ */
+ render: function() {
+ var me = this;
+
+ me.el = me.slider.innerEl.insertFirst({cls: me.cls});
+ if (me.disabled) {
+ me.disable();
+ }
+ me.initEvents();
+ },
+
+ /**
+ * @private
+ * move the thumb
+ */
+ move: function(v, animate){
+ if(!animate){
+ this.el.setLeft(v);
+ }else{
+ Ext.create('Ext.fx.Anim', {
+ target: this.el,
+ duration: 350,
+ to: {
+ left: v
+ }
+ });
+ }
+ },
+
+ /**
+ * @private
+ * Bring thumb dom element to front.
+ */
+ bringToFront: function() {
+ this.el.setStyle('zIndex', this.topZIndex);
+ },
+
+ /**
+ * @private
+ * Send thumb dom element to back.
+ */
+ sendToBack: function() {
+ this.el.setStyle('zIndex', '');
+ },
+
+ /**
+ * Enables the thumb if it is currently disabled
+ */
+ enable: function() {
+ var me = this;
+
+ me.disabled = false;
+ if (me.el) {
+ me.el.removeCls(me.slider.disabledCls);
+ }
+ },
+
+ /**
+ * Disables the thumb if it is currently enabled
+ */
+ disable: function() {
+ var me = this;
+
+ me.disabled = true;
+ if (me.el) {
+ me.el.addCls(me.slider.disabledCls);
+ }
+ },
+
+ /**
+ * Sets up an Ext.dd.DragTracker for this thumb
+ */
+ initEvents: function() {
+ var me = this,
+ el = me.el;
+
+ me.tracker = Ext.create('Ext.dd.DragTracker', {
+ onBeforeStart: Ext.Function.bind(me.onBeforeDragStart, me),
+ onStart : Ext.Function.bind(me.onDragStart, me),
+ onDrag : Ext.Function.bind(me.onDrag, me),
+ onEnd : Ext.Function.bind(me.onDragEnd, me),
+ tolerance : 3,
+ autoStart : 300,
+ overCls : Ext.baseCSSPrefix + 'slider-thumb-over'
+ });
+
+ me.tracker.initEl(el);
+ },
+
+ /**
+ * @private
+ * This is tied into the internal Ext.dd.DragTracker. If the slider is currently disabled,
+ * this returns false to disable the DragTracker too.
+ * @return {Boolean} False if the slider is currently disabled
+ */
+ onBeforeDragStart : function(e) {
+ if (this.disabled) {
+ return false;
+ } else {
+ this.slider.promoteThumb(this);
+ return true;
+ }
+ },
+
+ /**
+ * @private
+ * This is tied into the internal Ext.dd.DragTracker's onStart template method. Adds the drag CSS class
+ * to the thumb and fires the 'dragstart' event
+ */
+ onDragStart: function(e){
+ var me = this;
+
+ me.el.addCls(Ext.baseCSSPrefix + 'slider-thumb-drag');
+ me.dragging = true;
+ me.dragStartValue = me.value;
+
+ me.slider.fireEvent('dragstart', me.slider, e, me);
+ },
+
+ /**
+ * @private
+ * This is tied into the internal Ext.dd.DragTracker's onDrag template method. This is called every time
+ * the DragTracker detects a drag movement. It updates the Slider's value using the position of the drag
+ */
+ onDrag: function(e) {
+ var me = this,
+ slider = me.slider,
+ index = me.index,
+ newValue = me.getNewValue(),
+ above,
+ below;
+
+ if (me.constrain) {
+ above = slider.thumbs[index + 1];
+ below = slider.thumbs[index - 1];
+
+ if (below !== undefined && newValue <= below.value) {
+ newValue = below.value;
+ }
+
+ if (above !== undefined && newValue >= above.value) {
+ newValue = above.value;
+ }
+ }
+
+ slider.setValue(index, newValue, false);
+ slider.fireEvent('drag', slider, e, me);
+ },
+
+ getNewValue: function() {
+ var slider = this.slider,
+ pos = slider.innerEl.translatePoints(this.tracker.getXY());
+
+ return Ext.util.Format.round(slider.reverseValue(pos.left), slider.decimalPrecision);
+ },
+
+ /**
+ * @private
+ * This is tied to the internal Ext.dd.DragTracker's onEnd template method. Removes the drag CSS class and
+ * fires the 'changecomplete' event with the new value
+ */
+ onDragEnd: function(e) {
+ var me = this,
+ slider = me.slider,
+ value = me.value;
+
+ me.el.removeCls(Ext.baseCSSPrefix + 'slider-thumb-drag');
+
+ me.dragging = false;
+ slider.fireEvent('dragend', slider, e);
+
+ if (me.dragStartValue != value) {
+ slider.fireEvent('changecomplete', slider, value, me);
+ }
+ },
+
+ destroy: function() {
+ Ext.destroy(this.tracker);
+ },
+ statics: {
+ // Method overrides to support vertical dragging of thumb within slider
+ Vertical: {
+ getNewValue: function() {
+ var slider = this.slider,
+ innerEl = slider.innerEl,
+ pos = innerEl.translatePoints(this.tracker.getXY()),
+ bottom = innerEl.getHeight() - pos.top;
+
+ return Ext.util.Format.round(slider.reverseValue(bottom), slider.decimalPrecision);
+ },
+ move: function(v, animate) {
+ if (!animate) {
+ this.el.setBottom(v);
+ } else {
+ Ext.create('Ext.fx.Anim', {
+ target: this.el,
+ duration: 350,
+ to: {
+ bottom: v
+ }
+ });
+ }
+ }
+ }
+ }
+});
+
+/**
+ * Simple plugin for using an Ext.tip.Tip with a slider to show the slider value. In general this class is not created
+ * directly, instead pass the {@link Ext.slider.Multi#useTips} and {@link Ext.slider.Multi#tipText} configuration
+ * options to the slider directly.
+ *
+ * @example
+ * Ext.create('Ext.slider.Single', {
+ * width: 214,
+ * minValue: 0,
+ * maxValue: 100,
+ * useTips: true,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * Optionally provide your own tip text by passing tipText:
+ *
+ * @example
+ * Ext.create('Ext.slider.Single', {
+ * width: 214,
+ * minValue: 0,
+ * maxValue: 100,
+ * useTips: true,
+ * tipText: function(thumb){
+ * return Ext.String.format('**{0}% complete**', thumb.value);
+ * },
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.slider.Tip', {
+ extend: 'Ext.tip.Tip',
+ minWidth: 10,
+ alias: 'widget.slidertip',
+ offsets : [0, -10],
+
+ isSliderTip: true,
+
+ init: function(slider) {
+ var me = this;
+
+ slider.on({
+ scope : me,
+ dragstart: me.onSlide,
+ drag : me.onSlide,
+ dragend : me.hide,
+ destroy : me.destroy
+ });
+ },
+ /**
+ * @private
+ * Called whenever a dragstart or drag event is received on the associated Thumb.
+ * Aligns the Tip with the Thumb's new position.
+ * @param {Ext.slider.MultiSlider} slider The slider
+ * @param {Ext.EventObject} e The Event object
+ * @param {Ext.slider.Thumb} thumb The thumb that the Tip is attached to
+ */
+ onSlide : function(slider, e, thumb) {
+ var me = this;
+ me.show();
+ me.update(me.getText(thumb));
+ me.doComponentLayout();
+ me.el.alignTo(thumb.el, 'b-t?', me.offsets);
+ },
+
+ /**
+ * Used to create the text that appears in the Tip's body. By default this just returns the value of the Slider
+ * Thumb that the Tip is attached to. Override to customize.
+ * @param {Ext.slider.Thumb} thumb The Thumb that the Tip is attached to
+ * @return {String} The text to display in the tip
+ */
+ getText : function(thumb) {
+ return String(thumb.value);
+ }
+});
+/**
+ * Slider which supports vertical or horizontal orientation, keyboard adjustments, configurable snapping, axis clicking
+ * and animation. Can be added as an item to any container.
+ *
+ * Sliders can be created with more than one thumb handle by passing an array of values instead of a single one:
+ *
+ * @example
+ * Ext.create('Ext.slider.Multi', {
+ * width: 200,
+ * values: [25, 50, 75],
+ * increment: 5,
+ * minValue: 0,
+ * maxValue: 100,
+ *
+ * // this defaults to true, setting to false allows the thumbs to pass each other
+ * constrainThumbs: false,
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.slider.Multi', {
+ extend: 'Ext.form.field.Base',
+ alias: 'widget.multislider',
+ alternateClassName: 'Ext.slider.MultiSlider',
+
+ requires: [
+ 'Ext.slider.Thumb',
+ 'Ext.slider.Tip',
+ 'Ext.Number',
+ 'Ext.util.Format',
+ 'Ext.Template',
+ 'Ext.layout.component.field.Slider'
+ ],
+
+ // note: {id} here is really {inputId}, but {cmpId} is available
+ fieldSubTpl: [
+ '<div id="{id}" class="' + Ext.baseCSSPrefix + 'slider {fieldCls} {vertical}" aria-valuemin="{minValue}" aria-valuemax="{maxValue}" aria-valuenow="{value}" aria-valuetext="{value}">',
+ '<div id="{cmpId}-endEl" class="' + Ext.baseCSSPrefix + 'slider-end" role="presentation">',
+ '<div id="{cmpId}-innerEl" class="' + Ext.baseCSSPrefix + 'slider-inner" role="presentation">',
+ '<a id="{cmpId}-focusEl" class="' + Ext.baseCSSPrefix + 'slider-focus" href="#" tabIndex="-1" hidefocus="on" role="presentation"></a>',
+ '</div>',
+ '</div>',
+ '</div>',
+ {
+ disableFormats: true,
+ compiled: true
+ }
+ ],
+
+ /**
+ * @cfg {Number} value
+ * A value with which to initialize the slider. Defaults to minValue. Setting this will only result in the creation
+ * of a single slider thumb; if you want multiple thumbs then use the {@link #values} config instead.
+ */
+
+ /**
+ * @cfg {Number[]} values
+ * Array of Number values with which to initalize the slider. A separate slider thumb will be created for each value
+ * in this array. This will take precedence over the single {@link #value} config.
+ */
+
+ /**
+ * @cfg {Boolean} vertical
+ * Orient the Slider vertically rather than horizontally.
+ */
+ vertical: false,
+
+ /**
+ * @cfg {Number} minValue
+ * The minimum value for the Slider.
+ */
+ minValue: 0,
+
+ /**
+ * @cfg {Number} maxValue
+ * The maximum value for the Slider.
+ */
+ maxValue: 100,
+
+ /**
+ * @cfg {Number/Boolean} decimalPrecision The number of decimal places to which to round the Slider's value.
+ *
+ * To disable rounding, configure as **false**.
+ */
+ decimalPrecision: 0,
+
+ /**
+ * @cfg {Number} keyIncrement
+ * How many units to change the Slider when adjusting with keyboard navigation. If the increment
+ * config is larger, it will be used instead.
+ */
+ keyIncrement: 1,
+
+ /**
+ * @cfg {Number} increment
+ * How many units to change the slider when adjusting by drag and drop. Use this option to enable 'snapping'.
+ */
+ increment: 0,
+
+ /**
+ * @private
+ * @property {Number[]} clickRange
+ * Determines whether or not a click to the slider component is considered to be a user request to change the value. Specified as an array of [top, bottom],
+ * the click event's 'top' property is compared to these numbers and the click only considered a change request if it falls within them. e.g. if the 'top'
+ * value of the click event is 4 or 16, the click is not considered a change request as it falls outside of the [5, 15] range
+ */
+ clickRange: [5,15],
+
+ /**
+ * @cfg {Boolean} clickToChange
+ * Determines whether or not clicking on the Slider axis will change the slider.
+ */
+ clickToChange : true,
+
+ /**
+ * @cfg {Boolean} animate
+ * Turn on or off animation.
+ */
+ animate: true,
+
+ /**
+ * @property {Boolean} dragging
+ * True while the thumb is in a drag operation
+ */
+ dragging: false,
+
+ /**
+ * @cfg {Boolean} constrainThumbs
+ * True to disallow thumbs from overlapping one another.
+ */
+ constrainThumbs: true,
+
+ componentLayout: 'sliderfield',
+
+ /**
+ * @cfg {Boolean} useTips
+ * True to use an Ext.slider.Tip to display tips for the value.
+ */
+ useTips : true,
+
+ /**
+ * @cfg {Function} tipText
+ * A function used to display custom text for the slider tip. Defaults to null, which will use the default on the
+ * plugin.
+ */
+ tipText : null,
+
+ ariaRole: 'slider',
+
+ // private override
+ initValue: function() {
+ var me = this,
+ extValue = Ext.value,
+ // Fallback for initial values: values config -> value config -> minValue config -> 0
+ values = extValue(me.values, [extValue(me.value, extValue(me.minValue, 0))]),
+ i = 0,
+ len = values.length;
+
+ // Store for use in dirty check
+ me.originalValue = values;
+
+ // Add a thumb for each value
+ for (; i < len; i++) {
+ me.addThumb(values[i]);
+ }
+ },
+
+ // private override
+ initComponent : function() {
+ var me = this,
+ tipPlug,
+ hasTip;
+
+ /**
+ * @property {Array} thumbs
+ * Array containing references to each thumb
+ */
+ me.thumbs = [];
+
+ me.keyIncrement = Math.max(me.increment, me.keyIncrement);
+
+ me.addEvents(
+ /**
+ * @event beforechange
+ * Fires before the slider value is changed. By returning false from an event handler, you can cancel the
+ * event and prevent the slider from changing.
+ * @param {Ext.slider.Multi} slider The slider
+ * @param {Number} newValue The new value which the slider is being changed to.
+ * @param {Number} oldValue The old value which the slider was previously.
+ */
+ 'beforechange',
+
+ /**
+ * @event change
+ * Fires when the slider value is changed.
+ * @param {Ext.slider.Multi} slider The slider
+ * @param {Number} newValue The new value which the slider has been changed to.
+ * @param {Ext.slider.Thumb} thumb The thumb that was changed
+ */
+ 'change',
+
+ /**
+ * @event changecomplete
+ * Fires when the slider value is changed by the user and any drag operations have completed.
+ * @param {Ext.slider.Multi} slider The slider
+ * @param {Number} newValue The new value which the slider has been changed to.
+ * @param {Ext.slider.Thumb} thumb The thumb that was changed
+ */
+ 'changecomplete',
+
+ /**
+ * @event dragstart
+ * Fires after a drag operation has started.
+ * @param {Ext.slider.Multi} slider The slider
+ * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
+ */
+ 'dragstart',
+
+ /**
+ * @event drag
+ * Fires continuously during the drag operation while the mouse is moving.
+ * @param {Ext.slider.Multi} slider The slider
+ * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
+ */
+ 'drag',
+
+ /**
+ * @event dragend
+ * Fires after the drag operation has completed.
+ * @param {Ext.slider.Multi} slider The slider
+ * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
+ */
+ 'dragend'
+ );
+
+ if (me.vertical) {
+ Ext.apply(me, Ext.slider.Multi.Vertical);
+ }
+
+ me.callParent();
+
+ // only can use it if it exists.
+ if (me.useTips) {
+ tipPlug = me.tipText ? {getText: me.tipText} : {};
+ me.plugins = me.plugins || [];
+ Ext.each(me.plugins, function(plug){
+ if (plug.isSliderTip) {
+ hasTip = true;
+ return false;
+ }
+ });
+ if (!hasTip) {
+ me.plugins.push(Ext.create('Ext.slider.Tip', tipPlug));
+ }
+ }
+ },
+
+ /**
+ * Creates a new thumb and adds it to the slider
+ * @param {Number} value The initial value to set on the thumb. Defaults to 0
+ * @return {Ext.slider.Thumb} The thumb
+ */
+ addThumb: function(value) {
+ var me = this,
+ thumb = Ext.create('Ext.slider.Thumb', {
+ value : value,
+ slider : me,
+ index : me.thumbs.length,
+ constrain: me.constrainThumbs
+ });
+ me.thumbs.push(thumb);
+
+ //render the thumb now if needed
+ if (me.rendered) {
+ thumb.render();
+ }
+
+ return thumb;
+ },
+
+ /**
+ * @private
+ * Moves the given thumb above all other by increasing its z-index. This is called when as drag
+ * any thumb, so that the thumb that was just dragged is always at the highest z-index. This is
+ * required when the thumbs are stacked on top of each other at one of the ends of the slider's
+ * range, which can result in the user not being able to move any of them.
+ * @param {Ext.slider.Thumb} topThumb The thumb to move to the top
+ */
+ promoteThumb: function(topThumb) {
+ var thumbs = this.thumbs,
+ ln = thumbs.length,
+ zIndex, thumb, i;
+
+ for (i = 0; i < ln; i++) {
+ thumb = thumbs[i];
+
+ if (thumb == topThumb) {
+ thumb.bringToFront();
+ } else {
+ thumb.sendToBack();
+ }
+ }
+ },
+
+ // private override
+ onRender : function() {
+ var me = this,
+ i = 0,
+ thumbs = me.thumbs,
+ len = thumbs.length,
+ thumb;
+
+ Ext.applyIf(me.subTplData, {
+ vertical: me.vertical ? Ext.baseCSSPrefix + 'slider-vert' : Ext.baseCSSPrefix + 'slider-horz',
+ minValue: me.minValue,
+ maxValue: me.maxValue,
+ value: me.value
+ });
+
+ me.addChildEls('endEl', 'innerEl', 'focusEl');
+
+ me.callParent(arguments);
+
+ //render each thumb
+ for (; i < len; i++) {
+ thumbs[i].render();
+ }
+
+ //calculate the size of half a thumb
+ thumb = me.innerEl.down('.' + Ext.baseCSSPrefix + 'slider-thumb');
+ me.halfThumb = (me.vertical ? thumb.getHeight() : thumb.getWidth()) / 2;
+
+ },
+
+ /**
+ * Utility method to set the value of the field when the slider changes.
+ * @param {Object} slider The slider object.
+ * @param {Object} v The new value.
+ * @private
+ */
+ onChange : function(slider, v) {
+ this.setValue(v, undefined, true);
+ },
+
+ /**
+ * @private
+ * Adds keyboard and mouse listeners on this.el. Ignores click events on the internal focus element.
+ */
+ initEvents : function() {
+ var me = this;
+
+ me.mon(me.el, {
+ scope : me,
+ mousedown: me.onMouseDown,
+ keydown : me.onKeyDown,
+ change : me.onChange
+ });
+
+ me.focusEl.swallowEvent("click", true);
+ },
+
+ /**
+ * @private
+ * Mousedown handler for the slider. If the clickToChange is enabled and the click was not on the draggable 'thumb',
+ * this calculates the new value of the slider and tells the implementation (Horizontal or Vertical) to move the thumb
+ * @param {Ext.EventObject} e The click event
+ */
+ onMouseDown : function(e) {
+ var me = this,
+ thumbClicked = false,
+ i = 0,
+ thumbs = me.thumbs,
+ len = thumbs.length,
+ local;
+
+ if (me.disabled) {
+ return;
+ }
+
+ //see if the click was on any of the thumbs
+ for (; i < len; i++) {
+ thumbClicked = thumbClicked || e.target == thumbs[i].el.dom;
+ }
+
+ if (me.clickToChange && !thumbClicked) {
+ local = me.innerEl.translatePoints(e.getXY());
+ me.onClickChange(local);
+ }
+ me.focus();
+ },
+
+ /**
+ * @private
+ * Moves the thumb to the indicated position. Note that a Vertical implementation is provided in Ext.slider.Multi.Vertical.
+ * Only changes the value if the click was within this.clickRange.
+ * @param {Object} local Object containing top and left values for the click event.
+ */
+ onClickChange : function(local) {
+ var me = this,
+ thumb, index;
+
+ if (local.top > me.clickRange[0] && local.top < me.clickRange[1]) {
+ //find the nearest thumb to the click event
+ thumb = me.getNearest(local, 'left');
+ if (!thumb.disabled) {
+ index = thumb.index;
+ me.setValue(index, Ext.util.Format.round(me.reverseValue(local.left), me.decimalPrecision), undefined, true);
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Returns the nearest thumb to a click event, along with its distance
+ * @param {Object} local Object containing top and left values from a click event
+ * @param {String} prop The property of local to compare on. Use 'left' for horizontal sliders, 'top' for vertical ones
+ * @return {Object} The closest thumb object and its distance from the click event
+ */
+ getNearest: function(local, prop) {
+ var me = this,
+ localValue = prop == 'top' ? me.innerEl.getHeight() - local[prop] : local[prop],
+ clickValue = me.reverseValue(localValue),
+ nearestDistance = (me.maxValue - me.minValue) + 5, //add a small fudge for the end of the slider
+ index = 0,
+ nearest = null,
+ thumbs = me.thumbs,
+ i = 0,
+ len = thumbs.length,
+ thumb,
+ value,
+ dist;
+
+ for (; i < len; i++) {
+ thumb = me.thumbs[i];
+ value = thumb.value;
+ dist = Math.abs(value - clickValue);
+
+ if (Math.abs(dist <= nearestDistance)) {
+ nearest = thumb;
+ index = i;
+ nearestDistance = dist;
+ }
+ }
+ return nearest;
+ },
+
+ /**
+ * @private
+ * Handler for any keypresses captured by the slider. If the key is UP or RIGHT, the thumb is moved along to the right
+ * by this.keyIncrement. If DOWN or LEFT it is moved left. Pressing CTRL moves the slider to the end in either direction
+ * @param {Ext.EventObject} e The Event object
+ */
+ onKeyDown : function(e) {
+ /*
+ * The behaviour for keyboard handling with multiple thumbs is currently undefined.
+ * There's no real sane default for it, so leave it like this until we come up
+ * with a better way of doing it.
+ */
+ var me = this,
+ k,
+ val;
+
+ if(me.disabled || me.thumbs.length !== 1) {
+ e.preventDefault();
+ return;
+ }
+ k = e.getKey();
+
+ switch(k) {
+ case e.UP:
+ case e.RIGHT:
+ e.stopEvent();
+ val = e.ctrlKey ? me.maxValue : me.getValue(0) + me.keyIncrement;
+ me.setValue(0, val, undefined, true);
+ break;
+ case e.DOWN:
+ case e.LEFT:
+ e.stopEvent();
+ val = e.ctrlKey ? me.minValue : me.getValue(0) - me.keyIncrement;
+ me.setValue(0, val, undefined, true);
+ break;
+ default:
+ e.preventDefault();
+ }
+ },
+
+ // private
+ afterRender : function() {
+ var me = this,
+ i = 0,
+ thumbs = me.thumbs,
+ len = thumbs.length,
+ thumb,
+ v;
+
+ me.callParent(arguments);
+
+ for (; i < len; i++) {
+ thumb = thumbs[i];
+
+ if (thumb.value !== undefined) {
+ v = me.normalizeValue(thumb.value);
+ if (v !== thumb.value) {
+ // delete this.value;
+ me.setValue(i, v, false);
+ } else {
+ thumb.move(me.translateValue(v), false);
+ }
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Returns the ratio of pixels to mapped values. e.g. if the slider is 200px wide and maxValue - minValue is 100,
+ * the ratio is 2
+ * @return {Number} The ratio of pixels to mapped values
+ */
+ getRatio : function() {
+ var w = this.innerEl.getWidth(),
+ v = this.maxValue - this.minValue;
+ return v === 0 ? w : (w/v);
+ },
+
+ /**
+ * @private
+ * Returns a snapped, constrained value when given a desired value
+ * @param {Number} value Raw number value
+ * @return {Number} The raw value rounded to the correct d.p. and constrained within the set max and min values
+ */
+ normalizeValue : function(v) {
+ var me = this;
+
+ v = Ext.Number.snap(v, this.increment, this.minValue, this.maxValue);
+ v = Ext.util.Format.round(v, me.decimalPrecision);
+ v = Ext.Number.constrain(v, me.minValue, me.maxValue);
+ return v;
+ },
+
+ /**
+ * Sets the minimum value for the slider instance. If the current value is less than the minimum value, the current
+ * value will be changed.
+ * @param {Number} val The new minimum value
+ */
+ setMinValue : function(val) {
+ var me = this,
+ i = 0,
+ thumbs = me.thumbs,
+ len = thumbs.length,
+ t;
+
+ me.minValue = val;
+ if (me.rendered) {
+ me.inputEl.dom.setAttribute('aria-valuemin', val);
+ }
+
+ for (; i < len; ++i) {
+ t = thumbs[i];
+ t.value = t.value < val ? val : t.value;
+ }
+ me.syncThumbs();
+ },
+
+ /**
+ * Sets the maximum value for the slider instance. If the current value is more than the maximum value, the current
+ * value will be changed.
+ * @param {Number} val The new maximum value
+ */
+ setMaxValue : function(val) {
+ var me = this,
+ i = 0,
+ thumbs = me.thumbs,
+ len = thumbs.length,
+ t;
+
+ me.maxValue = val;
+ if (me.rendered) {
+ me.inputEl.dom.setAttribute('aria-valuemax', val);
+ }
+
+ for (; i < len; ++i) {
+ t = thumbs[i];
+ t.value = t.value > val ? val : t.value;
+ }
+ me.syncThumbs();
+ },
+
+ /**
+ * Programmatically sets the value of the Slider. Ensures that the value is constrained within the minValue and
+ * maxValue.
+ * @param {Number} index Index of the thumb to move
+ * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue)
+ * @param {Boolean} [animate=true] Turn on or off animation
+ */
+ setValue : function(index, value, animate, changeComplete) {
+ var me = this,
+ thumb = me.thumbs[index];
+
+ // ensures value is contstrained and snapped
+ value = me.normalizeValue(value);
+
+ if (value !== thumb.value && me.fireEvent('beforechange', me, value, thumb.value, thumb) !== false) {
+ thumb.value = value;
+ if (me.rendered) {
+ // TODO this only handles a single value; need a solution for exposing multiple values to aria.
+ // Perhaps this should go on each thumb element rather than the outer element.
+ me.inputEl.set({
+ 'aria-valuenow': value,
+ 'aria-valuetext': value
+ });
+
+ thumb.move(me.translateValue(value), Ext.isDefined(animate) ? animate !== false : me.animate);
+
+ me.fireEvent('change', me, value, thumb);
+ if (changeComplete) {
+ me.fireEvent('changecomplete', me, value, thumb);
+ }
+ }
+ }
+ },
+
+ /**
+ * @private
+ */
+ translateValue : function(v) {
+ var ratio = this.getRatio();
+ return (v * ratio) - (this.minValue * ratio) - this.halfThumb;
+ },
+
+ /**
+ * @private
+ * Given a pixel location along the slider, returns the mapped slider value for that pixel.
+ * E.g. if we have a slider 200px wide with minValue = 100 and maxValue = 500, reverseValue(50)
+ * returns 200
+ * @param {Number} pos The position along the slider to return a mapped value for
+ * @return {Number} The mapped value for the given position
+ */
+ reverseValue : function(pos) {
+ var ratio = this.getRatio();
+ return (pos + (this.minValue * ratio)) / ratio;
+ },
+
+ // private
+ focus : function() {
+ this.focusEl.focus(10);
+ },
+
+ //private
+ onDisable: function() {
+ var me = this,
+ i = 0,
+ thumbs = me.thumbs,
+ len = thumbs.length,
+ thumb,
+ el,
+ xy;
+
+ me.callParent();
+
+ for (; i < len; i++) {
+ thumb = thumbs[i];
+ el = thumb.el;
+
+ thumb.disable();
+
+ if(Ext.isIE) {
+ //IE breaks when using overflow visible and opacity other than 1.
+ //Create a place holder for the thumb and display it.
+ xy = el.getXY();
+ el.hide();
+
+ me.innerEl.addCls(me.disabledCls).dom.disabled = true;
+
+ if (!me.thumbHolder) {
+ me.thumbHolder = me.endEl.createChild({cls: Ext.baseCSSPrefix + 'slider-thumb ' + me.disabledCls});
+ }
+
+ me.thumbHolder.show().setXY(xy);
+ }
+ }
+ },
+
+ //private
+ onEnable: function() {
+ var me = this,
+ i = 0,
+ thumbs = me.thumbs,
+ len = thumbs.length,
+ thumb,
+ el;
+
+ this.callParent();
+
+ for (; i < len; i++) {
+ thumb = thumbs[i];
+ el = thumb.el;
+
+ thumb.enable();
+
+ if (Ext.isIE) {
+ me.innerEl.removeCls(me.disabledCls).dom.disabled = false;
+
+ if (me.thumbHolder) {
+ me.thumbHolder.hide();
+ }
+
+ el.show();
+ me.syncThumbs();
+ }
+ }
+ },
+
+ /**
+ * Synchronizes thumbs position to the proper proportion of the total component width based on the current slider
+ * {@link #value}. This will be called automatically when the Slider is resized by a layout, but if it is rendered
+ * auto width, this method can be called from another resize handler to sync the Slider if necessary.
+ */
+ syncThumbs : function() {
+ if (this.rendered) {
+ var thumbs = this.thumbs,
+ length = thumbs.length,
+ i = 0;
+
+ for (; i < length; i++) {
+ thumbs[i].move(this.translateValue(thumbs[i].value));
+ }
+ }
+ },
+
+ /**
+ * Returns the current value of the slider
+ * @param {Number} index The index of the thumb to return a value for
+ * @return {Number/Number[]} The current value of the slider at the given index, or an array of all thumb values if
+ * no index is given.
+ */
+ getValue : function(index) {
+ return Ext.isNumber(index) ? this.thumbs[index].value : this.getValues();
+ },
+
+ /**
+ * Returns an array of values - one for the location of each thumb
+ * @return {Number[]} The set of thumb values
+ */
+ getValues: function() {
+ var values = [],
+ i = 0,
+ thumbs = this.thumbs,
+ len = thumbs.length;
+
+ for (; i < len; i++) {
+ values.push(thumbs[i].value);
+ }
+
+ return values;
+ },
+
+ getSubmitValue: function() {
+ var me = this;
+ return (me.disabled || !me.submitValue) ? null : me.getValue();
+ },
+
+ reset: function() {
+ var me = this,
+ Array = Ext.Array;
+ Array.forEach(Array.from(me.originalValue), function(val, i) {
+ me.setValue(i, val);
+ });
+ me.clearInvalid();
+ // delete here so we reset back to the original state
+ delete me.wasValid;
+ },
+
+ // private
+ beforeDestroy : function() {
+ var me = this;
+
+ Ext.destroy(me.innerEl, me.endEl, me.focusEl);
+ Ext.each(me.thumbs, function(thumb) {
+ Ext.destroy(thumb);
+ }, me);
+
+ me.callParent();
+ },
+
+ statics: {
+ // Method overrides to support slider with vertical orientation
+ Vertical: {
+ getRatio : function() {
+ var h = this.innerEl.getHeight(),
+ v = this.maxValue - this.minValue;
+ return h/v;
+ },
+
+ onClickChange : function(local) {
+ var me = this,
+ thumb, index, bottom;
+
+ if (local.left > me.clickRange[0] && local.left < me.clickRange[1]) {
+ thumb = me.getNearest(local, 'top');
+ if (!thumb.disabled) {
+ index = thumb.index;
+ bottom = me.reverseValue(me.innerEl.getHeight() - local.top);
+
+ me.setValue(index, Ext.util.Format.round(me.minValue + bottom, me.decimalPrecision), undefined, true);
+ }
+ }
+ }
+ }
+ }
+});
+
+/**
+ * Slider which supports vertical or horizontal orientation, keyboard adjustments, configurable snapping, axis clicking
+ * and animation. Can be added as an item to any container.
+ *
+ * @example
+ * Ext.create('Ext.slider.Single', {
+ * width: 200,
+ * value: 50,
+ * increment: 10,
+ * minValue: 0,
+ * maxValue: 100,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * The class Ext.slider.Single is aliased to Ext.Slider for backwards compatibility.
+ */
+Ext.define('Ext.slider.Single', {
+ extend: 'Ext.slider.Multi',
+ alias: ['widget.slider', 'widget.sliderfield'],
+ alternateClassName: ['Ext.Slider', 'Ext.form.SliderField', 'Ext.slider.SingleSlider', 'Ext.slider.Slider'],
+
+ /**
+ * Returns the current value of the slider
+ * @return {Number} The current value of the slider
+ */
+ getValue: function() {
+ // just returns the value of the first thumb, which should be the only one in a single slider
+ return this.callParent([0]);
+ },
+
+ /**
+ * Programmatically sets the value of the Slider. Ensures that the value is constrained within the minValue and
+ * maxValue.
+ * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue)
+ * @param {Boolean} [animate] Turn on or off animation
+ */
+ setValue: function(value, animate) {
+ var args = Ext.toArray(arguments),
+ len = args.length;
+
+ // this is to maintain backwards compatiblity for sliders with only one thunb. Usually you must pass the thumb
+ // index to setValue, but if we only have one thumb we inject the index here first if given the multi-slider
+ // signature without the required index. The index will always be 0 for a single slider
+ if (len == 1 || (len <= 3 && typeof arguments[1] != 'number')) {
+ args.unshift(0);
+ }
+
+ return this.callParent(args);
+ },
+
+ // private
+ getNearest : function(){
+ // Since there's only 1 thumb, it's always the nearest
+ return this.thumbs[0];
+ }
+});
+
+/**
+ * @author Ed Spencer
+ * @class Ext.tab.Tab
+ * @extends Ext.button.Button
+ *
+ * <p>Represents a single Tab in a {@link Ext.tab.Panel TabPanel}. A Tab is simply a slightly customized {@link Ext.button.Button Button},
+ * styled to look like a tab. Tabs are optionally closable, and can also be disabled. Typically you will not
+ * need to create Tabs manually as the framework does so automatically when you use a {@link Ext.tab.Panel TabPanel}</p>
+ */
+Ext.define('Ext.tab.Tab', {
+ extend: 'Ext.button.Button',
+ alias: 'widget.tab',
+
+ requires: [
+ 'Ext.layout.component.Tab',
+ 'Ext.util.KeyNav'
+ ],
+
+ componentLayout: 'tab',
+
+ isTab: true,
+
+ baseCls: Ext.baseCSSPrefix + 'tab',
+
+ /**
+ * @cfg {String} activeCls
+ * The CSS class to be applied to a Tab when it is active.
+ * Providing your own CSS for this class enables you to customize the active state.
+ */
+ activeCls: 'active',
+
+ /**
+ * @cfg {String} disabledCls
+ * The CSS class to be applied to a Tab when it is disabled.
+ */
+
+ /**
+ * @cfg {String} closableCls
+ * The CSS class which is added to the tab when it is closable
+ */
+ closableCls: 'closable',
+
+ /**
+ * @cfg {Boolean} closable True to make the Tab start closable (the close icon will be visible).
+ */
+ closable: true,
+
+ /**
+ * @cfg {String} closeText
+ * The accessible text label for the close button link; only used when {@link #closable} = true.
+ */
+ closeText: 'Close Tab',
+
+ /**
+ * @property {Boolean} active
+ * Read-only property indicating that this tab is currently active. This is NOT a public configuration.
+ */
+ active: false,
+
+ /**
+ * @property closable
+ * @type Boolean
+ * True if the tab is currently closable
+ */
+
+ scale: false,
+
+ position: 'top',
+
+ initComponent: function() {
+ var me = this;
+
+ me.addEvents(
+ /**
+ * @event activate
+ * Fired when the tab is activated.
+ * @param {Ext.tab.Tab} this
+ */
+ 'activate',
+
+ /**
+ * @event deactivate
+ * Fired when the tab is deactivated.
+ * @param {Ext.tab.Tab} this
+ */
+ 'deactivate',
+
+ /**
+ * @event beforeclose
+ * Fires if the user clicks on the Tab's close button, but before the {@link #close} event is fired. Return
+ * false from any listener to stop the close event being fired
+ * @param {Ext.tab.Tab} tab The Tab object
+ */
+ 'beforeclose',
+
+ /**
+ * @event close
+ * Fires to indicate that the tab is to be closed, usually because the user has clicked the close button.
+ * @param {Ext.tab.Tab} tab The Tab object
+ */
+ 'close'
+ );
+
+ me.callParent(arguments);
+
+ if (me.card) {
+ me.setCard(me.card);
+ }
+ },
+
+ /**
+ * @ignore
+ */
+ onRender: function() {
+ var me = this,
+ tabBar = me.up('tabbar'),
+ tabPanel = me.up('tabpanel');
+
+ me.addClsWithUI(me.position);
+
+ // Set all the state classNames, as they need to include the UI
+ // me.disabledCls = me.getClsWithUIs('disabled');
+
+ me.syncClosableUI();
+
+ // Propagate minTabWidth and maxTabWidth settings from the owning TabBar then TabPanel
+ if (!me.minWidth) {
+ me.minWidth = (tabBar) ? tabBar.minTabWidth : me.minWidth;
+ if (!me.minWidth && tabPanel) {
+ me.minWidth = tabPanel.minTabWidth;
+ }
+ if (me.minWidth && me.iconCls) {
+ me.minWidth += 25;
+ }
+ }
+ if (!me.maxWidth) {
+ me.maxWidth = (tabBar) ? tabBar.maxTabWidth : me.maxWidth;
+ if (!me.maxWidth && tabPanel) {
+ me.maxWidth = tabPanel.maxTabWidth;
+ }
+ }
+
+ me.callParent(arguments);
+
+ if (me.active) {
+ me.activate(true);
+ }
+
+ me.syncClosableElements();
+
+ me.keyNav = Ext.create('Ext.util.KeyNav', me.el, {
+ enter: me.onEnterKey,
+ del: me.onDeleteKey,
+ scope: me
+ });
+ },
+
+ // inherit docs
+ enable : function(silent) {
+ var me = this;
+
+ me.callParent(arguments);
+
+ me.removeClsWithUI(me.position + '-disabled');
+
+ return me;
+ },
+
+ // inherit docs
+ disable : function(silent) {
+ var me = this;
+
+ me.callParent(arguments);
+
+ me.addClsWithUI(me.position + '-disabled');
+
+ return me;
+ },
+
+ /**
+ * @ignore
+ */
+ onDestroy: function() {
+ var me = this;
+
+ if (me.closeEl) {
+ me.closeEl.un('click', Ext.EventManager.preventDefault);
+ me.closeEl = null;
+ }
+
+ Ext.destroy(me.keyNav);
+ delete me.keyNav;
+
+ me.callParent(arguments);
+ },
+
+ /**
+ * Sets the tab as either closable or not
+ * @param {Boolean} closable Pass false to make the tab not closable. Otherwise the tab will be made closable (eg a
+ * close button will appear on the tab)
+ */
+ setClosable: function(closable) {
+ var me = this;
+
+ // Closable must be true if no args
+ closable = (!arguments.length || !!closable);
+
+ if (me.closable != closable) {
+ me.closable = closable;
+
+ // set property on the user-facing item ('card'):
+ if (me.card) {
+ me.card.closable = closable;
+ }
+
+ me.syncClosableUI();
+
+ if (me.rendered) {
+ me.syncClosableElements();
+
+ // Tab will change width to accommodate close icon
+ me.doComponentLayout();
+ if (me.ownerCt) {
+ me.ownerCt.doLayout();
+ }
+ }
+ }
+ },
+
+ /**
+ * This method ensures that the closeBtn element exists or not based on 'closable'.
+ * @private
+ */
+ syncClosableElements: function () {
+ var me = this;
+
+ if (me.closable) {
+ if (!me.closeEl) {
+ me.closeEl = me.el.createChild({
+ tag: 'a',
+ cls: me.baseCls + '-close-btn',
+ href: '#',
+ // html: me.closeText, // removed for EXTJSIV-1719, by rob@sencha.com
+ title: me.closeText
+ }).on('click', Ext.EventManager.preventDefault); // mon ???
+ }
+ } else {
+ var closeEl = me.closeEl;
+ if (closeEl) {
+ closeEl.un('click', Ext.EventManager.preventDefault);
+ closeEl.remove();
+ me.closeEl = null;
+ }
+ }
+ },
+
+ /**
+ * This method ensures that the UI classes are added or removed based on 'closable'.
+ * @private
+ */
+ syncClosableUI: function () {
+ var me = this, classes = [me.closableCls, me.closableCls + '-' + me.position];
+
+ if (me.closable) {
+ me.addClsWithUI(classes);
+ } else {
+ me.removeClsWithUI(classes);
+ }
+ },
+
+ /**
+ * Sets this tab's attached card. Usually this is handled automatically by the {@link Ext.tab.Panel} that this Tab
+ * belongs to and would not need to be done by the developer
+ * @param {Ext.Component} card The card to set
+ */
+ setCard: function(card) {
+ var me = this;
+
+ me.card = card;
+ me.setText(me.title || card.title);
+ me.setIconCls(me.iconCls || card.iconCls);
+ },
+
+ /**
+ * @private
+ * Listener attached to click events on the Tab's close button
+ */
+ onCloseClick: function() {
+ var me = this;
+
+ if (me.fireEvent('beforeclose', me) !== false) {
+ if (me.tabBar) {
+ if (me.tabBar.closeTab(me) === false) {
+ // beforeclose on the panel vetoed the event, stop here
+ return;
+ }
+ } else {
+ // if there's no tabbar, fire the close event
+ me.fireEvent('close', me);
+ }
+ }
+ },
+
+ /**
+ * Fires the close event on the tab.
+ * @private
+ */
+ fireClose: function(){
+ this.fireEvent('close', this);
+ },
+
+ /**
+ * @private
+ */
+ onEnterKey: function(e) {
+ var me = this;
+
+ if (me.tabBar) {
+ me.tabBar.onClick(e, me.el);
+ }
+ },
+
+ /**
+ * @private
+ */
+ onDeleteKey: function(e) {
+ var me = this;
+
+ if (me.closable) {
+ me.onCloseClick();
+ }
+ },
+
+ // @private
+ activate : function(supressEvent) {
+ var me = this;
+
+ me.active = true;
+ me.addClsWithUI([me.activeCls, me.position + '-' + me.activeCls]);
+
+ if (supressEvent !== true) {
+ me.fireEvent('activate', me);
+ }
+ },
+
+ // @private
+ deactivate : function(supressEvent) {
+ var me = this;
+
+ me.active = false;
+ me.removeClsWithUI([me.activeCls, me.position + '-' + me.activeCls]);
+
+ if (supressEvent !== true) {
+ me.fireEvent('deactivate', me);
+ }
+ }
+});
+
+/**
+ * @author Ed Spencer
+ * TabBar is used internally by a {@link Ext.tab.Panel TabPanel} and typically should not need to be created manually.
+ * The tab bar automatically removes the default title provided by {@link Ext.panel.Header}
+ */
+Ext.define('Ext.tab.Bar', {
+ extend: 'Ext.panel.Header',
+ alias: 'widget.tabbar',
+ baseCls: Ext.baseCSSPrefix + 'tab-bar',
+
+ requires: [
+ 'Ext.tab.Tab',
+ 'Ext.FocusManager'
+ ],
+
+ isTabBar: true,
+
+ /**
+ * @cfg {String} title @hide
+ */
+
+ /**
+ * @cfg {String} iconCls @hide
+ */
+
+ // @private
+ defaultType: 'tab',
+
+ /**
+ * @cfg {Boolean} plain
+ * True to not show the full background on the tabbar
+ */
+ plain: false,
+
+ // @private
+ renderTpl: [
+ '<div id="{id}-body" class="{baseCls}-body <tpl if="bodyCls"> {bodyCls}</tpl> <tpl if="ui"> {baseCls}-body-{ui}<tpl for="uiCls"> {parent.baseCls}-body-{parent.ui}-{.}</tpl></tpl>"<tpl if="bodyStyle"> style="{bodyStyle}"</tpl>></div>',
+ '<div id="{id}-strip" class="{baseCls}-strip<tpl if="ui"> {baseCls}-strip-{ui}<tpl for="uiCls"> {parent.baseCls}-strip-{parent.ui}-{.}</tpl></tpl>"></div>'
+ ],
+
+ /**
+ * @cfg {Number} minTabWidth
+ * The minimum width for a tab in this tab Bar. Defaults to the tab Panel's {@link Ext.tab.Panel#minTabWidth minTabWidth} value.
+ * @deprecated This config is deprecated. It is much easier to use the {@link Ext.tab.Panel#minTabWidth minTabWidth} config on the TabPanel.
+ */
+
+ /**
+ * @cfg {Number} maxTabWidth
+ * The maximum width for a tab in this tab Bar. Defaults to the tab Panel's {@link Ext.tab.Panel#maxTabWidth maxTabWidth} value.
+ * @deprecated This config is deprecated. It is much easier to use the {@link Ext.tab.Panel#maxTabWidth maxTabWidth} config on the TabPanel.
+ */
+
+ // @private
+ initComponent: function() {
+ var me = this,
+ keys;
+
+ if (me.plain) {
+ me.setUI(me.ui + '-plain');
+ }
+
+ me.addClsWithUI(me.dock);
+
+ me.addEvents(
+ /**
+ * @event change
+ * Fired when the currently-active tab has changed
+ * @param {Ext.tab.Bar} tabBar The TabBar
+ * @param {Ext.tab.Tab} tab The new Tab
+ * @param {Ext.Component} card The card that was just shown in the TabPanel
+ */
+ 'change'
+ );
+
+ me.addChildEls('body', 'strip');
+ me.callParent(arguments);
+
+ // TabBar must override the Header's align setting.
+ me.layout.align = (me.orientation == 'vertical') ? 'left' : 'top';
+ me.layout.overflowHandler = Ext.create('Ext.layout.container.boxOverflow.Scroller', me.layout);
+
+ me.remove(me.titleCmp);
+ delete me.titleCmp;
+
+ // Subscribe to Ext.FocusManager for key navigation
+ keys = me.orientation == 'vertical' ? ['up', 'down'] : ['left', 'right'];
+ Ext.FocusManager.subscribe(me, {
+ keys: keys
+ });
+
+ Ext.apply(me.renderData, {
+ bodyCls: me.bodyCls
+ });
+ },
+
+ // @private
+ onAdd: function(tab) {
+ tab.position = this.dock;
+ this.callParent(arguments);
+ },
+
+ onRemove: function(tab) {
+ var me = this;
+
+ if (tab === me.previousTab) {
+ me.previousTab = null;
+ }
+ if (me.items.getCount() === 0) {
+ me.activeTab = null;
+ }
+ me.callParent(arguments);
+ },
+
+ // @private
+ afterRender: function() {
+ var me = this;
+
+ me.mon(me.el, {
+ scope: me,
+ click: me.onClick,
+ delegate: '.' + Ext.baseCSSPrefix + 'tab'
+ });
+ me.callParent(arguments);
+
+ },
+
+ afterComponentLayout : function() {
+ var me = this;
+
+ me.callParent(arguments);
+ me.strip.setWidth(me.el.getWidth());
+ },
+
+ // @private
+ onClick: function(e, target) {
+ // The target might not be a valid tab el.
+ var tab = Ext.getCmp(target.id),
+ tabPanel = this.tabPanel;
+
+ target = e.getTarget();
+
+ if (tab && tab.isDisabled && !tab.isDisabled()) {
+ if (tab.closable && target === tab.closeEl.dom) {
+ tab.onCloseClick();
+ } else {
+ if (tabPanel) {
+ // TabPanel will card setActiveTab of the TabBar
+ tabPanel.setActiveTab(tab.card);
+ } else {
+ this.setActiveTab(tab);
+ }
+ tab.focus();
+ }
+ }
+ },
+
+ /**
+ * @private
+ * Closes the given tab by removing it from the TabBar and removing the corresponding card from the TabPanel
+ * @param {Ext.tab.Tab} tab The tab to close
+ */
+ closeTab: function(tab) {
+ var me = this,
+ card = tab.card,
+ tabPanel = me.tabPanel,
+ nextTab;
+
+ if (card && card.fireEvent('beforeclose', card) === false) {
+ return false;
+ }
+
+ if (tab.active && me.items.getCount() > 1) {
+ nextTab = me.previousTab || tab.next('tab') || me.items.first();
+ me.setActiveTab(nextTab);
+ if (tabPanel) {
+ tabPanel.setActiveTab(nextTab.card);
+ }
+ }
+ /*
+ * force the close event to fire. By the time this function returns,
+ * the tab is already destroyed and all listeners have been purged
+ * so the tab can't fire itself.
+ */
+ tab.fireClose();
+ me.remove(tab);
+
+ if (tabPanel && card) {
+ card.fireEvent('close', card);
+ tabPanel.remove(card);
+ }
+
+ if (nextTab) {
+ nextTab.focus();
+ }
+ },
+
+ /**
+ * @private
+ * Marks the given tab as active
+ * @param {Ext.tab.Tab} tab The tab to mark active
+ */
+ setActiveTab: function(tab) {
+ if (tab.disabled) {
+ return;
+ }
+ var me = this;
+ if (me.activeTab) {
+ me.previousTab = me.activeTab;
+ me.activeTab.deactivate();
+ }
+ tab.activate();
+
+ if (me.rendered) {
+ me.layout.layout();
+ tab.el && tab.el.scrollIntoView(me.layout.getRenderTarget());
+ }
+ me.activeTab = tab;
+ me.fireEvent('change', me, tab, tab.card);
+ }
+});
+
+/**
+ * @author Ed Spencer, Tommy Maintz, Brian Moeskau
+ *
+ * A basic tab container. TabPanels can be used exactly like a standard {@link Ext.panel.Panel} for
+ * layout purposes, but also have special support for containing child Components
+ * (`{@link Ext.container.Container#items items}`) that are managed using a
+ * {@link Ext.layout.container.Card CardLayout layout manager}, and displayed as separate tabs.
+ *
+ * **Note:** By default, a tab's close tool _destroys_ the child tab Component and all its descendants.
+ * This makes the child tab Component, and all its descendants **unusable**. To enable re-use of a tab,
+ * configure the TabPanel with `{@link #autoDestroy autoDestroy: false}`.
+ *
+ * ## TabPanel's layout
+ *
+ * TabPanels use a Dock layout to position the {@link Ext.tab.Bar TabBar} at the top of the widget.
+ * Panels added to the TabPanel will have their header hidden by default because the Tab will
+ * automatically take the Panel's configured title and icon.
+ *
+ * TabPanels use their {@link Ext.panel.Header header} or {@link Ext.panel.Panel#fbar footer}
+ * element (depending on the {@link #tabPosition} configuration) to accommodate the tab selector buttons.
+ * This means that a TabPanel will not display any configured title, and will not display any configured
+ * header {@link Ext.panel.Panel#tools tools}.
+ *
+ * To display a header, embed the TabPanel in a {@link Ext.panel.Panel Panel} which uses
+ * `{@link Ext.container.Container#layout layout: 'fit'}`.
+ *
+ * ## Controlling tabs
+ *
+ * Configuration options for the {@link Ext.tab.Tab} that represents the component can be passed in
+ * by specifying the tabConfig option:
+ *
+ * @example
+ * Ext.create('Ext.tab.Panel', {
+ * width: 400,
+ * height: 400,
+ * renderTo: document.body,
+ * items: [{
+ * title: 'Foo'
+ * }, {
+ * title: 'Bar',
+ * tabConfig: {
+ * title: 'Custom Title',
+ * tooltip: 'A button tooltip'
+ * }
+ * }]
+ * });
+ *
+ * # Examples
+ *
+ * Here is a basic TabPanel rendered to the body. This also shows the useful configuration {@link #activeTab},
+ * which allows you to set the active tab on render. If you do not set an {@link #activeTab}, no tabs will be
+ * active by default.
+ *
+ * @example
+ * Ext.create('Ext.tab.Panel', {
+ * width: 300,
+ * height: 200,
+ * activeTab: 0,
+ * items: [
+ * {
+ * title: 'Tab 1',
+ * bodyPadding: 10,
+ * html : 'A simple tab'
+ * },
+ * {
+ * title: 'Tab 2',
+ * html : 'Another one'
+ * }
+ * ],
+ * renderTo : Ext.getBody()
+ * });
+ *
+ * It is easy to control the visibility of items in the tab bar. Specify hidden: true to have the
+ * tab button hidden initially. Items can be subsequently hidden and show by accessing the
+ * tab property on the child item.
+ *
+ * @example
+ * var tabs = Ext.create('Ext.tab.Panel', {
+ * width: 400,
+ * height: 400,
+ * renderTo: document.body,
+ * items: [{
+ * title: 'Home',
+ * html: 'Home',
+ * itemId: 'home'
+ * }, {
+ * title: 'Users',
+ * html: 'Users',
+ * itemId: 'users',
+ * hidden: true
+ * }, {
+ * title: 'Tickets',
+ * html: 'Tickets',
+ * itemId: 'tickets'
+ * }]
+ * });
+ *
+ * setTimeout(function(){
+ * tabs.child('#home').tab.hide();
+ * var users = tabs.child('#users');
+ * users.tab.show();
+ * tabs.setActiveTab(users);
+ * }, 1000);
+ *
+ * You can remove the background of the TabBar by setting the {@link #plain} property to `true`.
+ *
+ * @example
+ * Ext.create('Ext.tab.Panel', {
+ * width: 300,
+ * height: 200,
+ * activeTab: 0,
+ * plain: true,
+ * items: [
+ * {
+ * title: 'Tab 1',
+ * bodyPadding: 10,
+ * html : 'A simple tab'
+ * },
+ * {
+ * title: 'Tab 2',
+ * html : 'Another one'
+ * }
+ * ],
+ * renderTo : Ext.getBody()
+ * });
+ *
+ * Another useful configuration of TabPanel is {@link #tabPosition}. This allows you to change the
+ * position where the tabs are displayed. The available options for this are `'top'` (default) and
+ * `'bottom'`.
+ *
+ * @example
+ * Ext.create('Ext.tab.Panel', {
+ * width: 300,
+ * height: 200,
+ * activeTab: 0,
+ * bodyPadding: 10,
+ * tabPosition: 'bottom',
+ * items: [
+ * {
+ * title: 'Tab 1',
+ * html : 'A simple tab'
+ * },
+ * {
+ * title: 'Tab 2',
+ * html : 'Another one'
+ * }
+ * ],
+ * renderTo : Ext.getBody()
+ * });
+ *
+ * The {@link #setActiveTab} is a very useful method in TabPanel which will allow you to change the
+ * current active tab. You can either give it an index or an instance of a tab. For example:
+ *
+ * @example
+ * var tabs = Ext.create('Ext.tab.Panel', {
+ * items: [
+ * {
+ * id : 'my-tab',
+ * title: 'Tab 1',
+ * html : 'A simple tab'
+ * },
+ * {
+ * title: 'Tab 2',
+ * html : 'Another one'
+ * }
+ * ],
+ * renderTo : Ext.getBody()
+ * });
+ *
+ * var tab = Ext.getCmp('my-tab');
+ *
+ * Ext.create('Ext.button.Button', {
+ * renderTo: Ext.getBody(),
+ * text : 'Select the first tab',
+ * scope : this,
+ * handler : function() {
+ * tabs.setActiveTab(tab);
+ * }
+ * });
+ *
+ * Ext.create('Ext.button.Button', {
+ * text : 'Select the second tab',
+ * scope : this,
+ * handler : function() {
+ * tabs.setActiveTab(1);
+ * },
+ * renderTo : Ext.getBody()
+ * });
+ *
+ * The {@link #getActiveTab} is a another useful method in TabPanel which will return the current active tab.
+ *
+ * @example
+ * var tabs = Ext.create('Ext.tab.Panel', {
+ * items: [
+ * {
+ * title: 'Tab 1',
+ * html : 'A simple tab'
+ * },
+ * {
+ * title: 'Tab 2',
+ * html : 'Another one'
+ * }
+ * ],
+ * renderTo : Ext.getBody()
+ * });
+ *
+ * Ext.create('Ext.button.Button', {
+ * text : 'Get active tab',
+ * scope : this,
+ * handler : function() {
+ * var tab = tabs.getActiveTab();
+ * alert('Current tab: ' + tab.title);
+ * },
+ * renderTo : Ext.getBody()
+ * });
+ *
+ * Adding a new tab is very simple with a TabPanel. You simple call the {@link #add} method with an config
+ * object for a panel.
+ *
+ * @example
+ * var tabs = Ext.create('Ext.tab.Panel', {
+ * items: [
+ * {
+ * title: 'Tab 1',
+ * html : 'A simple tab'
+ * },
+ * {
+ * title: 'Tab 2',
+ * html : 'Another one'
+ * }
+ * ],
+ * renderTo : Ext.getBody()
+ * });
+ *
+ * Ext.create('Ext.button.Button', {
+ * text : 'New tab',
+ * scope : this,
+ * handler : function() {
+ * var tab = tabs.add({
+ * // we use the tabs.items property to get the length of current items/tabs
+ * title: 'Tab ' + (tabs.items.length + 1),
+ * html : 'Another one'
+ * });
+ *
+ * tabs.setActiveTab(tab);
+ * },
+ * renderTo : Ext.getBody()
+ * });
+ *
+ * Additionally, removing a tab is very also simple with a TabPanel. You simple call the {@link #remove} method
+ * with an config object for a panel.
+ *
+ * @example
+ * var tabs = Ext.create('Ext.tab.Panel', {
+ * items: [
+ * {
+ * title: 'Tab 1',
+ * html : 'A simple tab'
+ * },
+ * {
+ * id : 'remove-this-tab',
+ * title: 'Tab 2',
+ * html : 'Another one'
+ * }
+ * ],
+ * renderTo : Ext.getBody()
+ * });
+ *
+ * Ext.create('Ext.button.Button', {
+ * text : 'Remove tab',
+ * scope : this,
+ * handler : function() {
+ * var tab = Ext.getCmp('remove-this-tab');
+ * tabs.remove(tab);
+ * },
+ * renderTo : Ext.getBody()
+ * });
+ */
+Ext.define('Ext.tab.Panel', {
+ extend: 'Ext.panel.Panel',
+ alias: 'widget.tabpanel',
+ alternateClassName: ['Ext.TabPanel'],
+
+ requires: ['Ext.layout.container.Card', 'Ext.tab.Bar'],
+
+ /**
+ * @cfg {String} tabPosition
+ * The position where the tab strip should be rendered. Can be `top` or `bottom`.
+ */
+ tabPosition : 'top',
+
+ /**
+ * @cfg {String/Number} activeItem
+ * Doesn't apply for {@link Ext.tab.Panel TabPanel}, use {@link #activeTab} instead.
+ */
+
+ /**
+ * @cfg {String/Number/Ext.Component} activeTab
+ * The tab to activate initially. Either an ID, index or the tab component itself.
+ */
+
+ /**
+ * @cfg {Object} tabBar
+ * Optional configuration object for the internal {@link Ext.tab.Bar}.
+ * If present, this is passed straight through to the TabBar's constructor
+ */
+
+ /**
+ * @cfg {Object} layout
+ * Optional configuration object for the internal {@link Ext.layout.container.Card card layout}.
+ * If present, this is passed straight through to the layout's constructor
+ */
+
+ /**
+ * @cfg {Boolean} removePanelHeader
+ * True to instruct each Panel added to the TabContainer to not render its header element.
+ * This is to ensure that the title of the panel does not appear twice.
+ */
+ removePanelHeader: true,
+
+ /**
+ * @cfg {Boolean} plain
+ * True to not show the full background on the TabBar.
+ */
+ plain: false,
+
+ /**
+ * @cfg {String} itemCls
+ * The class added to each child item of this TabPanel.
+ */
+ itemCls: 'x-tabpanel-child',
+
+ /**
+ * @cfg {Number} minTabWidth
+ * The minimum width for a tab in the {@link #tabBar}.
+ */
+ minTabWidth: undefined,
+
+ /**
+ * @cfg {Number} maxTabWidth The maximum width for each tab.
+ */
+ maxTabWidth: undefined,
+
+ /**
+ * @cfg {Boolean} deferredRender
+ *
+ * True by default to defer the rendering of child {@link Ext.container.Container#items items} to the browsers DOM
+ * until a tab is activated. False will render all contained {@link Ext.container.Container#items items} as soon as
+ * the {@link Ext.layout.container.Card layout} is rendered. If there is a significant amount of content or a lot of
+ * heavy controls being rendered into panels that are not displayed by default, setting this to true might improve
+ * performance.
+ *
+ * The deferredRender property is internally passed to the layout manager for TabPanels ({@link
+ * Ext.layout.container.Card}) as its {@link Ext.layout.container.Card#deferredRender} configuration value.
+ *
+ * **Note**: leaving deferredRender as true means that the content within an unactivated tab will not be available
+ */
+ deferredRender : true,
+
+ //inherit docs
+ initComponent: function() {
+ var me = this,
+ dockedItems = me.dockedItems || [],
+ activeTab = me.activeTab || 0;
+
+ me.layout = Ext.create('Ext.layout.container.Card', Ext.apply({
+ owner: me,
+ deferredRender: me.deferredRender,
+ itemCls: me.itemCls
+ }, me.layout));
+
+ /**
+ * @property {Ext.tab.Bar} tabBar Internal reference to the docked TabBar
+ */
+ me.tabBar = Ext.create('Ext.tab.Bar', Ext.apply({}, me.tabBar, {
+ dock: me.tabPosition,
+ plain: me.plain,
+ border: me.border,
+ cardLayout: me.layout,
+ tabPanel: me
+ }));
+
+ if (dockedItems && !Ext.isArray(dockedItems)) {
+ dockedItems = [dockedItems];
+ }
+
+ dockedItems.push(me.tabBar);
+ me.dockedItems = dockedItems;
+
+ me.addEvents(
+ /**
+ * @event
+ * Fires before a tab change (activated by {@link #setActiveTab}). Return false in any listener to cancel
+ * the tabchange
+ * @param {Ext.tab.Panel} tabPanel The TabPanel
+ * @param {Ext.Component} newCard The card that is about to be activated
+ * @param {Ext.Component} oldCard The card that is currently active
+ */
+ 'beforetabchange',
+
+ /**
+ * @event
+ * Fires when a new tab has been activated (activated by {@link #setActiveTab}).
+ * @param {Ext.tab.Panel} tabPanel The TabPanel
+ * @param {Ext.Component} newCard The newly activated item
+ * @param {Ext.Component} oldCard The previously active item
+ */
+ 'tabchange'
+ );
+ me.callParent(arguments);
+
+ //set the active tab
+ me.setActiveTab(activeTab);
+ //set the active tab after initial layout
+ me.on('afterlayout', me.afterInitialLayout, me, {single: true});
+ },
+
+ /**
+ * @private
+ * We have to wait until after the initial layout to visually activate the activeTab (if set).
+ * The active tab has different margins than normal tabs, so if the initial layout happens with
+ * a tab active, its layout will be offset improperly due to the active margin style. Waiting
+ * until after the initial layout avoids this issue.
+ */
+ afterInitialLayout: function() {
+ var me = this,
+ card = me.getComponent(me.activeTab);
+
+ if (card) {
+ me.layout.setActiveItem(card);
+ }
+ },
+
+ /**
+ * Makes the given card active. Makes it the visible card in the TabPanel's CardLayout and highlights the Tab.
+ * @param {String/Number/Ext.Component} card The card to make active. Either an ID, index or the component itself.
+ */
+ setActiveTab: function(card) {
+ var me = this,
+ previous;
+
+ card = me.getComponent(card);
+ if (card) {
+ previous = me.getActiveTab();
+
+ if (previous && previous !== card && me.fireEvent('beforetabchange', me, card, previous) === false) {
+ return false;
+ }
+
+ me.tabBar.setActiveTab(card.tab);
+ me.activeTab = card;
+ if (me.rendered) {
+ me.layout.setActiveItem(card);
+ }
+
+ if (previous && previous !== card) {
+ me.fireEvent('tabchange', me, card, previous);
+ }
+ }
+ },
+
+ /**
+ * Returns the item that is currently active inside this TabPanel. Note that before the TabPanel first activates a
+ * child component this will return whatever was configured in the {@link #activeTab} config option
+ * @return {String/Number/Ext.Component} The currently active item
+ */
+ getActiveTab: function() {
+ return this.activeTab;
+ },
+
+ /**
+ * Returns the {@link Ext.tab.Bar} currently used in this TabPanel
+ * @return {Ext.tab.Bar} The TabBar
+ */
+ getTabBar: function() {
+ return this.tabBar;
+ },
+
+ /**
+ * @ignore
+ * Makes sure we have a Tab for each item added to the TabPanel
+ */
+ onAdd: function(item, index) {
+ var me = this,
+ cfg = item.tabConfig || {},
+ defaultConfig = {
+ xtype: 'tab',
+ card: item,
+ disabled: item.disabled,
+ closable: item.closable,
+ hidden: item.hidden,
+ tabBar: me.tabBar
+ };
+
+ if (item.closeText) {
+ defaultConfig.closeText = item.closeText;
+ }
+ cfg = Ext.applyIf(cfg, defaultConfig);
+ item.tab = me.tabBar.insert(index, cfg);
+
+ item.on({
+ scope : me,
+ enable: me.onItemEnable,
+ disable: me.onItemDisable,
+ beforeshow: me.onItemBeforeShow,
+ iconchange: me.onItemIconChange,
+ titlechange: me.onItemTitleChange
+ });
+
+ if (item.isPanel) {
+ if (me.removePanelHeader) {
+ item.preventHeader = true;
+ if (item.rendered) {
+ item.updateHeader();
+ }
+ }
+ if (item.isPanel && me.border) {
+ item.setBorder(false);
+ }
+ }
+
+ // ensure that there is at least one active tab
+ if (this.rendered && me.items.getCount() === 1) {
+ me.setActiveTab(0);
+ }
+ },
+
+ /**
+ * @private
+ * Enable corresponding tab when item is enabled.
+ */
+ onItemEnable: function(item){
+ item.tab.enable();
+ },
+
+ /**
+ * @private
+ * Disable corresponding tab when item is enabled.
+ */
+ onItemDisable: function(item){
+ item.tab.disable();
+ },
+
+ /**
+ * @private
+ * Sets activeTab before item is shown.
+ */
+ onItemBeforeShow: function(item) {
+ if (item !== this.activeTab) {
+ this.setActiveTab(item);
+ return false;
+ }
+ },
+
+ /**
+ * @private
+ * Update the tab iconCls when panel iconCls has been set or changed.
+ */
+ onItemIconChange: function(item, newIconCls) {
+ item.tab.setIconCls(newIconCls);
+ this.getTabBar().doLayout();
+ },
+
+ /**
+ * @private
+ * Update the tab title when panel title has been set or changed.
+ */
+ onItemTitleChange: function(item, newTitle) {
+ item.tab.setText(newTitle);
+ this.getTabBar().doLayout();
+ },
+
+
+ /**
+ * @ignore
+ * If we're removing the currently active tab, activate the nearest one. The item is removed when we call super,
+ * so we can do preprocessing before then to find the card's index
+ */
+ doRemove: function(item, autoDestroy) {
+ var me = this,
+ items = me.items,
+ // At this point the item hasn't been removed from the items collection.
+ // As such, if we want to check if there are no more tabs left, we have to
+ // check for one, as opposed to 0.
+ hasItemsLeft = items.getCount() > 1;
+
+ if (me.destroying || !hasItemsLeft) {
+ me.activeTab = null;
+ } else if (item === me.activeTab) {
+ me.setActiveTab(item.next() || items.getAt(0));
+ }
+ me.callParent(arguments);
+
+ // Remove the two references
+ delete item.tab.card;
+ delete item.tab;
+ },
+
+ /**
+ * @ignore
+ * Makes sure we remove the corresponding Tab when an item is removed
+ */
+ onRemove: function(item, autoDestroy) {
+ var me = this;
+
+ item.un({
+ scope : me,
+ enable: me.onItemEnable,
+ disable: me.onItemDisable,
+ beforeshow: me.onItemBeforeShow
+ });
+ if (!me.destroying && item.tab.ownerCt == me.tabBar) {
+ me.tabBar.remove(item.tab);
+ }
+ }
+});
+
+/**
+ * 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 .x-toolbar-spacer {
+ * width: 2px;
+ * }
+ *
+ * Example:
+ *
+ * @example
+ * Ext.create('Ext.panel.Panel', {
+ * title: 'Toolbar Spacer Example',
+ * width: 300,
+ * height: 200,
+ * 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'
+ * ],
+ * renderTo: Ext.getBody()
+ * });
+ */
+Ext.define('Ext.toolbar.Spacer', {
+ extend: 'Ext.Component',
+ alias: 'widget.tbspacer',
+ alternateClassName: 'Ext.Toolbar.Spacer',
+ baseCls: Ext.baseCSSPrefix + 'toolbar-spacer',
+ focusable: false
+});
+/**
+ * @class Ext.tree.Column
+ * @extends Ext.grid.column.Column
+ *
+ * Provides indentation and folder structure markup for a Tree taking into account
+ * depth and position within the tree hierarchy.
+ *
+ * @private
+ */
+Ext.define('Ext.tree.Column', {
+ extend: 'Ext.grid.column.Column',
+ alias: 'widget.treecolumn',
+
+ initComponent: function() {
+ var origRenderer = this.renderer || this.defaultRenderer,
+ origScope = this.scope || window;
+
+ this.renderer = function(value, metaData, record, rowIdx, colIdx, store, view) {
+ var buf = [],
+ format = Ext.String.format,
+ depth = record.getDepth(),
+ treePrefix = Ext.baseCSSPrefix + 'tree-',
+ elbowPrefix = treePrefix + 'elbow-',
+ expanderCls = treePrefix + 'expander',
+ imgText = '<img src="{1}" class="{0}" />',
+ checkboxText= '<input type="button" role="checkbox" class="{0}" {1} />',
+ formattedValue = origRenderer.apply(origScope, arguments),
+ href = record.get('href'),
+ target = record.get('hrefTarget'),
+ cls = record.get('cls');
+
+ while (record) {
+ if (!record.isRoot() || (record.isRoot() && view.rootVisible)) {
+ if (record.getDepth() === depth) {
+ buf.unshift(format(imgText,
+ treePrefix + 'icon ' +
+ treePrefix + 'icon' + (record.get('icon') ? '-inline ' : (record.isLeaf() ? '-leaf ' : '-parent ')) +
+ (record.get('iconCls') || ''),
+ record.get('icon') || Ext.BLANK_IMAGE_URL
+ ));
+ if (record.get('checked') !== null) {
+ buf.unshift(format(
+ checkboxText,
+ (treePrefix + 'checkbox') + (record.get('checked') ? ' ' + treePrefix + 'checkbox-checked' : ''),
+ record.get('checked') ? 'aria-checked="true"' : ''
+ ));
+ if (record.get('checked')) {
+ metaData.tdCls += (' ' + treePrefix + 'checked');
+ }
+ }
+ if (record.isLast()) {
+ if (record.isExpandable()) {
+ buf.unshift(format(imgText, (elbowPrefix + 'end-plus ' + expanderCls), Ext.BLANK_IMAGE_URL));
+ } else {
+ buf.unshift(format(imgText, (elbowPrefix + 'end'), Ext.BLANK_IMAGE_URL));
+ }
+
+ } else {
+ if (record.isExpandable()) {
+ buf.unshift(format(imgText, (elbowPrefix + 'plus ' + expanderCls), Ext.BLANK_IMAGE_URL));
+ } else {
+ buf.unshift(format(imgText, (treePrefix + 'elbow'), Ext.BLANK_IMAGE_URL));
+ }
+ }
+ } else {
+ if (record.isLast() || record.getDepth() === 0) {
+ buf.unshift(format(imgText, (elbowPrefix + 'empty'), Ext.BLANK_IMAGE_URL));
+ } else if (record.getDepth() !== 0) {
+ buf.unshift(format(imgText, (elbowPrefix + 'line'), Ext.BLANK_IMAGE_URL));
+ }
+ }
+ }
+ record = record.parentNode;
+ }
+ if (href) {
+ buf.push('<a href="', href, '" target="', target, '">', formattedValue, '</a>');
+ } else {
+ buf.push(formattedValue);
+ }
+ if (cls) {
+ metaData.tdCls += ' ' + cls;
+ }
+ return buf.join('');
+ };
+ this.callParent(arguments);
+ },
+
+ defaultRenderer: function(value) {
+ return value;
+ }
+});
+/**
+ * Used as a view by {@link Ext.tree.Panel TreePanel}.
+ */
+Ext.define('Ext.tree.View', {
+ extend: 'Ext.view.Table',
+ alias: 'widget.treeview',
+
+ loadingCls: Ext.baseCSSPrefix + 'grid-tree-loading',
+ expandedCls: Ext.baseCSSPrefix + 'grid-tree-node-expanded',
+
+ expanderSelector: '.' + Ext.baseCSSPrefix + 'tree-expander',
+ checkboxSelector: '.' + Ext.baseCSSPrefix + 'tree-checkbox',
+ expanderIconOverCls: Ext.baseCSSPrefix + 'tree-expander-over',
+
+ // Class to add to the node wrap element used to hold nodes when a parent is being
+ // collapsed or expanded. During the animation, UI interaction is forbidden by testing
+ // for an ancestor node with this class.
+ nodeAnimWrapCls: Ext.baseCSSPrefix + 'tree-animator-wrap',
+
+ blockRefresh: true,
+
+ /**
+ * @cfg {Boolean} rootVisible
+ * False to hide the root node.
+ */
+ rootVisible: true,
+
+ /**
+ * @cfg {Boolean} animate
+ * True to enable animated expand/collapse (defaults to the value of {@link Ext#enableFx Ext.enableFx})
+ */
+
+ expandDuration: 250,
+ collapseDuration: 250,
+
+ toggleOnDblClick: true,
+
+ initComponent: function() {
+ var me = this;
+
+ if (me.initialConfig.animate === undefined) {
+ me.animate = Ext.enableFx;
+ }
+
+ me.store = Ext.create('Ext.data.NodeStore', {
+ recursive: true,
+ rootVisible: me.rootVisible,
+ listeners: {
+ beforeexpand: me.onBeforeExpand,
+ expand: me.onExpand,
+ beforecollapse: me.onBeforeCollapse,
+ collapse: me.onCollapse,
+ scope: me
+ }
+ });
+
+ if (me.node) {
+ me.setRootNode(me.node);
+ }
+ me.animQueue = {};
+ me.callParent(arguments);
+ },
+
+ processUIEvent: function(e) {
+ // If the clicked node is part of an animation, ignore the click.
+ // This is because during a collapse animation, the associated Records
+ // will already have been removed from the Store, and the event is not processable.
+ if (e.getTarget('.' + this.nodeAnimWrapCls, this.el)) {
+ return false;
+ }
+ return this.callParent(arguments);
+ },
+
+ onClear: function(){
+ this.store.removeAll();
+ },
+
+ setRootNode: function(node) {
+ var me = this;
+ me.store.setNode(node);
+ me.node = node;
+ if (!me.rootVisible) {
+ node.expand();
+ }
+ },
+
+ onRender: function() {
+ var me = this,
+ el;
+
+ me.callParent(arguments);
+
+ el = me.el;
+ el.on({
+ scope: me,
+ delegate: me.expanderSelector,
+ mouseover: me.onExpanderMouseOver,
+ mouseout: me.onExpanderMouseOut
+ });
+ el.on({
+ scope: me,
+ delegate: me.checkboxSelector,
+ click: me.onCheckboxChange
+ });
+ },
+
+ onCheckboxChange: function(e, t) {
+ var me = this,
+ item = e.getTarget(me.getItemSelector(), me.getTargetEl());
+
+ if (item) {
+ me.onCheckChange(me.getRecord(item));
+ }
+ },
+
+ onCheckChange: function(record){
+ var checked = record.get('checked');
+ if (Ext.isBoolean(checked)) {
+ checked = !checked;
+ record.set('checked', checked);
+ this.fireEvent('checkchange', record, checked);
+ }
+ },
+
+ getChecked: function() {
+ var checked = [];
+ this.node.cascadeBy(function(rec){
+ if (rec.get('checked')) {
+ checked.push(rec);
+ }
+ });
+ return checked;
+ },
+
+ isItemChecked: function(rec){
+ return rec.get('checked');
+ },
+
+ createAnimWrap: function(record, index) {
+ var thHtml = '',
+ headerCt = this.panel.headerCt,
+ headers = headerCt.getGridColumns(),
+ i = 0, len = headers.length, item,
+ node = this.getNode(record),
+ tmpEl, nodeEl;
+
+ for (; i < len; i++) {
+ item = headers[i];
+ thHtml += '<th style="width: ' + (item.hidden ? 0 : item.getDesiredWidth()) + 'px; height: 0px;"></th>';
+ }
+
+ nodeEl = Ext.get(node);
+ tmpEl = nodeEl.insertSibling({
+ tag: 'tr',
+ html: [
+ '<td colspan="' + headerCt.getColumnCount() + '">',
+ '<div class="' + this.nodeAnimWrapCls + '">',
+ '<table class="' + Ext.baseCSSPrefix + 'grid-table" style="width: ' + headerCt.getFullWidth() + 'px;"><tbody>',
+ thHtml,
+ '</tbody></table>',
+ '</div>',
+ '</td>'
+ ].join('')
+ }, 'after');
+
+ return {
+ record: record,
+ node: node,
+ el: tmpEl,
+ expanding: false,
+ collapsing: false,
+ animating: false,
+ animateEl: tmpEl.down('div'),
+ targetEl: tmpEl.down('tbody')
+ };
+ },
+
+ getAnimWrap: function(parent) {
+ if (!this.animate) {
+ return null;
+ }
+
+ // We are checking to see which parent is having the animation wrap
+ while (parent) {
+ if (parent.animWrap) {
+ return parent.animWrap;
+ }
+ parent = parent.parentNode;
+ }
+ return null;
+ },
+
+ doAdd: function(nodes, records, index) {
+ // If we are adding records which have a parent that is currently expanding
+ // lets add them to the animation wrap
+ var me = this,
+ record = records[0],
+ parent = record.parentNode,
+ a = me.all.elements,
+ relativeIndex = 0,
+ animWrap = me.getAnimWrap(parent),
+ targetEl, children, len;
+
+ if (!animWrap || !animWrap.expanding) {
+ me.resetScrollers();
+ return me.callParent(arguments);
+ }
+
+ // We need the parent that has the animWrap, not the nodes parent
+ parent = animWrap.record;
+
+ // If there is an anim wrap we do our special magic logic
+ targetEl = animWrap.targetEl;
+ children = targetEl.dom.childNodes;
+
+ // We subtract 1 from the childrens length because we have a tr in there with the th'es
+ len = children.length - 1;
+
+ // The relative index is the index in the full flat collection minus the index of the wraps parent
+ relativeIndex = index - me.indexOf(parent) - 1;
+
+ // If we are adding records to the wrap that have a higher relative index then there are currently children
+ // it means we have to append the nodes to the wrap
+ if (!len || relativeIndex >= len) {
+ targetEl.appendChild(nodes);
+ }
+ // If there are already more children then the relative index it means we are adding child nodes of
+ // some expanded node in the anim wrap. In this case we have to insert the nodes in the right location
+ else {
+ // +1 because of the tr with th'es that is already there
+ Ext.fly(children[relativeIndex + 1]).insertSibling(nodes, 'before', true);
+ }
+
+ // We also have to update the CompositeElementLite collection of the DataView
+ Ext.Array.insert(a, index, nodes);
+
+ // If we were in an animation we need to now change the animation
+ // because the targetEl just got higher.
+ if (animWrap.isAnimating) {
+ me.onExpand(parent);
+ }
+ },
+
+ beginBulkUpdate: function(){
+ this.bulkUpdate = true;
+ this.ownerCt.changingScrollbars = true;
+ },
+
+ endBulkUpdate: function(){
+ var me = this,
+ ownerCt = me.ownerCt;
+
+ me.bulkUpdate = false;
+ me.ownerCt.changingScrollbars = true;
+ me.resetScrollers();
+ },
+
+ onRemove : function(ds, record, index) {
+ var me = this,
+ bulk = me.bulkUpdate;
+
+ me.doRemove(record, index);
+ if (!bulk) {
+ me.updateIndexes(index);
+ }
+ if (me.store.getCount() === 0){
+ me.refresh();
+ }
+ if (!bulk) {
+ me.fireEvent('itemremove', record, index);
+ }
+ },
+
+ doRemove: function(record, index) {
+ // If we are adding records which have a parent that is currently expanding
+ // lets add them to the animation wrap
+ var me = this,
+ parent = record.parentNode,
+ all = me.all,
+ animWrap = me.getAnimWrap(record),
+ node = all.item(index).dom;
+
+ if (!animWrap || !animWrap.collapsing) {
+ me.resetScrollers();
+ return me.callParent(arguments);
+ }
+
+ animWrap.targetEl.appendChild(node);
+ all.removeElement(index);
+ },
+
+ onBeforeExpand: function(parent, records, index) {
+ var me = this,
+ animWrap;
+
+ if (!me.rendered || !me.animate) {
+ return;
+ }
+
+ if (me.getNode(parent)) {
+ animWrap = me.getAnimWrap(parent);
+ if (!animWrap) {
+ animWrap = parent.animWrap = me.createAnimWrap(parent);
+ animWrap.animateEl.setHeight(0);
+ }
+ else if (animWrap.collapsing) {
+ // If we expand this node while it is still expanding then we
+ // have to remove the nodes from the animWrap.
+ animWrap.targetEl.select(me.itemSelector).remove();
+ }
+ animWrap.expanding = true;
+ animWrap.collapsing = false;
+ }
+ },
+
+ onExpand: function(parent) {
+ var me = this,
+ queue = me.animQueue,
+ id = parent.getId(),
+ animWrap,
+ animateEl,
+ targetEl,
+ queueItem;
+
+ if (me.singleExpand) {
+ me.ensureSingleExpand(parent);
+ }
+
+ animWrap = me.getAnimWrap(parent);
+
+ if (!animWrap) {
+ me.resetScrollers();
+ return;
+ }
+
+ animateEl = animWrap.animateEl;
+ targetEl = animWrap.targetEl;
+
+ animateEl.stopAnimation();
+ // @TODO: we are setting it to 1 because quirks mode on IE seems to have issues with 0
+ queue[id] = true;
+ animateEl.slideIn('t', {
+ duration: me.expandDuration,
+ listeners: {
+ scope: me,
+ lastframe: function() {
+ // Move all the nodes out of the anim wrap to their proper location
+ animWrap.el.insertSibling(targetEl.query(me.itemSelector), 'before');
+ animWrap.el.remove();
+ me.resetScrollers();
+ delete animWrap.record.animWrap;
+ delete queue[id];
+ }
+ }
+ });
+
+ animWrap.isAnimating = true;
+ },
+
+ resetScrollers: function(){
+ if (!this.bulkUpdate) {
+ var panel = this.panel;
+
+ panel.determineScrollbars();
+ panel.invalidateScroller();
+ }
+ },
+
+ onBeforeCollapse: function(parent, records, index) {
+ var me = this,
+ animWrap;
+
+ if (!me.rendered || !me.animate) {
+ return;
+ }
+
+ if (me.getNode(parent)) {
+ animWrap = me.getAnimWrap(parent);
+ if (!animWrap) {
+ animWrap = parent.animWrap = me.createAnimWrap(parent, index);
+ }
+ else if (animWrap.expanding) {
+ // If we collapse this node while it is still expanding then we
+ // have to remove the nodes from the animWrap.
+ animWrap.targetEl.select(this.itemSelector).remove();
+ }
+ animWrap.expanding = false;
+ animWrap.collapsing = true;
+ }
+ },
+
+ onCollapse: function(parent) {
+ var me = this,
+ queue = me.animQueue,
+ id = parent.getId(),
+ animWrap = me.getAnimWrap(parent),
+ animateEl, targetEl;
+
+ if (!animWrap) {
+ me.resetScrollers();
+ return;
+ }
+
+ animateEl = animWrap.animateEl;
+ targetEl = animWrap.targetEl;
+
+ queue[id] = true;
+
+ // @TODO: we are setting it to 1 because quirks mode on IE seems to have issues with 0
+ animateEl.stopAnimation();
+ animateEl.slideOut('t', {
+ duration: me.collapseDuration,
+ listeners: {
+ scope: me,
+ lastframe: function() {
+ animWrap.el.remove();
+ delete animWrap.record.animWrap;
+ me.resetScrollers();
+ delete queue[id];
+ }
+ }
+ });
+ animWrap.isAnimating = true;
+ },
+
+ /**
+ * Checks if a node is currently undergoing animation
+ * @private
+ * @param {Ext.data.Model} node The node
+ * @return {Boolean} True if the node is animating
+ */
+ isAnimating: function(node) {
+ return !!this.animQueue[node.getId()];
+ },
+
+ collectData: function(records) {
+ var data = this.callParent(arguments),
+ rows = data.rows,
+ len = rows.length,
+ i = 0,
+ row, record;
+
+ for (; i < len; i++) {
+ row = rows[i];
+ record = records[i];
+ if (record.get('qtip')) {
+ row.rowAttr = 'data-qtip="' + record.get('qtip') + '"';
+ if (record.get('qtitle')) {
+ row.rowAttr += ' ' + 'data-qtitle="' + record.get('qtitle') + '"';
+ }
+ }
+ if (record.isExpanded()) {
+ row.rowCls = (row.rowCls || '') + ' ' + this.expandedCls;
+ }
+ if (record.isLoading()) {
+ row.rowCls = (row.rowCls || '') + ' ' + this.loadingCls;
+ }
+ }
+
+ return data;
+ },
+
+ /**
+ * Expands a record that is loaded in the view.
+ * @param {Ext.data.Model} record The record to expand
+ * @param {Boolean} deep (optional) True to expand nodes all the way down the tree hierarchy.
+ * @param {Function} callback (optional) The function to run after the expand is completed
+ * @param {Object} scope (optional) The scope of the callback function.
+ */
+ expand: function(record, deep, callback, scope) {
+ return record.expand(deep, callback, scope);
+ },
+
+ /**
+ * Collapses a record that is loaded in the view.
+ * @param {Ext.data.Model} record The record to collapse
+ * @param {Boolean} deep (optional) True to collapse nodes all the way up the tree hierarchy.
+ * @param {Function} callback (optional) The function to run after the collapse is completed
+ * @param {Object} scope (optional) The scope of the callback function.
+ */
+ collapse: function(record, deep, callback, scope) {
+ return record.collapse(deep, callback, scope);
+ },
+
+ /**
+ * Toggles a record between expanded and collapsed.
+ * @param {Ext.data.Model} recordInstance
+ */
+ toggle: function(record) {
+ this[record.isExpanded() ? 'collapse' : 'expand'](record);
+ },
+
+ onItemDblClick: function(record, item, index) {
+ this.callParent(arguments);
+ if (this.toggleOnDblClick) {
+ this.toggle(record);
+ }
+ },
+
+ onBeforeItemMouseDown: function(record, item, index, e) {
+ if (e.getTarget(this.expanderSelector, item)) {
+ return false;
+ }
+ return this.callParent(arguments);
+ },
+
+ onItemClick: function(record, item, index, e) {
+ if (e.getTarget(this.expanderSelector, item)) {
+ this.toggle(record);
+ return false;
+ }
+ return this.callParent(arguments);
+ },
+
+ onExpanderMouseOver: function(e, t) {
+ e.getTarget(this.cellSelector, 10, true).addCls(this.expanderIconOverCls);
+ },
+
+ onExpanderMouseOut: function(e, t) {
+ e.getTarget(this.cellSelector, 10, true).removeCls(this.expanderIconOverCls);
+ },
+
+ /**
+ * Gets the base TreeStore from the bound TreePanel.
+ */
+ getTreeStore: function() {
+ return this.panel.store;
+ },
+
+ ensureSingleExpand: function(node) {
+ var parent = node.parentNode;
+ if (parent) {
+ parent.eachChild(function(child) {
+ if (child !== node && child.isExpanded()) {
+ child.collapse();
+ }
+ });
+ }
+ }
+});
+/**
+ * The TreePanel provides tree-structured UI representation of tree-structured data.
+ * A TreePanel must be bound to a {@link Ext.data.TreeStore}. TreePanel's support
+ * multiple columns through the {@link #columns} configuration.
+ *
+ * Simple TreePanel using inline data:
+ *
+ * @example
+ * var store = Ext.create('Ext.data.TreeStore', {
+ * root: {
+ * expanded: true,
+ * children: [
+ * { text: "detention", leaf: true },
+ * { text: "homework", expanded: true, children: [
+ * { text: "book report", leaf: true },
+ * { text: "alegrbra", leaf: true}
+ * ] },
+ * { text: "buy lottery tickets", leaf: true }
+ * ]
+ * }
+ * });
+ *
+ * Ext.create('Ext.tree.Panel', {
+ * title: 'Simple Tree',
+ * width: 200,
+ * height: 150,
+ * store: store,
+ * rootVisible: false,
+ * renderTo: Ext.getBody()
+ * });
+ *
+ * For the tree node config options (like `text`, `leaf`, `expanded`), see the documentation of
+ * {@link Ext.data.NodeInterface NodeInterface} config options.
+ */
+Ext.define('Ext.tree.Panel', {
+ extend: 'Ext.panel.Table',
+ alias: 'widget.treepanel',
+ alternateClassName: ['Ext.tree.TreePanel', 'Ext.TreePanel'],
+ requires: ['Ext.tree.View', 'Ext.selection.TreeModel', 'Ext.tree.Column'],
+ viewType: 'treeview',
+ selType: 'treemodel',
+
+ treeCls: Ext.baseCSSPrefix + 'tree-panel',
+
+ deferRowRender: false,
+
+ /**
+ * @cfg {Boolean} lines False to disable tree lines.
+ */
+ lines: true,
+
+ /**
+ * @cfg {Boolean} useArrows True to use Vista-style arrows in the tree.
+ */
+ useArrows: false,
+
+ /**
+ * @cfg {Boolean} singleExpand True if only 1 node per branch may be expanded.
+ */
+ singleExpand: false,
+
+ ddConfig: {
+ enableDrag: true,
+ enableDrop: true
+ },
+
+ /**
+ * @cfg {Boolean} animate True to enable animated expand/collapse. Defaults to the value of {@link Ext#enableFx}.
+ */
+
+ /**
+ * @cfg {Boolean} rootVisible False to hide the root node.
+ */
+ rootVisible: true,
+
+ /**
+ * @cfg {Boolean} displayField The field inside the model that will be used as the node's text.
+ */
+ displayField: 'text',
+
+ /**
+ * @cfg {Ext.data.Model/Ext.data.NodeInterface/Object} root
+ * Allows you to not specify a store on this TreePanel. This is useful for creating a simple tree with preloaded
+ * data without having to specify a TreeStore and Model. A store and model will be created and root will be passed
+ * to that store. For example:
+ *
+ * Ext.create('Ext.tree.Panel', {
+ * title: 'Simple Tree',
+ * root: {
+ * text: "Root node",
+ * expanded: true,
+ * children: [
+ * { text: "Child 1", leaf: true },
+ * { text: "Child 2", leaf: true }
+ * ]
+ * },
+ * renderTo: Ext.getBody()
+ * });
+ */
+ root: null,
+
+ // Required for the Lockable Mixin. These are the configurations which will be copied to the
+ // normal and locked sub tablepanels
+ normalCfgCopy: ['displayField', 'root', 'singleExpand', 'useArrows', 'lines', 'rootVisible', 'scroll'],
+ lockedCfgCopy: ['displayField', 'root', 'singleExpand', 'useArrows', 'lines', 'rootVisible'],
+
+ /**
+ * @cfg {Boolean} hideHeaders True to hide the headers. Defaults to `undefined`.
+ */
+
+ /**
+ * @cfg {Boolean} folderSort True to automatically prepend a leaf sorter to the store. Defaults to `undefined`.
+ */
+
+ constructor: function(config) {
+ config = config || {};
+ if (config.animate === undefined) {
+ config.animate = Ext.enableFx;
+ }
+ this.enableAnimations = config.animate;
+ delete config.animate;
+
+ this.callParent([config]);
+ },
+
+ initComponent: function() {
+ var me = this,
+ cls = [me.treeCls];
+
+ if (me.useArrows) {
+ cls.push(Ext.baseCSSPrefix + 'tree-arrows');
+ me.lines = false;
+ }
+
+ if (me.lines) {
+ cls.push(Ext.baseCSSPrefix + 'tree-lines');
+ } else if (!me.useArrows) {
+ cls.push(Ext.baseCSSPrefix + 'tree-no-lines');
+ }
+
+ if (Ext.isString(me.store)) {
+ me.store = Ext.StoreMgr.lookup(me.store);
+ } else if (!me.store || Ext.isObject(me.store) && !me.store.isStore) {
+ me.store = Ext.create('Ext.data.TreeStore', Ext.apply({}, me.store || {}, {
+ root: me.root,
+ fields: me.fields,
+ model: me.model,
+ folderSort: me.folderSort
+ }));
+ } else if (me.root) {
+ me.store = Ext.data.StoreManager.lookup(me.store);
+ me.store.setRootNode(me.root);
+ if (me.folderSort !== undefined) {
+ me.store.folderSort = me.folderSort;
+ me.store.sort();
+ }
+ }
+
+ // I'm not sure if we want to this. It might be confusing
+ // if (me.initialConfig.rootVisible === undefined && !me.getRootNode()) {
+ // me.rootVisible = false;
+ // }
+
+ me.viewConfig = Ext.applyIf(me.viewConfig || {}, {
+ rootVisible: me.rootVisible,
+ animate: me.enableAnimations,
+ singleExpand: me.singleExpand,
+ node: me.store.getRootNode(),
+ hideHeaders: me.hideHeaders
+ });
+
+ me.mon(me.store, {
+ scope: me,
+ rootchange: me.onRootChange,
+ clear: me.onClear
+ });
+
+ me.relayEvents(me.store, [
+ /**
+ * @event beforeload
+ * @alias Ext.data.Store#beforeload
+ */
+ 'beforeload',
+
+ /**
+ * @event load
+ * @alias Ext.data.Store#load
+ */
+ 'load'
+ ]);
+
+ me.store.on({
+ /**
+ * @event itemappend
+ * @alias Ext.data.TreeStore#append
+ */
+ append: me.createRelayer('itemappend'),
+
+ /**
+ * @event itemremove
+ * @alias Ext.data.TreeStore#remove
+ */
+ remove: me.createRelayer('itemremove'),
+
+ /**
+ * @event itemmove
+ * @alias Ext.data.TreeStore#move
+ */
+ move: me.createRelayer('itemmove'),
+
+ /**
+ * @event iteminsert
+ * @alias Ext.data.TreeStore#insert
+ */
+ insert: me.createRelayer('iteminsert'),
+
+ /**
+ * @event beforeitemappend
+ * @alias Ext.data.TreeStore#beforeappend
+ */
+ beforeappend: me.createRelayer('beforeitemappend'),
+
+ /**
+ * @event beforeitemremove
+ * @alias Ext.data.TreeStore#beforeremove
+ */
+ beforeremove: me.createRelayer('beforeitemremove'),
+
+ /**
+ * @event beforeitemmove
+ * @alias Ext.data.TreeStore#beforemove
+ */
+ beforemove: me.createRelayer('beforeitemmove'),
+
+ /**
+ * @event beforeiteminsert
+ * @alias Ext.data.TreeStore#beforeinsert
+ */
+ beforeinsert: me.createRelayer('beforeiteminsert'),
+
+ /**
+ * @event itemexpand
+ * @alias Ext.data.TreeStore#expand
+ */
+ expand: me.createRelayer('itemexpand'),
+
+ /**
+ * @event itemcollapse
+ * @alias Ext.data.TreeStore#collapse
+ */
+ collapse: me.createRelayer('itemcollapse'),
+
+ /**
+ * @event beforeitemexpand
+ * @alias Ext.data.TreeStore#beforeexpand
+ */
+ beforeexpand: me.createRelayer('beforeitemexpand'),
+
+ /**
+ * @event beforeitemcollapse
+ * @alias Ext.data.TreeStore#beforecollapse
+ */
+ beforecollapse: me.createRelayer('beforeitemcollapse')
+ });
+
+ // If the user specifies the headers collection manually then dont inject our own
+ if (!me.columns) {
+ if (me.initialConfig.hideHeaders === undefined) {
+ me.hideHeaders = true;
+ }
+ me.columns = [{
+ xtype : 'treecolumn',
+ text : 'Name',
+ flex : 1,
+ dataIndex: me.displayField
+ }];
+ }
+
+ if (me.cls) {
+ cls.push(me.cls);
+ }
+ me.cls = cls.join(' ');
+ me.callParent();
+
+ me.relayEvents(me.getView(), [
+ /**
+ * @event checkchange
+ * Fires when a node with a checkbox's checked property changes
+ * @param {Ext.data.Model} node The node who's checked property was changed
+ * @param {Boolean} checked The node's new checked state
+ */
+ 'checkchange'
+ ]);
+
+ // If the root is not visible and there is no rootnode defined, then just lets load the store
+ if (!me.getView().rootVisible && !me.getRootNode()) {
+ me.setRootNode({
+ expanded: true
+ });
+ }
+ },
+
+ onClear: function(){
+ this.view.onClear();
+ },
+
+ /**
+ * Sets root node of this tree.
+ * @param {Ext.data.Model/Ext.data.NodeInterface/Object} root
+ * @return {Ext.data.NodeInterface} The new root
+ */
+ setRootNode: function() {
+ return this.store.setRootNode.apply(this.store, arguments);
+ },
+
+ /**
+ * Returns the root node for this tree.
+ * @return {Ext.data.NodeInterface}
+ */
+ getRootNode: function() {
+ return this.store.getRootNode();
+ },
+
+ onRootChange: function(root) {
+ this.view.setRootNode(root);
+ },
+
+ /**
+ * Retrieve an array of checked records.
+ * @return {Ext.data.Model[]} An array containing the checked records
+ */
+ getChecked: function() {
+ return this.getView().getChecked();
+ },
+
+ isItemChecked: function(rec) {
+ return rec.get('checked');
+ },
+
+ /**
+ * Expand all nodes
+ * @param {Function} callback (optional) A function to execute when the expand finishes.
+ * @param {Object} scope (optional) The scope of the callback function
+ */
+ expandAll : function(callback, scope) {
+ var root = this.getRootNode(),
+ animate = this.enableAnimations,
+ view = this.getView();
+ if (root) {
+ if (!animate) {
+ view.beginBulkUpdate();
+ }
+ root.expand(true, callback, scope);
+ if (!animate) {
+ view.endBulkUpdate();
+ }
+ }
+ },
+
+ /**
+ * Collapse all nodes
+ * @param {Function} callback (optional) A function to execute when the collapse finishes.
+ * @param {Object} scope (optional) The scope of the callback function
+ */
+ collapseAll : function(callback, scope) {
+ var root = this.getRootNode(),
+ animate = this.enableAnimations,
+ view = this.getView();
+
+ if (root) {
+ if (!animate) {
+ view.beginBulkUpdate();
+ }
+ if (view.rootVisible) {
+ root.collapse(true, callback, scope);
+ } else {
+ root.collapseChildren(true, callback, scope);
+ }
+ if (!animate) {
+ view.endBulkUpdate();
+ }
+ }
+ },
+
+ /**
+ * Expand the tree to the path of a particular node.
+ * @param {String} path The path to expand. The path should include a leading separator.
+ * @param {String} field (optional) The field to get the data from. Defaults to the model idProperty.
+ * @param {String} separator (optional) A separator to use. Defaults to `'/'`.
+ * @param {Function} callback (optional) A function to execute when the expand finishes. The callback will be called with
+ * (success, lastNode) where success is if the expand was successful and lastNode is the last node that was expanded.
+ * @param {Object} scope (optional) The scope of the callback function
+ */
+ expandPath: function(path, field, separator, callback, scope) {
+ var me = this,
+ current = me.getRootNode(),
+ index = 1,
+ view = me.getView(),
+ keys,
+ expander;
+
+ field = field || me.getRootNode().idProperty;
+ separator = separator || '/';
+
+ if (Ext.isEmpty(path)) {
+ Ext.callback(callback, scope || me, [false, null]);
+ return;
+ }
+
+ keys = path.split(separator);
+ if (current.get(field) != keys[1]) {
+ // invalid root
+ Ext.callback(callback, scope || me, [false, current]);
+ return;
+ }
+
+ expander = function(){
+ if (++index === keys.length) {
+ Ext.callback(callback, scope || me, [true, current]);
+ return;
+ }
+ var node = current.findChild(field, keys[index]);
+ if (!node) {
+ Ext.callback(callback, scope || me, [false, current]);
+ return;
+ }
+ current = node;
+ current.expand(false, expander);
+ };
+ current.expand(false, expander);
+ },
+
+ /**
+ * Expand the tree to the path of a particular node, then select it.
+ * @param {String} path The path to select. The path should include a leading separator.
+ * @param {String} field (optional) The field to get the data from. Defaults to the model idProperty.
+ * @param {String} separator (optional) A separator to use. Defaults to `'/'`.
+ * @param {Function} callback (optional) A function to execute when the select finishes. The callback will be called with
+ * (bSuccess, oLastNode) where bSuccess is if the select was successful and oLastNode is the last node that was expanded.
+ * @param {Object} scope (optional) The scope of the callback function
+ */
+ selectPath: function(path, field, separator, callback, scope) {
+ var me = this,
+ keys,
+ last;
+
+ field = field || me.getRootNode().idProperty;
+ separator = separator || '/';
+
+ keys = path.split(separator);
+ last = keys.pop();
+
+ me.expandPath(keys.join(separator), field, separator, function(success, node){
+ var doSuccess = false;
+ if (success && node) {
+ node = node.findChild(field, last);
+ if (node) {
+ me.getSelectionModel().select(node);
+ Ext.callback(callback, scope || me, [true, node]);
+ doSuccess = true;
+ }
+ } else if (node === me.getRootNode()) {
+ doSuccess = true;
+ }
+ Ext.callback(callback, scope || me, [doSuccess, node]);
+ }, me);
+ }
+});
+
+/**
+ * @class Ext.view.DragZone
+ * @extends Ext.dd.DragZone
+ * @private
+ */
+Ext.define('Ext.view.DragZone', {
+ extend: 'Ext.dd.DragZone',
+ containerScroll: false,
+
+ constructor: function(config) {
+ var me = this;
+
+ Ext.apply(me, config);
+
+ // Create a ddGroup unless one has been configured.
+ // User configuration of ddGroups allows users to specify which
+ // DD instances can interact with each other. Using one
+ // based on the id of the View would isolate it and mean it can only
+ // interact with a DropZone on the same View also using a generated ID.
+ if (!me.ddGroup) {
+ me.ddGroup = 'view-dd-zone-' + me.view.id;
+ }
+
+ // Ext.dd.DragDrop instances are keyed by the ID of their encapsulating element.
+ // So a View's DragZone cannot use the View's main element because the DropZone must use that
+ // because the DropZone may need to scroll on hover at a scrolling boundary, and it is the View's
+ // main element which handles scrolling.
+ // We use the View's parent element to drag from. Ideally, we would use the internal structure, but that
+ // is transient; DataView's recreate the internal structure dynamically as data changes.
+ // TODO: Ext 5.0 DragDrop must allow multiple DD objects to share the same element.
+ me.callParent([me.view.el.dom.parentNode]);
+
+ me.ddel = Ext.get(document.createElement('div'));
+ me.ddel.addCls(Ext.baseCSSPrefix + 'grid-dd-wrap');
+ },
+
+ init: function(id, sGroup, config) {
+ this.initTarget(id, sGroup, config);
+ this.view.mon(this.view, {
+ itemmousedown: this.onItemMouseDown,
+ scope: this
+ });
+ },
+
+ onItemMouseDown: function(view, record, item, index, e) {
+ if (!this.isPreventDrag(e, record, item, index)) {
+ this.handleMouseDown(e);
+
+ // If we want to allow dragging of multi-selections, then veto the following handlers (which, in the absence of ctrlKey, would deselect)
+ // if the mousedowned record is selected
+ if (view.getSelectionModel().selectionMode == 'MULTI' && !e.ctrlKey && view.getSelectionModel().isSelected(record)) {
+ return false;
+ }
+ }
+ },
+
+ // private template method
+ isPreventDrag: function(e) {
+ return false;
+ },
+
+ getDragData: function(e) {
+ var view = this.view,
+ item = e.getTarget(view.getItemSelector()),
+ record, selectionModel, records;
+
+ if (item) {
+ record = view.getRecord(item);
+ selectionModel = view.getSelectionModel();
+ records = selectionModel.getSelection();
+ return {
+ copy: this.view.copy || (this.view.allowCopy && e.ctrlKey),
+ event: new Ext.EventObjectImpl(e),
+ view: view,
+ ddel: this.ddel,
+ item: item,
+ records: records,
+ fromPosition: Ext.fly(item).getXY()
+ };
+ }
+ },
+
+ onInitDrag: function(x, y) {
+ var me = this,
+ data = me.dragData,
+ view = data.view,
+ selectionModel = view.getSelectionModel(),
+ record = view.getRecord(data.item),
+ e = data.event;
+
+ // Update the selection to match what would have been selected if the user had
+ // done a full click on the target node rather than starting a drag from it
+ if (!selectionModel.isSelected(record) || e.hasModifier()) {
+ selectionModel.selectWithEvent(record, e, true);
+ }
+ data.records = selectionModel.getSelection();
+
+ me.ddel.update(me.getDragText());
+ me.proxy.update(me.ddel.dom);
+ me.onStartDrag(x, y);
+ return true;
+ },
+
+ getDragText: function() {
+ var count = this.dragData.records.length;
+ return Ext.String.format(this.dragText, count, count == 1 ? '' : 's');
+ },
+
+ getRepairXY : function(e, data){
+ return data ? data.fromPosition : false;
+ }
+});
+Ext.define('Ext.tree.ViewDragZone', {
+ extend: 'Ext.view.DragZone',
+
+ isPreventDrag: function(e, record) {
+ return (record.get('allowDrag') === false) || !!e.getTarget(this.view.expanderSelector);
+ },
+
+ afterRepair: function() {
+ var me = this,
+ view = me.view,
+ selectedRowCls = view.selectedItemCls,
+ records = me.dragData.records,
+ fly = Ext.fly;
+
+ if (Ext.enableFx && me.repairHighlight) {
+ // Roll through all records and highlight all the ones we attempted to drag.
+ Ext.Array.forEach(records, function(record) {
+ // anonymous fns below, don't hoist up unless below is wrapped in
+ // a self-executing function passing in item.
+ var item = view.getNode(record);
+
+ // We must remove the selected row class before animating, because
+ // the selected row class declares !important on its background-color.
+ fly(item.firstChild).highlight(me.repairHighlightColor, {
+ listeners: {
+ beforeanimate: function() {
+ if (view.isSelected(item)) {
+ fly(item).removeCls(selectedRowCls);
+ }
+ },
+ afteranimate: function() {
+ if (view.isSelected(item)) {
+ fly(item).addCls(selectedRowCls);
+ }
+ }
+ }
+ });
+ });
+ }
+ me.dragging = false;
+ }
+});
+/**
+ * @class Ext.tree.ViewDropZone
+ * @extends Ext.view.DropZone
+ * @private
+ */
+Ext.define('Ext.tree.ViewDropZone', {
+ extend: 'Ext.view.DropZone',
+
+ /**
+ * @cfg {Boolean} allowParentInsert
+ * Allow inserting a dragged node between an expanded parent node and its first child that will become a
+ * sibling of the parent when dropped.
+ */
+ allowParentInserts: false,
+
+ /**
+ * @cfg {String} allowContainerDrop
+ * True if drops on the tree container (outside of a specific tree node) are allowed.
+ */
+ allowContainerDrops: false,
+
+ /**
+ * @cfg {String} appendOnly
+ * True if the tree should only allow append drops (use for trees which are sorted).
+ */
+ appendOnly: false,
+
+ /**
+ * @cfg {String} expandDelay
+ * The delay in milliseconds to wait before expanding a target tree node while dragging a droppable node
+ * over the target.
+ */
+ expandDelay : 500,
+
+ indicatorCls: 'x-tree-ddindicator',
+
+ // private
+ expandNode : function(node) {
+ var view = this.view;
+ if (!node.isLeaf() && !node.isExpanded()) {
+ view.expand(node);
+ this.expandProcId = false;
+ }
+ },
+
+ // private
+ queueExpand : function(node) {
+ this.expandProcId = Ext.Function.defer(this.expandNode, this.expandDelay, this, [node]);
+ },
+
+ // private
+ cancelExpand : function() {
+ if (this.expandProcId) {
+ clearTimeout(this.expandProcId);
+ this.expandProcId = false;
+ }
+ },
+
+ getPosition: function(e, node) {
+ var view = this.view,
+ record = view.getRecord(node),
+ y = e.getPageY(),
+ noAppend = record.isLeaf(),
+ noBelow = false,
+ region = Ext.fly(node).getRegion(),
+ fragment;
+
+ // If we are dragging on top of the root node of the tree, we always want to append.
+ if (record.isRoot()) {
+ return 'append';
+ }
+
+ // Return 'append' if the node we are dragging on top of is not a leaf else return false.
+ if (this.appendOnly) {
+ return noAppend ? false : 'append';
+ }
+
+ if (!this.allowParentInsert) {
+ noBelow = record.hasChildNodes() && record.isExpanded();
+ }
+
+ fragment = (region.bottom - region.top) / (noAppend ? 2 : 3);
+ if (y >= region.top && y < (region.top + fragment)) {
+ return 'before';
+ }
+ else if (!noBelow && (noAppend || (y >= (region.bottom - fragment) && y <= region.bottom))) {
+ return 'after';
+ }
+ else {
+ return 'append';
+ }
+ },
+
+ isValidDropPoint : function(node, position, dragZone, e, data) {
+ if (!node || !data.item) {
+ return false;
+ }
+
+ var view = this.view,
+ targetNode = view.getRecord(node),
+ draggedRecords = data.records,
+ dataLength = draggedRecords.length,
+ ln = draggedRecords.length,
+ i, record;
+
+ // No drop position, or dragged records: invalid drop point
+ if (!(targetNode && position && dataLength)) {
+ return false;
+ }
+
+ // If the targetNode is within the folder we are dragging
+ for (i = 0; i < ln; i++) {
+ record = draggedRecords[i];
+ if (record.isNode && record.contains(targetNode)) {
+ return false;
+ }
+ }
+
+ // Respect the allowDrop field on Tree nodes
+ if (position === 'append' && targetNode.get('allowDrop') === false) {
+ return false;
+ }
+ else if (position != 'append' && targetNode.parentNode.get('allowDrop') === false) {
+ return false;
+ }
+
+ // If the target record is in the dragged dataset, then invalid drop
+ if (Ext.Array.contains(draggedRecords, targetNode)) {
+ return false;
+ }
+
+ // @TODO: fire some event to notify that there is a valid drop possible for the node you're dragging
+ // Yes: this.fireViewEvent(blah....) fires an event through the owning View.
+ return true;
+ },
+
+ onNodeOver : function(node, dragZone, e, data) {
+ var position = this.getPosition(e, node),
+ returnCls = this.dropNotAllowed,
+ view = this.view,
+ targetNode = view.getRecord(node),
+ indicator = this.getIndicator(),
+ indicatorX = 0,
+ indicatorY = 0;
+
+ // auto node expand check
+ this.cancelExpand();
+ if (position == 'append' && !this.expandProcId && !Ext.Array.contains(data.records, targetNode) && !targetNode.isLeaf() && !targetNode.isExpanded()) {
+ this.queueExpand(targetNode);
+ }
+
+
+ if (this.isValidDropPoint(node, position, dragZone, e, data)) {
+ this.valid = true;
+ this.currentPosition = position;
+ this.overRecord = targetNode;
+
+ indicator.setWidth(Ext.fly(node).getWidth());
+ indicatorY = Ext.fly(node).getY() - Ext.fly(view.el).getY() - 1;
+
+ /*
+ * In the code below we show the proxy again. The reason for doing this is showing the indicator will
+ * call toFront, causing it to get a new z-index which can sometimes push the proxy behind it. We always
+ * want the proxy to be above, so calling show on the proxy will call toFront and bring it forward.
+ */
+ if (position == 'before') {
+ returnCls = targetNode.isFirst() ? Ext.baseCSSPrefix + 'tree-drop-ok-above' : Ext.baseCSSPrefix + 'tree-drop-ok-between';
+ indicator.showAt(0, indicatorY);
+ dragZone.proxy.show();
+ } else if (position == 'after') {
+ returnCls = targetNode.isLast() ? Ext.baseCSSPrefix + 'tree-drop-ok-below' : Ext.baseCSSPrefix + 'tree-drop-ok-between';
+ indicatorY += Ext.fly(node).getHeight();
+ indicator.showAt(0, indicatorY);
+ dragZone.proxy.show();
+ } else {
+ returnCls = Ext.baseCSSPrefix + 'tree-drop-ok-append';
+ // @TODO: set a class on the parent folder node to be able to style it
+ indicator.hide();
+ }
+ } else {
+ this.valid = false;
+ }
+
+ this.currentCls = returnCls;
+ return returnCls;
+ },
+
+ onContainerOver : function(dd, e, data) {
+ return e.getTarget('.' + this.indicatorCls) ? this.currentCls : this.dropNotAllowed;
+ },
+
+ notifyOut: function() {
+ this.callParent(arguments);
+ this.cancelExpand();
+ },
+
+ handleNodeDrop : function(data, targetNode, position) {
+ var me = this,
+ view = me.view,
+ parentNode = targetNode.parentNode,
+ store = view.getStore(),
+ recordDomNodes = [],
+ records, i, len,
+ insertionMethod, argList,
+ needTargetExpand,
+ transferData,
+ processDrop;
+
+ // If the copy flag is set, create a copy of the Models with the same IDs
+ if (data.copy) {
+ records = data.records;
+ data.records = [];
+ for (i = 0, len = records.length; i < len; i++) {
+ data.records.push(Ext.apply({}, records[i].data));
+ }
+ }
+
+ // Cancel any pending expand operation
+ me.cancelExpand();
+
+ // Grab a reference to the correct node insertion method.
+ // Create an arg list array intended for the apply method of the
+ // chosen node insertion method.
+ // Ensure the target object for the method is referenced by 'targetNode'
+ if (position == 'before') {
+ insertionMethod = parentNode.insertBefore;
+ argList = [null, targetNode];
+ targetNode = parentNode;
+ }
+ else if (position == 'after') {
+ if (targetNode.nextSibling) {
+ insertionMethod = parentNode.insertBefore;
+ argList = [null, targetNode.nextSibling];
+ }
+ else {
+ insertionMethod = parentNode.appendChild;
+ argList = [null];
+ }
+ targetNode = parentNode;
+ }
+ else {
+ if (!targetNode.isExpanded()) {
+ needTargetExpand = true;
+ }
+ insertionMethod = targetNode.appendChild;
+ argList = [null];
+ }
+
+ // A function to transfer the data into the destination tree
+ transferData = function() {
+ var node;
+ for (i = 0, len = data.records.length; i < len; i++) {
+ argList[0] = data.records[i];
+ node = insertionMethod.apply(targetNode, argList);
+
+ if (Ext.enableFx && me.dropHighlight) {
+ recordDomNodes.push(view.getNode(node));
+ }
+ }
+
+ // Kick off highlights after everything's been inserted, so they are
+ // more in sync without insertion/render overhead.
+ if (Ext.enableFx && me.dropHighlight) {
+ //FIXME: the check for n.firstChild is not a great solution here. Ideally the line should simply read
+ //Ext.fly(n.firstChild) but this yields errors in IE6 and 7. See ticket EXTJSIV-1705 for more details
+ Ext.Array.forEach(recordDomNodes, function(n) {
+ if (n) {
+ Ext.fly(n.firstChild ? n.firstChild : n).highlight(me.dropHighlightColor);
+ }
+ });
+ }
+ };
+
+ // If dropping right on an unexpanded node, transfer the data after it is expanded.
+ if (needTargetExpand) {
+ targetNode.expand(false, transferData);
+ }
+ // Otherwise, call the data transfer function immediately
+ else {
+ transferData();
+ }
+ }
+});
+/**
+ * This plugin provides drag and/or drop functionality for a TreeView.
+ *
+ * It creates a specialized instance of {@link Ext.dd.DragZone DragZone} which knows how to drag out of a
+ * {@link Ext.tree.View TreeView} and loads the data object which is passed to a cooperating
+ * {@link Ext.dd.DragZone DragZone}'s methods with the following properties:
+ *
+ * - copy : Boolean
+ *
+ * The value of the TreeView's `copy` property, or `true` if the TreeView was configured with `allowCopy: true` *and*
+ * the control key was pressed when the drag operation was begun.
+ *
+ * - view : TreeView
+ *
+ * The source TreeView from which the drag originated.
+ *
+ * - ddel : HtmlElement
+ *
+ * The drag proxy element which moves with the mouse
+ *
+ * - item : HtmlElement
+ *
+ * The TreeView node upon which the mousedown event was registered.
+ *
+ * - records : Array
+ *
+ * An Array of {@link Ext.data.Model Models} representing the selected data being dragged from the source TreeView.
+ *
+ * It also creates a specialized instance of {@link Ext.dd.DropZone} which cooperates with other DropZones which are
+ * members of the same ddGroup which processes such data objects.
+ *
+ * Adding this plugin to a view means that two new events may be fired from the client TreeView, {@link #beforedrop} and
+ * {@link #drop}.
+ *
+ * Note that the plugin must be added to the tree view, not to the tree panel. For example using viewConfig:
+ *
+ * viewConfig: {
+ * plugins: { ptype: 'treeviewdragdrop' }
+ * }
+ */
+Ext.define('Ext.tree.plugin.TreeViewDragDrop', {
+ extend: 'Ext.AbstractPlugin',
+ alias: 'plugin.treeviewdragdrop',
+
+ uses: [
+ 'Ext.tree.ViewDragZone',
+ 'Ext.tree.ViewDropZone'
+ ],
+
+ /**
+ * @event beforedrop
+ *
+ * **This event is fired through the TreeView. Add listeners to the TreeView object**
+ *
+ * Fired when a drop gesture has been triggered by a mouseup event in a valid drop position in the TreeView.
+ *
+ * @param {HTMLElement} node The TreeView node **if any** over which the mouse was positioned.
+ *
+ * Returning `false` to this event signals that the drop gesture was invalid, and if the drag proxy will animate
+ * back to the point from which the drag began.
+ *
+ * Returning `0` To this event signals that the data transfer operation should not take place, but that the gesture
+ * was valid, and that the repair operation should not take place.
+ *
+ * Any other return value continues with the data transfer operation.
+ *
+ * @param {Object} data The data object gathered at mousedown time by the cooperating
+ * {@link Ext.dd.DragZone DragZone}'s {@link Ext.dd.DragZone#getDragData getDragData} method it contains the following
+ * properties:
+ * @param {Boolean} data.copy The value of the TreeView's `copy` property, or `true` if the TreeView was configured with
+ * `allowCopy: true` and the control key was pressed when the drag operation was begun
+ * @param {Ext.tree.View} data.view The source TreeView from which the drag originated.
+ * @param {HTMLElement} data.ddel The drag proxy element which moves with the mouse
+ * @param {HTMLElement} data.item The TreeView node upon which the mousedown event was registered.
+ * @param {Ext.data.Model[]} data.records An Array of {@link Ext.data.Model Model}s representing the selected data being
+ * dragged from the source TreeView.
+ *
+ * @param {Ext.data.Model} overModel The Model over which the drop gesture took place.
+ *
+ * @param {String} dropPosition `"before"`, `"after"` or `"append"` depending on whether the mouse is above or below
+ * the midline of the node, or the node is a branch node which accepts new child nodes.
+ *
+ * @param {Function} dropFunction A function to call to complete the data transfer operation and either move or copy
+ * Model instances from the source View's Store to the destination View's Store.
+ *
+ * This is useful when you want to perform some kind of asynchronous processing before confirming the drop, such as
+ * an {@link Ext.window.MessageBox#confirm confirm} call, or an Ajax request.
+ *
+ * Return `0` from this event handler, and call the `dropFunction` at any time to perform the data transfer.
+ */
+
+ /**
+ * @event drop
+ *
+ * **This event is fired through the TreeView. Add listeners to the TreeView object** Fired when a drop operation
+ * has been completed and the data has been moved or copied.
+ *
+ * @param {HTMLElement} node The TreeView node **if any** over which the mouse was positioned.
+ *
+ * @param {Object} data The data object gathered at mousedown time by the cooperating
+ * {@link Ext.dd.DragZone DragZone}'s {@link Ext.dd.DragZone#getDragData getDragData} method it contains the following
+ * properties:
+ * @param {Boolean} data.copy The value of the TreeView's `copy` property, or `true` if the TreeView was configured with
+ * `allowCopy: true` and the control key was pressed when the drag operation was begun
+ * @param {Ext.tree.View} data.view The source TreeView from which the drag originated.
+ * @param {HTMLElement} data.ddel The drag proxy element which moves with the mouse
+ * @param {HTMLElement} data.item The TreeView node upon which the mousedown event was registered.
+ * @param {Ext.data.Model[]} data.records An Array of {@link Ext.data.Model Model}s representing the selected data being
+ * dragged from the source TreeView.
+ *
+ * @param {Ext.data.Model} overModel The Model over which the drop gesture took place.
+ *
+ * @param {String} dropPosition `"before"`, `"after"` or `"append"` depending on whether the mouse is above or below
+ * the midline of the node, or the node is a branch node which accepts new child nodes.
+ */
+
+ dragText : '{0} selected node{1}',
+
+ /**
+ * @cfg {Boolean} allowParentInsert
+ * Allow inserting a dragged node between an expanded parent node and its first child that will become a sibling of
+ * the parent when dropped.
+ */
+ allowParentInserts: false,
+
+ /**
+ * @cfg {String} allowContainerDrop
+ * True if drops on the tree container (outside of a specific tree node) are allowed.
+ */
+ allowContainerDrops: false,
+
+ /**
+ * @cfg {String} appendOnly
+ * True if the tree should only allow append drops (use for trees which are sorted).
+ */
+ appendOnly: false,
+
+ /**
+ * @cfg {String} ddGroup
+ * A named drag drop group to which this object belongs. If a group is specified, then both the DragZones and
+ * DropZone used by this plugin will only interact with other drag drop objects in the same group.
+ */
+ ddGroup : "TreeDD",
+
+ /**
+ * @cfg {String} dragGroup
+ * The ddGroup to which the DragZone will belong.
+ *
+ * This defines which other DropZones the DragZone will interact with. Drag/DropZones only interact with other
+ * Drag/DropZones which are members of the same ddGroup.
+ */
+
+ /**
+ * @cfg {String} dropGroup
+ * The ddGroup to which the DropZone will belong.
+ *
+ * This defines which other DragZones the DropZone will interact with. Drag/DropZones only interact with other
+ * Drag/DropZones which are members of the same ddGroup.
+ */
+
+ /**
+ * @cfg {String} expandDelay
+ * The delay in milliseconds to wait before expanding a target tree node while dragging a droppable node over the
+ * target.
+ */
+ expandDelay : 1000,
+
+ /**
+ * @cfg {Boolean} enableDrop
+ * Set to `false` to disallow the View from accepting drop gestures.
+ */
+ enableDrop: true,
+
+ /**
+ * @cfg {Boolean} enableDrag
+ * Set to `false` to disallow dragging items from the View.
+ */
+ enableDrag: true,
+
+ /**
+ * @cfg {String} nodeHighlightColor
+ * The color to use when visually highlighting the dragged or dropped node (default value is light blue).
+ * The color must be a 6 digit hex value, without a preceding '#'. See also {@link #nodeHighlightOnDrop} and
+ * {@link #nodeHighlightOnRepair}.
+ */
+ nodeHighlightColor: 'c3daf9',
+
+ /**
+ * @cfg {Boolean} nodeHighlightOnDrop
+ * Whether or not to highlight any nodes after they are
+ * successfully dropped on their target. Defaults to the value of `Ext.enableFx`.
+ * See also {@link #nodeHighlightColor} and {@link #nodeHighlightOnRepair}.
+ */
+ nodeHighlightOnDrop: Ext.enableFx,
+
+ /**
+ * @cfg {Boolean} nodeHighlightOnRepair
+ * Whether or not to highlight any nodes after they are
+ * repaired from an unsuccessful drag/drop. Defaults to the value of `Ext.enableFx`.
+ * See also {@link #nodeHighlightColor} and {@link #nodeHighlightOnDrop}.
+ */
+ nodeHighlightOnRepair: Ext.enableFx,
+
+ init : function(view) {
+ view.on('render', this.onViewRender, this, {single: true});
+ },
+
+ /**
+ * @private
+ * AbstractComponent calls destroy on all its plugins at destroy time.
+ */
+ destroy: function() {
+ Ext.destroy(this.dragZone, this.dropZone);
+ },
+
+ onViewRender : function(view) {
+ var me = this;
+
+ if (me.enableDrag) {
+ me.dragZone = Ext.create('Ext.tree.ViewDragZone', {
+ view: view,
+ ddGroup: me.dragGroup || me.ddGroup,
+ dragText: me.dragText,
+ repairHighlightColor: me.nodeHighlightColor,
+ repairHighlight: me.nodeHighlightOnRepair
+ });
+ }
+
+ if (me.enableDrop) {
+ me.dropZone = Ext.create('Ext.tree.ViewDropZone', {
+ view: view,
+ ddGroup: me.dropGroup || me.ddGroup,
+ allowContainerDrops: me.allowContainerDrops,
+ appendOnly: me.appendOnly,
+ allowParentInserts: me.allowParentInserts,
+ expandDelay: me.expandDelay,
+ dropHighlightColor: me.nodeHighlightColor,
+ dropHighlight: me.nodeHighlightOnDrop
+ });
+ }
+ }
+});
+/**
+ * @class Ext.util.Cookies
+
+Utility class for setting/reading values from browser cookies.
+Values can be written using the {@link #set} method.
+Values can be read using the {@link #get} method.
+A cookie can be invalidated on the client machine using the {@link #clear} method.
+
+ * @markdown
+ * @singleton
+ */
+Ext.define('Ext.util.Cookies', {
+ singleton: true,
+
+ /**
+ * Create a cookie with the specified name and value. Additional settings
+ * for the cookie may be optionally specified (for example: expiration,
+ * access restriction, SSL).
+ * @param {String} name The name of the cookie to set.
+ * @param {Object} value The value to set for the cookie.
+ * @param {Object} expires (Optional) Specify an expiration date the
+ * cookie is to persist until. Note that the specified Date object will
+ * be converted to Greenwich Mean Time (GMT).
+ * @param {String} path (Optional) Setting a path on the cookie restricts
+ * access to pages that match that path. Defaults to all pages (<tt>'/'</tt>).
+ * @param {String} domain (Optional) Setting a domain restricts access to
+ * pages on a given domain (typically used to allow cookie access across
+ * subdomains). For example, "sencha.com" will create a cookie that can be
+ * accessed from any subdomain of sencha.com, including www.sencha.com,
+ * support.sencha.com, etc.
+ * @param {Boolean} secure (Optional) Specify true to indicate that the cookie
+ * should only be accessible via SSL on a page using the HTTPS protocol.
+ * Defaults to <tt>false</tt>. Note that this will only work if the page
+ * calling this code uses the HTTPS protocol, otherwise the cookie will be
+ * created with default options.
+ */
+ set : function(name, value){
+ var argv = arguments,
+ argc = arguments.length,
+ expires = (argc > 2) ? argv[2] : null,
+ path = (argc > 3) ? argv[3] : '/',
+ domain = (argc > 4) ? argv[4] : null,
+ secure = (argc > 5) ? argv[5] : false;
+
+ document.cookie = name + "=" + escape(value) + ((expires === null) ? "" : ("; expires=" + expires.toGMTString())) + ((path === null) ? "" : ("; path=" + path)) + ((domain === null) ? "" : ("; domain=" + domain)) + ((secure === true) ? "; secure" : "");
+ },
+
+ /**
+ * Retrieves cookies that are accessible by the current page. If a cookie
+ * does not exist, <code>get()</code> returns <tt>null</tt>. The following
+ * example retrieves the cookie called "valid" and stores the String value
+ * in the variable <tt>validStatus</tt>.
+ * <pre><code>
+ * var validStatus = Ext.util.Cookies.get("valid");
+ * </code></pre>
+ * @param {String} name The name of the cookie to get
+ * @return {Object} Returns the cookie value for the specified name;
+ * null if the cookie name does not exist.
+ */
+ get : function(name){
+ var arg = name + "=",
+ alen = arg.length,
+ clen = document.cookie.length,
+ i = 0,
+ j = 0;
+
+ while(i < clen){
+ j = i + alen;
+ if(document.cookie.substring(i, j) == arg){
+ return this.getCookieVal(j);
+ }
+ i = document.cookie.indexOf(" ", i) + 1;
+ if(i === 0){
+ break;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Removes a cookie with the provided name from the browser
+ * if found by setting its expiration date to sometime in the past.
+ * @param {String} name The name of the cookie to remove
+ * @param {String} path (optional) The path for the cookie. This must be included if you included a path while setting the cookie.
+ */
+ clear : function(name, path){
+ if(this.get(name)){
+ path = path || '/';
+ document.cookie = name + '=' + '; expires=Thu, 01-Jan-70 00:00:01 GMT; path=' + path;
+ }
+ },
+
+ /**
+ * @private
+ */
+ getCookieVal : function(offset){
+ var endstr = document.cookie.indexOf(";", offset);
+ if(endstr == -1){
+ endstr = document.cookie.length;
+ }
+ return unescape(document.cookie.substring(offset, endstr));
+ }
+});
+
+/**
+ * @class Ext.util.CSS
+ * Utility class for manipulating CSS rules
+ * @singleton
+ */
+Ext.define('Ext.util.CSS', function() {
+ var rules = null;
+ var doc = document;
+
+ var camelRe = /(-[a-z])/gi;
+ var camelFn = function(m, a){ return a.charAt(1).toUpperCase(); };
+
+ return {
+
+ singleton: true,
+
+ constructor: function() {
+ this.rules = {};
+ this.initialized = false;
+ },
+
+ /**
+ * Creates a stylesheet from a text blob of rules.
+ * These rules will be wrapped in a STYLE tag and appended to the HEAD of the document.
+ * @param {String} cssText The text containing the css rules
+ * @param {String} id An id to add to the stylesheet for later removal
+ * @return {CSSStyleSheet}
+ */
+ createStyleSheet : function(cssText, id) {
+ var ss,
+ head = doc.getElementsByTagName("head")[0],
+ styleEl = doc.createElement("style");
+
+ styleEl.setAttribute("type", "text/css");
+ if (id) {
+ styleEl.setAttribute("id", id);
+ }
+
+ if (Ext.isIE) {
+ head.appendChild(styleEl);
+ ss = styleEl.styleSheet;
+ ss.cssText = cssText;
+ } else {
+ try{
+ styleEl.appendChild(doc.createTextNode(cssText));
+ } catch(e) {
+ styleEl.cssText = cssText;
+ }
+ head.appendChild(styleEl);
+ ss = styleEl.styleSheet ? styleEl.styleSheet : (styleEl.sheet || doc.styleSheets[doc.styleSheets.length-1]);
+ }
+ this.cacheStyleSheet(ss);
+ return ss;
+ },
+
+ /**
+ * Removes a style or link tag by id
+ * @param {String} id The id of the tag
+ */
+ removeStyleSheet : function(id) {
+ var existing = document.getElementById(id);
+ if (existing) {
+ existing.parentNode.removeChild(existing);
+ }
+ },
+
+ /**
+ * Dynamically swaps an existing stylesheet reference for a new one
+ * @param {String} id The id of an existing link tag to remove
+ * @param {String} url The href of the new stylesheet to include
+ */
+ swapStyleSheet : function(id, url) {
+ var doc = document;
+ this.removeStyleSheet(id);
+ var ss = doc.createElement("link");
+ ss.setAttribute("rel", "stylesheet");
+ ss.setAttribute("type", "text/css");
+ ss.setAttribute("id", id);
+ ss.setAttribute("href", url);
+ doc.getElementsByTagName("head")[0].appendChild(ss);
+ },
+
+ /**
+ * Refresh the rule cache if you have dynamically added stylesheets
+ * @return {Object} An object (hash) of rules indexed by selector
+ */
+ refreshCache : function() {
+ return this.getRules(true);
+ },
+
+ // private
+ cacheStyleSheet : function(ss) {
+ if(!rules){
+ rules = {};
+ }
+ try {// try catch for cross domain access issue
+ var ssRules = ss.cssRules || ss.rules,
+ selectorText,
+ i = ssRules.length - 1,
+ j,
+ selectors;
+
+ for (; i >= 0; --i) {
+ selectorText = ssRules[i].selectorText;
+ if (selectorText) {
+
+ // Split in case there are multiple, comma-delimited selectors
+ selectorText = selectorText.split(',');
+ selectors = selectorText.length;
+ for (j = 0; j < selectors; j++) {
+ rules[Ext.String.trim(selectorText[j]).toLowerCase()] = ssRules[i];
+ }
+ }
+ }
+ } catch(e) {}
+ },
+
+ /**
+ * Gets all css rules for the document
+ * @param {Boolean} refreshCache true to refresh the internal cache
+ * @return {Object} An object (hash) of rules indexed by selector
+ */
+ getRules : function(refreshCache) {
+ if (rules === null || refreshCache) {
+ rules = {};
+ var ds = doc.styleSheets,
+ i = 0,
+ len = ds.length;
+
+ for (; i < len; i++) {
+ try {
+ if (!ds[i].disabled) {
+ this.cacheStyleSheet(ds[i]);
+ }
+ } catch(e) {}
+ }
+ }
+ return rules;
+ },
+
+ /**
+ * Gets an an individual CSS rule by selector(s)
+ * @param {String/String[]} selector The CSS selector or an array of selectors to try. The first selector that is found is returned.
+ * @param {Boolean} refreshCache true to refresh the internal cache if you have recently updated any rules or added styles dynamically
+ * @return {CSSStyleRule} The CSS rule or null if one is not found
+ */
+ getRule: function(selector, refreshCache) {
+ var rs = this.getRules(refreshCache);
+ if (!Ext.isArray(selector)) {
+ return rs[selector.toLowerCase()];
+ }
+ for (var i = 0; i < selector.length; i++) {
+ if (rs[selector[i]]) {
+ return rs[selector[i].toLowerCase()];
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Updates a rule property
+ * @param {String/String[]} selector If it's an array it tries each selector until it finds one. Stops immediately once one is found.
+ * @param {String} property The css property
+ * @param {String} value The new value for the property
+ * @return {Boolean} true If a rule was found and updated
+ */
+ updateRule : function(selector, property, value){
+ if (!Ext.isArray(selector)) {
+ var rule = this.getRule(selector);
+ if (rule) {
+ rule.style[property.replace(camelRe, camelFn)] = value;
+ return true;
+ }
+ } else {
+ for (var i = 0; i < selector.length; i++) {
+ if (this.updateRule(selector[i], property, value)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ };
+}());
+/**
+ * @class Ext.util.History
+ *
+ * History management component that allows you to register arbitrary tokens that signify application
+ * history state on navigation actions. You can then handle the history {@link #change} event in order
+ * to reset your application UI to the appropriate state when the user navigates forward or backward through
+ * the browser history stack.
+ *
+ * ## Initializing
+ * The {@link #init} method of the History object must be called before using History. This sets up the internal
+ * state and must be the first thing called before using History.
+ *
+ * ## Setup
+ * The History objects requires elements on the page to keep track of the browser history. For older versions of IE,
+ * an IFrame is required to do the tracking. For other browsers, a hidden field can be used. The history objects expects
+ * these to be on the page before the {@link #init} method is called. The following markup is suggested in order
+ * to support all browsers:
+ *
+ * <form id="history-form" class="x-hide-display">
+ * <input type="hidden" id="x-history-field" />
+ * <iframe id="x-history-frame"></iframe>
+ * </form>
+ *
+ * @singleton
+ */
+Ext.define('Ext.util.History', {
+ singleton: true,
+ alternateClassName: 'Ext.History',
+ mixins: {
+ observable: 'Ext.util.Observable'
+ },
+
+ constructor: function() {
+ var me = this;
+ me.oldIEMode = Ext.isIE6 || Ext.isIE7 || !Ext.isStrict && Ext.isIE8;
+ me.iframe = null;
+ me.hiddenField = null;
+ me.ready = false;
+ me.currentToken = null;
+ },
+
+ getHash: function() {
+ var href = window.location.href,
+ i = href.indexOf("#");
+
+ return i >= 0 ? href.substr(i + 1) : null;
+ },
+
+ doSave: function() {
+ this.hiddenField.value = this.currentToken;
+ },
+
+
+ handleStateChange: function(token) {
+ this.currentToken = token;
+ this.fireEvent('change', token);
+ },
+
+ updateIFrame: function(token) {
+ var html = '<html><body><div id="state">' +
+ Ext.util.Format.htmlEncode(token) +
+ '</div></body></html>';
+
+ try {
+ var doc = this.iframe.contentWindow.document;
+ doc.open();
+ doc.write(html);
+ doc.close();
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+
+ checkIFrame: function () {
+ var me = this,
+ contentWindow = me.iframe.contentWindow;
+
+ if (!contentWindow || !contentWindow.document) {
+ Ext.Function.defer(this.checkIFrame, 10, this);
+ return;
+ }
+
+ var doc = contentWindow.document,
+ elem = doc.getElementById("state"),
+ oldToken = elem ? elem.innerText : null,
+ oldHash = me.getHash();
+
+ Ext.TaskManager.start({
+ run: function () {
+ var doc = contentWindow.document,
+ elem = doc.getElementById("state"),
+ newToken = elem ? elem.innerText : null,
+ newHash = me.getHash();
+
+ if (newToken !== oldToken) {
+ oldToken = newToken;
+ me.handleStateChange(newToken);
+ window.top.location.hash = newToken;
+ oldHash = newToken;
+ me.doSave();
+ } else if (newHash !== oldHash) {
+ oldHash = newHash;
+ me.updateIFrame(newHash);
+ }
+ },
+ interval: 50,
+ scope: me
+ });
+ me.ready = true;
+ me.fireEvent('ready', me);
+ },
+
+ startUp: function () {
+ var me = this;
+
+ me.currentToken = me.hiddenField.value || this.getHash();
+
+ if (me.oldIEMode) {
+ me.checkIFrame();
+ } else {
+ var hash = me.getHash();
+ Ext.TaskManager.start({
+ run: function () {
+ var newHash = me.getHash();
+ if (newHash !== hash) {
+ hash = newHash;
+ me.handleStateChange(hash);
+ me.doSave();
+ }
+ },
+ interval: 50,
+ scope: me
+ });
+ me.ready = true;
+ me.fireEvent('ready', me);
+ }
+
+ },
+
+ /**
+ * The id of the hidden field required for storing the current history token.
+ * @type String
+ * @property
+ */
+ fieldId: Ext.baseCSSPrefix + 'history-field',
+ /**
+ * The id of the iframe required by IE to manage the history stack.
+ * @type String
+ * @property
+ */
+ iframeId: Ext.baseCSSPrefix + 'history-frame',
+
+ /**
+ * 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 scope (`this` reference) in which the callback is executed. Defaults to the browser window.
+ */
+ init: function (onReady, scope) {
+ var me = this;
+
+ if (me.ready) {
+ Ext.callback(onReady, scope, [me]);
+ return;
+ }
+
+ if (!Ext.isReady) {
+ Ext.onReady(function() {
+ me.init(onReady, scope);
+ });
+ return;
+ }
+
+ me.hiddenField = Ext.getDom(me.fieldId);
+
+ if (me.oldIEMode) {
+ me.iframe = Ext.getDom(me.iframeId);
+ }
+
+ me.addEvents(
+ /**
+ * @event ready
+ * Fires when the Ext.util.History singleton has been initialized and is ready for use.
+ * @param {Ext.util.History} The Ext.util.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) {
+ me.on('ready', onReady, scope, {single: true});
+ }
+ me.startUp();
+ },
+
+ /**
+ * Add a new token to the history stack. This can be any arbitrary value, although it would
+ * commonly be the concatenation of a component id and another id marking the specific history
+ * state of that component. Example usage:
+ *
+ * // Handle tab changes on a TabPanel
+ * tabPanel.on('tabchange', function(tabPanel, tab){
+ * Ext.History.add(tabPanel.id + ':' + tab.id);
+ * });
+ *
+ * @param {String} token The value that defines a particular application-specific history state
+ * @param {Boolean} [preventDuplicates=true] When true, if the passed token matches the current token
+ * it will not save a new history step. Set to false if the same state can be saved more than once
+ * at the same history stack location.
+ */
+ add: function (token, preventDup) {
+ var me = this;
+
+ if (preventDup !== false) {
+ if (me.getToken() === token) {
+ return true;
+ }
+ }
+
+ if (me.oldIEMode) {
+ return me.updateIFrame(token);
+ } else {
+ window.top.location.hash = token;
+ return true;
+ }
+ },
+
+ /**
+ * Programmatically steps back one step in browser history (equivalent to the user pressing the Back button).
+ */
+ back: function() {
+ window.history.go(-1);
+ },
+
+ /**
+ * Programmatically steps forward one step in browser history (equivalent to the user pressing the Forward button).
+ */
+ forward: function(){
+ window.history.go(1);
+ },
+
+ /**
+ * Retrieves the currently-active history token.
+ * @return {String} The token
+ */
+ getToken: function() {
+ return this.ready ? this.currentToken : this.getHash();
+ }
+});
+/**
+ * @class Ext.view.TableChunker
+ *
+ * Produces optimized XTemplates for chunks of tables to be
+ * used in grids, trees and other table based widgets.
+ *
+ * @singleton
+ */
+Ext.define('Ext.view.TableChunker', {
+ singleton: true,
+ requires: ['Ext.XTemplate'],
+ metaTableTpl: [
+ '{[this.openTableWrap()]}',
+ '<table class="' + Ext.baseCSSPrefix + 'grid-table ' + Ext.baseCSSPrefix + 'grid-table-resizer" border="0" cellspacing="0" cellpadding="0" {[this.embedFullWidth()]}>',
+ '<tbody>',
+ '<tr class="' + Ext.baseCSSPrefix + 'grid-header-row">',
+ '<tpl for="columns">',
+ '<th class="' + Ext.baseCSSPrefix + 'grid-col-resizer-{id}" style="width: {width}px; height: 0px;"></th>',
+ '</tpl>',
+ '</tr>',
+ '{[this.openRows()]}',
+ '{row}',
+ '<tpl for="features">',
+ '{[this.embedFeature(values, parent, xindex, xcount)]}',
+ '</tpl>',
+ '{[this.closeRows()]}',
+ '</tbody>',
+ '</table>',
+ '{[this.closeTableWrap()]}'
+ ],
+
+ constructor: function() {
+ Ext.XTemplate.prototype.recurse = function(values, reference) {
+ return this.apply(reference ? values[reference] : values);
+ };
+ },
+
+ embedFeature: function(values, parent, x, xcount) {
+ var tpl = '';
+ if (!values.disabled) {
+ tpl = values.getFeatureTpl(values, parent, x, xcount);
+ }
+ return tpl;
+ },
+
+ embedFullWidth: function() {
+ return 'style="width: {fullWidth}px;"';
+ },
+
+ openRows: function() {
+ return '<tpl for="rows">';
+ },
+
+ closeRows: function() {
+ return '</tpl>';
+ },
+
+ metaRowTpl: [
+ '<tr class="' + Ext.baseCSSPrefix + 'grid-row {addlSelector} {[this.embedRowCls()]}" {[this.embedRowAttr()]}>',
+ '<tpl for="columns">',
+ '<td class="{cls} ' + Ext.baseCSSPrefix + 'grid-cell ' + Ext.baseCSSPrefix + 'grid-cell-{columnId} {{id}-modified} {{id}-tdCls} {[this.firstOrLastCls(xindex, xcount)]}" {{id}-tdAttr}><div unselectable="on" class="' + Ext.baseCSSPrefix + 'grid-cell-inner ' + Ext.baseCSSPrefix + 'unselectable" style="{{id}-style}; text-align: {align};">{{id}}</div></td>',
+ '</tpl>',
+ '</tr>'
+ ],
+
+ firstOrLastCls: function(xindex, xcount) {
+ var cssCls = '';
+ if (xindex === 1) {
+ cssCls = Ext.baseCSSPrefix + 'grid-cell-first';
+ } else if (xindex === xcount) {
+ cssCls = Ext.baseCSSPrefix + 'grid-cell-last';
+ }
+ return cssCls;
+ },
+
+ embedRowCls: function() {
+ return '{rowCls}';
+ },
+
+ embedRowAttr: function() {
+ return '{rowAttr}';
+ },
+
+ openTableWrap: function() {
+ return '';
+ },
+
+ closeTableWrap: function() {
+ return '';
+ },
+
+ getTableTpl: function(cfg, textOnly) {
+ var tpl,
+ tableTplMemberFns = {
+ openRows: this.openRows,
+ closeRows: this.closeRows,
+ embedFeature: this.embedFeature,
+ embedFullWidth: this.embedFullWidth,
+ openTableWrap: this.openTableWrap,
+ closeTableWrap: this.closeTableWrap
+ },
+ tplMemberFns = {},
+ features = cfg.features || [],
+ ln = features.length,
+ i = 0,
+ memberFns = {
+ embedRowCls: this.embedRowCls,
+ embedRowAttr: this.embedRowAttr,
+ firstOrLastCls: this.firstOrLastCls
+ },
+ // copy the default
+ metaRowTpl = Array.prototype.slice.call(this.metaRowTpl, 0),
+ metaTableTpl;
+
+ for (; i < ln; i++) {
+ if (!features[i].disabled) {
+ features[i].mutateMetaRowTpl(metaRowTpl);
+ Ext.apply(memberFns, features[i].getMetaRowTplFragments());
+ Ext.apply(tplMemberFns, features[i].getFragmentTpl());
+ Ext.apply(tableTplMemberFns, features[i].getTableFragments());
+ }
+ }
+
+ metaRowTpl = Ext.create('Ext.XTemplate', metaRowTpl.join(''), memberFns);
+ cfg.row = metaRowTpl.applyTemplate(cfg);
+
+ metaTableTpl = Ext.create('Ext.XTemplate', this.metaTableTpl.join(''), tableTplMemberFns);
+
+ tpl = metaTableTpl.applyTemplate(cfg);
+
+ // TODO: Investigate eliminating.
+ if (!textOnly) {
+ tpl = Ext.create('Ext.XTemplate', tpl, tplMemberFns);
+ }
+ return tpl;
+
+ }
+});
+
+
+
+