X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/6746dc89c47ed01b165cc1152533605f97eb8e8d..f562e4c6e5fac7bcb445985b99acbea4d706e6f0:/docs/extjs/ext-all-debug.js diff --git a/docs/extjs/ext-all-debug.js b/docs/extjs/ext-all-debug.js new file mode 100644 index 00000000..4949fab0 --- /dev/null +++ b/docs/extjs/ext-all-debug.js @@ -0,0 +1,133879 @@ +/* + +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 overrides 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 , 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 + * @docauthor Jacky Nguyen + * @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: + *

+         * 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
+         * 
+ * @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 (this) 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. + *

+    // alternate sort directions
+    sort = Ext.String.toggle(sort, 'ASC', 'DESC');
+
+    // instead of conditional logic:
+    sort = (sort == 'ASC' ? 'DESC' : 'ASC');
+       
+ * @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: + * + *

+var s = Ext.String.leftPad('123', 5, '0');
+// s now contains the string: '00123'
+       
+ * @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: + *

+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>'
+       
+ * @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 + * @docauthor Jacky Nguyen + * + * 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 + * @docauthor Jacky Nguyen + * @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 + * PHP's date() function, and the formats that are + * supported will provide results equivalent to their PHP versions. + * + * The following is a list of all currently supported formats: + *
+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)\/
+
+ * + * Example usage (note that you must escape format specifiers with '\\' to render them as character literals): + *

+// 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
+
+ * + * 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. + *

+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"
+};
+
+ * + * Example usage: + *

+var dt = new Date();
+console.log(Ext.Date.format(dt, Ext.Date.patterns.ShortDate));
+
+ *

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}.

+ * @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 false. + * @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 + }; + }, + + /** + *

An object hash in which each property is a date parsing function. The property name is the + * format string which that function parses.

+ *

This object is automatically populated with date parsing functions as + * date formats are requested for Ext standard formatting strings.

+ *

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}.

+ *

Example:


+Ext.Date.parseFunctions['x-date-format'] = myDateParser;
+
+ *

A parsing function should return a Date object, and is passed the following parameters:

    + *
  • date : String
    The date string to parse.
  • + *
  • strict : Boolean
    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.
  • + *

+ *

To enable Dates to also be formatted 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: [], + + /** + *

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.

+ *

This object is automatically populated with date formatting functions as + * date formats are requested for Ext standard formatting strings.

+ *

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:


+Ext.Date.formatFunctions['x-date-format'] = myDateFormatter;
+
+ *

A formatting function should return a string representation of the passed Date object, and is passed the following parameters:

    + *
  • date : Date
    The Date to format.
  • + *

+ *

To enable date strings to also be parsed 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", + + /** + *

An object hash containing default date values used during date parsing.

+ *

The following properties are available:

    + *
  • y : Number
    The default year value. (defaults to undefined)
  • + *
  • m : Number
    The default 1-based month value. (defaults to undefined)
  • + *
  • d : Number
    The default day value. (defaults to undefined)
  • + *
  • h : Number
    The default hour value. (defaults to undefined)
  • + *
  • i : Number
    The default minute value. (defaults to undefined)
  • + *
  • s : Number
    The default second value. (defaults to undefined)
  • + *
  • ms : Number
    The default millisecond value. (defaults to undefined)
  • + *

+ *

Override these properties to customize the default date values used by the {@link #parse} method.

+ *

Note: In countries which experience Daylight Saving Time (i.e. DST), the h, i, s + * and ms properties may coincide with the exact time in which DST takes effect. + * It is the responsiblity of the developer to account for this.

+ * Example Usage: + *

+// 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
+
+ * @property defaults + * @type Object + */ + defaults: {}, + + /** + * @property {String[]} dayNames + * An array of textual day names. + * Override these values for international dates. + * Example: + *

+Ext.Date.dayNames = [
+    'SundayInYourLang',
+    'MondayInYourLang',
+    ...
+];
+
+ */ + dayNames : [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday" + ], + + /** + * @property {String[]} monthNames + * An array of textual month names. + * Override these values for international dates. + * Example: + *

+Ext.Date.monthNames = [
+    'JanInYourLang',
+    'FebInYourLang',
+    ...
+];
+
+ */ + 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: + *

+Ext.Date.monthNumbers = {
+    'ShortJanNameInYourLang':0,
+    'ShortFebNameInYourLang':1,
+    ...
+};
+
+ */ + 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 + *

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.

+ *

This may be overridden in a locale file.

+ */ + 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: + *

+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
+
+ * @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). + *

Example:


+//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
+
+ * @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: + *

+var dt = new Date('1/10/2007'),
+    firstDay = Ext.Date.getFirstDayOfMonth(dt);
+console.log(Ext.Date.dayNames[firstDay]); //output: 'Monday'
+     * 
+ * @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: + *

+var dt = new Date('1/10/2007'),
+    lastDay = Ext.Date.getLastDayOfMonth(dt);
+console.log(Ext.Date.dayNames[lastDay]); //output: 'Wednesday'
+     * 
+ * @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: + *

+//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'
+     * 
+ * @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: + *

+// 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'
+
+     * 
+ * + * @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 + * @docauthor Jacky Nguyen + * @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; + }, + + // + /** + * 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; + }), + // + + /** + * 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; + }, + + // + /** + * 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]; + } + } + // + else if (key === 'config' && my.config && mixin.config) { + Ext.Object.merge(my.config, mixin.config); + } + // + } + } + + 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; + }, + // + + /** + * 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 + * @docauthor Jacky Nguyen + * @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; + + // + // 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]; + } + } + } + // + + // + // Merge the parent class' config object without referencing it + if (parentPrototype.config) { + clsPrototype.config = Ext.Object.merge({}, parentPrototype.config); + } + else { + clsPrototype.config = {}; + } + // + + // + if (clsPrototype.$onExtended) { + clsPrototype.$onExtended.call(cls, cls, data); + } + + if (data.onClassExtended) { + clsPrototype.$onExtended = data.onClassExtended; + delete data.onClassExtended; + } + // + + }, true); + + // + /** + * @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; + }); + // + + // + /** + * @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; + }); + // + + // + /** + * @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; + }); + // + + // + /** + * @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]); + } + } + } + }); + }); + + // + + Class.setDefaultPreprocessors([ + 'extend' + // + ,'statics' + // + // + ,'inheritableStatics' + // + // + ,'config' + // + // + ,'mixins' + // + ]); + + // + // 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' + // + ,'statics' + // + // + ,'inheritableStatics' + // + // + ,'mixins' + // + // + ,'config' + // + ]; + + 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; + }; + // + +})(); + +/** + * @author Jacky Nguyen + * @docauthor Jacky Nguyen + * @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; + // + + /** + * @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 + * @docauthor Jacky Nguyen + * + * 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.: + * + * + * + * + * 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 + * @docauthor Brian Moeskau + * + * 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(""); + }; + + /** + *

Encodes a Date. This returns the actual string which is inserted into the JSON string as the literal expression. + * The returned value includes enclosing double quotation marks.

+ *

The default return format is "yyyy-mm-ddThh:mm:ss".

+ *

To override this:


+Ext.JSON.encodeDate = function(d) {
+    return Ext.Date.format(d, '"Y-m-d"');
+};
+     
+ * @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.

+ 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 + +- The Learning Center +- The FAQ +- The forums + + * @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, undefined if not found, or null 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 ('about:blank', except for IE in secure mode, which is 'javascript:""'). + * @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. + * Note: Native JSON methods will not work with objects that have functions. + * Also, property names must be quoted, otherwise the data will not parse. (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: + *

+// 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
+}
+         * 
+ * Note: 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. + *

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 + * true, then DOM event listeners are also removed from all child nodes. The body node + * will be ignored if passed in.

+ * @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==', + + /** + *

Utility method for returning a default value if the passed value is empty.

+ *

The value is deemed to be empty if it is

    + *
  • null
  • + *
  • undefined
  • + *
  • an empty array
  • + *
  • a zero length string (Unless the allowBlank parameter is true)
  • + *
+ * @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 @ suffix. + *

+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
+    }
+});
+         * 
+ * @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('
'), + 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: + *

+// 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.
+         * 
+ * @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. + *

+// Example:
+Ext.invoke(Ext.query("p"), "getAttribute", "id");
+// [el1.getAttribute("id"), el2.getAttribute("id"), ..., elN.getAttribute("id")]
+         * 
+ * @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; + }, + + /** + *

Zips N sets together.

+ *

+// 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"]
+         * 
+ * @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 = /(?:)((\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 + *

The character that the {@link #number} function uses as a thousand separator.

+ *

This may be overridden in a locale file.

+ */ + thousandSeparator: ',', + + /** + * @property {String} decimalSeparator + *

The character that the {@link #number} function uses as a decimal point.

+ *

This may be overridden in a locale file.

+ */ + decimalSeparator: '.', + + /** + * @property {Number} currencyPrecision + *

The number of decimal places that the {@link #currency} function displays.

+ *

This may be overridden in a locale file.

+ */ + currencyPrecision: 2, + + /** + * @property {String} currencySign + *

The currency sign that the {@link #currency} function displays.

+ *

This may be overridden in a locale file.

+ */ + currencySign: '$', + + /** + * @property {Boolean} currencyAtEnd + *

This may be set to true to make the {@link #currency} function + * append the currency sign to the formatted value.

+ *

This may be overridden in a locale file.

+ */ + 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 parse() 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:

+         * var tpl = new Ext.Template('{value} * 10 = {value:math("* 10")}');
+         * 
+ * @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; + }, + + /** + *

Formats the passed number according to the passed format string.

+ *

The number of digits after the decimal separator character specifies the number of + * decimal places in the resulting string. The local-specific decimal character is used in the result.

+ *

The presence of a thousand separator character in the format string specifies that + * the locale-specific thousand separator (if any) is inserted separating thousand groups.

+ *

By default, "," is expected as the thousand separator, and "." is expected as the decimal separator.

+ *

New to Ext JS 4

+ *

Locale-specific characters are always used in the formatted output when inserting + * thousand and decimal separators.

+ *

The format string must specify separator characters according to US/UK conventions ("," as the + * thousand separator, and "." as the decimal separator)

+ *

To allow specification of format strings according to local conventions for separator characters, add + * the string /i to the end of the format string.

+ *
examples (123456.789): + *
+ * 0 - (123456) show only digits, no precision
+ * 0.00 - (123456.78) show only digits, 2 precision
+ * 0.0000 - (123456.7890) show only digits, 4 precision
+ * 0,000 - (123,456) show comma and digits, no precision
+ * 0,000.00 - (123,456.78) show comma and digits, 2 precision
+ * 0,0.00 - (123,456.78) shortcut method, show comma and digits, 2 precision
+ * 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 + *
+ * @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, '
'); + }, + + /** + * 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: + *

+// 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
+});
+
+ * 
+ *

See the {@link #start} method for details about how to configure a task object.

+ * 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

A config object that supports the following properties:

    + *
  • run : Function

    The function to execute each time the task is invoked. The + * function will be called at each interval and passed the args argument if specified, and the + * current invocation count if not.

    + *

    If a particular scope (this reference) is required, be sure to specify it using the scope argument.

    + *

    Return false from this function to terminate the task.

  • + *
  • interval : Number
    The frequency in milliseconds with which the task + * should be invoked.
  • + *
  • args : Array
    (optional) An array of arguments to be passed to the function + * specified by run. If not specified, the current invocation count is passed.
  • + *
  • scope : Object
    (optional) The scope (this reference) in which to execute the + * run function. Defaults to the task config object.
  • + *
  • duration : Number
    (optional) The length of time in milliseconds to invoke + * the task before stopping automatically (defaults to indefinite).
  • + *
  • repeat : Number
    (optional) The number of times to invoke the task before + * stopping automatically (defaults to indefinite).
  • + *

+ *

Before each invocation, Ext injects the property taskRunCount into the task object so + * that calculations based on the repeat count can be performed.

+ * @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. + *

+// 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);
+
+ *

See the {@link #start} method for details about how to configure a task object.

+ * @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 = [ + '
', + '
', + '
', + '
', + '
', + '
', + '
' + ].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 = ""; + 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 + * + *

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.

+ * + *

DomHelper element specification object

+ *

A specification object is used when creating elements. Attributes of this object + * are assumed to be element attributes, except for 4 special attributes: + *

    + *
  • tag :
    The tag name of the element
  • + *
  • children : or cn
    An array of the + * same kind of element definition objects to be created and appended. These can be nested + * as deep as you want.
  • + *
  • cls :
    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.
  • + *
  • html :
    The innerHTML for the element
  • + *

+ *

NOTE: For other arbitrary attributes, the value will currently not 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 must manually HTML-encode it beforehand (see {@link Ext.String#htmlEncode}) or risk + * malformed HTML being created. This behavior may change in a future release.

+ * + *

Insertion methods

+ *

Commonly used insertion methods: + *

    + *
  • {@link #append} :
  • + *
  • {@link #insertBefore} :
  • + *
  • {@link #insertAfter} :
  • + *
  • {@link #overwrite} :
  • + *
  • {@link #createTemplate} :
  • + *
  • {@link #insertHtml} :
  • + *

+ * + *

Example

+ *

This is an example, where an unordered list with 3 children items is appended to an existing + * element with id 'my-div':
+


+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
+);
+ 

+ *

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:


+dh.append('my-ul', [
+    {tag: 'li', id: 'item3', html: 'List Item 3'},
+    {tag: 'li', id: 'item4', html: 'List Item 4'}
+]);
+ * 

+ * + *

Templating

+ *

The real power is in the built-in templating. Instead of creating or appending any elements, + * {@link #createTemplate} 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: + *


+// 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
+}
+ * 

+ *

An example using a template:


+var html = '{2}';
+
+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"]);
+ * 

+ * + *

The same example using named parameters:


+var html = '{text}';
+
+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"
+});
+ * 

+ * + *

Compiling Templates

+ *

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. + *


+var html = '{text}';
+
+var tpl = new Ext.DomHelper.createTemplate(html);
+tpl.compile();
+
+//... use template like normal
+ * 

+ * + *

Performance Boost

+ *

DomHelper will transparently create HTML fragments when it can. Using HTML fragments instead + * of DOM can significantly boost performance.

+ *

Element creation specification parameters may also be strings. If {@link #useDom} is false, + * then the string is used as innerHTML. If {@link #useDom} is true, a string specification + * results in the creation of a text node. Usage:

+ *

+Ext.DomHelper.useDom = true; // force it to use DOM; reduces performance
+ * 
+ * @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 = '', + te = '
', + tbs = ts+'', + tbe = ''+te, + trs = tbs + '', + tre = ''+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 += ''; + } + } + 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: `
Contents
` + * + * Using different `where` values inserts element to the following places: + * + * - beforeBegin: `
Contents
` + * - afterBegin: `
Contents
` + * - beforeEnd: `
Contents
` + * - afterEnd: `
Contents
` + * + * @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). +

+DomQuery supports most of the CSS3 selectors spec, along with some custom selectors and basic XPath.

+ +

+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. +

+

Element Selectors:

+
    +
  • * any element
  • +
  • E an element with the tag E
  • +
  • E F All descendent elements of E that have the tag F
  • +
  • E > F or E/F all direct children elements of E that have the tag F
  • +
  • E + F all elements with the tag F that are immediately preceded by an element with the tag E
  • +
  • E ~ F all elements with the tag F that are preceded by a sibling element with the tag E
  • +
+

Attribute Selectors:

+

The use of @ and quotes are optional. For example, div[@foo='bar'] is also a valid attribute selector.

+
    +
  • E[foo] has an attribute "foo"
  • +
  • E[foo=bar] has an attribute "foo" that equals "bar"
  • +
  • E[foo^=bar] has an attribute "foo" that starts with "bar"
  • +
  • E[foo$=bar] has an attribute "foo" that ends with "bar"
  • +
  • E[foo*=bar] has an attribute "foo" that contains the substring "bar"
  • +
  • E[foo%=2] has an attribute "foo" that is evenly divisible by 2
  • +
  • E[foo!=bar] attribute "foo" does not equal "bar"
  • +
+

Pseudo Classes:

+
    +
  • E:first-child E is the first child of its parent
  • +
  • E:last-child E is the last child of its parent
  • +
  • E:nth-child(n) E is the nth child of its parent (1 based as per the spec)
  • +
  • E:nth-child(odd) E is an odd child of its parent
  • +
  • E:nth-child(even) E is an even child of its parent
  • +
  • E:only-child E is the only child of its parent
  • +
  • E:checked E is an element that is has a checked attribute that is true (e.g. a radio or checkbox)
  • +
  • E:first the first E in the resultset
  • +
  • E:last the last E in the resultset
  • +
  • E:nth(n) the nth E in the resultset (1 based)
  • +
  • E:odd shortcut for :nth-child(odd)
  • +
  • E:even shortcut for :nth-child(even)
  • +
  • E:contains(foo) E's innerHTML contains the substring "foo"
  • +
  • E:nodeValue(foo) E contains a textNode with a nodeValue that equals "foo"
  • +
  • E:not(S) an E element that does not match simple selector S
  • +
  • E:has(S) an E element that has a descendent that matches simple selector S
  • +
  • E:next(S) an E element whose next sibling matches simple selector S
  • +
  • E:prev(S) an E element whose previous sibling matches simple selector S
  • +
  • E:any(S1|S2|S2) an E element which matches any of the simple selectors S1, S2 or S3//\\
  • +
+

CSS Value Selectors:

+
    +
  • E{display=none} css value "display" that equals "none"
  • +
  • E{display^=none} css value "display" that starts with "none"
  • +
  • E{display$=none} css value "display" that ends with "none"
  • +
  • E{display*=none} css value "display" that contains the substring "none"
  • +
  • E{display%=2} css value "display" that is evenly divisible by 2
  • +
  • E{display!=none} css value "display" that does not equal "none"
  • +
+ * @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 c= where c 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: + *
+ *

paragraph one

+ *

paragraph two

+ *

paragraph three

+ *
+ * + * // 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 = '
'; + + // 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 true for + * the default animation ({duration: .35, easing: 'easeIn'}) + * @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:
    + *
  • A Number specifying the new width in this Element's {@link #defaultUnit}s (by default, pixels).
  • + *
  • A String used to set the CSS width style. Animation may not be used. + *
+ * @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. + *

+// 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"); }
+});
+         * 
+ * @param {Number/String} height The new height. This may be one of:
    + *
  • A Number specifying the new height in this Element's {@link #defaultUnit}s (by default, pixels.)
  • + *
  • A String used to set the CSS height style. Animation may not be used.
  • + *
+ * @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 'lr' would get the border left width + the border right 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 'lr' would get the padding left + the padding right. + * @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 {@link #unclip} 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 {@link #clip} 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; + }, + + /** + *

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.

+ *

This special markup is used throughout Ext when box wrapping elements ({@link Ext.button.Button}, + * {@link Ext.panel.Panel} when {@link Ext.panel.Panel#frame frame=true}, {@link Ext.window.Window}). The markup + * is of this form:

+ *

+    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>';
+        * 
+ *

Example usage:

+ *

+    // 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");
+        * 
+ * @param {String} class (optional) A base CSS class to apply to the containing wrapper element + * (defaults to 'x-box'). 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", "
" + Ext.String.format(ELEMENT.boxMarkup, cls) + "
")); + 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:
    + *
  • A Number specifying the new width in this Element's {@link #defaultUnit}s (by default, pixels).
  • + *
  • A String used to set the CSS width style. Animation may not be used. + *
  • A size object in the format {width: widthValue, height: heightValue}.
  • + *
+ * @param {Number/String} height The new height. This may be one of:
    + *
  • A Number specifying the new height in this Element's {@link #defaultUnit}s (by default, pixels).
  • + *
  • A String used to set the CSS height style. Animation may not be used.
  • + *
+ * @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; + }, + + /** + *

Returns the dimensions of the element available to lay content out in.

+ *

If the element (or any ancestor element) has CSS style display : none, the dimensions will be zero.

+ * example:

+        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.
+        * 
+ * + * 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; + }, + + /** + *

Returns the dimensions of the element available to lay content out in.

+ * + * 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: + *

    + *
  • border-width -> borderWidth
  • + *
  • padding-top -> paddingTop
  • + *
+ * @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. + *

+ * The sample code below would return an object with 2 properties, one + * for background-color and one for color.

+ *

+var css = 'background-color: red;color: blue; ';
+console.log(Ext.Element.parseStyles(css));
+     * 
+ * @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 + *

This class encapsulates a collection of DOM elements, providing methods to filter + * members, or to perform collective actions upon the whole set.

+ *

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.

+ * Example:

+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);
+
+ */ +Ext.CompositeElementLite = function(els, root){ + /** + *

The Array of DOM elements which this CompositeElement encapsulates. Read-only.

+ *

This will not usually 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.

+ *

For example to add the nextAll method to the class to add all + * following siblings of selected elements, the code would be

+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);
+    }
+});
+ * @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; iCalls the passed function for each element in this composite.

+ * @param {Function} fn The function to call. The function is passed the following parameters:
    + *
  • el : Element
    The current Element in the iteration. + * This is the flyweight (shared) Ext.Element instance, so if you require a + * a reference to the dom node, use el.dom.
  • + *
  • c : Composite
    This Composite object.
  • + *
  • idx : Number
    The zero-based index in the iteration.
  • + *
+ * @param {Object} [scope] The scope (this 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 + *
  • el : Ext.Element
    The current DOM element.
  • + *
  • index : Number
    The current index within the collection.
  • + * + * @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 this reference) in which the + * function is called. If not specified, this 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, this 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 (this 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 is) 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:
      + *
    • evt : EventObject
      The {@link Ext.EventObject EventObject} describing the event.
    • + *
    • t : Element
      The {@link Ext.Element 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. Defaults to the 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. Defaults to the Element.
    • + *
    • delegate : String
      A simple selector to filter the target or look for a descendant of the target
    • + *
    • 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
    • + *
    • delay : Number
      The number of milliseconds to delay the invocation of the handler after te 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 : 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.
    • + *

    + *

    See {@link Ext.Element#addListener} for examples of how to use these options.

    + */ + 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. 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. + */ + 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 (this 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:
    
    +// 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!');
    +    }
    +});
    +
    + * @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: + *

    HTMLEvents

    + *
      + *
    • load
    • + *
    • unload
    • + *
    • select
    • + *
    • change
    • + *
    • submit
    • + *
    • reset
    • + *
    • resize
    • + *
    • scroll
    • + *
    + *

    MouseEvents

    + *
      + *
    • click
    • + *
    • dblclick
    • + *
    • mousedown
    • + *
    • mouseup
    • + *
    • mouseover
    • + *
    • mousemove
    • + *
    • mouseout
    • + *
    + *

    UIEvents

    + *
      + *
    • focusin
    • + *
    • focusout
    • + *
    • activate
    • + *
    • focus
    • + *
    • blur
    • + *
    + * @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 was moved + * back in, the function is not called. + * @param {Number} delay The delay in milliseconds 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 (this 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:
    
    +// 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);
    +    
    + */ + 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 += ''; + + interval = setInterval(function(){ + if (!document.getElementById(id)) { + return false; + } + clearInterval(interval); + var DOC = document, + hd = DOC.getElementsByTagName("head")[0], + re = /(?:]*)?>)((\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(/(?:)((\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: + *
      + *
    • Blank: Defaults to aligning the element's top-left corner to the target's bottom-left corner ("tl-bl").
    • + *
    • One anchor (deprecated): 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. This method has been + * deprecated in favor of the newer two anchor syntax below.
    • + *
    • Two anchors: 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.
    • + *
    + * 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: +
    +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
    +
    +Example Usage: +
    
    +// 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]);
    +
    + * @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; + }, + + /** + *

    Returns the [X, Y] vector by which this element must be translated to make a best attempt + * to constrain within the passed constraint. Returns false is this element does not need to be moved.

    + *

    Priority is given to constraining the top and left within the constraint.

    + *

    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.

    + * @param constrainTo {Mixed} The Element or {@link Ext.util.Region Region} into which this element is to be constrained. + * @param proposedPosition {Array} A proposed [X, Y] position to test for validity and to produce a vector for instead + * of using this Element's current position; + * @returns {Number[]/Boolean} If this element needs to be translated, an [X, Y] + * vector by which this element must be translated. Otherwise, false. + */ + 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
    
    +{
    +    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>
    +}
    +
    + * 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 content 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
    
    +{
    +    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>
    +}
    +
    + * 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:
      + *
    • A Number specifying the new width in this Element's {@link #defaultUnit}s (by default, pixels)
    • + *
    • A String used to set the CSS width style. Animation may not be used. + *
    + * @param {Number/String} height The new height. This may be one of:
      + *
    • A Number specifying the new height in this Element's {@link #defaultUnit}s (by default, pixels)
    • + *
    • A String used to set the CSS height style. Animation may not be used.
    • + *
    + * @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: + * {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 (this 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 + *

    This class encapsulates a collection of DOM elements, providing methods to filter + * members, or to perform collective actions upon the whole set.

    + *

    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.

    + *

    All methods return this and can be chained.

    + * Usage: +
    
    +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);
    +
    + */ +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 + */ +Ext.define('Ext.util.Animate', { + + uses: ['Ext.fx.Manager', 'Ext.fx.Anim'], + + /** + *

    Perform custom animation on this object.

    + *

    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.

    + *

    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 to is mandatory.

    + *

    Properties include

      + *
    • from
      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.
    • + *
    • to
      An object which specifies end values for the properties being animated.
    • + *
    • duration
      The duration in milliseconds for which the animation will run.
    • + *
    • easing
      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:
        + *
      • ease
      • + *
      • easeIn
      • + *
      • easeOut
      • + *
      • easeInOut
      • + *
      • backIn
      • + *
      • backOut
      • + *
      • elasticIn
      • + *
      • elasticOut
      • + *
      • bounceIn
      • + *
      • bounceOut
      • + *
    • + *
    • keyframes
      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.
    • + *
    • listeners
      This is a standard {@link Ext.util.Observable#listeners listeners} configuration object which may be used + * to inject behaviour at either the beforeanimate event or the afteranimate event.
    • + *

    + *

    Animating an {@link Ext.Element Element}

    + * When animating an Element, the following properties may be specified in from, to, and keyframe objects:
      + *
    • x
      The page X position in pixels.
    • + *
    • y
      The page Y position in pixels
    • + *
    • left
      The element's CSS left value. Units must be supplied.
    • + *
    • top
      The element's CSS top value. Units must be supplied.
    • + *
    • width
      The element's CSS width value. Units must be supplied.
    • + *
    • height
      The element's CSS height value. Units must be supplied.
    • + *
    • scrollLeft
      The element's scrollLeft value.
    • + *
    • scrollTop
      The element's scrollLeft value.
    • + *
    • opacity
      The element's opacity value. This must be a value between 0 and 1.
    • + *
    + *

    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.

    + *

    Animating a {@link Ext.Component Component}

    + * When animating an Element, the following properties may be specified in from, to, and keyframe objects:
      + *
    • x
      The Component's page X position in pixels.
    • + *
    • y
      The Component's page Y position in pixels
    • + *
    • left
      The Component's left value in pixels.
    • + *
    • top
      The Component's top value in pixels.
    • + *
    • width
      The Component's width value in pixels.
    • + *
    • width
      The Component's width value in pixels.
    • + *
    • dynamic
      Specify as true to update the Component's layout (if it is a Container) at every frame + * of the animation. Use sparingly as laying out on every intermediate size change is an expensive operation.
    • + *
    + *

    For example, to animate a Window to a new size, ensuring that its internal layout, and any shadow is correct:

    + *
    
    +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,
    +        }
    +    });
    +});
    +
    + *

    For performance reasons, by default, the internal layout is only updated when the Window reaches its final "to" size. If dynamic updating of the Window's child + * Components is required, then configure the animation with dynamic: true and the two child items will maintain their proportions during the animation.

    + * @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 + *

    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.

    + * + *

    This class provides methods for encoding and decoding typed 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.

    + */ +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 'ext-' + */ + 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 :() + re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/, + method: filterByPseudo + }, { + // checks for {} + 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.

    + * @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 + *

    + * 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: + *

    
    +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);
    +});
    + * 
    + *

    + * + *

    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}. + *

    + */ +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 id 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

    The key to associate with the item, or the new item.

    + *

    If a {@link #getKey} implementation was specified for this HashMap, + * or if the key of the stored items is in a property called id, + * the HashMap will be able to derive the key for the new item. + * In this case just pass the new item in this parameter.

    + * @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, undefined 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: + *
      + *
    • key : String

      The key of the item

    • + *
    • value : Number

      The value of the item

    • + *
    • length : Number

      The total number of items in the hash

    • + *
    + * @param {Function} fn The function to execute. + * @param {Object} scope The scope to execute in. Defaults to this. + * @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 undefined 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: +
    
    +// in your initialization function
    +init : function(){
    +   Ext.state.Manager.setProvider(new Ext.state.CookieProvider());
    +   var win = new Window(...);
    +   win.restoreState();
    +}
    + 
    + * 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 + */ +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 + *

    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 {@link #stateId} 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.

    + *

    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.

    + *

    To set the state provider for the current page:

    + *
    
    +Ext.state.Manager.setProvider(new Ext.state.CookieProvider({
    +    expires: new Date(new Date().getTime()+(1000*60*60*24*7)), //7 days from now
    +}));
    +     * 
    + *

    A stateful object attempts to save state when one of the events + * listed in the {@link #stateEvents} configuration fires.

    + *

    To save state, a stateful object first serializes its state by + * calling {@link #getState}. 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.

    + *

    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 {@link #stateId}.

    + *

    During construction, a stateful object attempts to restore + * its state by calling {@link Ext.state.Manager#get} passing the + * {@link #stateId}

    + *

    The resulting object is passed to {@link #applyState}. + * The default implementation of {@link #applyState} simply copies + * properties into the object, but a developer may override this to support + * more behaviour.

    + *

    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.

    + */ + stateful: true, + + /** + * @cfg {String} stateId + * The unique id for this object to use for state management purposes. + *

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

    + */ + + /** + * @cfg {String[]} stateEvents + *

    An array of events that, when fired, should trigger this object to + * save its state. Defaults to none. stateEvents may be any type + * of event supported by this object, including browser or custom events + * (e.g., ['click', 'customerchange']).

    + *

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

    + */ + + /** + * @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 applyState. 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 applyState. 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 + * getState() 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 + * getState() 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 + *

    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}).

    + *

    This object also provides a registry of available Component classes + * indexed by a mnemonic code known as the Component's {@link Ext.Component#xtype xtype}. + * The xtype provides a way to avoid instantiating child Components + * when creating a full, nested config object for a complete Ext page.

    + *

    A child Component may be specified simply as a config object + * as long as the correct {@link Ext.Component#xtype xtype} is specified so that if and when the Component + * needs rendering, the correct type can be looked up for lazy instantiation.

    + *

    For a list of all available {@link Ext.Component#xtype xtypes}, see {@link Ext.Component}.

    + * @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 xtype. (Optional if the config contains a xtype). + * @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: [ + * '

    {title}

    ', + * '

    {desc}

    ' + * ], + * 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: [ + * '

    {title}

    ', + * '

    {msg}

    ', + * ], + * 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: [ + '', + '
    {parent.baseCls}-{parent.ui}-{.}-tl" style="background-position: {tl}; padding-left: {frameWidth}px" role="presentation">', + '
    {parent.baseCls}-{parent.ui}-{.}-tr" style="background-position: {tr}; padding-right: {frameWidth}px" role="presentation">', + '
    {parent.baseCls}-{parent.ui}-{.}-tc" style="background-position: {tc}; height: {frameWidth}px" role="presentation">
    ', + '
    ', + '
    ', + '
    ', + '
    {parent.baseCls}-{parent.ui}-{.}-ml" style="background-position: {ml}; padding-left: {frameWidth}px" role="presentation">', + '
    {parent.baseCls}-{parent.ui}-{.}-mr" style="background-position: {mr}; padding-right: {frameWidth}px" role="presentation">', + '
    {parent.baseCls}-{parent.ui}-{.}-mc" role="presentation">
    ', + '
    ', + '
    ', + '', + '
    {parent.baseCls}-{parent.ui}-{.}-bl" style="background-position: {bl}; padding-left: {frameWidth}px" role="presentation">', + '
    {parent.baseCls}-{parent.ui}-{.}-br" style="background-position: {br}; padding-right: {frameWidth}px" role="presentation">', + '
    {parent.baseCls}-{parent.ui}-{.}-bc" style="background-position: {bc}; height: {frameWidth}px" role="presentation">
    ', + '
    ', + '
    ', + '
    ' + ], + + frameTableTpl: [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '
    {parent.baseCls}-{parent.ui}-{.}-tl" style="background-position: {tl}; padding-left:{frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-tc" style="background-position: {tc}; height: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-tr" style="background-position: {tr}; padding-left: {frameWidth}px" role="presentation">
    {parent.baseCls}-{parent.ui}-{.}-ml" style="background-position: {ml}; padding-left: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-mc" style="background-position: 0 0;" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-mr" style="background-position: {mr}; padding-left: {frameWidth}px" role="presentation">
    {parent.baseCls}-{parent.ui}-{.}-bl" style="background-position: {bl}; padding-left: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-bc" style="background-position: {bc}; height: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-br" style="background-position: {br}; padding-left: {frameWidth}px" role="presentation">
    ' + ], + + /** + * @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 `
    ` Element or the id of the `` + * 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 `` element temporarily modified to have its [target][] set to refer to a dynamically + * generated, hidden `', + { + 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: [ + '' + ], + 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('', 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 = '
    ' + html + '
    '; + } + } + 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('
    '); + 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','

    '); + 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 + * + * 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 + * + *

    This class is used only by the grid's HeaderContainer docked child.

    + * + *

    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.

    + * + *

    Also, after every layout, after all headers have attained their 'stretchmax' height, it goes through and calls + * setPadding on the columns so that they lay out correctly.

    + */ +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: [ + '
    ', + '
    ', + '
    ' + ], + + 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: [ + // '', + // '
    ', + // '
    ' + //], + /** + * @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 = '
    '; + 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 true to stripe the rows. Default is true. + *

    This causes the CSS class x-grid-row-alt 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 background-color style using the '!important' + * modifier, or which uses a CSS selector of higher specificity.

    + */ + 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 + * ``, 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('{1}', 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: [ + '
    ', + '
    ', + '
    ', + '
    ', + '
    ' + ], + + 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 + '
    ' : '', + errors = []; + + Ext.Array.forEach(me.query('>[isFormField]'), function(field) { + errors = errors.concat( + Ext.Array.map(field.getErrors(), function(e) { + return '
  • ' + e + '
  • '; + }) + ); + }, me); + + return dirtyText + '
      ' + errors.join('') + '
    '; + }, + + 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, + + /** + * true 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 grid columns 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 visible columns in the grid. This goes down to the lowest column header + * level, and does not return grouped 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 grouped 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: + '
    ' + + '' + + '{text}' + + '' + + ''+ + '
    '+ + '
    ' + + '
    ', + + /** + * @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 false to prevent the column from being resizable. Defaults to true + */ + + /** + * @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: '
    ', + 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 false 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: '
    ', + 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 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 += '' + (item.altText || me.altText) + ''; + } + 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 true. + */ + 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 '
    '; + }, + closeTableWrap: function() { + return '
    '; + } + }; + } +}); + +/** + * @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} hideGroupedHeadertrue to hide the header that is currently grouped. + */ + hideGroupedHeader : false, + + /** + * @cfg {Boolean} startCollapsed true to start all groups collapsed + */ + startCollapsed : false, + + /** + * @cfg {Boolean} enableGroupingMenu true to enable the grouping control in the header menu + */ + enableGroupingMenu : true, + + /** + * @cfg {Boolean} enableNoGroups true 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 [ + '', + // group row tpl + '', + // this is the rowbody + '', + '' + ].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 ''; + }, + + // injected into rowtpl and wrapped around metaRowTpl + // becomes part of the standard tpl + closeRow: function() { + return ''; + }, + + // 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 so we can attach it + tpl = tpl.replace('', ''); + tpl += '{[this.printSummaryRow(xindex)]}'; + } + 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 [ + '', + '', + '' + ].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('
    {collapsed}' + me.groupHeaderTpl + '
    {[this.recurse(values)]}
    ', + '
    {rowBody}
    ', + '
    '); + // 1 + metaRowTpl.unshift('
    '); + + // 3 + metaRowTpl.push('
    '); + // 4 + metaRowTpl.push(''); + }, + + 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 '{[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: + *
    
    +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'
    +    }
    +});
    +
    + */ + + /** + * @cfg {Object} source A data object to use as the data source of the grid (see {@link #setSource} for details). + */ + + /** + * @cfg {Object} customRenderers An object containing name/value pairs of custom renderer type definitions that allow + * the grid to support custom rendering of fields. By default, the grid supports strongly-typed rendering + * of strings, dates, numbers and booleans using built-in form editors, but any custom type can be supported and + * associated with the type of the value. The name of the renderer type should correspond with the name of the property + * that it will render. Example usage: + *
    
    +var grid = Ext.create('Ext.grid.property.Grid', {
    +    customRenderers: {
    +        Available: function(v){
    +            if (v) {
    +                return 'Yes';
    +            } else {
    +                return 'No';
    +            }
    +        }
    +    },
    +    source: {
    +        Available: true
    +    }
    +});
    +
    + */ + + /** + * @cfg {String} valueField + * Optional. The name of the field from the property store to use as the value field name. Defaults to 'value' + * 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 'name' + * 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 115. + */ + + // 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: + *
    
    +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"
    +});
    +
    + * @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 false. + */ + setProperty: function(prop, value, create) { + this.propStore.setValue(prop, value, create); + }, + + /** + * Removes a property from the grid. + * @param {String} prop The name of the property to remove + */ + removeProperty: function(prop) { + this.propStore.remove(prop); + } + + /** + * @cfg store + * @hide + */ + /** + * @cfg colModel + * @hide + */ + /** + * @cfg cm + * @hide + */ + /** + * @cfg columns + * @hide + */ +}); +/** + * @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: + *
    
    +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);
    +
    + * @constructor + * @param {Object} config A data object in the format:
    
    +{
    +    name: [name],
    +    value: [value]
    +}
    + * 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 + *

    Sizes the Component's body element to fit exactly within the content box of the Component's encapsulating element.

    + */ + 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 true. + */ + 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, not 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 true 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. One 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 {@link #performCollapse} false. + */ +Ext.define('Ext.resizer.Splitter', { + extend: 'Ext.Component', + requires: ['Ext.XTemplate'], + uses: ['Ext.resizer.SplitterTracker'], + alias: 'widget.splitter', + + renderTpl: [ + '', + '

     
    ', + '' + ], + + baseCls: Ext.baseCSSPrefix + 'splitter', + collapsedClsInternal: Ext.baseCSSPrefix + 'splitter-collapsed', + + /** + * @cfg {Boolean} collapsible + * true 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 + *

    Set to false to prevent this Splitter's mini-collapse tool from managing the collapse + * state of the {@link #collapseTarget}.

    + */ + + /** + * @cfg {Boolean} collapseOnDblClick + * true 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 + *

    A string describing the relative position of the immediate sibling Panel to collapse. May be 'prev' or 'next' (Defaults to 'next')

    + *

    Or the immediate sibling Panel to collapse.

    + *

    The orientation of the mini-collapse tool will be inferred from this setting.

    + *

    Note that only Panels may be collapsed.

    + */ + collapseTarget: 'next', + + /** + * @property orientation + * @type String + * Orientation of this Splitter. 'vertical' when used in an hbox layout, 'horizontal' + * 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: '

    Welcome to the Wizard!

    Step 1 of 3

    ' + * },{ + * id: 'card-1', + * html: '

    Step 2 of 3

    ' + * },{ + * id: 'card-2', + * html: '

    Congratulations!

    Step 3 of 3 - Complete

    ' + * }], + * 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 `` 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 elements. + */ + + /** + * @cfg {Object} tdAttrs + * An object containing properties which are added to the {@link Ext.DomHelper DomHelper} specification used to + * create the layout's '; + } + + nodeEl = Ext.get(node); + tmpEl = nodeEl.insertSibling({ + tag: 'tr', + html: [ + '' + ].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 ('/'). + * @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 false. 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, get() returns null. The following + * example retrieves the cookie called "valid" and stores the String value + * in the variable validStatus. + *
    
    +     * var validStatus = Ext.util.Cookies.get("valid");
    +     * 
    + * @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: + * + * + * + * + * + * + * @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 = '
    ' + + Ext.util.Format.htmlEncode(token) + + '
    '; + + 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()]}', + '
    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: [ + '', + '{text}', + '', + '', + 'target="{hrefTarget}" hidefocus="true" unselectable="on">', + '', + 'style="margin-right: 17px;" >{text}', + '', + '', + '', + '', + '' + ], + + 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: [''], + + /** + * @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: + * + * - close + * - minimize + * - maximize + * - restore + * - toggle + * - gear + * - prev + * - next + * - pin + * - unpin + * - right + * - left + * - down + * - up + * - refresh + * - plus + * - minus + * - search + * - save + * - help + * - print + * - expand + * - 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 true 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 '
     
    '; + }, + + // 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: [ + '
    ', + '', + '
    ', + { + 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 + * + *

    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}

    + */ +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: [ + '
    {bodyCls} {baseCls}-body-{ui} {parent.baseCls}-body-{parent.ui}-{.}" style="{bodyStyle}">
    ', + '
    {baseCls}-strip-{ui} {parent.baseCls}-strip-{parent.ui}-{.}">
    ' + ], + + /** + * @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 = '', + checkboxText= '', + 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('', formattedValue, ''); + } 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 += '
    ', + '
    ', + '', + thHtml, + '
    ', + '
    ', + '
    ', + '', + '', + '', + '', + '', + '', + '{[this.openRows()]}', + '{row}', + '', + '{[this.embedFeature(values, parent, xindex, xcount)]}', + '', + '{[this.closeRows()]}', + '', + '
    ', + '{[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 ''; + }, + + closeRows: function() { + return ''; + }, + + metaRowTpl: [ + '', + '', + '
    {{id}}
    ', + '
    ', + '' + ], + + 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; + + } +}); + + + +