Skip to content

Instantly share code, notes, and snippets.

@mravey
Created August 29, 2012 12:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mravey/3511876 to your computer and use it in GitHub Desktop.
Save mravey/3511876 to your computer and use it in GitHub Desktop.
Change each bar color in Touch Charts
Ext.setup({
onReady: function() {
var data = [];
for (var i = 0; i < 10; i++) {
data.push({
x: i,
y: parseInt(Math.random() * 100)
});
}
var colors = ['blue', 'yellow', 'red', 'green', 'gray'];
var store = new Ext.data.JsonStore({
fields: ['x', 'y'],
data: data
});
var chart = new Ext.chart.Chart({
store: store,
axes: [{
type: 'Category',
fields: ['x'],
position: 'left'
}, {
type: 'Numeric',
fields: ['y'],
position: 'bottom'
}],
series: [{
type: 'bar',
xField: 'x',
yField: 'y',
axis: 'bottom',
renderer: function(sprite, record, attributes, index, store) {
attributes.fill = colors[index%colors.length];
return attributes;
}
}]
});
new Ext.chart.Panel({
fullscreen: true,
chart: chart
});
chart.redraw();
}
});
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" type="text/css" href="sencha-touch.css" media="all">
<link rel="stylesheet" type="text/css" href="touch-charts-full.css" media="all">
<script type="text/javascript" src="sencha-touch-debug.js"></script>
<script type="text/javascript" src="touch-charts-debug.js"></script>
<script type="text/javascript" src="app.js"></script>
</head>
<body>
</body>
</html>
This file has been truncated, but you can view the full file.
/*
This file is part of Sencha Touch 2
Copyright (c) 2012 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 },
emptyFn = function(){},
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: emptyFn,
baseCSSPrefix: Ext.buildSettings.baseCSSPrefix,
/**
* Copies all the properties of config to object if they don't already exist.
* @param {Object} object The receiver of the properties
* @param {Object} config The source of the properties
* @return {Object} returns obj
*/
applyIf: function(object, config) {
var property;
if (object) {
for (property in config) {
if (object[property] === undefined) {
object[property] = config[property];
}
}
}
return object;
},
/**
* Iterates either an array or an object. This method delegates to
* {@link Ext.Array#each Ext.Array.each} if the given value is iterable, and {@link Ext.Object#each Ext.Object.each} otherwise.
*
* @param {Object/Array} object The object or array to be iterated.
* @param {Function} fn The function to be called for each iteration. See and {@link Ext.Array#each Ext.Array.each} and
* {@link Ext.Object#each Ext.Object.each} for detailed lists of arguments passed to this function depending on the given object
* type that is being iterated.
* @param {Object} scope (Optional) The scope (`this` reference) in which the specified function is executed.
* Defaults to the object being iterated itself.
* @markdown
*/
iterate: function(object, fn, scope) {
if (Ext.isEmpty(object)) {
return;
}
if (scope === undefined) {
scope = object;
}
if (Ext.isIterable(object)) {
Ext.Array.each.call(Ext.Array, object, fn, scope);
}
else {
Ext.Object.each.call(Ext.Object, object, fn, scope);
}
}
});
Ext.apply(Ext, {
/**
* This method deprecated. Use {@link Ext#define Ext.define} instead.
* @method
* @param {Function} superclass
* @param {Object} overrides
* @return {Function} The subclass constructor from the <tt>overrides</tt> parameter, or a generated one if not provided.
* @deprecated 4.0.0 Please 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);
};
}
if (!superclass) {
Ext.Error.raise({
sourceClass: 'Ext',
sourceMethod: 'extend',
msg: 'Attempting to extend from a class which has not been loaded on the page.'
});
}
// 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.
*
* @param {Object} cls The class to override
* @param {Object} overrides The properties to add to origClass. This should be specified as an object literal
* containing one or more properties.
* @method override
* @markdown
* @deprecated 4.1.0 Please use {@link Ext#define Ext.define} instead
*/
override: function(cls, overrides) {
if (cls.$isClass) {
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';
}
Ext.Error.raise({
sourceClass: 'Ext',
sourceMethod: 'typeOf',
msg: 'Failed to determine the type of the specified value "' + value + '". This is most likely a bug.'
});
},
/**
* 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]';
},
/**
* @private
*/
isSimpleObject: function(value) {
return value instanceof Object && value.constructor === Object;
},
/**
* Returns true if the passed value is a JavaScript 'primitive', a string, number or boolean.
* @param {Object} value The value to test
* @return {Boolean}
*/
isPrimitive: function(value) {
var type = typeof value;
return type === 'string' || type === 'number' || type === 'boolean';
},
/**
* Returns true if the passed value is a JavaScript Function, false otherwise.
* @param {Object} value The value to test
* @return {Boolean}
* @method
*/
isFunction:
// Safari 3.x and 4.x returns 'function' for typeof <NodeList>, hence we need to fall back to using
// Object.prorotype.toString (slower)
(typeof document !== 'undefined' && typeof document.getElementsByTagName('body') === 'function') ? function(value) {
return toString.call(value) === '[object Function]';
} : function(value) {
return typeof value === 'function';
},
/**
* Returns true if the passed value is a number. Returns false for non-finite numbers.
* @param {Object} value The value to test
* @return {Boolean}
*/
isNumber: function(value) {
return typeof value === 'number' && isFinite(value);
},
/**
* Validates that a value is numeric.
* @param {Object} value Examples: 1, '1', '2.34'
* @return {Boolean} True if numeric, false otherwise
*/
isNumeric: function(value) {
return !isNaN(parseFloat(value)) && isFinite(value);
},
/**
* Returns true if the passed value is a string.
* @param {Object} value The value to test
* @return {Boolean}
*/
isString: function(value) {
return typeof value === 'string';
},
/**
* Returns true if the passed value is a boolean.
*
* @param {Object} value The value to test
* @return {Boolean}
*/
isBoolean: function(value) {
return typeof value === 'boolean';
},
/**
* Returns true if the passed value is an HTMLElement
* @param {Object} value The value to test
* @return {Boolean}
*/
isElement: function(value) {
return value ? value.nodeType === 1 : false;
},
/**
* Returns true if the passed value is a TextNode
* @param {Object} value The value to test
* @return {Boolean}
*/
isTextNode: function(value) {
return value ? value.nodeName === "#text" : false;
},
/**
* Returns true if the passed value is defined.
* @param {Object} value The value to test
* @return {Boolean}
*/
isDefined: function(value) {
return typeof value !== 'undefined';
},
/**
* Returns true if the passed value is iterable, false otherwise
* @param {Object} value The value to test
* @return {Boolean}
*/
isIterable: function(value) {
return (value && typeof value !== 'string') ? value.length !== undefined : false;
}
});
Ext.apply(Ext, {
/**
* Clone almost any type of variable including array, object, DOM nodes and Date without keeping the old reference
* @param {Object} item The variable to clone
* @return {Object} clone
*/
clone: function(item) {
if (item === null || item === undefined) {
return item;
}
// DOM nodes
if (item.nodeType && item.cloneNode) {
return item.cloneNode(true);
}
// Strings
var type = toString.call(item);
// Dates
if (type === '[object Date]') {
return new Date(item.getTime());
}
var i, j, k, clone, key;
// Arrays
if (type === '[object Array]') {
i = item.length;
clone = [];
while (i--) {
clone[i] = Ext.clone(item[i]);
}
}
// Objects
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),
ln = args.length;
if (ln > 0) {
args[ln - 1] = 'var Ext=window.' + this.getUniqueGlobalNamespace() + ';' + args[ln - 1];
}
return Function.prototype.constructor.apply(Function.prototype, args);
},
/**
* @private
*/
globalEval: ('execScript' in global) ? function(code) {
global.execScript(code)
} : function(code) {
(function(){
eval(code);
})();
},
/**
* @private
* @property
*/
Logger: {
log: function(message, priority) {
if ('console' in global) {
if (!priority || !(priority in global.console)) {
priority = 'log';
}
message = '[' + priority.toUpperCase() + '] ' + message;
global.console[priority](message);
}
},
verbose: function(message) {
this.log(message, 'verbose');
},
info: function(message) {
this.log(message, 'info');
},
warn: function(message) {
this.log(message, 'warn');
},
error: function(message) {
throw new Error(message);
},
deprecate: function(message) {
this.log(message, 'warn');
}
}
});
/**
* Old alias to {@link Ext#typeOf}
* @deprecated 4.0.0 Please use {@link Ext#typeOf} instead
* @method
* @alias Ext#typeOf
*/
Ext.type = Ext.typeOf;
})();
/**
* @author Jacky Nguyen <jacky@sencha.com>
* @docauthor Jacky Nguyen <jacky@sencha.com>
* @class Ext.Version
*
* A utility class that wrap around a string version number and provide convenient
* method to perform comparison. See also: {@link Ext.Version#compare compare}. Example:
var version = new Ext.Version('1.0.2beta');
console.log("Version is " + version); // Version is 1.0.2beta
console.log(version.getMajor()); // 1
console.log(version.getMinor()); // 0
console.log(version.getPatch()); // 2
console.log(version.getBuild()); // 0
console.log(version.getRelease()); // beta
console.log(version.isGreaterThan('1.0.1')); // True
console.log(version.isGreaterThan('1.0.2alpha')); // True
console.log(version.isGreaterThan('1.0.2RC')); // False
console.log(version.isGreaterThan('1.0.2')); // False
console.log(version.isLessThan('1.0.2')); // True
console.log(version.match(1.0)); // True
console.log(version.match('1.0.2')); // True
* @markdown
*/
(function() {
// Current core version
var version = '4.1.0', Version;
Ext.Version = Version = Ext.extend(Object, {
/**
* Creates new Version 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 toNumber = this.toNumber,
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 = toNumber(parts.shift());
this.minor = toNumber(parts.shift());
this.patch = toNumber(parts.shift());
this.build = toNumber(parts.shift());
return this;
},
toNumber: function(value) {
value = parseInt(value || 0, 10);
if (isNaN(value)) {
value = 0;
}
return value;
},
/**
* 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 greater than or equal to the supplied argument
* @param {String/Number} target The version to compare with
* @return {Boolean} True if this version if greater than or equal to the target, false otherwise
*/
isGreaterThanOrEqual: function(target) {
return Version.compare(this.version, target) >= 0;
},
/**
* 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 if less than or equal to the supplied argument
* @param {String/Number} target The version to compare with
* @return {Boolean} True if this version if less than or equal to the target, false otherwise
*/
isLessThanOrEqual: function(target) {
return Version.compare(this.version, target) <= 0;
},
/**
* Returns whether this version equals to the supplied argument
* @param {String/Number} target The version to compare with
* @return {Boolean} True if this version equals to the target, false otherwise
*/
equals: function(target) {
return Version.compare(this.version, target) === 0;
},
/**
* Returns whether this version matches the supplied argument. Example:
* <pre><code>
* var version = new Ext.Version('1.0.2beta');
* console.log(version.match(1)); // True
* console.log(version.match(1.0)); // True
* console.log(version.match('1.0.2')); // True
* console.log(version.match('1.0.2RC')); // False
* </code></pre>
* @param {String/Number} target The version to compare with
* @return {Boolean} True if this version matches the target, false otherwise
*/
match: function(target) {
target = String(target);
return this.version.substr(0, target.length) === target;
},
/**
* Returns this format: [major, minor, patch, build, release]. Useful for comparison
* @return {Number[]}
*/
toArray: function() {
return [this.getMajor(), this.getMinor(), this.getPatch(), this.getBuild(), this.getRelease()];
},
/**
* Returns shortVersion version without dots and release
* @return {String}
*/
getShortVersion: function() {
return this.shortVersion;
},
/**
* Convenient alias to {@link Ext.Version#isGreaterThan isGreaterThan}
* @param {String/Number} target
* @return {Boolean}
*/
gt: function() {
return this.isGreaterThan.apply(this, arguments);
},
/**
* Convenient alias to {@link Ext.Version#isLessThan isLessThan}
* @param {String/Number} target
* @return {Boolean}
*/
lt: function() {
return this.isLessThan.apply(this, arguments);
},
/**
* Convenient alias to {@link Ext.Version#isGreaterThanOrEqual isGreaterThanOrEqual}
* @param {String/Number} target
* @return {Boolean}
*/
gtEq: function() {
return this.isGreaterThanOrEqual.apply(this, arguments);
},
/**
* Convenient alias to {@link Ext.Version#isLessThanOrEqual isLessThanOrEqual}
* @param {String/Number} target
* @return {Boolean}
*/
ltEq: function() {
return this.isLessThanOrEqual.apply(this, arguments);
}
});
Ext.apply(Version, {
// @private
releaseValueMap: {
'dev': -6,
'alpha': -5,
'a': -5,
'beta': -4,
'b': -4,
'rc': -3,
'#': -2,
'p': -1,
'pl': -1
},
/**
* Converts a version component to a comparable value
*
* @static
* @param {Object} value The value to convert
* @return {Object}
*/
getComponentValue: function(value) {
return !value ? 0 : (isNaN(value) ? this.releaseValueMap[value] || value : parseInt(value, 10));
},
/**
* Compare 2 specified versions, starting from left to right. If a part contains special version strings,
* they are handled in the following order:
* 'dev' < 'alpha' = 'a' < 'beta' = 'b' < 'RC' = 'rc' < '#' < 'pl' = 'p' < 'anything else'
*
* @static
* @param {String} current The current version to compare to
* @param {String} target The target version to compare to
* @return {Number} Returns -1 if the current version is smaller than the target version, 1 if greater, and 0 if they're equivalent
*/
compare: function(current, target) {
var currentValue, targetValue, i;
current = new Version(current).toArray();
target = new Version(target).toArray();
for (i = 0; i < Math.max(current.length, target.length); i++) {
currentValue = this.getComponentValue(current[i]);
targetValue = this.getComponentValue(target[i]);
if (currentValue < targetValue) {
return -1;
} else if (currentValue > targetValue) {
return 1;
}
}
return 0;
}
});
Ext.apply(Ext, {
/**
* @private
*/
versions: {},
/**
* @private
*/
lastRegisteredVersion: null,
/**
* Set version number for the given package name.
*
* @param {String} packageName The package name, for example: 'core', 'touch', 'extjs'
* @param {String/Ext.Version} version The version, for example: '1.2.3alpha', '2.4.0-dev'
* @return {Ext}
*/
setVersion: function(packageName, version) {
Ext.versions[packageName] = new Version(version);
Ext.lastRegisteredVersion = Ext.versions[packageName];
return this;
},
/**
* Get the version number of the supplied package name; will return the last registered version
* (last Ext.setVersion call) if there's no package name given.
*
* @param {String} packageName (Optional) The package name, for example: 'core', 'touch', 'extjs'
* @return {Ext.Version} The version
*/
getVersion: function(packageName) {
if (packageName === undefined) {
return Ext.lastRegisteredVersion;
}
return Ext.versions[packageName];
},
/**
* Create a closure for deprecated code.
*
// This means Ext.oldMethod is only supported in 4.0.0beta and older.
// If Ext.getVersion('extjs') returns a version that is later than '4.0.0beta', for example '4.0.0RC',
// the closure will not be invoked
Ext.deprecate('extjs', '4.0.0beta', function() {
Ext.oldMethod = Ext.newMethod;
...
});
* @param {String} packageName The package name
* @param {String} since The last version before it's deprecated
* @param {Function} closure The callback function to be executed with the specified version is less than the current version
* @param {Object} scope The execution scope (<tt>this</tt>) if the closure
* @markdown
*/
deprecate: function(packageName, since, closure, scope) {
if (Version.compare(Ext.getVersion(packageName), since) < 1) {
closure.call(scope);
}
}
}); // End Versioning
Ext.setVersion('core', version);
})();
/**
* @class Ext.String
*
* A collection of useful static methods to deal with strings
* @singleton
*/
Ext.String = {
trimRegex: /^[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+|[\x09\x0a\x0b\x0c\x0d\x20\xa0\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000]+$/g,
escapeRe: /('|\\)/g,
formatRe: /\{(\d+)\}/g,
escapeRegexRe: /([-.*+?^${}()|[\]\/\\])/g,
/**
* Convert certain characters (&, <, >, and ") to their HTML character equivalents for literal display in web pages.
* @param {String} value The string to encode
* @return {String} The encoded text
* @method
*/
htmlEncode: (function() {
var entities = {
'&': '&amp;',
'>': '&gt;',
'<': '&lt;',
'"': '&quot;'
}, 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 = {
'&amp;': '&',
'&gt;': '>',
'&lt;': '<',
'&quot;': '"'
}, keys = [], p, regex;
for (p in entities) {
keys.push(p);
}
regex = new RegExp('(' + keys.join('|') + '|&#[0-9]{1,5};' + ')', 'g');
return function(value) {
return (!value) ? value : String(value).replace(regex, function(match, capture) {
if (capture in entities) {
return entities[capture];
} else {
return String.fromCharCode(parseInt(capture.substr(2), 10));
}
});
};
})(),
/**
* Appends content to the query string of a URL, handling logic for whether to place
* a question mark or ampersand.
* @param {String} url The URL to append to.
* @param {String} string The content to append to the URL.
* @return (String) The resulting URL
*/
urlAppend : function(url, string) {
if (!Ext.isEmpty(string)) {
return url + (url.indexOf('?') === -1 ? '?' : '&') + string;
}
return url;
},
/**
* Trims whitespace from either end of a string, leaving spaces within the string intact. Example:
* @example
var s = ' foo bar ';
alert('-' + s + '-'); //alerts "- foo bar -"
alert('-' + Ext.String.trim(s) + '-'); //alerts "-foo bar-"
* @param {String} string The string to escape
* @return {String} The trimmed string
*/
trim: function(string) {
return string.replace(Ext.String.trimRegex, "");
},
/**
* Capitalize the given string
* @param {String} string
* @return {String}
*/
capitalize: function(string) {
return string.charAt(0).toUpperCase() + string.substr(1);
},
/**
* Truncate a string and add an ellipsis ('...') to the end if it exceeds the specified length
* @param {String} value The string to truncate
* @param {Number} length The maximum length to allow before truncating
* @param {Boolean} word True to try to find a common word break
* @return {String} The converted text
*/
ellipsis: function(value, len, word) {
if (value && value.length > len) {
if (word) {
var vs = value.substr(0, len - 2),
index = Math.max(vs.lastIndexOf(' '), vs.lastIndexOf('.'), vs.lastIndexOf('!'), vs.lastIndexOf('?'));
if (index !== -1 && index >= (len - 15)) {
return vs.substr(0, index) + "...";
}
}
return value.substr(0, len - 3) + "...";
}
return value;
},
/**
* Escapes the passed string for use in a regular expression
* @param {String} string
* @return {String}
*/
escapeRegex: function(string) {
return string.replace(Ext.String.escapeRegexRe, "\\$1");
},
/**
* Escapes the passed string for ' and \
* @param {String} string The string to escape
* @return {String} The escaped string
*/
escape: function(string) {
return string.replace(Ext.String.escapeRe, "\\$1");
},
/**
* Utility function that allows you to easily switch a string between two alternating values. The passed value
* is compared to the current string, and if they are equal, the other value that was passed in is returned. If
* they are already different, the first value passed in is returned. Note that this method returns the new value
* but does not change the current string.
* <pre><code>
// alternate sort directions
sort = Ext.String.toggle(sort, 'ASC', 'DESC');
// instead of conditional logic:
sort = (sort == 'ASC' ? 'DESC' : 'ASC');
</code></pre>
* @param {String} string The current string
* @param {String} value The value to compare to the current string
* @param {String} other The new value to use if the string already equals the first value passed in
* @return {String} The new value
*/
toggle: function(string, value, other) {
return string === value ? other : value;
},
/**
* Pads the left side of a string with a specified character. This is especially useful
* for normalizing number and date strings. Example usage:
*
* <pre><code>
var s = Ext.String.leftPad('123', 5, '0');
// s now contains the string: '00123'
</code></pre>
* @param {String} string The original string
* @param {Number} size The total length of the output string
* @param {String} character (optional) The character with which to pad the original string (defaults to empty string " ")
* @return {String} The padded string
*/
leftPad: function(string, size, character) {
var result = String(string);
character = character || " ";
while (result.length < size) {
result = character + result;
}
return result;
},
/**
* Allows you to define a tokenized string and pass an arbitrary number of arguments to replace the tokens. Each
* token must be unique, and must increment in the format {0}, {1}, etc. Example usage:
* <pre><code>
var cls = 'my-class', text = 'Some text';
var s = Ext.String.format('&lt;div class="{0}">{1}&lt;/div>', cls, text);
// s now contains the string: '&lt;div class="my-class">Some text&lt;/div>'
</code></pre>
* @param {String} string The tokenized string to be formatted
* @param {String} value1 The value to replace token {0}
* @param {String} value2 Etc...
* @return {String} The formatted string
*/
format: function(format) {
var args = Ext.Array.toArray(arguments, 1);
return format.replace(Ext.String.formatRe, function(m, i) {
return args[i];
});
},
/**
* Returns a string with a specified number of repititions a given string pattern.
* The pattern be separated by a different string.
*
* var s = Ext.String.repeat('---', 4); // = '------------'
* var t = Ext.String.repeat('--', 3, '/'); // = '--/--/--'
*
* @param {String} pattern The pattern to repeat.
* @param {Number} count The number of times to repeat the pattern (may be 0).
* @param {String} sep An option string to separate each pattern.
*/
repeat: function(pattern, count, sep) {
for (var buf = [], i = count; i--; ) {
buf.push(pattern);
}
return buf.join(sep || '');
}
};
/**
* Old alias to {@link Ext.String#htmlEncode}
* @deprecated Use {@link Ext.String#htmlEncode} instead
* @method
* @member Ext
* @alias Ext.String#htmlEncode
*/
Ext.htmlEncode = Ext.String.htmlEncode;
/**
* Old alias to {@link Ext.String#htmlDecode}
* @deprecated Use {@link Ext.String#htmlDecode} instead
* @method
* @member Ext
* @alias Ext.String#htmlDecode
*/
Ext.htmlDecode = Ext.String.htmlDecode;
/**
* Old alias to {@link Ext.String#urlAppend}
* @deprecated Use {@link Ext.String#urlAppend} instead
* @method
* @member Ext
* @alias Ext.String#urlAppend
*/
Ext.urlAppend = Ext.String.urlAppend;
/**
* @class Ext.Array
* @singleton
* @author Jacky Nguyen <jacky@sencha.com>
* @docauthor Jacky Nguyen <jacky@sencha.com>
*
* A set of useful static methods to deal with arrays; provide missing methods for older browsers.
*/
(function() {
var arrayPrototype = Array.prototype,
slice = arrayPrototype.slice,
supportsSplice = function () {
var array = [],
lengthBefore,
j = 20;
if (!array.splice) {
return false;
}
// This detects a bug in IE8 splice method:
// see http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/
while (j--) {
array.push("A");
}
array.splice(15, 0, "F", "F", "F", "F", "F","F","F","F","F","F","F","F","F","F","F","F","F","F","F","F","F");
lengthBefore = array.length; //41
array.splice(13, 0, "XXX"); // add one element
if (lengthBefore+1 != array.length) {
return false;
}
// end IE8 bug
return true;
}(),
supportsForEach = 'forEach' in arrayPrototype,
supportsMap = 'map' in arrayPrototype,
supportsIndexOf = 'indexOf' in arrayPrototype,
supportsEvery = 'every' in arrayPrototype,
supportsSome = 'some' in arrayPrototype,
supportsFilter = 'filter' in arrayPrototype,
supportsSort = function() {
var a = [1,2,3,4,5].sort(function(){ return 0; });
return a[0] === 1 && a[1] === 2 && a[2] === 3 && a[3] === 4 && a[4] === 5;
}(),
supportsSliceOnNodeList = true,
ExtArray;
try {
// IE 6 - 8 will throw an error when using Array.prototype.slice on NodeList
if (typeof document !== 'undefined') {
slice.call(document.getElementsByTagName('body'));
}
} catch (e) {
supportsSliceOnNodeList = false;
}
function fixArrayIndex (array, index) {
return (index < 0) ? Math.max(0, array.length + index)
: Math.min(array.length, index);
}
/*
Does the same work as splice, but with a slightly more convenient signature. The splice
method has bugs in IE8, so this is the implementation we use on that platform.
The rippling of items in the array can be tricky. Consider two use cases:
index=2
removeCount=2
/=====\
+---+---+---+---+---+---+---+---+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+---+---+---+---+---+---+---+---+
/ \/ \/ \/ \
/ /\ /\ /\ \
/ / \/ \/ \ +--------------------------+
/ / /\ /\ +--------------------------+ \
/ / / \/ +--------------------------+ \ \
/ / / /+--------------------------+ \ \ \
/ / / / \ \ \ \
v v v v v v v v
+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+
| 0 | 1 | 4 | 5 | 6 | 7 | | 0 | 1 | a | b | c | 4 | 5 | 6 | 7 |
+---+---+---+---+---+---+ +---+---+---+---+---+---+---+---+---+
A B \=========/
insert=[a,b,c]
In case A, it is obvious that copying of [4,5,6,7] must be left-to-right so
that we don't end up with [0,1,6,7,6,7]. In case B, we have the opposite; we
must go right-to-left or else we would end up with [0,1,a,b,c,4,4,4,4].
*/
function replaceSim (array, index, removeCount, insert) {
var add = insert ? insert.length : 0,
length = array.length,
pos = fixArrayIndex(array, index);
// we try to use Array.push when we can for efficiency...
if (pos === length) {
if (add) {
array.push.apply(array, insert);
}
} else {
var remove = Math.min(removeCount, length - pos),
tailOldPos = pos + remove,
tailNewPos = tailOldPos + add - remove,
tailCount = length - tailOldPos,
lengthAfterRemove = length - remove,
i;
if (tailNewPos < tailOldPos) { // case A
for (i = 0; i < tailCount; ++i) {
array[tailNewPos+i] = array[tailOldPos+i];
}
} else if (tailNewPos > tailOldPos) { // case B
for (i = tailCount; i--; ) {
array[tailNewPos+i] = array[tailOldPos+i];
}
} // else, add == remove (nothing to do)
if (add && pos === lengthAfterRemove) {
array.length = lengthAfterRemove; // truncate array
array.push.apply(array, insert);
} else {
array.length = lengthAfterRemove + add; // reserves space
for (i = 0; i < add; ++i) {
array[pos+i] = insert[i];
}
}
}
return array;
}
function replaceNative (array, index, removeCount, insert) {
if (insert && insert.length) {
if (index < array.length) {
array.splice.apply(array, [index, removeCount].concat(insert));
} else {
array.push.apply(array, insert);
}
} else {
array.splice(index, removeCount);
}
return array;
}
function eraseSim (array, index, removeCount) {
return replaceSim(array, index, removeCount);
}
function eraseNative (array, index, removeCount) {
array.splice(index, removeCount);
return array;
}
function spliceSim (array, index, removeCount) {
var pos = fixArrayIndex(array, index),
removed = array.slice(index, fixArrayIndex(array, pos+removeCount));
if (arguments.length < 4) {
replaceSim(array, pos, removeCount);
} else {
replaceSim(array, pos, removeCount, slice.call(arguments, 3));
}
return removed;
}
function spliceNative (array) {
return array.splice.apply(array, slice.call(arguments, 1));
}
var erase = supportsSplice ? eraseNative : eraseSim,
replace = supportsSplice ? replaceNative : replaceSim,
splice = supportsSplice ? spliceNative : spliceSim;
// NOTE: from here on, use erase, replace or splice (not native methods)...
ExtArray = Ext.Array = {
/**
* Iterates an array or an iterable value and invoke the given callback function for each item.
*
* var countries = ['Vietnam', 'Singapore', 'United States', 'Russia'];
*
* Ext.Array.each(countries, function(name, index, countriesItSelf) {
* console.log(name);
* });
*
* var sum = function() {
* var sum = 0;
*
* Ext.Array.each(arguments, function(value) {
* sum += value;
* });
*
* return sum;
* };
*
* sum(1, 2, 3); // returns 6
*
* The iteration can be stopped by returning false in the function callback.
*
* Ext.Array.each(countries, function(name, index, countriesItSelf) {
* if (name === 'Singapore') {
* return false; // break here
* }
* });
*
* {@link Ext#each Ext.each} is alias for {@link Ext.Array#each Ext.Array.each}
*
* @param {Array/NodeList/Object} iterable The value to be iterated. If this
* argument is not iterable, the callback function is called once.
* @param {Function} fn The callback function. If it returns false, the iteration stops and this method returns
* the current `index`.
* @param {Object} fn.item The item at the current `index` in the passed `array`
* @param {Number} fn.index The current `index` within the `array`
* @param {Array} fn.allItems The `array` itself which was passed as the first argument
* @param {Boolean} fn.return Return false to stop iteration.
* @param {Object} scope (Optional) The scope (`this` reference) in which the specified function is executed.
* @param {Boolean} reverse (Optional) Reverse the iteration order (loop from the end to the beginning)
* Defaults false
* @return {Boolean} See description for the `fn` parameter.
*/
each: function(array, fn, scope, reverse) {
array = ExtArray.from(array);
var i,
ln = array.length;
if (reverse !== true) {
for (i = 0; i < ln; i++) {
if (fn.call(scope || array[i], array[i], i, array) === false) {
return i;
}
}
}
else {
for (i = ln - 1; i > -1; i--) {
if (fn.call(scope || array[i], array[i], i, array) === false) {
return i;
}
}
}
return true;
},
/**
* Iterates an array and invoke the given callback function for each item. Note that this will simply
* delegate to the native Array.prototype.forEach method if supported. It doesn't support stopping the
* iteration by returning false in the callback function like {@link Ext.Array#each}. However, performance
* could be much better in modern browsers comparing with {@link Ext.Array#each}
*
* @param {Array} array The array to iterate
* @param {Function} fn The callback function.
* @param {Object} fn.item The item at the current `index` in the passed `array`
* @param {Number} fn.index The current `index` within the `array`
* @param {Array} fn.allItems The `array` itself which was passed as the first argument
* @param {Object} scope (Optional) The execution scope (`this`) in which the specified function is executed.
*/
forEach: supportsForEach ? function(array, fn, scope) {
return array.forEach(fn, scope);
} : function(array, 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: (supportsIndexOf) ? function(array, item, from) {
return array.indexOf(item, from);
} : function(array, 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: supportsIndexOf ? function(array, item) {
return array.indexOf(item) !== -1;
} : function(array, item) {
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: supportsMap ? function(array, fn, scope) {
return array.map(fn, scope);
} : function(array, 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 (!fn) {
Ext.Error.raise('Ext.Array.every must have a callback function passed as second argument.');
}
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 (!fn) {
Ext.Error.raise('Ext.Array.some must have a callback function passed as second argument.');
}
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 ExtArray.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.
*/
slice: function(array, begin, end) {
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;
},
_replaceSim: replaceSim, // for unit testing
_spliceSim: spliceSim,
/**
* 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 Please 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 Please 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 Please 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 Please 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 Please 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 Please 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 Please 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 Please 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.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;
}
};
})();
/**
* This method is deprecated, please use {@link Ext.Number#from Ext.Number.from} instead
*
* @deprecated 4.0.0 Replaced by Ext.Number.from
* @member Ext
* @method num
*/
Ext.num = function() {
return Ext.Number.from.apply(this, arguments);
};
/**
* @author Jacky Nguyen <jacky@sencha.com>
* @docauthor Jacky Nguyen <jacky@sencha.com>
* @class Ext.Object
*
* A collection of useful static methods to deal with objects
*
* @singleton
*/
(function() {
// The "constructor" for chain:
var TemplateClass = function(){};
var ExtObject = Ext.Object = {
/**
* Returns a new object with the given object as the prototype chain.
* @param {Object} object The prototype chain for the new object.
*/
chain: function (object) {
TemplateClass.prototype = object;
var result = new TemplateClass();
TemplateClass.prototype = null;
return result;
},
/**
* Convert 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} value
* @param {Boolean} recursive
* @markdown
*/
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 (optional) Whether or not to interpret the object in recursive format.
* (PHP / Ruby on Rails servers and similar). Defaults to false
* @return {String} queryString
* @markdown
*/
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 (Optional) Whether or not to recursively decode the string. This format is supported by
* PHP / Ruby on Rails servers and similar. Defaults to false
* @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(/^([^\[]+)/);
if (!matchedName) {
throw new Error('[Ext.Object.fromQueryString] Malformed query string given, failed parsing name from "' + part + '"');
}
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;
},
/**
* Iterate through an object and invoke the given callback function for each iteration. The iteration can be stop
* 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. Passed arguments for each iteration are:
- {String} `key`
- {Mixed} `value`
- {Object} `object` The object itself
* @param {Object} scope (Optional) The execution scope (`this`) of the callback function
* @markdown
*/
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);
// 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} source ...
* @return {Object} merged The object that is created as a result of merging all the objects passed in.
* @markdown
*/
merge: function(source) {
var i = 1,
ln = arguments.length,
mergeFn = ExtObject.merge,
cloneFn = Ext.clone,
object, key, value, sourceKey;
for (; i < ln; i++) {
object = arguments[i];
for (key in object) {
value = object[key];
if (value && value.constructor === Object) {
sourceKey = source[key];
if (sourceKey && sourceKey.constructor === Object) {
mergeFn(sourceKey, value);
}
else {
source[key] = cloneFn(value);
}
}
else {
source[key] = value;
}
}
}
return source;
},
/**
* @private
* @param source
*/
mergeIf: function(source) {
var i = 1,
ln = arguments.length,
cloneFn = Ext.clone,
object, key, value;
for (; i < ln; i++) {
object = arguments[i];
for (key in object) {
if (!(key in source)) {
value = object[key];
if (value && value.constructor === Object) {
source[key] = cloneFn(value);
}
else {
source[key] = value;
}
}
}
}
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(sencha, 'food')); // alerts 'loves'
* @param {Object} object
* @param {Object} value The value to find
* @markdown
*/
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
* @markdown
*/
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) ? 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
* @markdown
*/
getSize: function(object) {
var size = 0,
property;
for (property in object) {
if (object.hasOwnProperty(property)) {
size++;
}
}
return size;
},
/**
* @private
*/
classify: function(object) {
var prototype = object,
objectProperties = [],
propertyClassesMap = {},
objectClass = function() {
var i = 0,
ln = objectProperties.length,
property;
for (; i < ln; i++) {
property = objectProperties[i];
this[property] = new propertyClassesMap[property];
}
},
key, value;
for (key in object) {
if (object.hasOwnProperty(key)) {
value = object[key];
if (value && value.constructor === Object) {
objectProperties.push(key);
propertyClassesMap[key] = ExtObject.classify(value);
}
}
}
objectClass.prototype = prototype;
return objectClass;
},
defineProperty: ('defineProperty' in Object) ? Object.defineProperty : function(object, name, descriptor) {
if (descriptor.get) {
object.__defineGetter__(name, descriptor.get);
}
if (descriptor.set) {
object.__defineSetter__(name, descriptor.set);
}
}
};
/**
* A convenient alias method for {@link Ext.Object#merge}
*
* @member Ext
* @method merge
*/
Ext.merge = Ext.Object.merge;
/**
* @private
*/
Ext.mergeIf = Ext.Object.mergeIf;
/**
* A convenient alias method for {@link Ext.Object#toQueryString}
*
* @member Ext
* @method urlEncode
* @deprecated 4.0.0 Please use {@link Ext.Object#toQueryString 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 + ExtObject.toQueryString.apply(ExtObject, args);
};
/**
* A convenient alias method for {@link Ext.Object#fromQueryString}
*
* @member Ext
* @method urlDecode
* @deprecated 4.0.0 Please use {@link Ext.Object#fromQueryString Ext.Object.fromQueryString} instead
*/
Ext.urlDecode = function() {
return ExtObject.fromQueryString.apply(ExtObject, arguments);
};
})();
/**
* @class Ext.Function
*
* A collection of useful static methods to deal with function callbacks
* @singleton
* @alternateClassName Ext.util.Functions
*/
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 (!Ext.isArray(args)) {
args = Ext.Array.clone(args);
}
return function() {
args.push.apply(args, arguments);
return fn.apply(scope || this, args);
};
},
/**
* 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);
};
},
/**
* Create a "clone" of the provided method. The returned method will call the given
* method passing along all arguments and the "this" pointer and return its result.
*
* @param {Function} method
* @return {Function} cloneFn
*/
clone: function(method) {
return function() {
return method.apply(this, 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,
args = Array.prototype.slice.call(arguments);
setTimeout(function() {
fn.apply(me, args);
}, 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, scope, args, appendArgs) {
fn = Ext.Function.bind(fn, scope, 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} originalFn 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(originalFn, newFn, scope) {
if (!newFn) {
return originalFn;
}
else {
return function() {
var result = originalFn.apply(this, arguments);
newFn.apply(scope || this, arguments);
return result;
};
}
},
/**
* 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) {
var timerId;
return function() {
if (!scope) {
scope = this;
}
if (!args) {
args = Array.prototype.slice.call(arguments);
}
if (timerId) {
clearTimeout(timerId);
timerId = null;
}
timerId = setTimeout(function(){
fn.apply(scope, args);
}, 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);
}
};
},
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;
};
},
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');
/**
* @class Ext.JSON
* Modified version of Douglas Crockford"s json.js that doesn"t
* mess with the Object prototype
* http://www.json.org/js.html
* @singleton
*/
Ext.JSON = new(function() {
var useHasOwn = !! {}.hasOwnProperty,
isNative = function() {
var useNative = null;
return function() {
if (useNative === null) {
useNative = Ext.USE_NATIVE_JSON && window.JSON && JSON.toString() == '[object JSON]';
}
return useNative;
};
}(),
pad = function(n) {
return n < 10 ? "0" + n : n;
},
doDecode = function(json) {
return eval("(" + json + ')');
},
doEncode = function(o) {
if (!Ext.isDefined(o) || o === null) {
return "null";
} else if (Ext.isArray(o)) {
return encodeArray(o);
} else if (Ext.isDate(o)) {
return Ext.JSON.encodeDate(o);
} else if (Ext.isString(o)) {
return encodeString(o);
} else if (typeof o == "number") {
//don't use isNumber here, since finite checks happen inside isNumber
return isFinite(o) ? String(o) : "null";
} else if (Ext.isBoolean(o)) {
return String(o);
} else if (Ext.isObject(o)) {
return encodeObject(o);
} else if (typeof o === "function") {
return "null";
}
return 'undefined';
},
m = {
"\b": '\\b',
"\t": '\\t',
"\n": '\\n',
"\f": '\\f',
"\r": '\\r',
'"': '\\"',
"\\": '\\\\',
'\x0b': '\\u000b' //ie doesn't handle \v
},
charToReplace = /[\\\"\x00-\x1f\x7f-\uffff]/g,
encodeString = function(s) {
return '"' + s.replace(charToReplace, function(a) {
var c = m[a];
return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"';
},
encodeArray = function(o) {
var a = ["[", ""],
// Note empty string in case there are no serializable members.
len = o.length,
i;
for (i = 0; i < len; i += 1) {
a.push(doEncode(o[i]), ',');
}
// Overwrite trailing comma (or empty string)
a[a.length - 1] = ']';
return a.join("");
},
encodeObject = function(o) {
var a = ["{", ""],
// Note empty string in case there are no serializable members.
i;
for (i in o) {
if (!useHasOwn || o.hasOwnProperty(i)) {
a.push(doEncode(i), ":", doEncode(o[i]), ',');
}
}
// Overwrite trailing comma (or empty string)
a[a.length - 1] = '}';
return a.join("");
};
/**
* <p>Encodes a Date. This returns the actual string which is inserted into the JSON string as the literal expression.
* <b>The returned value includes enclosing double quotation marks.</b></p>
* <p>The default return format is "yyyy-mm-ddThh:mm:ss".</p>
* <p>To override this:</p><pre><code>
Ext.JSON.encodeDate = function(d) {
return Ext.Date.format(d, '"Y-m-d"');
};
</code></pre>
* @param {Date} d The Date to encode
* @return {String} The string literal to use in a JSON string.
*/
this.encodeDate = function(o) {
return '"' + o.getFullYear() + "-"
+ pad(o.getMonth() + 1) + "-"
+ pad(o.getDate()) + "T"
+ pad(o.getHours()) + ":"
+ pad(o.getMinutes()) + ":"
+ pad(o.getSeconds()) + '"';
};
/**
* Encodes an Object, Array or other value
* @param {Object} o The variable to encode
* @return {String} The JSON string
*/
this.encode = function() {
var ec;
return function(o) {
if (!ec) {
// setup encoding function on first access
ec = isNative() ? JSON.stringify : doEncode;
}
return ec(o);
};
}();
/**
* Decodes (parses) a JSON string to an object. If the JSON is invalid, this function throws a SyntaxError unless the safe option is set.
* @param {String} json The JSON string
* @param {Boolean} safe (optional) Whether to return null or throw an exception if the JSON is invalid.
* @return {Object} The resulting object
*/
this.decode = function() {
var dc;
return function(json, safe) {
if (!dc) {
// setup decoding function on first access
dc = isNative() ? JSON.parse : doDecode;
}
try {
return dc(json);
} catch (e) {
if (safe === true) {
return null;
}
Ext.Error.raise({
sourceClass: "Ext.JSON",
sourceMethod: "decode",
msg: "You're trying to decode an invalid JSON String: " + json
});
}
};
}();
})();
/**
* Shorthand for {@link Ext.JSON#encode}
* @member Ext
* @method encode
* @alias Ext.JSON#encode
*/
Ext.encode = Ext.JSON.encode;
/**
* Shorthand for {@link Ext.JSON#decode}
* @member Ext
* @method decode
* @alias Ext.JSON#decode
*/
Ext.decode = Ext.JSON.decode;
Ext.Error = {
raise: function(object) {
throw new Error(object.msg);
}
};
/**
*
*/
Ext.Date = {
now: Date.now,
/**
* @private
* Private for now
*/
toString: function(date) {
if (!date) {
date = new 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');
}
};
/**
* @class Ext.Base
*
* @author Jacky Nguyen <jacky@sencha.com>
* @aside guide class_system
* @aside video class-system
*
* 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.
*
* See the [Class System Guide](#!/guide/class_system) for more.
*
*/
(function(flexSetter) {
var noArgs = [],
Base = function(){};
// These static properties will be copied to every newly created class with {@link Ext#define}
Ext.apply(Base, {
$className: 'Ext.Base',
$isClass: true,
/**
* 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
* @static
* @inheritable
*/
extend: function(parent) {
var parentPrototype = parent.prototype,
basePrototype, prototype, i, ln, name, statics;
prototype = this.prototype = Ext.Object.chain(parentPrototype);
prototype.self = this;
this.superclass = prototype.superclass = parentPrototype;
if (!parent.$isClass) {
basePrototype = Ext.Base.prototype;
for (i in basePrototype) {
if (i in prototype) {
prototype[i] = basePrototype[i];
}
}
}
// Statics inheritance
statics = parentPrototype.$inheritableStatics;
if (statics) {
for (i = 0,ln = statics.length; i < ln; i++) {
name = statics[i];
if (!this.hasOwnProperty(name)) {
this[name] = parent[name];
}
}
}
if (parent.$onExtended) {
this.$onExtended = parent.$onExtended.slice();
}
prototype.config = prototype.defaultConfig = new prototype.configClass;
prototype.initConfigList = prototype.initConfigList.slice();
prototype.initConfigMap = Ext.Object.chain(prototype.initConfigMap);
},
/**
* @private
* @static
* @inheritable
*/
'$onExtended': [],
/**
* @private
* @static
* @inheritable
*/
triggerExtended: function() {
var callbacks = this.$onExtended,
ln = callbacks.length,
i, callback;
if (ln > 0) {
for (i = 0; i < ln; i++) {
callback = callbacks[i];
callback.fn.apply(callback.scope || this, arguments);
}
}
},
/**
* @private
* @static
* @inheritable
*/
onExtended: function(fn, scope) {
this.$onExtended.push({
fn: fn,
scope: scope
});
return this;
},
/**
* @private
* @static
* @inheritable
*/
addConfig: function(config, fullMerge) {
var prototype = this.prototype,
initConfigList = prototype.initConfigList,
initConfigMap = prototype.initConfigMap,
defaultConfig = prototype.defaultConfig,
hasInitConfigItem, name, value;
fullMerge = Boolean(fullMerge);
for (name in config) {
if (config.hasOwnProperty(name) && (fullMerge || !(name in defaultConfig))) {
value = config[name];
hasInitConfigItem = initConfigMap[name];
if (value !== null) {
if (!hasInitConfigItem) {
initConfigMap[name] = true;
initConfigList.push(name);
}
}
else if (hasInitConfigItem) {
initConfigMap[name] = false;
Ext.Array.remove(initConfigList, name);
}
}
}
if (fullMerge) {
Ext.merge(defaultConfig, config);
}
else {
Ext.mergeIf(defaultConfig, config);
}
prototype.configClass = Ext.Object.classify(defaultConfig);
},
/**
* 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) {
var member, name;
var className = Ext.getClassName(this);
for (name in members) {
if (members.hasOwnProperty(name)) {
member = members[name];
if (typeof member == 'function') {
member.displayName = className + '.' + name;
}
this[name] = member;
}
}
return this;
},
/**
* @private
* @static
* @inheritable
*/
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 = {};
}
var className = Ext.getClassName(this);
for (name in members) {
if (members.hasOwnProperty(name)) {
member = members[name];
if (typeof member == 'function') {
member.displayName = className + '.' + 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
*/
addMembers: function(members) {
var prototype = this.prototype,
enumerables = Ext.enumerables,
names = [],
i, ln, name, member;
var className = this.$className || '';
for (name in members) {
names.push(name);
}
if (enumerables) {
names.push.apply(names, enumerables);
}
for (i = 0,ln = names.length; i < ln; i++) {
name = names[i];
if (members.hasOwnProperty(name)) {
member = members[name];
if (typeof member == 'function' && !member.$isClass && member !== Ext.emptyFn) {
member.$owner = this;
member.$name = name;
member.displayName = className + '#' + name;
}
prototype[name] = member;
}
}
return this;
},
/**
* @private
* @static
* @inheritable
*/
addMember: function(name, member) {
if (typeof member == 'function' && !member.$isClass && member !== Ext.emptyFn) {
member.$owner = this;
member.$name = name;
member.displayName = (this.$className || '') + '#' + name;
}
this.prototype[name] = member;
return this;
},
/**
* @private
* @static
* @inheritable
*/
implement: function() {
this.addMembers.apply(this, arguments);
},
/**
* 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 {Array/String} members The names of the members to borrow
* @return {Ext.Base} this
* @static
* @inheritable
* @private
*/
borrow: function(fromClass, members) {
var prototype = this.prototype,
fromPrototype = fromClass.prototype,
className = Ext.getClassName(this),
i, ln, name, fn, toBorrow;
members = Ext.Array.from(members);
for (i = 0,ln = members.length; i < ln; i++) {
name = members[i];
toBorrow = fromPrototype[name];
if (typeof toBorrow == 'function') {
fn = function() {
return toBorrow.apply(this, arguments);
};
if (className) {
fn.displayName = className + '#' + name;
}
fn.$owner = this;
fn.$name = name;
prototype[name] = fn;
}
else {
prototype[name] = toBorrow;
}
}
return this;
},
/**
* Override members of this class. Overridden methods can be invoked via
* {@link Ext.Base#callParent}.
*
* Ext.define('My.Cat', {
* constructor: function() {
* alert("I'm a cat!");
* }
* });
*
* My.Cat.override({
* constructor: function() {
* alert("I'm going to be a cat!");
*
* var instance = this.callParent(arguments);
*
* alert("Meeeeoooowwww");
*
* return instance;
* }
* });
*
* var kitty = new My.Cat(); // alerts "I'm going to be a cat!"
* // alerts "I'm a cat!"
* // alerts "Meeeeoooowwww"
*
* As of 4.1, direct use of this method is deprecated. Use {@link Ext#define Ext.define}
* instead:
*
* Ext.define('My.CatOverride', {
* override: 'My.Cat',
* constructor: function() {
* alert("I'm going to be a cat!");
*
* var instance = this.callParent(arguments);
*
* alert("Meeeeoooowwww");
*
* return instance;
* }
* });
*
* The above accomplishes the same result but can be managed by the {@link Ext.Loader}
* which can properly order the override and its target class and the build process
* can determine whether the override is needed based on the required state of the
* target class (My.Cat).
*
* @param {Object} members The properties to add to this class. This should be
* specified as an object literal containing one or more properties.
* @return {Ext.Base} this class
* @static
* @inheritable
* @deprecated 4.1.0 Please use {@link Ext#define Ext.define} instead
*/
override: function(members) {
var me = this,
enumerables = Ext.enumerables,
target = me.prototype,
cloneFunction = Ext.Function.clone,
name, index, member, statics, names, previous;
if (arguments.length === 2) {
name = members;
members = {};
members[name] = arguments[1];
enumerables = null;
}
do {
names = []; // clean slate for prototype (1st pass) and static (2nd pass)
statics = null; // not needed 1st pass, but needs to be cleared for 2nd pass
for (name in members) { // hasOwnProperty is checked in the next loop...
if (name == 'statics') {
statics = members[name];
}
else if (name == 'config') {
me.addConfig(members[name], true);
}
else {
names.push(name);
}
}
if (enumerables) {
names.push.apply(names, enumerables);
}
for (index = names.length; index--; ) {
name = names[index];
if (members.hasOwnProperty(name)) {
member = members[name];
if (typeof member == 'function' && !member.$className && member !== Ext.emptyFn) {
if (typeof member.$owner != 'undefined') {
member = cloneFunction(member);
}
var className = me.$className;
if (className) {
member.displayName = className + '#' + name;
}
member.$owner = me;
member.$name = name;
previous = target[name];
if (previous) {
member.$previous = previous;
}
}
target[name] = member;
}
}
target = me; // 2nd pass is for statics
members = statics; // statics will be null on 2nd pass
} while (members);
return this;
},
/**
* @private
* @static
* @inheritable
*/
callParent: function(args) {
var method;
// This code is intentionally inlined for the least amount of debugger stepping
return (method = this.callParent.caller) && (method.$previous ||
((method = method.$owner ? method : method.caller) &&
method.$owner.superclass.$class[method.$name])).apply(this, args || noArgs);
},
/**
* Used internally by the mixins pre-processor
* @private
* @static
* @inheritable
*/
mixin: function(name, mixinClass) {
var mixin = mixinClass.prototype,
prototype = this.prototype,
key;
if (typeof mixin.onClassMixedIn != 'undefined') {
mixin.onClassMixedIn.call(mixinClass, this);
}
if (!prototype.hasOwnProperty('mixins')) {
if ('mixins' in prototype) {
prototype.mixins = Ext.Object.chain(prototype.mixins);
}
else {
prototype.mixins = {};
}
}
for (key in mixin) {
if (key === 'mixins') {
Ext.merge(prototype.mixins, mixin[key]);
}
else if (typeof prototype[key] == 'undefined' && key != 'mixinId' && key != 'config') {
prototype[key] = mixin[key];
}
}
if ('config' in mixin) {
this.addConfig(mixin.config, false);
}
prototype.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.override(alias, function() {
return this[origin].apply(this, arguments);
});
}),
/**
* @private
* @static
* @inheritable
*/
addXtype: function(xtype) {
var prototype = this.prototype,
xtypesMap = prototype.xtypesMap,
xtypes = prototype.xtypes,
xtypesChain = prototype.xtypesChain;
if (!prototype.hasOwnProperty('xtypesMap')) {
xtypesMap = prototype.xtypesMap = Ext.merge({}, prototype.xtypesMap || {});
xtypes = prototype.xtypes = prototype.xtypes ? [].concat(prototype.xtypes) : [];
xtypesChain = prototype.xtypesChain = prototype.xtypesChain ? [].concat(prototype.xtypesChain) : [];
prototype.xtype = xtype;
}
if (!xtypesMap[xtype]) {
xtypesMap[xtype] = true;
xtypes.push(xtype);
xtypesChain.push(xtype);
Ext.ClassManager.setAlias(this, 'widget.' + xtype);
}
return this;
}
});
Base.implement({
isInstance: true,
$className: 'Ext.Base',
configClass: Ext.emptyFn,
initConfigList: [],
initConfigMap: {},
/**
* 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++;
* },
*
* 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 "parent" method of the current method. That is the method previously
* overridden by derivation or by an override (see {@link Ext#define}).
*
* Ext.define('My.Base', {
* constructor: function (x) {
* this.x = x;
* },
*
* statics: {
* method: function (x) {
* return x;
* }
* }
* });
*
* Ext.define('My.Derived', {
* extend: 'My.Base',
*
* constructor: function () {
* this.callParent([21]);
* }
* });
*
* var obj = new My.Derived();
*
* alert(obj.x); // alerts 21
*
* This can be used with an override as follows:
*
* Ext.define('My.DerivedOverride', {
* override: 'My.Derived',
*
* constructor: function (x) {
* this.callParent([x*2]); // calls original My.Derived constructor
* }
* });
*
* var obj = new My.Derived();
*
* alert(obj.x); // now alerts 42
*
* This also works with static methods.
*
* Ext.define('My.Derived2', {
* extend: 'My.Base',
*
* statics: {
* method: function (x) {
* return this.callParent([x*2]); // calls My.Base.method
* }
* }
* });
*
* alert(My.Base.method(10); // alerts 10
* alert(My.Derived2.method(10); // alerts 20
*
* Lastly, it also works with overridden static methods.
*
* Ext.define('My.Derived2Override', {
* override: 'My.Derived2',
*
* statics: {
* method: function (x) {
* return this.callParent([x*2]); // calls My.Derived2.method
* }
* }
* });
*
* alert(My.Derived2.method(10); // now alerts 40
*
* @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 of calling the parent method
*/
callParent: function(args) {
// NOTE: this code is deliberately as few expressions (and no function calls)
// as possible so that a debugger can skip over this noise with the minimum number
// of steps. Basically, just hit Step Into until you are where you really wanted
// to be.
var method,
superMethod = (method = this.callParent.caller) && (method.$previous ||
((method = method.$owner ? method : method.caller) &&
method.$owner.superclass[method.$name]));
if (!superMethod) {
method = this.callParent.caller;
var parentClass, methodName;
if (!method.$owner) {
if (!method.caller) {
throw new Error("Attempting to call a protected method from the public scope, which is not allowed");
}
method = method.caller;
}
parentClass = method.$owner.superclass;
methodName = method.$name;
if (!(methodName in parentClass)) {
throw new Error("this.callParent() was called but there's no such method (" + methodName +
") found in the parent class (" + (Ext.getClassName(parentClass) || 'Object') + ")");
}
}
return superMethod.apply(this, args || noArgs);
},
/**
* @property {Ext.Class} self
*
* 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); / dependentOL on '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'
*
* @protected
*/
self: Base,
// Default constructor, simply returns `this`
constructor: function() {
return this;
},
wasInstantiated: false,
/**
* 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);
* }
* });
*
* var awesome = new My.awesome.Class({
* name: 'Super Awesome'
* });
*
* alert(awesome.getName()); // 'Super Awesome'
*
* @protected
* @param {Object} instanceConfig
* @return {Object} mixins The mixin prototypes as key - value pairs
*/
initConfig: function(instanceConfig) {
var configNameCache = Ext.Class.configNameCache,
prototype = this.self.prototype,
initConfigList = this.initConfigList,
initConfigMap = this.initConfigMap,
config = new this.configClass,
defaultConfig = this.defaultConfig,
i, ln, name, value, nameMap, getName;
this.initConfig = Ext.emptyFn;
this.initialConfig = instanceConfig || {};
if (instanceConfig) {
Ext.merge(config, instanceConfig);
}
this.config = config;
// Optimize initConfigList *once* per class based on the existence of apply* and update* methods
// Happens only once during the first instantiation
if (!prototype.hasOwnProperty('wasInstantiated')) {
prototype.wasInstantiated = true;
for (i = 0,ln = initConfigList.length; i < ln; i++) {
name = initConfigList[i];
nameMap = configNameCache[name];
value = defaultConfig[name];
if (!(nameMap.apply in prototype)
&& !(nameMap.update in prototype)
&& prototype[nameMap.set].$isDefault
&& typeof value != 'object') {
prototype[nameMap.internal] = defaultConfig[name];
initConfigMap[name] = false;
Ext.Array.remove(initConfigList, name);
i--;
ln--;
}
}
}
if (instanceConfig) {
initConfigList = initConfigList.slice();
for (name in instanceConfig) {
if (name in defaultConfig && !initConfigMap[name]) {
initConfigList.push(name);
}
}
}
// Point all getters to the initGetters
for (i = 0,ln = initConfigList.length; i < ln; i++) {
name = initConfigList[i];
nameMap = configNameCache[name];
this[nameMap.get] = this[nameMap.initGet];
}
this.beforeInitConfig(config);
for (i = 0,ln = initConfigList.length; i < ln; i++) {
name = initConfigList[i];
nameMap = configNameCache[name];
getName = nameMap.get;
if (this.hasOwnProperty(getName)) {
this[nameMap.set].call(this, config[name]);
delete this[getName];
}
}
return this;
},
beforeInitConfig: Ext.emptyFn,
/**
* @private
*/
getCurrentConfig: function() {
var defaultConfig = this.defaultConfig,
configNameCache = Ext.Class.configNameCache,
config = {},
name, nameMap;
for (name in defaultConfig) {
nameMap = configNameCache[name];
config[name] = this[nameMap.get].call(this);
}
return config;
},
/**
* @private
*/
setConfig: function(config, applyIfNotSet) {
if (!config) {
return this;
}
var configNameCache = Ext.Class.configNameCache,
currentConfig = this.config,
defaultConfig = this.defaultConfig,
initialConfig = this.initialConfig,
configList = [],
name, i, ln, nameMap;
applyIfNotSet = Boolean(applyIfNotSet);
for (name in config) {
if ((applyIfNotSet && (name in initialConfig))) {
continue;
}
currentConfig[name] = config[name];
if (name in defaultConfig) {
configList.push(name);
nameMap = configNameCache[name];
this[nameMap.get] = this[nameMap.initGet];
}
}
for (i = 0,ln = configList.length; i < ln; i++) {
name = configList[i];
nameMap = configNameCache[name];
this[nameMap.set].call(this, config[name]);
delete this[nameMap.get];
}
return this;
},
/**
* @private
*/
getConfig: function(name) {
return this[Ext.Class.configNameCache[name].get].call(this);
},
/**
* @private
*/
hasConfig: function(name) {
return (name in this.defaultConfig);
},
/**
* Returns the initial configuration passed to constructor.
*
* @param {String} [name] When supplied, value for particular configuration
* option is returned, otherwise the full config object is returned.
* @return {Object/Mixed}
*/
getInitialConfig: function(name) {
var config = this.config;
if (!name) {
return config;
}
else {
return config[name];
}
},
/**
* @private
*/
onConfigUpdate: function(names, callback, scope) {
var self = this.self,
className = self.$className,
i, ln, name,
updaterName, updater, newUpdater;
names = Ext.Array.from(names);
scope = scope || this;
for (i = 0,ln = names.length; i < ln; i++) {
name = names[i];
updaterName = 'update' + Ext.String.capitalize(name);
updater = this[updaterName] || Ext.emptyFn;
newUpdater = function() {
updater.apply(this, arguments);
scope[callback].apply(scope, arguments);
};
newUpdater.$name = updaterName;
newUpdater.$owner = self;
newUpdater.displayName = className + '#' + updaterName;
this[updaterName] = newUpdater;
}
},
/**
* @private
*/
destroy: function() {
this.destroy = Ext.emptyFn;
this.isDestroyed = true;
}
});
/**
* Call the original method that was previously overridden with {@link Ext.Base#override}
*
* Ext.define('My.Cat', {
* constructor: function() {
* alert("I'm a cat!");
* }
* });
*
* 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
* from the current method, for example: `this.callOverridden(arguments)`
* @return {Object} Returns the result of calling the overridden method
* @protected
*/
Base.prototype.callOverridden = Base.prototype.callParent;
Ext.Base = Base;
})(Ext.Function.flexSetter);
/**
* @class Ext.Class
*
* @author Jacky Nguyen <jacky@sencha.com>
* @aside guide class_system
* @aside video class-system
*
* 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 ExtClass,
Base = Ext.Base,
baseStaticMembers = [],
baseStaticMember, baseStaticMemberLength;
for (baseStaticMember in Base) {
if (Base.hasOwnProperty(baseStaticMember)) {
baseStaticMembers.push(baseStaticMember);
}
}
baseStaticMemberLength = baseStaticMembers.length;
/**
* @method constructor
* Creates a new anonymous class.
*
* @param {Object} data An object represent the properties of this class
* @param {Function} onCreated 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 = ExtClass = function(Class, data, onCreated) {
if (typeof Class != 'function') {
onCreated = data;
data = Class;
Class = null;
}
if (!data) {
data = {};
}
Class = ExtClass.create(Class);
ExtClass.process(Class, data, onCreated);
return Class;
};
Ext.apply(ExtClass, {
/**
* @private
* @static
*/
onBeforeCreated: function(Class, data, hooks) {
Class.addMembers(data);
hooks.onCreated.call(Class, Class);
},
/**
* @private
* @static
*/
create: function(Class) {
var name, i;
if (!Class) {
Class = function() {
return this.constructor.apply(this, arguments);
};
}
for (i = 0; i < baseStaticMemberLength; i++) {
name = baseStaticMembers[i];
Class[name] = Base[name];
}
return Class;
},
/**
* @private
* @static
*/
process: function(Class, data, onCreated) {
var preprocessorStack = data.preprocessors || ExtClass.defaultPreprocessors,
preprocessors = this.preprocessors,
hooks = {
onBeforeCreated: this.onBeforeCreated,
onCreated: onCreated || Ext.emptyFn
},
index = 0,
name, preprocessor, properties,
i, ln, fn, property, process;
delete data.preprocessors;
process = function(Class, data, hooks) {
fn = null;
while (fn === null) {
name = preprocessorStack[index++];
if (name) {
preprocessor = preprocessors[name];
properties = preprocessor.properties;
if (properties === true) {
fn = preprocessor.fn;
}
else {
for (i = 0,ln = properties.length; i < ln; i++) {
property = properties[i];
if (data.hasOwnProperty(property)) {
fn = preprocessor.fn;
break;
}
}
}
}
else {
hooks.onBeforeCreated.apply(this, arguments);
return;
}
}
if (fn.call(this, Class, data, hooks, process) !== false) {
process.apply(this, arguments);
}
};
process.call(this, Class, data, hooks);
},
/**
* @private
* @static
*/
preprocessors: {},
/**
* Register a new pre-processor to be used during the class creation process
*
* @private
* @static
* @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);
* }
* });
*
* Passed arguments for this function are:
*
* - `{Function} cls`: The created class
* - `{Object} data`: The set of properties passed in {@link Ext.Class} constructor
* - `{Function} fn`: The callback function that <b>must</b> to be executed when this pre-processor finishes,
* regardless of whether the processing is synchronous or aynchronous
*
* @return {Ext.Class} this
*/
registerPreprocessor: function(name, fn, properties, position, relativeTo) {
if (!position) {
position = 'last';
}
if (!properties) {
properties = [name];
}
this.preprocessors[name] = {
name: name,
properties: properties || false,
fn: fn
};
this.setDefaultPreprocessorPosition(name, position, relativeTo);
return this;
},
/**
* Retrieve a pre-processor callback function by its name, which has been registered before
*
* @private
* @static
* @param {String} name
* @return {Function} preprocessor
*/
getPreprocessor: function(name) {
return this.preprocessors[name];
},
/**
* @private
* @static
*/
getPreprocessors: function() {
return this.preprocessors;
},
/**
* @private
* @static
*/
defaultPreprocessors: [],
/**
* Retrieve the array stack of default pre-processors
* @private
* @static
* @return {Function} defaultPreprocessors
*/
getDefaultPreprocessors: function() {
return this.defaultPreprocessors;
},
/**
* Set the default array stack of default pre-processors
*
* @private
* @static
* @param {Array} preprocessors
* @return {Ext.Class} this
*/
setDefaultPreprocessors: function(preprocessors) {
this.defaultPreprocessors = Ext.Array.from(preprocessors);
return this;
},
/**
* Insert 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);
* }
* }).insertDefaultPreprocessor('debug', 'last');
*
* @private
* @static
* @param {String} name The pre-processor name. Note that it needs to be registered with
* {@link Ext.Class#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
*/
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;
},
/**
* @private
* @static
*/
configNameCache: {},
/**
* @private
* @static
*/
getConfigNameMap: function(name) {
var cache = this.configNameCache,
map = cache[name],
capitalizedName;
if (!map) {
capitalizedName = name.charAt(0).toUpperCase() + name.substr(1);
map = cache[name] = {
name: name,
internal: '_' + name,
initializing: 'is' + capitalizedName + 'Initializing',
apply: 'apply' + capitalizedName,
update: 'update' + capitalizedName,
set: 'set' + capitalizedName,
get: 'get' + capitalizedName,
initGet: 'initGet' + capitalizedName,
doSet : 'doSet' + capitalizedName,
changeEvent: name.toLowerCase() + 'change'
}
}
return map;
},
/**
* @private
* @static
*/
generateSetter: function(nameMap) {
var internalName = nameMap.internal,
getName = nameMap.get,
applyName = nameMap.apply,
updateName = nameMap.update,
setter;
setter = function(value) {
var oldValue = this[internalName],
applier = this[applyName],
updater = this[updateName];
delete this[getName];
if (applier) {
value = applier.call(this, value, oldValue);
}
if (typeof value != 'undefined') {
this[internalName] = value;
if (updater && value !== oldValue) {
updater.call(this, value, oldValue);
}
}
return this;
};
setter.$isDefault = true;
return setter;
},
/**
* @private
* @static
*/
generateInitGetter: function(nameMap) {
var name = nameMap.name,
setName = nameMap.set,
getName = nameMap.get,
initializingName = nameMap.initializing;
return function() {
this[initializingName] = true;
delete this[getName];
this[setName].call(this, this.config[name]);
delete this[initializingName];
return this[getName].apply(this, arguments);
}
},
/**
* @private
* @static
*/
generateGetter: function(nameMap) {
var internalName = nameMap.internal;
return function() {
return this[internalName];
}
}
});
/**
* @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]); }
* });
*/
ExtClass.registerPreprocessor('extend', function(Class, data) {
var Base = Ext.Base,
basePrototype = Base.prototype,
extend = data.extend,
Parent, parentPrototype, name;
delete data.extend;
if (extend && extend !== Object) {
Parent = extend;
}
else {
Parent = Base;
}
parentPrototype = Parent.prototype;
if (!Parent.$isClass) {
for (name in basePrototype) {
if (!parentPrototype[name]) {
parentPrototype[name] = basePrototype[name];
}
}
}
Class.extend(Parent);
Class.triggerExtended.apply(Class, arguments);
if (data.onClassExtended) {
Class.onExtended(data.onClassExtended, Class);
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');
*/
ExtClass.registerPreprocessor('statics', function(Class, data) {
Class.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.
*/
ExtClass.registerPreprocessor('inheritableStatics', function(Class, data) {
Class.addInheritableStatics(data.inheritableStatics);
delete data.inheritableStatics;
});
/**
* @cfg {Object} config
*
* List of configuration options with their default values.
*
* **Note** You need to make sure {@link Ext.Base#initConfig} is called from your constructor if you are defining
* your own class or singleton, unless you are extending a Component. Otherwise the generated getter and setter
* methods will not be initialized.
*
* Each config item will have its own setter and getter method automatically generated inside the class prototype
* during class creation time, if the class does not have those methods explicitly defined.
*
* As an example, let’s convert the name property of a Person class to be a config item, then add extra age and
* gender items.
*
* Ext.define('My.sample.Person', {
* config: {
* name: 'Mr. Unknown',
* age: 0,
* gender: 'Male'
* },
*
* constructor: function(config) {
* this.initConfig(config);
*
* return this;
* }
*
* // ...
* });
*
* Within the class, this.name still has the default value of “Mr. Unknown”. However, it’s now publicly accessible
* without sacrificing encapsulation, via setter and getter methods.
*
* var jacky = new Person({
* name: "Jacky",
* age: 35
* });
*
* alert(jacky.getAge()); // alerts 35
* alert(jacky.getGender()); // alerts "Male"
*
* jacky.walk(10); // alerts "Jacky is walking 10 steps"
*
* jacky.setName("Mr. Nguyen");
* alert(jacky.getName()); // alerts "Mr. Nguyen"
*
* jacky.walk(10); // alerts "Mr. Nguyen is walking 10 steps"
*
* Notice that we changed the class constructor to invoke this.initConfig() and pass in the provided config object.
* Two key things happened:
*
* - The provided config object when the class is instantiated is recursively merged with the default config object.
* - All corresponding setter methods are called with the merged values.
*
* Beside storing the given values, thoughout the frameworks, setters generally have two key responsibilities:
*
* - Filtering / validation / transformation of the given value before it’s actually stored within the instance.
* - Notification (such as firing events) / post-processing after the value has been set, or changed from a
* previous value.
*
* By standardize this common pattern, the default generated setters provide two extra template methods that you
* can put your own custom logics into, i.e: a “applyFoo” and “updateFoo” method for a “foo” config item, which are
* executed before and after the value is actually set, respectively. Back to the example class, let’s validate that
* age must be a valid positive number, and fire an 'agechange' if the value is modified.
*
* Ext.define('My.sample.Person', {
* config: { .... },
*
* constructor: { .... }
*
* applyAge: function(age) {
* if (typeof age != 'number' || age &lt; 0) {
* console.warn("Invalid age, must be a positive number");
* return;
* }
*
* return age;
* },
*
* updateAge: function(newAge, oldAge) {
* // age has changed from "oldAge" to "newAge"
* this.fireEvent('agechange', this, newAge, oldAge);
* }
*
* // ....
* });
*
* var jacky = new Person({
* name: "Jacky",
* age: 'invalid'
* });
*
* alert(jacky.getAge()); // alerts 0
*
* alert(jacky.setAge(-100)); // alerts 0
* alert(jacky.getAge()); // alerts 0
*
* alert(jacky.setAge(35)); // alerts 0
* alert(jacky.getAge()); // alerts 35
*
* In other words, when leveraging the config feature, you mostly never need to define setter and getter methods
* explicitly. Instead, "apply*" and "update*" methods should be implemented where neccessary. Your code will be
* consistent throughout and only contain the minimal logic that you actually care about.
*
* When it comes to inheritance, the default config of the parent class is automatically, recursively merged with
* the child’s default config. The same applies for mixins.
*/
ExtClass.registerPreprocessor('config', function(Class, data) {
var config = data.config,
prototype = Class.prototype,
defaultConfig = prototype.config,
nameMap, name, setName, getName, initGetName, internalName, value;
delete data.config;
for (name in config) {
// Once per config item, per class hierachy
if (config.hasOwnProperty(name) && !(name in defaultConfig)) {
value = config[name];
nameMap = this.getConfigNameMap(name);
setName = nameMap.set;
getName = nameMap.get;
initGetName = nameMap.initGet;
internalName = nameMap.internal;
data[initGetName] = this.generateInitGetter(nameMap);
if (value === null && !data.hasOwnProperty(internalName)) {
data[internalName] = null;
}
if (!data.hasOwnProperty(getName)) {
data[getName] = this.generateGetter(nameMap);
}
if (!data.hasOwnProperty(setName)) {
data[setName] = this.generateSetter(nameMap);
}
}
}
Class.addConfig(config, true);
});
/**
* @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'
* }
* })
*/
ExtClass.registerPreprocessor('mixins', function(Class, data, hooks) {
var mixins = data.mixins,
name, mixin, i, ln;
delete data.mixins;
Ext.Function.interceptBefore(hooks, 'onCreated', function() {
if (mixins instanceof Array) {
for (i = 0,ln = mixins.length; i < ln; i++) {
mixin = mixins[i];
name = mixin.prototype.mixinId || mixin.$className;
Class.mixin(name, mixin);
}
}
else {
for (name in mixins) {
if (mixins.hasOwnProperty(name)) {
Class.mixin(name, mixins[name]);
}
}
}
});
});
// Backwards compatible
Ext.extend = function(Class, Parent, members) {
if (arguments.length === 2 && Ext.isObject(Parent)) {
members = Parent;
Parent = Class;
Class = null;
}
var cls;
if (!Parent) {
throw new Error("[Ext.extend] Attempting to extend from a class which has not been loaded on the page.");
}
members.extend = Parent;
members.preprocessors = [
'extend'
,'statics'
,'inheritableStatics'
,'mixins'
,'config'
];
if (Class) {
cls = new ExtClass(Class, members);
}
else {
cls = new ExtClass(members);
}
cls.prototype.override = function(o) {
for (var m in o) {
if (o.hasOwnProperty(m)) {
this[m] = o[m];
}
}
};
return cls;
};
})();
/**
* @class Ext.ClassManager
*
* @author Jacky Nguyen <jacky@sencha.com>
* @aside guide class_system
* @aside video class-system
*
* 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.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, arraySlice, arrayFrom, global) {
var Manager = Ext.ClassManager = {
/**
* @property classes
* @type Object
* 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: {},
nameToAlternates: {}
},
/** @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 existCache = this.existCache,
i, ln, part, root, parts;
if (typeof className != 'string' || className.length < 1) {
throw new Error("[Ext.ClassManager] Invalid classname, must be a string and must not be empty");
}
if (this.classes[className] || existCache[className]) {
return true;
}
root = 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];
}
}
existCache[className] = true;
this.triggerCreated(className);
return true;
},
/**
* @private
*/
createdListeners: [],
/**
* @private
*/
nameCreatedListeners: {},
/**
* @private
*/
triggerCreated: function(className) {
var listeners = this.createdListeners,
nameListeners = this.nameCreatedListeners,
alternateNames = this.maps.nameToAlternates[className],
names = [className],
i, ln, j, subLn, listener, name;
for (i = 0,ln = listeners.length; i < ln; i++) {
listener = listeners[i];
listener.fn.call(listener.scope, className);
}
if (alternateNames) {
names.push.apply(names, alternateNames);
}
for (i = 0,ln = names.length; i < ln; i++) {
name = names[i];
listeners = nameListeners[name];
if (listeners) {
for (j = 0,subLn = listeners.length; j < subLn; j++) {
listener = listeners[j];
listener.fn.call(listener.scope, name);
}
delete nameListeners[name];
}
}
},
/**
* @private
*/
onCreated: function(fn, scope, className) {
var listeners = this.createdListeners,
nameListeners = this.nameCreatedListeners,
listener = {
fn: fn,
scope: scope
};
if (className) {
if (this.isCreated(className)) {
fn.call(scope, className);
return;
}
if (!nameListeners[className]) {
nameListeners[className] = [];
}
nameListeners[className].push(listener);
}
else {
listeners.push(listener);
}
},
/**
* Supports namespace rewriting
* @private
*/
parseNamespace: function(namespace) {
if (typeof namespace != 'string') {
throw new Error("[Ext.ClassManager] Invalid namespace, must be a string");
}
var cache = this.namespaceParseCache;
if (this.enableNamespaceParseCache) {
if (cache.hasOwnProperty(namespace)) {
return cache[namespace];
}
}
var parts = [],
rewrites = this.namespaceRewrites,
root = global,
name = namespace,
rewrite, from, to, i, ln;
for (i = 0, ln = rewrites.length; i < ln; i++) {
rewrite = rewrites[i];
from = rewrite.from;
to = rewrite.to;
if (name === from || name.substring(0, from.length) === from) {
name = name.substring(from.length);
if (typeof to != 'string') {
root = to;
} else {
parts = parts.concat(to.split('.'));
}
break;
}
}
parts.push(root);
parts = parts.concat(name.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 {Mixed} value
*/
setNamespace: function(name, value) {
var root = 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 = 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 me = this,
maps = me.maps,
nameToAlternates = maps.nameToAlternates,
targetName = me.getName(value),
alternates;
me.classes[name] = me.setNamespace(name, value);
if (targetName && targetName !== name) {
maps.alternateToName[name] = targetName;
alternates = nameToAlternates[targetName] || (nameToAlternates[targetName] = []);
alternates.push(name);
}
return this;
},
/**
* Retrieve a class by its name.
*
* @param {String} name
* @return {Ext.Class} class
*/
get: function(name) {
var classes = this.classes;
if (classes[name]) {
return classes[name];
}
var root = 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) {
if (aliasToNameMap[alias]) {
Ext.Logger.info("[Ext.ClassManager] Overriding existing alias: '" + alias + "' " +
"of: '" + aliasToNameMap[alias] + "' with: '" + className + "'. Be sure it's intentional.");
}
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 {Array} aliases
*/
getAliasesByName: function(name) {
return this.maps.nameToAliases[name] || [];
},
/**
* Get the name of the class by its reference or its instance;
* usually invoked by the shorthand {@link Ext#getClassName Ext.getClassName}
* Ext.ClassManager.getName(Ext.Action); // returns "Ext.Action"
* @param {Ext.Class/Object} object
* @return {String} className
* @markdown
*/
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. This is usually invoked by the shorthand {@link Ext#getClass Ext.getClass}
*
* var component = new Ext.Component();
*
* Ext.ClassManager.getClass(component); // returns Ext.Component
*
* @param {Object} object
* @return {Ext.Class} class
*/
getClass: function(object) {
return object && object.self || null;
},
/**
* @private
*/
create: function(className, data, createdFn) {
if (typeof className != 'string') {
throw new Error("[Ext.define] Invalid class name '" + className + "' specified, must be a non-empty string");
}
data.$className = className;
return new Class(data, function() {
var postprocessorStack = data.postprocessors || Manager.defaultPostprocessors,
registeredPostprocessors = Manager.postprocessors,
index = 0,
postprocessors = [],
postprocessor, process, i, ln, j, subLn, postprocessorProperties, postprocessorProperty;
delete data.postprocessors;
for (i = 0,ln = postprocessorStack.length; i < ln; i++) {
postprocessor = postprocessorStack[i];
if (typeof postprocessor == 'string') {
postprocessor = registeredPostprocessors[postprocessor];
postprocessorProperties = postprocessor.properties;
if (postprocessorProperties === true) {
postprocessors.push(postprocessor.fn);
}
else if (postprocessorProperties) {
for (j = 0,subLn = postprocessorProperties.length; j < subLn; j++) {
postprocessorProperty = postprocessorProperties[j];
if (data.hasOwnProperty(postprocessorProperty)) {
postprocessors.push(postprocessor.fn);
break;
}
}
}
}
else {
postprocessors.push(postprocessor);
}
}
process = function(clsName, cls, clsData) {
postprocessor = postprocessors[index++];
if (!postprocessor) {
Manager.set(className, cls);
if (createdFn) {
createdFn.call(cls, cls);
}
Manager.triggerCreated(className);
return;
}
if (postprocessor.call(this, clsName, cls, clsData, process) !== false) {
process.apply(this, arguments);
}
};
process.call(Manager, className, this, data);
});
},
createOverride: function(className, data) {
var overriddenClassName = data.override;
delete data.override;
this.existCache[className] = true;
// Override the target class right after it's created
this.onCreated(function() {
this.get(overriddenClassName).override(data);
// This push the overridding file itself into Ext.Loader.history
// Hence if the target class never exists, the overriding file will
// never be included in the build
this.triggerCreated(className);
}, this, overriddenClassName);
return this;
},
/**
* Instantiate a class by its alias; usually invoked by the convenient shorthand {@link Ext#createByAlias Ext.createByAlias}
* 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, ... });
*
* @param {String} alias
* @param {Mixed...} args Additional arguments after the alias will be passed to the class constructor.
* @return {Object} instance
*/
instantiateByAlias: function() {
var alias = arguments[0],
args = arraySlice.call(arguments),
className = this.getNameByAlias(alias);
if (!className) {
className = this.maps.aliasToName[alias];
if (!className) {
throw new Error("[Ext.createByAlias] Cannot create an instance of unrecognized alias: " + alias);
}
Ext.Logger.warn("[Ext.Loader] Synchronously loading '" + className + "'; consider adding " +
"Ext.require('" + alias + "') above Ext.onReady");
Ext.syncRequire(className);
}
args[0] = className;
return this.instantiate.apply(this, args);
},
/**
* Instantiate a class by either full name, alias or alternate name; usually invoked by the convenient
* shorthand {@link Ext#create Ext.create}
*
* 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, ... });
*
* @param {String} name
* @param {Mixed} args,... Additional arguments after the name will be passed to the class' constructor.
* @return {Object} instance
*/
instantiate: function() {
var name = arguments[0],
args = arraySlice.call(arguments, 1),
alias = name,
possibleName, cls;
if (typeof name != 'function') {
if ((typeof name != 'string' || name.length < 1)) {
throw new Error("[Ext.create] Invalid class name or alias '" + name + "' specified, must be a non-empty string");
}
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.Logger.warn("[Ext.Loader] Synchronously loading '" + name + "'; consider adding '" +
((possibleName) ? alias : name) + "' explicitly as a require of the corresponding class");
Ext.syncRequire(name);
cls = this.get(name);
}
if (!cls) {
throw new Error("[Ext.create] Cannot create an instance of unrecognized class name / alias: " + alias);
}
if (typeof cls != 'function') {
throw new Error("[Ext.create] '" + name + "' is a singleton and cannot be instantiated");
}
return this.getInstantiator(args.length)(cls, args);
},
/**
* @private
* @param name
* @param args
*/
dynInstantiate: function(name, args) {
args = arrayFrom(args, true);
args.unshift(name);
return this.instantiate.apply(this, args);
},
/**
* @private
* @param length
*/
getInstantiator: function(length) {
var instantiators = this.instantiators,
instantiator;
instantiator = instantiators[length];
if (!instantiator) {
var i = length,
args = [];
for (i = 0; i < length; i++) {
args.push('a[' + i + ']');
}
instantiator = instantiators[length] = new Function('c', 'a', 'return new c(' + args.join(',') + ')');
instantiator.displayName = "Ext.ClassManager.instantiate" + length;
}
return instantiator;
},
/**
* @private
*/
postprocessors: {},
/**
* @private
*/
defaultPostprocessors: [],
/**
* Register a post-processor function.
*
* @private
* @param {String} name
* @param {Function} postprocessor
*/
registerPostprocessor: function(name, fn, properties, position, relativeTo) {
if (!position) {
position = 'last';
}
if (!properties) {
properties = [name];
}
this.postprocessors[name] = {
name: name,
properties: properties || false,
fn: fn
};
this.setDefaultPostprocessorPosition(name, position, relativeTo);
return this;
},
/**
* Set the default post processors array stack which are applied to every class.
*
* @private
* @param {String/Array} The name of a registered post processor or an array of registered names.
* @return {Ext.ClassManager} this
*/
setDefaultPostprocessors: function(postprocessors) {
this.defaultPostprocessors = arrayFrom(postprocessors);
return this;
},
/**
* Insert this post-processor at a specific position in the stack, optionally relative to
* any existing post-processor
*
* @private
* @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 {Array} classNames
* @markdown
*/
getNamesByExpression: function(expression) {
var nameToAliasesMap = this.maps.nameToAliases,
names = [],
name, alias, aliases, possibleName, regex, i, ln;
if (typeof expression != 'string' || expression.length < 1) {
throw new Error("[Ext.ClassManager.getNamesByExpression] Expression " + expression + " is invalid, must be a non-empty string");
}
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;
}
};
/**
* @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;
for (i = 0,ln = aliases.length; i < ln; i++) {
alias = aliases[i];
this.setAlias(cls, alias);
}
}, ['xtype', '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];
if (typeof alternate != 'string') {
throw new Error("[Ext.define] Invalid alternate of: '" + alternate + "' for class: '" + name + "'; must be a valid string");
}
this.set(alternate, cls);
}
});
Ext.apply(Ext, {
/**
* Convenient shorthand, see {@link Ext.ClassManager#instantiate}
* @member Ext
* @method create
*/
create: alias(Manager, 'instantiate'),
/**
* 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')
*
* @member Ext
* @method widget
*/
widget: function(name) {
var args = arraySlice.call(arguments);
args[0] = 'widget.' + name;
return Manager.instantiateByAlias.apply(Manager, args);
},
/**
* Convenient shorthand, see {@link Ext.ClassManager#instantiateByAlias}
* @member Ext
* @method createByAlias
*/
createByAlias: alias(Manager, 'instantiateByAlias'),
/**
* Defines a class or override. A basic class is defined like this:
*
* Ext.define('My.awesome.Class', {
* someProperty: 'something',
*
* someMethod: function(s) {
* console.log(s + this.someProperty);
* }
* });
*
* var obj = new My.awesome.Class();
*
* obj.someMethod('Say '); // logs 'Say something' to the console
*
* To defines 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.callParent(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.callParent(arguments); // calls My.app.Panel's constructor
* //...
* }
* });
*
* Another use for an override 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.callParent(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.callParent([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.
*
* @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 listed below:
*
* - `mixins`
* - `statics`
* - `config`
* - `alias`
* - `self`
* - `singleton`
* - `alternateClassName`
* - `override`
*
* @param {Function} createdFn Optional callback to execute after the class (or override)
* is created. The execution scope (`this`) will be the newly created class itself.
* @return {Ext.Base}
*
* @member Ext
* @method define
*/
define: function (className, data, createdFn) {
if ('override' in data) {
return Manager.createOverride.apply(Manager, arguments);
}
return Manager.create.apply(Manager, arguments);
},
/**
* Convenient shorthand for {@link Ext.ClassManager#getName}.
* @member Ext
* @method getClassName
* @inheritdoc Ext.ClassManager#getName
*/
getClassName: alias(Manager, 'getName'),
/**
* Returns the display name for object. This name is looked for in order from the following places:
*
* - `displayName` field of the object.
* - `$name` and `$class` fields of the object.
* - '$className` field of the object.
*
* This method is used by {@link Ext.Logger#log} to display information about objects.
*
* @param {Mixed} [object] The object who's display name to determine.
* @return {String} The determined display name, or "Anonymous" if none found.
* @member Ext
*/
getDisplayName: function(object) {
if (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';
},
/**
* Convenient shorthand, see {@link Ext.ClassManager#getClass}
* @member Ext
* @method 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) { ... };
*
* @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)
* @function
* @member Ext
* @method namespace
*/
namespace: alias(Manager, 'createNamespaces')
});
/**
* Old name for {@link Ext#widget}.
* @deprecated 4.0.0 Please use {@link Ext#widget} instead.
* @method createWidget
* @member Ext
*/
Ext.createWidget = Ext.widget;
/**
* Convenient alias for {@link Ext#namespace Ext.namespace}
* @member Ext
* @method ns
*/
Ext.ns = Ext.namespace;
Class.registerPreprocessor('className', function(cls, data) {
if (data.$className) {
cls.$className = data.$className;
cls.displayName = cls.$className;
}
}, true, 'first');
Class.registerPreprocessor('alias', function(cls, data) {
var prototype = cls.prototype,
xtypes = arrayFrom(data.xtype),
aliases = arrayFrom(data.alias),
widgetPrefix = 'widget.',
widgetPrefixLength = widgetPrefix.length,
xtypesChain = Array.prototype.slice.call(prototype.xtypesChain || []),
xtypesMap = Ext.merge({}, prototype.xtypesMap || {}),
i, ln, alias, xtype;
for (i = 0,ln = aliases.length; i < ln; i++) {
alias = aliases[i];
if (typeof alias != 'string' || alias.length < 1) {
throw new Error("[Ext.define] Invalid alias of: '" + alias + "' for class: '" + name + "'; must be a valid string");
}
if (alias.substring(0, widgetPrefixLength) === widgetPrefix) {
xtype = alias.substring(widgetPrefixLength);
Ext.Array.include(xtypes, xtype);
}
}
cls.xtype = data.xtype = xtypes[0];
data.xtypes = xtypes;
for (i = 0,ln = xtypes.length; i < ln; i++) {
xtype = xtypes[i];
if (!xtypesMap[xtype]) {
xtypesMap[xtype] = true;
xtypesChain.push(xtype);
}
}
data.xtypesChain = xtypesChain;
data.xtypesMap = xtypesMap;
Ext.Function.interceptAfter(data, 'onClassCreated', function() {
var mixins = prototype.mixins,
key, mixin;
for (key in mixins) {
if (mixins.hasOwnProperty(key)) {
mixin = mixins[key];
xtypes = mixin.xtypes;
if (xtypes) {
for (i = 0,ln = xtypes.length; i < ln; i++) {
xtype = xtypes[i];
if (!xtypesMap[xtype]) {
xtypesMap[xtype] = true;
xtypesChain.push(xtype);
}
}
}
}
}
});
for (i = 0,ln = xtypes.length; i < ln; i++) {
xtype = xtypes[i];
if (typeof xtype != 'string' || xtype.length < 1) {
throw new Error("[Ext.define] Invalid xtype of: '" + xtype + "' for class: '" + name + "'; must be a valid non-empty string");
}
Ext.Array.include(aliases, widgetPrefix + xtype);
}
data.alias = aliases;
}, ['xtype', 'alias']);
})(Ext.Class, Ext.Function.alias, Array.prototype.slice, Ext.Array.from, Ext.global);
/**
* @class Ext.Loader
*
* @author Jacky Nguyen <jacky@sencha.com>
* @docauthor Jacky Nguyen <jacky@sencha.com>
* @aside guide mvc_dependencies
*
* 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.
*
* **Note** The Loader is only enabled by default in development versions of the library (eg sencha-touch-debug.js). To
* explicitly enable the loader, use `Ext.Loader.setConfig({ enabled: true });` before the start of your script.
*
* ## 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`, i.e:
*
* 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.
*
* @singleton
*/
(function(Manager, Class, flexSetter, alias, pass, arrayFrom, arrayErase, arrayInclude) {
var
dependencyProperties = ['extend', 'mixins', 'requires'],
Loader;
Loader = Ext.Loader = {
/**
* @private
*/
isInHistory: {},
/**
* 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.
*
* @property history
* @type Array
*/
history: [],
/**
* Configuration
* @private
*/
config: {
/**
* Whether or not to enable the dynamic dependency loading feature
* Defaults to false
* @cfg {Boolean} enabled
*/
enabled: false,
/**
* @cfg {Boolean} disableCaching
* Appends current timestamp to script files to prevent caching
* Defaults to true
*/
disableCaching: true,
/**
* @cfg {String} disableCachingParam
* The get parameter name for the cache buster's timestamp.
* Defaults to '_dc'
*/
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, <code>Other.awesome.Class</code>
* will simply be loaded from <code>./Other/awesome/Class.js</code>
*/
paths: {
'Ext': '.'
}
},
/**
* Set the configuration for the loader. This should be called right after ext-(debug).js
* is included in the page, and before Ext.onReady. i.e:
*
* <script type="text/javascript" src="ext-core-debug.js"></script>
* <script type="text/javascript">
* Ext.Loader.setConfig({
* enabled: true,
* paths: {
* 'My': 'my_own_path'
* }
* });
* <script>
* <script type="text/javascript">
* Ext.require(...);
*
* Ext.onReady(function() {
* // application code here
* });
* </script>
*
* Refer to config options of {@link Ext.Loader} for the list of possible properties
*
* @param {Object} config The config object to override the default values
* @return {Ext.Loader} this
* @markdown
*/
setConfig: function(name, value) {
if (Ext.isObject(name) && arguments.length === 1) {
Ext.merge(this.config, name);
}
else {
this.config[name] = (Ext.isObject(value)) ? Ext.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/Mixed}
*/
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;
},
/**
* Loads all classes by the given names and all their direct dependencies; optionally executes the given callback function when
* finishes, within the optional scope. This method is aliased by {@link Ext#require Ext.require} for convenience
* @param {String/Array} 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/Array} excludes (Optional) Classes to be excluded, useful when being used with expressions
* @markdown
*/
require: function(expressions, fn, scope, excludes) {
if (fn) {
fn.call(scope);
}
},
/**
* 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. This method is aliased by {@link Ext#syncRequire} for convenience
* @param {String/Array} 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/Array} excludes (Optional) Classes to be excluded, useful when being used with expressions
* @markdown
*/
syncRequire: function() {},
/**
* 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, eg:
*
* Ext.exclude('Ext.data.*').require('*');
*
* Ext.exclude('widget.button*').require('widget.*');
*
* @param {Array} excludes
* @return {Object} object contains `require` method for chaining
* @markdown
*/
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);
}
};
},
/**
* Add a 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 (<code>this</code>) 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);
};
}
fn.call(scope);
}
};
Ext.apply(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
*/
isClassFileLoaded: {},
/**
* @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: {},
/**
* @private
*/
syncModeEnabled: false,
scriptElements: {},
/**
* 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 queue = this.queue,
ln = queue.length,
i, item, j, requires, references;
if (ln === 0) {
this.triggerReady();
return;
}
for (i = 0; i < ln; i++) {
item = queue[i];
if (item) {
requires = item.requires;
references = item.references;
// 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
arrayErase(requires, j, 1);
}
else {
j++;
}
} while (j < requires.length);
if (item.requires.length === 0) {
arrayErase(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;
},
removeScriptElement: function(url) {
var scriptElements = this.scriptElements;
if (scriptElements[url]) {
this.cleanupScriptElement(scriptElements[url], true);
delete scriptElements[url];
}
return this;
},
/**
* @private
*/
cleanupScriptElement: function(script, remove) {
script.onload = null;
script.onreadystatechange = null;
script.onerror = null;
if (remove) {
this.documentHead.removeChild(script);
}
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,
isFileLoaded = this.isFileLoaded,
scriptElements = this.scriptElements,
noCacheUrl = url + (this.getConfig('disableCaching') ? ('?' + this.getConfig('disableCachingParam') + '=' + Ext.Date.now()) : ''),
isCrossOriginRestricted = false,
xhr, status, onScriptError;
if (isFileLoaded[url]) {
return this;
}
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() {
if (!isFileLoaded[url]) {
scriptElements[url] = me.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope);
}
});
}
else {
scriptElements[url] = this.injectScriptElement(noCacheUrl, onLoad, onScriptError, scope);
}
}
else {
if (typeof XMLHttpRequest != 'undefined') {
xhr = new XMLHttpRequest();
} else {
xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
try {
xhr.open('GET', url, false);
xhr.send(null);
}
catch (e) {
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);
}
status = (xhr.status === 1223) ? 204 : xhr.status;
if (status === 0 || (status >= 200 && status < 300)) {
// Debugger friendly, file names are still shown even though they're eval'ed code
// Breakpoints work on both Firebug and Chrome's Web Inspector
Ext.globalEval(xhr.responseText + "\n//@ sourceURL=" + url);
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;
}
},
// documented above
syncRequire: function() {
var syncModeEnabled = this.syncModeEnabled;
if (!syncModeEnabled) {
this.syncModeEnabled = true;
}
this.require.apply(this, arguments);
if (!syncModeEnabled) {
this.syncModeEnabled = false;
}
this.refreshQueue();
},
// documented above
require: function(expressions, fn, scope, excludes) {
var excluded = {},
included = {},
queue = this.queue,
classNameToFilePathMap = this.classNameToFilePathMap,
isClassFileLoaded = this.isClassFileLoaded,
excludedClassNames = [],
possibleClassNames = [],
classNames = [],
references = [],
callback,
syncModeEnabled,
filePath, expression, exclude, className,
possibleClassName, i, j, ln, subLn;
if (excludes) {
excludes = arrayFrom(excludes);
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;
}
}
}
}
expressions = arrayFrom(expressions);
if (fn) {
if (fn.length > 0) {
callback = function() {
var classes = [],
i, ln, name;
for (i = 0,ln = references.length; i < ln; i++) {
name = references[i];
classes.push(Manager.get(name));
}
return fn.apply(this, classes);
};
}
else {
callback = fn;
}
}
else {
callback = Ext.emptyFn;
}
scope = scope || Ext.global;
for (i = 0,ln = expressions.length; i < ln; i++) {
expression = expressions[i];
if (typeof expression == 'string' && expression.length > 0) {
possibleClassNames = Manager.getNamesByExpression(expression);
subLn = possibleClassNames.length;
for (j = 0; j < subLn; j++) {
possibleClassName = possibleClassNames[j];
if (excluded[possibleClassName] !== true) {
references.push(possibleClassName);
if (!Manager.isCreated(possibleClassName) && !included[possibleClassName]) {
included[possibleClassName] = true;
classNames.push(possibleClassName);
}
}
}
}
}
// If the dynamic dependency feature is not being used, throw an error
// if the dependencies are not defined
if (classNames.length > 0) {
if (!this.config.enabled) {
throw new Error("Ext.Loader is not enabled, so dependencies cannot be resolved dynamically. " +
"Missing required class" + ((classNames.length > 1) ? "es" : "") + ": " + classNames.join(', '));
}
}
else {
callback.call(scope);
return this;
}
syncModeEnabled = this.syncModeEnabled;
if (!syncModeEnabled) {
queue.push({
requires: classNames.slice(), // this array will be modified as the queue is processed,
// so we need a copy of it
callback: callback,
scope: scope
});
}
ln = classNames.length;
for (i = 0; i < ln; i++) {
className = classNames[i];
filePath = this.getPath(className);
// If we are synchronously loading a file that has already been asychronously loaded before
// we need to destroy the script tag and revert the count
// This file will then be forced loaded in synchronous
if (syncModeEnabled && isClassFileLoaded.hasOwnProperty(className)) {
this.numPendingFiles--;
this.removeScriptElement(filePath);
delete isClassFileLoaded[className];
}
if (!isClassFileLoaded.hasOwnProperty(className)) {
isClassFileLoaded[className] = false;
classNameToFilePathMap[className] = filePath;
this.numPendingFiles++;
this.loadScriptFile(
filePath,
pass(this.onFileLoaded, [className, filePath], this),
pass(this.onFileLoadError, [className, filePath]),
this,
syncModeEnabled
);
}
}
if (syncModeEnabled) {
callback.call(scope);
if (ln === 1) {
return Manager.get(className);
}
}
return this;
},
/**
* @private
* @param {String} className
* @param {String} filePath
*/
onFileLoaded: function(className, filePath) {
this.numLoadedFiles++;
this.isClassFileLoaded[className] = true;
this.isFileLoaded[filePath] = true;
this.numPendingFiles--;
if (this.numPendingFiles === 0) {
this.refreshQueue();
}
if (!this.syncModeEnabled && this.numPendingFiles === 0 && this.isLoading && !this.hasFileLoadError) {
var queue = this.queue,
missingClasses = [],
missingPaths = [],
requires,
i, ln, j, subLn;
for (i = 0,ln = queue.length; i < ln; i++) {
requires = queue[i].requires;
for (j = 0,subLn = requires.length; j < subLn; j++) {
if (this.isClassFileLoaded[requires[j]]) {
missingClasses.push(requires[j]);
}
}
}
if (missingClasses.length < 1) {
return;
}
missingClasses = Ext.Array.filter(Ext.Array.unique(missingClasses), function(item) {
return !this.requiresMap.hasOwnProperty(item);
}, this);
for (i = 0,ln = missingClasses.length; i < ln; i++) {
missingPaths.push(this.classNameToFilePathMap[missingClasses[i]]);
}
throw new Error("The following classes are not declared even if their files have been " +
"loaded: '" + missingClasses.join("', '") + "'. Please check the source code of their " +
"corresponding files for possible typos: '" + missingPaths.join("', '"));
}
},
/**
* @private
*/
onFileLoadError: function(className, filePath, errorMessage, isSynchronous) {
this.numPendingFiles--;
this.hasFileLoadError = true;
throw new Error("[Ext.Loader] " + errorMessage);
},
/**
* @private
*/
addOptionalRequires: function(requires) {
var optionalRequires = this.optionalRequires,
i, ln, require;
requires = arrayFrom(requires);
for (i = 0, ln = requires.length; i < ln; i++) {
require = requires[i];
arrayInclude(optionalRequires, require);
}
return this;
},
/**
* @private
*/
triggerReady: function(force) {
var readyListeners = this.readyListeners,
optionalRequires = this.optionalRequires,
listener;
if (this.isLoading || force) {
this.isLoading = false;
if (optionalRequires.length !== 0) {
// Clone then empty the array to eliminate potential recursive loop issue
optionalRequires = optionalRequires.slice();
// Empty the original array
this.optionalRequires.length = 0;
this.require(optionalRequires, 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;
},
// duplicate definition (documented above)
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) {
var isInHistory = this.isInHistory;
if (className && this.isClassFileLoaded.hasOwnProperty(className) && !isInHistory[className]) {
isInHistory[className] = true;
this.history.push(className);
}
return this;
}
});
/**
* Convenient alias of {@link Ext.Loader#require}. Please see the introduction documentation of
* {@link Ext.Loader} for examples.
* @member Ext
* @method require
* @inheritdoc Ext.Loader#require
*/
Ext.require = alias(Loader, 'require');
/**
* Synchronous version of {@link Ext#require}, convenient alias of {@link Ext.Loader#syncRequire}.
* @member Ext
* @method syncRequire
* @inheritdoc Ext.Loader#syncRequire
*/
Ext.syncRequire = alias(Loader, 'syncRequire');
/**
* Convenient shortcut to {@link Ext.Loader#exclude}.
* @member Ext
* @method exclude
* @inheritdoc Ext.Loader#exclude
*/
Ext.exclude = alias(Loader, 'exclude');
/**
* Adds a listener to be notified when the document is ready (before onload and before images are loaded).
* Shorthand of {@link Ext.Loader#onReady}(fn, scope, true, options).
*
* @param {Function} fn The method the event invokes.
* @param {Object} [scope] The scope in which the handler function executes. Defaults to the browser window.
* @param {Boolean} [options] Options object as passed to {@link Ext.Element#addListener}. It is recommended
* that the options `{single: true}` be used so that the handler is removed on first invocation.
* @member Ext
* @method onReady
*/
Ext.onReady = function(fn, scope, options) {
Loader.onReady(fn, scope, true, options);
};
Class.registerPreprocessor('loader', function(cls, data, hooks, continueFn) {
var me = this,
dependencies = [],
className = Manager.getName(cls),
i, j, ln, subLn, value, propertyName, propertyValue;
/*
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.mixin.Observable';
}
}
which will later be transformed into:
{
extend: Ext.MyClass,
requires: [Ext.some.OtherClass],
mixins: {
observable: Ext.mixin.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) {
return;
}
var deadlockPath = [],
requiresMap = Loader.requiresMap,
detectDeadlock;
/*
Automatically detect deadlocks before-hand,
will throw an error with detailed path for ease of debugging. Examples of deadlock cases:
- A extends B, then B extends A
- A requires B, B requires C, then C requires A
The detectDeadlock function will recursively transverse till the leaf, hence it can detect deadlocks
no matter how deep the path is.
*/
if (className) {
requiresMap[className] = dependencies;
if (!Loader.requiredByMap) Loader.requiredByMap = {};
Ext.Array.each(dependencies, function(dependency){
if (!Loader.requiredByMap[dependency]) Loader.requiredByMap[dependency] = [];
Loader.requiredByMap[dependency].push(className);
});
detectDeadlock = function(cls) {
deadlockPath.push(cls);
if (requiresMap[cls]) {
if (Ext.Array.contains(requiresMap[cls], className)) {
throw new Error("Deadlock detected while loading dependencies! '" + className + "' and '" +
deadlockPath[1] + "' " + "mutually require each other. Path: " +
deadlockPath.join(' -> ') + " -> " + deadlockPath[0]);
}
for (i = 0,ln = requiresMap[cls].length; i < ln; i++) {
detectDeadlock(requiresMap[cls][i]);
}
}
};
detectDeadlock(className);
}
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, hooks);
});
return false;
}, true, 'after', 'className');
/**
* @cfg {String[]} uses
* @member Ext.Class
* List of optional classes to load together with this class. These aren't neccessarily loaded before
* this class is created, but are guaranteed to be available before Ext.onReady listeners are
* invoked
*/
Manager.registerPostprocessor('uses', function(name, cls, data) {
var uses = arrayFrom(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.onCreated(function(className) {
this.historyPush(className);
}, Loader);
})(Ext.ClassManager, Ext.Class, Ext.Function.flexSetter, Ext.Function.alias,
Ext.Function.pass, Ext.Array.from, Ext.Array.erase, Ext.Array.include);
/*
This file is part of Sencha Touch 2
Copyright (c) 2012 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.EventManager
*
* This object has been deprecated in Sencha Touch 2.0.0. Please refer to the method documentation for specific alternatives.
*
* @deprecated 2.0.0
* @singleton
* @private
*/
/**
* @class Ext
*
* Ext is the global namespace for the whole Sencha Touch framework. Every class, function and configuration for the
* whole framework exists under this single global variable. The Ext singleton itself contains a set of useful helper
* functions (like {@link #apply}, {@link #min} and others), but most of the framework that you use day to day exists
* in specialized classes (for example {@link Ext.Panel}, {@link Ext.Carousel} and others).
*
* If you are new to Sencha Touch we recommend starting with the [Getting Started Guide][getting_started] to
* get a feel for how the framework operates. After that, use the more focused guides on subjects like panels, forms and data
* to broaden your understanding. The MVC guides take you through the process of building full applications using the
* framework, and detail how to deploy them to production.
*
* The functions listed below are mostly utility functions used internally by many of the classes shipped in the
* framework, but also often useful in your own apps.
*
* A method that is crucial to beginning your application is {@link #setup Ext.setup}. Please refer to it's documentation, or the
* [Getting Started Guide][getting_started] as a reference on beginning your application.
*
* Ext.setup({
* onReady: function() {
* Ext.Viewport.add({
* xtype: 'component',
* html: 'Hello world!'
* });
* }
* });
*
* [getting_started]: #!/guide/getting_started
*/
Ext.setVersion('touch', '2.0.0');
Ext.apply(Ext, {
/**
* The version of the framework
* @type String
*/
version: Ext.getVersion('touch'),
/**
* @private
*/
idSeed: 0,
/**
* Repaints the whole page. This fixes frequently encountered painting issues in mobile Safari.
*/
repaint: function() {
var mask = Ext.getBody().createChild({
cls: Ext.baseCSSPrefix + 'mask ' + Ext.baseCSSPrefix + 'mask-transparent'
});
setTimeout(function() {
mask.destroy();
}, 0);
},
/**
* Generates unique ids. If the element already has an id, it is unchanged
* @param {Mixed} 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) {
if (el && el.id) {
return el.id;
}
el = Ext.getDom(el) || {};
if (el === document || el === document.documentElement) {
el.id = 'ext-application';
}
else if (el === document.body) {
el.id = 'ext-viewport';
}
else if (el === window) {
el.id = 'ext-window';
}
el.id = el.id || ((prefix || 'ext-element-') + (++Ext.idSeed));
return el.id;
},
/**
* Returns the current document body as an {@link Ext.Element}.
* @return Ext.Element The document body
*/
getBody: function() {
if (!Ext.documentBodyElement) {
if (!document.body) {
throw new Error("[Ext.getBody] document.body does not exist at this point");
}
Ext.documentBodyElement = Ext.get(document.body);
}
return Ext.documentBodyElement;
},
/**
* Returns the current document head as an {@link Ext.Element}.
* @return Ext.Element The document head
*/
getHead: function() {
if (!Ext.documentHeadElement) {
Ext.documentHeadElement = Ext.get(document.head || document.getElementsByTagName('head')[0]);
}
return Ext.documentHeadElement;
},
/**
* Returns the current HTML document object as an {@link Ext.Element}.
* @return Ext.Element The document
*/
getDoc: function() {
if (!Ext.documentElement) {
Ext.documentElement = Ext.get(document);
}
return Ext.documentElement;
},
/**
* This is shorthand reference to {@link Ext.ComponentMgr#get}.
* Looks up an existing {@link Ext.Component Component} by {@link Ext.Component#getId id}
* @param {String} id The component {@link Ext.Component#getId id}
* @return Ext.Component The Component, <tt>undefined</tt> if not found, or <tt>null</tt> if a
* Class was found.
*/
getCmp: function(id) {
return Ext.ComponentMgr.get(id);
},
/**
* 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 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}.
* Any number of elements and/or components can be passed into this function in a single
* call as separate arguments.
* @param {Mixed...} args An {@link Ext.Element}, {@link Ext.Component}, or an Array of either of these to destroy
*/
destroy: function() {
var args = arguments,
ln = args.length,
i, item;
for (i = 0; i < ln; i++) {
item = args[i];
if (item) {
if (Ext.isArray(item)) {
this.destroy.apply(this, item);
}
else if (Ext.isFunction(item.destroy)) {
item.destroy();
}
}
}
},
/**
* Return the dom node for the passed String (id), dom node, or Ext.Element.
* Here are some examples:
* <pre><code>
// gets dom node based on id
var elDom = Ext.getDom('elId');
// gets dom node based on the dom node
var elDom1 = Ext.getDom(elDom);
// If we don&#39;t know if we are working with an
// Ext.Element or a dom node use Ext.getDom
function(el){
var dom = Ext.getDom(el);
// do something with the dom node
}
</code></pre>
* <b>Note</b>: the dom node to be found actually needs to exist (be rendered, etc)
* when this method is called to be successful.
* @param {Mixed} el
* @return HTMLElement
*/
getDom: function(el) {
if (!el || !document) {
return null;
}
return el.dom ? el.dom : (typeof el == 'string' ? document.getElementById(el) : el);
},
/**
* <p>Removes this element from the document, removes all DOM event listeners, and deletes the cache reference.
* All DOM event listeners are removed from this element.
* @param {HTMLElement} node The node to remove
*/
removeNode: function(node) {
if (node && node.parentNode && node.tagName != 'BODY') {
Ext.get(node).clearListeners();
node.parentNode.removeChild(node);
delete Ext.cache[node.id];
}
},
/**
* @private
*/
defaultSetupConfig: {
eventPublishers: {
dom: {
xclass: 'Ext.event.publisher.Dom'
},
touchGesture: {
xclass: 'Ext.event.publisher.TouchGesture',
recognizers: {
drag: {
xclass: 'Ext.event.recognizer.Drag'
},
tap: {
xclass: 'Ext.event.recognizer.Tap'
},
doubleTap: {
xclass: 'Ext.event.recognizer.DoubleTap'
},
longPress: {
xclass: 'Ext.event.recognizer.LongPress'
},
swipe: {
xclass: 'Ext.event.recognizer.HorizontalSwipe'
},
pinch: {
xclass: 'Ext.event.recognizer.Pinch'
},
rotate: {
xclass: 'Ext.event.recognizer.Rotate'
}
}
},
componentDelegation: {
xclass: 'Ext.event.publisher.ComponentDelegation'
},
componentPaint: {
xclass: 'Ext.event.publisher.ComponentPaint'
},
componentSize: {
xclass: 'Ext.event.publisher.ComponentSize'
}
},
logger: {
enabled: true,
xclass: 'Ext.log.Logger',
minPriority: 'deprecate',
writers: {
console: {
xclass: 'Ext.log.writer.Console',
throwOnErrors: true,
formatter: {
xclass: 'Ext.log.formatter.Default'
}
}
}
},
animator: {
xclass: 'Ext.fx.Runner'
},
viewport: {
xclass: 'Ext.viewport.Viewport'
}
},
/**
* @private
*/
isSetup: false,
/**
* @private
*/
setupListeners: [],
/**
* @private
*/
onSetup: function(fn, scope) {
if (Ext.isSetup) {
fn.call(scope);
}
else {
Ext.setupListeners.push({
fn: fn,
scope: scope
});
}
},
/**
* Ext.setup is used to launch a basic application. It handles creating an {@link Ext.Viewport} instance for you.
*
* Ext.setup({
* onReady: function() {
* Ext.Viewport.add({
* xtype: 'component',
* html: 'Hello world!'
* });
* }
* });
*
* @param {Object} config An object with the following config options:
*
* @param {Function} config.onReady
* A function to be called when the application is ready. Your application logic should be here. Please see the example above.
*
* @param {Object} config.viewport
* An object to be used when creating the global {@link Ext.Viewport} instance. Please refer to the {@link Ext.Viewport}
* documentation for more information.
*
* Ext.setup({
* viewport: {
* layout: 'vbox'
* },
* onReady: function() {
* Ext.Viewport.add({
* flex: 1,
* html: 'top (flex: 1)'
* });
*
* Ext.Viewport.add({
* flex: 4,
* html: 'bottom (flex: 4)'
* });
* }
* });
*
* @param {String/Object} config.icon
* A icon configuration for this application. This will work on iOS and Android applications which are saved to the homescreen.
*
* You can either pass a string which will be applied to all different sizes:
*
* Ext.setup({
* icon: 'icon.png',
* onReady: function() {
* console.log('Launch...');
* }
* });
*
* Or an object which has a location for different sizes:
*
* Ext.setup({
* icon: {
* '57': 'icon57.png',
* '77': 'icon77.png',
* '114': 'icon114.png'
* },
* onReady: function() {
* console.log('Launch...');
* }
* });
*
* Android devices will alway use the 57px version.
*
* @param {String} config.icon.57 The icon to be used on non-retna display devices (iPhone 3GS and below).
* @param {String} config.icon.77 The icon to be used on the iPad.
* @param {String} config.icon.114 The icon to be used on retna display devices (iPhone 4 and above).
*
* @param {Boolean} glossOnIcon
* True to add a gloss effect to the icon. This is ignored on Android (it will *not* add gloss).
*
* @param {String} phoneStartupScreen
* Sets the apple-touch-icon `<meta>` tag so your home screen application can have a startup screen on phones.
* Please look here for more information: http://developer.apple.com/library/IOs/#documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html
*
* @param {String} tabletStartupScreen
* Sets the apple-touch-icon `<meta>` tag so your home screen application can have a startup screen on tablets.
* Please look here for more information: http://developer.apple.com/library/IOs/#documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html
*
* @param {String} statusBarStyle
* The style of status bar to be shown on applications added to the iOS homescreen. Valid options are:
*
* * `default`
* * `black`
* * `black-translucent`
*
* @param {String[]} config.requires
* An array of required classes for your application which will be automatically loaded if {@link Ext.Loader#enabled} is set
* to `true`. Please refer to {@link Ext.Loader} and {@link Ext.Loader#require} for more information.
*
* Ext.setup({
* requires: ['Ext.Button', 'Ext.tab.Panel'],
* onReady: function() {
* //...
* }
* });
*
* @param {Object} config.eventPublishers
* Sencha Touch, by default, includes various {@link Ext.event.recognizer.Recognizer} subclasses to recognise events fired
* in your application. The list of default recognisers can be found in the documentation for {@link Ext.event.recognizer.Recognizer}.
*
* To change the default recognisers, you can use the following syntax:
*
* Ext.setup({
* eventPublishers: {
* touchGesture: {
* recognizers: {
* swipe: {
* //this will include both vertical and horizontal swipe recognisers
* xclass: 'Ext.event.recognizer.Swipe'
* }
* }
* }
* },
* onReady: function() {
* //...
* }
* });
*
* You can also disable recognizers using this syntax:
*
* Ext.setup({
* eventPublishers: {
* touchGesture: {
* recognizers: {
* swipe: null,
* pinch: null,
* rotate: null
* }
* }
* },
* onReady: function() {
* //...
* }
* });
*/
setup: function(config) {
var defaultSetupConfig = Ext.defaultSetupConfig,
emptyFn = Ext.emptyFn,
onReady = config.onReady || emptyFn,
onUpdated = config.onUpdated || emptyFn,
scope = config.scope,
requires = Ext.Array.from(config.requires),
extOnReady = Ext.onReady,
icon = config.icon,
callback, viewport, precomposed;
Ext.setup = function() {
throw new Error("Ext.setup has already been called before");
};
delete config.requires;
delete config.onReady;
delete config.onUpdated;
delete config.scope;
Ext.require(['Ext.event.Dispatcher', 'Ext.MessageBox']);
callback = function() {
var listeners = Ext.setupListeners,
ln = listeners.length,
i, listener;
delete Ext.setupListeners;
Ext.isSetup = true;
for (i = 0; i < ln; i++) {
listener = listeners[i];
listener.fn.call(listener.scope);
}
Ext.onReady = extOnReady;
Ext.onReady(onReady, scope);
};
Ext.onUpdated = onUpdated;
Ext.onReady = function(fn, scope) {
var origin = onReady;
onReady = function() {
origin();
Ext.onReady(fn, scope);
};
};
config = Ext.merge({}, defaultSetupConfig, config);
Ext.onDocumentReady(function() {
Ext.factoryConfig(config, function(data) {
Ext.event.Dispatcher.getInstance().setPublishers(data.eventPublishers);
if (data.logger) {
Ext.Logger = data.logger;
}
if (data.animator) {
Ext.Animator = data.animator;
}
if (data.viewport) {
Ext.Viewport = viewport = data.viewport;
if (!scope) {
scope = viewport;
}
Ext.require(requires, function() {
Ext.Viewport.on('ready', callback, null, {single: true});
});
}
else {
Ext.require(requires, callback);
}
});
});
function addMeta(name, content) {
var meta = document.createElement('meta');
meta.setAttribute('name', name);
meta.setAttribute('content', content);
Ext.getHead().append(meta);
}
function addLink(rel, href, sizes) {
var link = document.createElement('link');
link.setAttribute('rel', rel);
link.setAttribute('href', href);
if (sizes) {
link.setAttribute('sizes', sizes);
}
Ext.getHead().append(link);
}
var phoneIcon = config.phoneIcon,
tabletIcon = config.tabletIcon,
tabletStartupScreen = config.tabletStartupScreen,
statusBarStyle = config.statusBarStyle,
phoneStartupScreen = config.phoneStartupScreen,
isIpad = Ext.os.is.iPad,
retina = window.devicePixelRatio > 1;
addMeta('viewport', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no');
addMeta('apple-mobile-web-app-capable', 'yes');
addMeta('apple-touch-fullscreen', 'yes');
//status bar style
if (Ext.isString(statusBarStyle)) {
addMeta('apple-mobile-web-app-status-bar-style', 'statusBarStyle');
}
//startup screens
if (tabletStartupScreen && isIpad) {
addLink('apple-touch-startup-image', tabletStartupScreen);
}
if (phoneStartupScreen && !isIpad) {
addLink('apple-touch-startup-image', phoneStartupScreen);
}
// icon
if (Ext.isString(icon) || Ext.isString(phoneIcon) || Ext.isString(tabletIcon)) {
icon = {
'57': phoneIcon || tabletIcon || icon,
'72': tabletIcon || phoneIcon || icon,
'114': phoneIcon || tabletIcon || icon,
'144': tabletIcon || phoneIcon || icon
};
}
precomposed = (Ext.os.is.iOS && config.glossOnIcon === false) ? '-precomposed' : '';
if (icon) {
var iconString = 'apple-touch-icon' + precomposed,
iconPath;
// Add the default icon
addLink(iconString, icon['57'] || icon['72'] || icon['114'] || icon['144']);
// Loop through each icon size and add it
for (iconPath in icon) {
addLink(iconString, icon[iconPath], iconPath + 'x' + iconPath);
}
}
},
/**
* @member Ext
* @method application
*
* Loads Ext.app.Application class and starts it up with given configuration after the page is ready.
*
* Ext.application({
* launch: function() {
* alert('Application launched!');
* }
* });
*
* See {@link Ext.app.Application} for details.
*
* @param {Object} config An object with the following config options:
*
* @param {Function} config.launch
* A function to be called when the application is ready. Your application logic should be here. Please see {@link Ext.app.Application}
* for details.
*
* @param {Object} config.viewport
* An object to be used when creating the global {@link Ext.Viewport} instance. Please refer to the {@link Ext.Viewport}
* documentation for more information.
*
* Ext.application({
* viewport: {
* layout: 'vbox'
* },
* launch: function() {
* Ext.Viewport.add({
* flex: 1,
* html: 'top (flex: 1)'
* });
*
* Ext.Viewport.add({
* flex: 4,
* html: 'bottom (flex: 4)'
* });
* }
* });
*
* @param {String/Object} config.icon
* A icon configuration for this application. This will only apply to iOS applications which are saved to the homescreen.
*
* You can either pass a string which will be applied to all different sizes:
*
* Ext.setup({
* icon: 'icon.png',
* onReady: function() {
* console.log('Launch...');
* }
* });
*
* Or an object which has a location for different sizes:
*
* Ext.setup({
* icon: {
* '57': 'icon57.png',
* '77': 'icon77.png',
* '114': 'icon114.png'
* },
* onReady: function() {
* console.log('Launch...');
* }
* });
*
* @param {String} config.icon.57 The icon to be used on non-retna display devices (iPhone 3GS and below).
* @param {String} config.icon.77 The icon to be used on the iPad.
* @param {String} config.icon.114 The icon to be used on retna display devices (iPhone 4 and above).
*
* @param {Boolean} glossOnIcon
* True to add a gloss effect to the icon.
*
* @param {String} phoneStartupScreen
* Sets the apple-touch-icon `<meta>` tag so your home screen application can have a startup screen on phones.
* Please look here for more information: http://developer.apple.com/library/IOs/#documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html
*
* @param {String} tabletStartupScreen
* Sets the apple-touch-icon `<meta>` tag so your home screen application can have a startup screen on tablets.
* Please look here for more information: http://developer.apple.com/library/IOs/#documentation/AppleApplications/Reference/SafariWebContent/ConfiguringWebApplications/ConfiguringWebApplications.html
*
* @param {String} statusBarStyle
* The style of status bar to be shown on applications added to the iOS homescreen. Valid options are:
*
* * `default`
* * `black`
* * `black-translucent`
*
* @param {String[]} config.requires
* An array of required classes for your application which will be automatically loaded if {@link Ext.Loader#enabled} is set
* to `true`. Please refer to {@link Ext.Loader} and {@link Ext.Loader#require} for more information.
*
* Ext.application({
* requires: ['Ext.Button', 'Ext.tab.Panel'],
* launch: function() {
* //...
* }
* });
*
* @param {Object} config.eventPublishers
* Sencha Touch, by default, includes various {@link Ext.event.recognizer.Recognizer} subclasses to recognise events fired
* in your application. The list of default recognisers can be found in the documentation for {@link Ext.event.recognizer.Recognizer}.
*
* To change the default recognisers, you can use the following syntax:
*
* Ext.application({
* eventPublishers: {
* touchGesture: {
* recognizers: {
* swipe: {
* //this will include both vertical and horizontal swipe recognisers
* xclass: 'Ext.event.recognizer.Swipe'
* }
* }
* }
* },
* launch: function() {
* //...
* }
* });
*
* You can also disable recognizers using this syntax:
*
* Ext.application({
* eventPublishers: {
* touchGesture: {
* recognizers: {
* swipe: null,
* pinch: null,
* rotate: null
* }
* }
* },
* launch: function() {
* //...
* }
* });
*/
application: function(config) {
var appName = config.name,
onReady, scope, requires;
if (!config) {
config = {};
}
if (!Ext.Loader.config.paths[appName]) {
Ext.Loader.setPath(appName, config.appFolder || 'app');
}
requires = Ext.Array.from(config.requires);
config.requires = ['Ext.app.Application'];
onReady = config.onReady;
scope = config.scope;
config.onReady = function() {
config.requires = requires;
new Ext.app.Application(config);
if (onReady) {
onReady.call(scope);
}
};
Ext.setup(config);
},
/**
* @private
* @param config
* @param callback
* @member Ext
*/
factoryConfig: function(config, callback) {
var isSimpleObject = Ext.isSimpleObject(config);
if (isSimpleObject && config.xclass) {
var className = config.xclass;
delete config.xclass;
Ext.require(className, function() {
Ext.factoryConfig(config, function(cfg) {
callback(Ext.create(className, cfg));
});
});
return;
}
var isArray = Ext.isArray(config),
keys = [],
key, value, i, ln;
if (isSimpleObject || isArray) {
if (isSimpleObject) {
for (key in config) {
if (config.hasOwnProperty(key)) {
value = config[key];
if (Ext.isSimpleObject(value) || Ext.isArray(value)) {
keys.push(key);
}
}
}
}
else {
for (i = 0,ln = config.length; i < ln; i++) {
value = config[i];
if (Ext.isSimpleObject(value) || Ext.isArray(value)) {
keys.push(i);
}
}
}
i = 0;
ln = keys.length;
if (ln === 0) {
callback(config);
return;
}
function fn(value) {
config[key] = value;
i++;
factory();
}
function factory() {
if (i >= ln) {
callback(config);
return;
}
key = keys[i];
value = config[key];
Ext.factoryConfig(value, fn);
}
factory();
return;
}
callback(config);
},
/**
* @private
* @param config
* @param classReference
* @member Ext
*/
factory: function(config, classReference, instance, aliasNamespace) {
var manager = Ext.ClassManager,
newInstance;
// If config is falsy or a valid instance, destroy the current instance
// (if it exists) and replace with the new one
if (!config || config.isInstance) {
if (instance && instance !== config) {
instance.destroy();
}
return config;
}
if (aliasNamespace) {
// If config is a string value, treat is as an alias
if (typeof config == 'string') {
return manager.instantiateByAlias(aliasNamespace + '.' + config);
}
// Same if 'type' is given in config
else if (Ext.isObject(config) && 'type' in config) {
return manager.instantiateByAlias(aliasNamespace + '.' + config.type, config);
}
}
else if (typeof config == 'string') {
return Ext.getCmp(config);
}
if (config === true) {
if (instance) {
return instance;
}
else {
return manager.instantiate(classReference);
}
}
if (!Ext.isObject(config)) {
Ext.Logger.error("Invalid config, must be a valid config object");
}
if ('xtype' in config) {
newInstance = manager.instantiateByAlias('widget.' + config.xtype, config);
}
if ('xclass' in config) {
newInstance = manager.instantiate(config.xclass, config);
}
if (newInstance) {
if (instance) {
instance.destroy();
}
return newInstance;
}
if (instance) {
return instance.setConfig(config);
}
return manager.instantiate(classReference, config);
},
/**
* @private
* @member Ext
*/
deprecateClassMember: function(cls, oldName, newName, message) {
return this.deprecateProperty(cls.prototype, oldName, newName, message);
},
/**
* @private
* @member Ext
*/
deprecateClassMembers: function(cls, members) {
var prototype = cls.prototype,
oldName, newName;
for (oldName in members) {
if (members.hasOwnProperty(oldName)) {
newName = members[oldName];
this.deprecateProperty(prototype, oldName, newName);
}
}
},
/**
* @private
* @member Ext
*/
deprecateProperty: function(object, oldName, newName, message) {
if (!message) {
message = "'" + oldName + "' is deprecated";
}
if (newName) {
message += ", please use '" + newName + "' instead";
}
if (newName) {
Ext.Object.defineProperty(object, oldName, {
get: function() {
Ext.Logger.deprecate(message, 1);
return this[newName];
},
set: function(value) {
Ext.Logger.deprecate(message, 1);
this[newName] = value;
},
configurable: true
});
}
},
/**
* @private
* @member Ext
*/
deprecatePropertyValue: function(object, name, value, message) {
Ext.Object.defineProperty(object, name, {
get: function() {
Ext.Logger.deprecate(message, 1);
return value;
},
configurable: true
});
},
/**
* @private
* @member Ext
*/
deprecateMethod: function(object, name, method, message) {
object[name] = function() {
Ext.Logger.deprecate(message, 2);
if (method) {
return method.apply(this, arguments);
}
};
},
/**
* @private
* @member Ext
*/
deprecateClassMethod: function(cls, name, method, message) {
if (typeof name != 'string') {
var from, to;
for (from in name) {
if (name.hasOwnProperty(from)) {
to = name[from];
Ext.deprecateClassMethod(cls, from, to);
}
}
return;
}
var isLateBinding = typeof method == 'string',
member;
if (!message) {
message = "'" + name + "()' is deprecated, please use '" + (isLateBinding ? method : method.name) +
"()' instead";
}
if (isLateBinding) {
member = function() {
Ext.Logger.deprecate(message, this);
return this[method].apply(this, arguments);
};
}
else {
member = function() {
Ext.Logger.deprecate(message, this);
return method.apply(this, arguments);
};
}
if (name in cls.prototype) {
Ext.Object.defineProperty(cls.prototype, name, {
value: null,
writable: true,
configurable: true
});
}
cls.addMember(name, member);
},
/**
* Useful snippet to show an exact, narrowed-down list of top-level Components that are not yet destroyed.
* @private
*/
showLeaks: function() {
var map = Ext.ComponentManager.all.map,
leaks = [],
parent;
Ext.Object.each(map, function(id, component) {
while ((parent = component.getParent()) && map.hasOwnProperty(parent.getId())) {
component = parent;
}
if (leaks.indexOf(component) === -1) {
leaks.push(component);
}
});
console.log(leaks);
},
/**
* True when the document is fully initialized and ready for action
* @type Boolean
* @member Ext
*/
isReady : false,
/**
* @private
* @member Ext
*/
readyListeners: [],
/**
* @private
* @member Ext
*/
triggerReady: function() {
var listeners = Ext.readyListeners,
i, ln, listener;
if (!Ext.isReady) {
Ext.isReady = true;
for (i = 0,ln = listeners.length; i < ln; i++) {
listener = listeners[i];
listener.fn.call(listener.scope);
}
delete Ext.readyListeners;
}
},
/**
* @private
* @member Ext
*/
onDocumentReady: function(fn, scope) {
if (Ext.isReady) {
fn.call(scope);
}
else {
var triggerFn = Ext.triggerReady;
Ext.readyListeners.push({
fn: fn,
scope: scope
});
if (Ext.browser.is.PhoneGap && !Ext.os.is.Desktop) {
if (!Ext.readyListenerAttached) {
Ext.readyListenerAttached = true;
document.addEventListener('deviceready', triggerFn, false);
}
}
else {
if (document.readyState.match(/interactive|complete|loaded/) !== null) {
triggerFn();
}
else if (!Ext.readyListenerAttached) {
Ext.readyListenerAttached = true;
window.addEventListener('DOMContentLoaded', triggerFn, false);
}
}
}
},
/**
* Calls function after specified delay, or right away when delay == 0.
* @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.
* @member Ext
*/
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);
}
}
}
});
/**
* @aside guide environment_package
*
* Provides useful information about the current browser. Should not be manually instantiated unless for unit-testing;
* access the global instance stored in Ext.browser instead. Example:
*
* <pre><code>
* if (Ext.browser.is.IE) {
* // IE specific code here
* }
*
* if (Ext.browser.is.WebKit) {
* // WebKit specific code here
* }
*
* console.log("Version " + Ext.browser.version);
* </code></pre>
*
* For a full list of supported values, refer to: {@link Ext.env.Browser#is}
*/
Ext.define('Ext.env.Browser', {
requires: ['Ext.Version'],
statics: {
browserNames: {
ie: 'IE',
firefox: 'Firefox',
safari: 'Safari',
chrome: 'Chrome',
opera: 'Opera',
dolfin: 'Dolfin',
webosbrowser: 'webOSBrowser',
chromeMobile: 'ChromeMobile',
silk: 'Silk',
other: 'Other'
},
engineNames: {
webkit: 'WebKit',
gecko: 'Gecko',
presto: 'Presto',
trident: 'Trident',
other: 'Other'
},
enginePrefixes: {
webkit: 'AppleWebKit/',
gecko: 'Gecko/',
presto: 'Presto/',
trident: 'Trident/'
},
browserPrefixes: {
ie: 'MSIE ',
firefox: 'Firefox/',
chrome: 'Chrome/',
safari: 'Version/',
opera: 'Opera/',
dolfin: 'Dolfin/',
webosbrowser: 'wOSBrowser/',
chromeMobile: 'CrMo/',
silk: 'Silk/'
}
},
styleDashPrefixes: {
WebKit: '-webkit-',
Gecko: '-moz-',
Trident: '-ms-',
Presto: '-o-',
Other: ''
},
stylePrefixes: {
WebKit: 'Webkit',
Gecko: 'Moz',
Trident: 'ms',
Presto: 'O',
Other: ''
},
propertyPrefixes: {
WebKit: 'webkit',
Gecko: 'moz',
Trident: 'ms',
Presto: 'o',
Other: ''
},
// scope: Ext.env.Browser.prototype
/**
* A "hybrid" property, can be either accessed as a method call, i.e:
* <pre><code>
* if (Ext.browser.is('IE')) { ... }
* </code></pre>
*
* or as an object with boolean properties, i.e:
* <pre><code>
* if (Ext.browser.is.IE) { ... }
* </code></pre>
*
* Versions can be conveniently checked as well. For example:
* <pre><code>
* if (Ext.browser.is.IE6) { ... } // Equivalent to (Ext.browser.is.IE && Ext.browser.version.equals(6))
* </code></pre>
*
* Note that only {@link Ext.Version#getMajor major component} and {@link Ext.Version#getShortVersion simplified}
* value of the version are available via direct property checking.
*
* Supported values are: IE, Firefox, Safari, Chrome, Opera, WebKit, Gecko, Presto, Trident and Other
*
* @param {String} value The OS name to check
* @return {Boolean}
*/
is: Ext.emptyFn,
/**
* Read-only - the full name of the current browser
* Possible values are: IE, Firefox, Safari, Chrome, Opera and Other
* @type String
*/
name: null,
/**
* Read-only, refer to {@link Ext.Version}
* @type Ext.Version
*/
version: null,
/**
* Read-only - the full name of the current browser's engine
* Possible values are: WebKit, Gecko, Presto, Trident and Other
* @type String
*/
engineName: null,
/**
* Read-only, refer to {@link Ext.Version}
* @type Ext.Version
*/
engineVersion: null,
setFlag: function(name, value) {
if (typeof value == 'undefined') {
value = true;
}
this.is[name] = value;
this.is[name.toLowerCase()] = value;
return this;
},
constructor: function(userAgent) {
/**
* @property {String}
* Browser User Agent string.
*/
this.userAgent = userAgent;
is = this.is = function(name) {
return is[name] === true;
};
var statics = this.statics(),
browserMatch = userAgent.match(new RegExp('((?:' + Ext.Object.getValues(statics.browserPrefixes).join(')|(?:') + '))([\\w\\._]+)')),
engineMatch = userAgent.match(new RegExp('((?:' + Ext.Object.getValues(statics.enginePrefixes).join(')|(?:') + '))([\\w\\._]+)')),
browserNames = statics.browserNames,
browserName = browserNames.other,
engineNames = statics.engineNames,
engineName = engineNames.other,
browserVersion = '',
engineVersion = '',
isWebView = false,
is, i, name;
if (browserMatch) {
browserName = browserNames[Ext.Object.getKey(statics.browserPrefixes, browserMatch[1])];
browserVersion = new Ext.Version(browserMatch[2]);
}
if (engineMatch) {
engineName = engineNames[Ext.Object.getKey(statics.enginePrefixes, engineMatch[1])];
engineVersion = new Ext.Version(engineMatch[2]);
}
// Facebook changes the userAgent when you view a website within their iOS app. For some reason, the strip out information
// about the browser, so we have to detect that and fake it...
if (userAgent.match(/FB/) && browserName == "Other") {
browserName = browserNames.safari;
engineName = engineNames.webkit;
}
Ext.apply(this, {
engineName: engineName,
engineVersion: engineVersion,
name: browserName,
version: browserVersion
});
this.setFlag(browserName);
if (browserVersion) {
this.setFlag(browserName + (browserVersion.getMajor() || ''));
this.setFlag(browserName + browserVersion.getShortVersion());
}
for (i in browserNames) {
if (browserNames.hasOwnProperty(i)) {
name = browserNames[i];
this.setFlag(name, browserName === name);
}
}
this.setFlag(name);
if (engineVersion) {
this.setFlag(engineName + (engineVersion.getMajor() || ''));
this.setFlag(engineName + engineVersion.getShortVersion());
}
for (i in engineNames) {
if (engineNames.hasOwnProperty(i)) {
name = engineNames[i];
this.setFlag(name, engineName === name);
}
}
this.setFlag('Standalone', !!navigator.standalone);
if (typeof window.PhoneGap != 'undefined') {
isWebView = true;
this.setFlag('PhoneGap');
}
else if (!!window.isNK) {
isWebView = true;
this.setFlag('Sencha');
}
// Flag to check if it we are in the WebView
this.setFlag('WebView', isWebView);
/**
* @property {Boolean}
* True if browser is using strict mode.
*/
this.isStrict = document.compatMode == "CSS1Compat";
/**
* @property {Boolean}
* True if page is running over SSL.
*/
this.isSecure = /^https/i.test(window.location.protocol);
return this;
},
getStyleDashPrefix: function() {
return this.styleDashPrefixes[this.engineName];
},
getStylePrefix: function() {
return this.stylePrefixes[this.engineName];
},
getVendorProperyName: function(name) {
var prefix = this.propertyPrefixes[this.engineName];
if (prefix.length > 0) {
return prefix + Ext.String.capitalize(name);
}
return name;
}
}, function() {
var browserEnv = Ext.browser = new this(Ext.global.navigator.userAgent);
});
/**
* @aside guide environment_package
*
* Provide useful information about the current operating system environment. Access the global instance stored in
* Ext.os. Example:
*
* if (Ext.os.is.Windows) {
* // Windows specific code here
* }
*
* if (Ext.os.is.iOS) {
* // iPad, iPod, iPhone, etc.
* }
*
* console.log("Version " + Ext.os.version);
*
* For a full list of supported values, refer to: {@link Ext.env.OS#is}
*/
Ext.define('Ext.env.OS', {
requires: ['Ext.Version'],
statics: {
names: {
ios: 'iOS',
android: 'Android',
webos: 'webOS',
blackberry: 'BlackBerry',
rimTablet: 'RIMTablet',
mac: 'MacOS',
win: 'Windows',
linux: 'Linux',
bada: 'Bada',
other: 'Other'
},
prefixes: {
ios: 'i(?:Pad|Phone|Pod)(?:.*)CPU(?: iPhone)? OS ',
android: '(Android |HTC_|Silk/)', // Some HTC devices ship with an OSX userAgent by default,
// so we need to add a direct check for HTC_
blackberry: 'BlackBerry(?:.*)Version\/',
rimTablet: 'RIM Tablet OS ',
webos: '(?:webOS|hpwOS)\/',
bada: 'Bada\/'
}
},
/**
* A "hybrid" property, can be either accessed as a method call, i.e:
*
* if (Ext.os.is('Android')) { ... }
*
* or as an object with boolean properties, i.e:
*
* if (Ext.os.is.Android) { ... }
*
* Versions can be conveniently checked as well. For example:
*
* if (Ext.os.is.Android2) { ... } // Equivalent to (Ext.os.is.Android && Ext.os.version.equals(2))
*
* if (Ext.os.is.iOS32) { ... } // Equivalent to (Ext.os.is.iOS && Ext.os.version.equals(3.2))
*
* Note that only {@link Ext.Version#getMajor major component} and {@link Ext.Version#getShortVersion simplified}
* value of the version are available via direct property checking. Supported values are: iOS, iPad, iPhone, iPod,
* Android, WebOS, BlackBerry, Bada, MacOS, Windows, Linux and Other
* @param {String} value The OS name to check
* @return {Boolean}
*/
is: Ext.emptyFn,
/**
* @property {String} [name=null]
* Read-only - the full name of the current operating system Possible values are: iOS, Android, WebOS, BlackBerry,
* MacOS, Windows, Linux and Other
*/
name: null,
/**
* @property {Ext.Version} [version=null]
* Read-only, refer to {@link Ext.Version}
*/
version: null,
setFlag: function(name, value) {
if (typeof value == 'undefined') {
value = true;
}
this.is[name] = value;
this.is[name.toLowerCase()] = value;
return this;
},
constructor: function(userAgent, platform) {
var statics = this.statics(),
names = statics.names,
prefixes = statics.prefixes,
name,
version = '',
i, prefix, match, item, is;
is = this.is = function(name) {
return this.is[name] === true;
};
for (i in prefixes) {
if (prefixes.hasOwnProperty(i)) {
prefix = prefixes[i];
match = userAgent.match(new RegExp('(?:'+prefix+')([^\\s;]+)'));
if (match) {
name = names[i];
// This is here because some HTC android devices show an OSX Snow Leopard userAgent by default.
// And the Kindle Fire doesn't have any indicator of Android as the OS in its User Agent
if (match[1] && (match[1] == "HTC_" || match[1] == "Silk/")) {
version = new Ext.Version("2.3");
} else {
version = new Ext.Version(match[match.length - 1]);
}
break;
}
}
}
if (!name) {
name = names[(userAgent.toLowerCase().match(/mac|win|linux/) || ['other'])[0]];
version = new Ext.Version('');
}
this.name = name;
this.version = version;
if (platform) {
this.setFlag(platform);
}
this.setFlag(name);
if (version) {
this.setFlag(name + (version.getMajor() || ''));
this.setFlag(name + version.getShortVersion());
}
for (i in names) {
if (names.hasOwnProperty(i)) {
item = names[i];
if (!is.hasOwnProperty(name)) {
this.setFlag(item, (name === item));
}
}
}
return this;
}
}, function() {
var navigation = Ext.global.navigator,
userAgent = navigation.userAgent,
osEnv, osName, deviceType;
Ext.os = osEnv = new this(userAgent, navigation.platform);
osName = osEnv.name;
var search = window.location.search.match(/deviceType=(Tablet|Phone)/),
nativeDeviceType = window.deviceType;
// Override deviceType by adding a get variable of deviceType. NEEDED FOR DOCS APP.
// E.g: example/kitchen-sink.html?deviceType=Phone
if (search && search[1]) {
deviceType = search[1];
}
else if (nativeDeviceType === 'iPhone') {
deviceType = 'Phone';
}
else if (nativeDeviceType === 'iPad') {
deviceType = 'Tablet';
}
else {
if (!osEnv.is.Android && !osEnv.is.iOS && /Windows|Linux|MacOS/.test(osName)) {
deviceType = 'Desktop';
}
else if (osEnv.is.iPad || osEnv.is.Android3 || (osEnv.is.Android4 && userAgent.search(/mobile/i) == -1)) {
deviceType = 'Tablet';
}
else {
deviceType = 'Phone';
}
}
osEnv.setFlag(deviceType, true);
osEnv.deviceType = deviceType;
/**
* @class Ext.is
* Used to detect if the current browser supports a certain feature, and the type of the current browser.
* @deprecated 2.0.0
* Please refer to the {@link Ext.env.Browser}, {@link Ext.env.OS} and {@link Ext.feature.has} classes instead.
*/
});
/**
* @aside guide environment_package
*
* A class to detect if the current browser supports various features.
*
* Please refer to the documentation of {@link Ext.feature.has} on how to use it.
*
* if (Ext.feature.has.Canvas) {
* // do some cool things with canvas here
* }
*/
Ext.define('Ext.env.Feature', {
requires: ['Ext.env.Browser', 'Ext.env.OS'],
constructor: function() {
this.testElements = {};
this.has = function(name) {
return !!this.has[name];
};
return this;
},
getTestElement: function(tag, createNew) {
if (tag === undefined) {
tag = 'div';
}
else if (typeof tag !== 'string') {
return tag;
}
if (createNew) {
return document.createElement(tag);
}
if (!this.testElements[tag]) {
this.testElements[tag] = document.createElement(tag);
}
return this.testElements[tag];
},
isStyleSupported: function(name, tag) {
var elementStyle = this.getTestElement(tag).style,
cName = Ext.String.capitalize(name);
if (typeof elementStyle[name] !== 'undefined'
|| typeof elementStyle[Ext.browser.getStylePrefix(name) + cName] !== 'undefined') {
return true;
}
return false;
},
isEventSupported: function(name, tag) {
if (tag === undefined) {
tag = window;
}
var element = this.getTestElement(tag),
eventName = 'on' + name.toLowerCase(),
isSupported = (eventName in element);
if (!isSupported) {
if (element.setAttribute && element.removeAttribute) {
element.setAttribute(eventName, '');
isSupported = typeof element[eventName] === 'function';
if (typeof element[eventName] !== 'undefined') {
element[eventName] = undefined;
}
element.removeAttribute(eventName);
}
}
return isSupported;
},
getSupportedPropertyName: function(object, name) {
var vendorName = Ext.browser.getVendorProperyName(name);
if (vendorName in object) {
return vendorName;
}
else if (name in object) {
return name;
}
return null;
},
registerTest: Ext.Function.flexSetter(function(name, fn) {
this.has[name] = fn.call(this);
return this;
})
}, function() {
Ext.feature = new this;
var has = Ext.feature.has;
/**
* @class Ext.feature.has
* A simple class to verify if a browser feature exists or not on the current device.
*
* if (Ext.feature.has.Canvas) {
* // do some cool things with canvas here
* }
*
* See the list of properties below too see which features are available for detection.
*/
Ext.feature.registerTest({
/**
* @member Ext.feature.has
* @property {Boolean} Canvas
* True if the current device supports Canvas.
*/
Canvas: function() {
var element = this.getTestElement('canvas');
return !!(element && element.getContext && element.getContext('2d'));
},
/**
* @member Ext.feature.has
* @property {Boolean} Svg
* True if the current device supports SVG.
*/
Svg: function() {
var doc = document;
return !!(doc.createElementNS && !!doc.createElementNS("http:/" + "/www.w3.org/2000/svg", "svg").createSVGRect);
},
/**
* @member Ext.feature.has
* @property {Boolean} Vml
* True if the current device supports VML.
*/
Vml: function() {
var element = this.getTestElement(),
ret = false;
element.innerHTML = "<!--[if vml]><br><![endif]-->";
ret = (element.childNodes.length === 1);
element.innerHTML = "";
return ret;
},
/**
* @member Ext.feature.has
* @property {Boolean} Touch
* True if the current device supports touch events (`touchstart`).
*/
Touch: function() {
return this.isEventSupported('touchstart') && !(Ext.os && Ext.os.name.match(/Windows|MacOS|Linux/));
},
/**
* @member Ext.feature.has
* @property {Boolean} Orientation
* True if the current device supports different orientations.
*/
Orientation: function() {
return ('orientation' in window) && this.isEventSupported('orientationchange');
},
/**
* @member Ext.feature.has
* @property {Boolean} OrientationChange
* True if the current device supports the `orientationchange` event.
*/
OrientationChange: function() {
return this.isEventSupported('orientationchange');
},
/**
* @member Ext.feature.has
* @property {Boolean} DeviceMotion
* True if the current device supports the `devicemotion` event.
*/
DeviceMotion: function() {
return this.isEventSupported('devicemotion');
},
/**
* @member Ext.feature.has
* @property {Boolean} Geolocation
* True if the current device supports Geolocation.
*/
Geolocation: function() {
return 'geolocation' in window.navigator;
},
/**
* @member Ext.feature.has
* @property {Boolean} SqlDatabase
* True if the current device supports SQL Databases.
*/
SqlDatabase: function() {
return 'openDatabase' in window;
},
/**
* @member Ext.feature.has
* @property {Boolean} WebSockets
* True if the current device supports WebSockets.
*/
WebSockets: function() {
return 'WebSocket' in window;
},
/**
* @member Ext.feature.has
* @property {Boolean} Range
* True if the current device supports [DOM document fragments.][1]
*
* [1]: https://developer.mozilla.org/en/DOM/range
*/
Range: function() {
return !!document.createRange;
},
/**
* @member Ext.feature.has
* @property {Boolean} CreateContextualFragment
* True if the current device supports HTML fragment parsing using [range.createContextualFragment()][1].
*
* [1]: https://developer.mozilla.org/en/DOM/range.createContextualFragment
*/
CreateContextualFragment: function() {
var range = !!document.createRange ? document.createRange() : false;
return range && !!range.createContextualFragment;
},
/**
* @member Ext.feature.has
* @property {Boolean} History
* True if the current device supports history management with [history.pushState()][1].
*
* [1]: https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#The_pushState().C2.A0method
*/
History: function() {
return ('history' in window && 'pushState' in window.history);
},
/**
* @member Ext.feature.has
* @property {Boolean} CssTransforms
* True if the current device supports CSS Transform animations.
*/
CssTransforms: function() {
return this.isStyleSupported('transform');
},
/**
* @member Ext.feature.has
* @property {Boolean} Css3dTransforms
* True if the current device supports CSS 3D Transform animations.
*/
Css3dTransforms: function() {
// See https://sencha.jira.com/browse/TOUCH-1544
return this.has('CssTransforms') && this.isStyleSupported('perspective') && !Ext.os.is.Android2;
},
/**
* @member Ext.feature.has
* @property {Boolean} CssAnimations
* True if the current device supports CSS Animations.
*/
CssAnimations: function() {
return this.isStyleSupported('animationName');
},
/**
* @member Ext.feature.has
* @property {Boolean} CssTransitions
* True if the current device supports CSS Transitions.
*/
CssTransitions: function() {
return this.isStyleSupported('transitionProperty');
},
/**
* @member Ext.feature.has
* @property {Boolean} Audio
* True if the current device supports the `<audio>` tag.
*/
Audio: function() {
return !!this.getTestElement('audio').canPlayType;
},
/**
* @member Ext.feature.has
* @property {Boolean} Video
* True if the current device supports the `<video>` tag.
*/
Video: function() {
return !!this.getTestElement('video').canPlayType;
},
/**
* @member Ext.feature.has
* @property {Boolean} ClassList
* True if document environment supports the HTML5 classList API.
*/
ClassList: function() {
return "classList" in this.getTestElement();
}
});
});
/**
* @class Ext.DomQuery
* @alternateClassName Ext.dom.Query
*
* Provides functionality to select elements on the page based on a CSS selector. 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.
*
* ## 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] has an attribute "foo" that 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"
*/
Ext.define('Ext.dom.Query', {
/**
* Selects a group of elements.
* @param {String} selector The selector/xpath query (can be a comma separated list of selectors)
* @param {HTMLElement/String} [root] 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.
*/
select: function(q, root) {
var results = [],
nodes,
i,
j,
qlen,
nlen;
root = root || document;
if (typeof root == 'string') {
root = document.getElementById(root);
}
q = q.split(",");
for (i = 0,qlen = q.length; i < qlen; i++) {
if (typeof q[i] == 'string') {
//support for node attribute selection
if (q[i][0] == '@') {
nodes = root.getAttributeNode(q[i].substring(1));
results.push(nodes);
}
else {
nodes = root.querySelectorAll(q[i]);
for (j = 0,nlen = nodes.length; j < nlen; j++) {
results.push(nodes[j]);
}
}
}
}
return results;
},
/**
* Selects a single element.
* @param {String} selector The selector/xpath query
* @param {HTMLElement/String} [root] The start of the query (defaults to document).
* @return {HTMLElement} The DOM element which matched the selector.
*/
selectNode: function(q, root) {
return this.select(q, root)[0];
},
/**
* Returns true if the passed element(s) match the passed simple selector (e.g. div.some-class or span:first-child)
* @param {String/HTMLElement/Array} el An element id, element or array of elements
* @param {String} selector The simple selector to test
* @return {Boolean}
*/
is: function(el, q) {
if (typeof el == "string") {
el = document.getElementById(el);
}
return this.select(q).indexOf(el) !== -1;
},
isXml: function(el) {
var docEl = (el ? el.ownerDocument || el : 0).documentElement;
return docEl ? docEl.nodeName !== "HTML" : false;
}
}, function() {
Ext.ns('Ext.core');
Ext.core.DomQuery = Ext.DomQuery = new this();
Ext.query = Ext.Function.alias(Ext.DomQuery, 'select');
});
/**
* @class Ext.DomHelper
* @alternateClassName Ext.dom.Helper
*
* 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
*
* ## Insertion methods
*
* Commonly used insertion methods:
*
* * {@link #append}
* * {@link #insertBefore}
* * {@link #insertAfter}
* * {@link #overwrite}
* * {@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, 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 = '"{0}" href="{1}" class="nav">{2}';
*
* var tpl = new Ext.DomHelper.createTemplate(html);
* tpl.append('blog-roll', ['link1', 'http://www.tommymaintz.com/', "Tommy's Site"]);
* tpl.append('blog-roll', ['link2', 'http://www.avins.org/', "Jamie's Site"]);
*
* The same example using named parameters:
*
* var html = '"{id}" href="{url}" class="nav">{text}';
*
* var tpl = new Ext.DomHelper.createTemplate(html);
* tpl.append('blog-roll', {
* id: 'link1',
* url: 'http://www.tommymaintz.com/',
* text: "Tommy's Site"
* });
* tpl.append('blog-roll', {
* id: 'link2',
* url: 'http://www.avins.org/',
* text: "Jamie'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 "compiling" the template. The way
* "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 = '"{id}" href="{url}" class="nav">{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 useDom is false, then the string is used as
* innerHTML. If 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
*
*/
Ext.define('Ext.dom.Helper', {
emptyTags : /^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i,
confRe : /tag|children|cn|html|tpl|tplData$/i,
endRe : /end/i,
attribXlat: { cls : 'class', htmlFor : 'for' },
closeTags: {},
decamelizeName : function () {
var camelCaseRe = /([a-z])([A-Z])/g,
cache = {};
function decamel (match, p1, p2) {
return p1 + '-' + p2.toLowerCase();
}
return function (s) {
return cache[s] || (cache[s] = s.replace(camelCaseRe, decamel));
};
}(),
generateMarkup: function(spec, buffer) {
var me = this,
attr, val, tag, i, closeTags;
if (typeof spec == "string") {
buffer.push(spec);
} else if (Ext.isArray(spec)) {
for (i = 0; i < spec.length; i++) {
if (spec[i]) {
me.generateMarkup(spec[i], buffer);
}
}
} else {
tag = spec.tag || 'div';
buffer.push('<', tag);
for (attr in spec) {
if (spec.hasOwnProperty(attr)) {
val = spec[attr];
if (!me.confRe.test(attr)) {
if (typeof val == "object") {
buffer.push(' ', attr, '="');
me.generateStyles(val, buffer).push('"');
} else {
buffer.push(' ', me.attribXlat[attr] || attr, '="', val, '"');
}
}
}
}
// Now either just close the tag or try to add children and close the tag.
if (me.emptyTags.test(tag)) {
buffer.push('/>');
} else {
buffer.push('>');
// Apply the tpl html, and cn specifications
if ((val = spec.tpl)) {
val.applyOut(spec.tplData, buffer);
}
if ((val = spec.html)) {
buffer.push(val);
}
if ((val = spec.cn || spec.children)) {
me.generateMarkup(val, buffer);
}
// we generate a lot of close tags, so cache them rather than push 3 parts
closeTags = me.closeTags;
buffer.push(closeTags[tag] || (closeTags[tag] = '</' + tag + '>'));
}
}
return buffer;
},
/**
* Converts the styles from the given object to text. The styles are CSS style names
* with their associated value.
*
* The basic form of this method returns a string:
*
* var s = Ext.DomHelper.generateStyles({
* backgroundColor: 'red'
* });
*
* // s = 'background-color:red;'
*
* Alternatively, this method can append to an output array.
*
* var buf = [];
*
* ...
*
* Ext.DomHelper.generateStyles({
* backgroundColor: 'red'
* }, buf);
*
* In this case, the style text is pushed on to the array and the array is returned.
*
* @param {Object} styles The object describing the styles.
* @param {String[]} [buffer] The output buffer.
* @return {String/String[]} If buffer is passed, it is returned. Otherwise the style
* string is returned.
*/
generateStyles: function (styles, buffer) {
var a = buffer || [],
name;
for (name in styles) {
if (styles.hasOwnProperty(name)) {
a.push(this.decamelizeName(name), ':', styles[name], ';');
}
}
return buffer || a.join('');
},
/**
* Returns the markup for the passed Element(s) config.
* @param {Object} spec The DOM object spec (and children)
* @return {String}
*/
markup: function(spec) {
if (typeof spec == "string") {
return spec;
}
var buf = this.generateMarkup(spec, []);
return buf.join('');
},
/**
* 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) {
Ext.fly(el).applyStyles(styles);
},
/**
* @private
* Fix for browsers which no longer support createContextualFragment
*/
createContextualFragment: function(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;
},
/**
* Inserts an HTML fragment into the DOM.
* @param {String} where Where to insert the html in relation to el - beforeBegin, afterBegin, beforeEnd, afterEnd.
*
* For example take the following HTML: `<div>Contents</div>`
*
* Using different `where` values inserts element to the following places:
*
* - beforeBegin: `<HERE><div>Contents</div>`
* - afterBegin: `<div><HERE>Contents</div>`
* - beforeEnd: `<div>Contents<HERE></div>`
* - afterEnd: `<div>Contents</div><HERE>`
*
* @param {HTMLElement/TextNode} el The context element
* @param {String} html The HTML fragment
* @return {HTMLElement} The new node
*/
insertHtml: function(where, el, html) {
var setStart, range, frag, rangeEl, isBeforeBegin, isAfterBegin;
where = where.toLowerCase();
if (Ext.isTextNode(el)) {
if (where == 'afterbegin' ) {
where = 'beforebegin';
}
else if (where == 'beforeend') {
where = 'afterend';
}
}
isBeforeBegin = where == 'beforebegin';
isAfterBegin = where == 'afterbegin';
range = Ext.feature.has.CreateContextualFragment ? el.ownerDocument.createRange() : undefined;
setStart = 'setStart' + (this.endRe.test(where) ? 'After' : 'Before');
if (isBeforeBegin || where == 'afterend') {
if (range) {
range[setStart](el);
frag = range.createContextualFragment(html);
}
else {
frag = this.createContextualFragment(html);
}
el.parentNode.insertBefore(frag, isBeforeBegin ? el : el.nextSibling);
return el[(isBeforeBegin ? 'previous' : 'next') + 'Sibling'];
}
else {
rangeEl = (isAfterBegin ? 'first' : 'last') + 'Child';
if (el.firstChild) {
if (range) {
range[setStart](el[rangeEl]);
frag = range.createContextualFragment(html);
} else {
frag = this.createContextualFragment(html);
}
if (isAfterBegin) {
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] true to return a Ext.Element
* @return {HTMLElement/Ext.Element} The new node
*/
insertBefore: function(el, o, returnElement) {
return this.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] true to return a Ext.Element
* @return {HTMLElement/Ext.Element} The new node
*/
insertAfter: function(el, o, returnElement) {
return this.doInsert(el, o, returnElement, 'afterend');
},
/**
* 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] true to return a Ext.Element
* @return {HTMLElement/Ext.Element} The new node
*/
insertFirst: function(el, o, returnElement) {
return this.doInsert(el, o, returnElement, 'afterbegin');
},
/**
* 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] true to return a Ext.Element
* @return {HTMLElement/Ext.Element} The new node
*/
append: function(el, o, returnElement) {
return this.doInsert(el, o, returnElement, 'beforeend');
},
/**
* 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] true to return a Ext.Element
* @return {HTMLElement/Ext.Element} The new node
*/
overwrite: function(el, o, returnElement) {
el = Ext.getDom(el);
el.innerHTML = this.markup(o);
return returnElement ? Ext.get(el.firstChild) : el.firstChild;
},
doInsert: function(el, o, returnElement, pos) {
var newNode = this.insertHtml(pos, Ext.getDom(el), this.markup(o));
return returnElement ? Ext.get(newNode, true) : newNode;
},
/**
* 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 = this.markup(o);
return new Ext.Template(html);
}
}, function() {
Ext.ns('Ext.core');
Ext.core.DomHelper = Ext.DomHelper = new this;
});
/**
* An Identifiable mixin.
* @private
*/
Ext.define('Ext.mixin.Identifiable', {
statics: {
uniqueIds: {}
},
isIdentifiable: true,
mixinId: 'identifiable',
idCleanRegex: /\.|[^\w\-]/g,
defaultIdPrefix: 'ext-',
defaultIdSeparator: '-',
getOptimizedId: function() {
return this.id;
},
getUniqueId: function() {
var id = this.id,
prototype, separator, xtype, uniqueIds, prefix;
if (!id) {
prototype = this.self.prototype;
separator = this.defaultIdSeparator;
uniqueIds = Ext.mixin.Identifiable.uniqueIds;
if (!prototype.hasOwnProperty('identifiablePrefix')) {
xtype = this.xtype;
if (xtype) {
prefix = this.defaultIdPrefix + xtype + separator;
}
else {
prefix = prototype.$className.replace(this.idCleanRegex, separator).toLowerCase() + separator;
}
prototype.identifiablePrefix = prefix;
}
prefix = this.identifiablePrefix;
if (!uniqueIds.hasOwnProperty(prefix)) {
uniqueIds[prefix] = 0;
}
id = this.id = prefix + (++uniqueIds[prefix]);
}
this.getUniqueId = this.getOptimizedId;
return id;
},
setId: function(id) {
this.id = id;
},
/**
* Retrieves the id of this component. Will autogenerate an id if one has not already been set.
* @return {String} id
*/
getId: function() {
var id = this.id;
if (!id) {
id = this.getUniqueId();
}
this.getId = this.getOptimizedId;
return id;
}
});
/**
* Encapsulates a DOM element, adding simple DOM manipulation facilities, normalizing for browser differences.
*
* All instances of this class inherit the methods of Ext.Fx 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. To access the
* underlying browser event, see Ext.EventObject.browserEvent. Some older browsers may not support the full range of
* events. Which events are supported is beyond the control of Sencha Touch.
*
* ## Usage
*
* // by id
* var el = Ext.get("my-div");
*
* // by DOM element reference
* var el = Ext.get(myDivElement);
*
* ## Composite (Collections of) Elements
*
* For working with collections of Elements, see Ext.CompositeElement
*
* @mixins Ext.mixin.Observable
*/
Ext.define('Ext.dom.Element', {
alternateClassName: 'Ext.Element',
mixins: [
'Ext.mixin.Identifiable'
],
requires: [
'Ext.dom.Query',
'Ext.dom.Helper'
],
observableType: 'element',
xtype: 'element',
statics: {
CREATE_ATTRIBUTES: {
style: 'style',
className: 'className',
cls: 'cls',
classList: 'classList',
text: 'text',
hidden: 'hidden',
html: 'html',
children: 'children'
},
create: function(attributes, domNode) {
var ATTRIBUTES = this.CREATE_ATTRIBUTES,
element, elementStyle, tag, value, name, i, ln;
if (!attributes) {
attributes = {};
}
if (attributes.isElement) {
return attributes.dom;
}
else if ('nodeType' in attributes) {
return attributes;
}
if (typeof attributes == 'string') {
return document.createTextNode(attributes);
}
tag = attributes.tag;
if (!tag) {
tag = 'div';
}
element = document.createElement(tag);
elementStyle = element.style;
for (name in attributes) {
if (name != 'tag' && attributes.hasOwnProperty(name)) {
value = attributes[name];
switch (name) {
case ATTRIBUTES.style:
if (typeof value == 'string') {
element.setAttribute(name, value);
}
else {
for (i in value) {
if (value.hasOwnProperty(i)) {
elementStyle[i] = value[i];
}
}
}
break;
case ATTRIBUTES.className:
case ATTRIBUTES.cls:
element.className = value;
break;
case ATTRIBUTES.classList:
element.className = value.join(' ');
break;
case ATTRIBUTES.text:
element.textContent = value;
break;
case ATTRIBUTES.hidden:
if (value) {
element.style.display = 'none';
}
break;
case ATTRIBUTES.html:
element.innerHTML = value;
break;
case ATTRIBUTES.children:
for (i = 0,ln = value.length; i < ln; i++) {
element.appendChild(this.create(value[i], true));
}
break;
default:
element.setAttribute(name, value);
}
}
}
if (domNode) {
return element;
}
else {
return this.get(element);
}
},
documentElement: null,
cache: {},
/**
* Retrieves Ext.dom.Element objects. {@link Ext#get} is alias for {@link Ext.dom.Element#get}.
*
* **This method does not retrieve {@link Ext.Component Component}s.** This method retrieves Ext.dom.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.dom.Element} The Element object (or null if no matching element was found)
* @static
* @inheritable
*/
get: function(element) {
var cache = this.cache,
instance, dom, id;
if (!element) {
return null;
}
if (typeof element == 'string') {
if (cache.hasOwnProperty(element)) {
return cache[element];
}
if (!(dom = document.getElementById(element))) {
return null;
}
cache[element] = instance = new this(dom);
return instance;
}
if ('tagName' in element) { // dom element
id = element.id;
if (cache.hasOwnProperty(id)) {
return cache[id];
}
instance = new this(element);
cache[instance.getId()] = instance;
return instance;
}
if (element.isElement) {
return element;
}
if (element.isComposite) {
return element;
}
if (Ext.isArray(element)) {
return this.select(element);
}
if (element === document) {
// create a bogus element object representing the document object
if (!this.documentElement) {
this.documentElement = new this(document.documentElement);
this.documentElement.setId('ext-application');
}
return this.documentElement;
}
return null;
},
data: function(element, key, value) {
var cache = Ext.cache,
id, data;
element = this.get(element);
if (!element) {
return null;
}
id = element.id;
data = cache[id].data;
if (!data) {
cache[id].data = data = {};
}
if (arguments.length == 2) {
return data[key];
}
else {
return (data[key] = value);
}
}
},
isElement: true,
constructor: function(dom) {
if (typeof dom == 'string') {
dom = document.getElementById(dom);
}
if (!dom) {
throw new Error("Invalid domNode reference or an id of an existing domNode: " + dom);
}
/**
* The DOM element
* @property dom
* @type HTMLElement
*/
this.dom = dom;
this.getUniqueId();
},
attach: function (dom) {
this.dom = dom;
this.id = dom.id;
return this;
},
getUniqueId: function() {
var id = this.id,
dom;
if (!id) {
dom = this.dom;
if (dom.id.length > 0) {
this.id = id = dom.id;
}
else {
dom.id = id = this.mixins.identifiable.getUniqueId.call(this);
}
this.self.cache[id] = this;
}
return id;
},
setId: function(id) {
var currentId = this.id,
cache = this.self.cache;
if (currentId) {
delete cache[currentId];
}
this.dom.id = id;
/**
* The DOM element ID
* @property id
* @type String
*/
this.id = id;
cache[id] = this;
return this;
},
/**
* Sets the innerHTML of this element.
* @param {String} html The new HTML
*/
setHtml: function(html) {
this.dom.innerHTML = html;
},
/**
* Returns the innerHTML of an Element.
* @return {String}
*/
getHtml: function() {
return this.dom.innerHTML;
},
setText: function(text) {
this.dom.textContent = text;
},
redraw: function() {
var dom = this.dom,
domStyle = dom.style;
domStyle.display = 'none';
dom.offsetHeight;
domStyle.display = '';
},
isPainted: function() {
var dom = this.dom;
return Boolean(dom && dom.offsetParent);
},
/**
* Sets the passed attributes as attributes of this element (a style attribute can be a string, object or function)
* @param {Object} attributes The object with the attributes
* @param {Boolean} [useSet=true] false to override the default setAttribute to use expandos.
* @return {Ext.dom.Element} this
*/
set: function(attributes, useSet) {
var dom = this.dom,
attribute, value;
for (attribute in attributes) {
if (attributes.hasOwnProperty(attribute)) {
value = attributes[attribute];
if (attribute == 'style') {
this.applyStyles(value);
}
else if (attribute == 'cls') {
dom.className = value;
}
else if (useSet !== false) {
if (value === undefined) {
dom.removeAttribute(attribute);
} else {
dom.setAttribute(attribute, value);
}
}
else {
dom[attribute] = value;
}
}
}
return this;
},
/**
* 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(selector) {
return Ext.DomQuery.is(this.dom, selector);
},
/**
* 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 value = this.dom.value;
return asNumber ? parseInt(value, 10) : value;
},
/**
* Returns the value of an attribute from the element's underlying DOM node.
* @param {String} name The attribute name
* @param {String} [namespace] The namespace in which to look for the attribute
* @return {String} The attribute value
*/
getAttribute: function(name, namespace) {
var dom = this.dom;
return dom.getAttributeNS(namespace, name) || dom.getAttribute(namespace + ":" + name)
|| dom.getAttribute(name) || dom[name];
},
/**
* Removes this element's dom reference. Note that event and cache removal is handled at {@link Ext#removeNode}
*/
destroy: function() {
this.isDestroyed = true;
var cache = Ext.Element.cache,
dom = this.dom;
if (dom && dom.parentNode && dom.tagName != 'BODY') {
dom.parentNode.removeChild(dom);
}
delete cache[this.id];
delete this.dom;
}
}, function(Element) {
Ext.elements = Ext.cache = Element.cache;
this.addStatics({
Fly: new Ext.Class({
extend: Element,
constructor: function(dom) {
this.dom = dom;
}
}),
_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.dom.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.dom.Element
* class.
*
* @param {String/HTMLElement} element The dom node or id
* @param {String} [named] Allows for creation of named reusable flyweights to prevent conflicts (e.g.
* internally Ext uses "_global")
* @return {Ext.dom.Element} The shared Element object (or null if no matching element was found)
* @static
*/
fly: function(element, named) {
var fly = null,
flyweights = Element._flyweights,
cachedElement;
named = named || '_global';
element = Ext.getDom(element);
if (element) {
fly = flyweights[named] || (flyweights[named] = new Element.Fly());
fly.dom = element;
fly.isSynchronized = false;
cachedElement = Ext.cache[element.id];
if (cachedElement && cachedElement.isElement) {
cachedElement.isSynchronized = false;
}
}
return fly;
}
});
/**
* @member Ext
* @method get
* @alias Ext.dom.Element#get
*/
Ext.get = function(element) {
return Element.get.call(Element, element);
};
/**
* @member Ext
* @method fly
* @alias Ext.dom.Element#fly
*/
Ext.fly = function() {
return Element.fly.apply(Element, arguments);
};
Ext.ClassManager.onCreated(function() {
Element.mixin('observable', Ext.mixin.Observable);
}, null, 'Ext.mixin.Observable');
});
/**
* @class Ext.dom.Element
*/
Ext.dom.Element.addStatics({
unitRe: /\d+(px|em|%|en|ex|pt|in|cm|mm|pc)$/i,
camelRe: /(-[a-z])/gi,
cssRe: /([a-z0-9-]+)\s*:\s*([^;\s]+(?:\s*[^;\s]+)*);?/gi,
opacityRe: /alpha\(opacity=(.*)\)/i,
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'},
/**
* Test if size has a unit, otherwise appends the passed unit string, or the default for this Element.
* @param size {Object} The size to set
* @param units {String} The units to append to a numeric size value
* @private
* @static
*/
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 === undefined || size === null) {
return size || '';
}
// Otherwise, warn if it's not a valid CSS measurement
if (!this.unitRe.test(size)) {
Ext.Logger.warn("Warning, size detected as NaN on Element.addUnits.");
return size || '';
}
return size;
},
/**
* @static
* @private
*/
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;
},
/**
* 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 (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) {
// TODO: Mobile optimization?
// if (prop == 'float') {
// prop = Ext.supports.Float ? 'cssFloat' : 'styleFloat';
// }
return this.propertyCache[prop] || (this.propertyCache[prop] = prop.replace(this.camelRe, this.camelReplaceFn));
},
/**
* 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.dom.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.dom.Element
*/
/**
* @class Ext.dom.Element
*/
Ext.dom.Element.addMembers({
/**
* Appends the passed element(s) to this element
* @param {HTMLElement/Ext.dom.Element} element a DOM Node or an existing Element.
* @return {Ext.dom.Element} This element
*/
appendChild: function(element) {
this.dom.appendChild(Ext.getDom(element));
return this;
},
removeChild: function(element) {
this.dom.removeChild(Ext.getDom(element));
return this;
},
append: function() {
this.appendChild.apply(this, arguments);
},
/**
* Appends this element to the passed element
* @param {String/HTMLElement/Ext.dom.Element} el The new parent element.
* The id of the node, a DOM Node or an existing Element.
* @return {Ext.dom.Element} This element
*/
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.dom.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.dom.Element} This element
*/
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.dom.Element} el The element to insert after.
* The id of the node, a DOM Node or an existing Element.
* @return {Ext.dom.Element} This element
*/
insertAfter: function(el) {
el = Ext.getDom(el);
el.parentNode.insertBefore(this.dom, el.nextSibling);
return this;
},
/**
* Inserts an element as the first child of this element
* @param {String/HTMLElement/Ext.dom.Element} element The id or element to insert
* @return {Ext.dom.Element} this
*/
insertFirst: function(element) {
var elementDom = Ext.getDom(element),
dom = this.dom,
firstChild = dom.firstChild;
if (!firstChild) {
dom.appendChild(elementDom);
}
else {
dom.insertBefore(elementDom, firstChild);
}
return this;
},
/**
* Inserts (or creates) the passed element (or DomHelper config) as a sibling of this element
* @param {String/HTMLElement/Ext.dom.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 raw DOM element instead of Ext.dom.Element
* @return {Ext.dom.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.core.DomHelper.append(me.dom.parentNode, el, !returnDom);
} else {
rt = Ext.core.DomHelper[isAfter ? 'insertAfter' : 'insertBefore'](me.dom, el, !returnDom);
}
}
return rt;
},
/**
* Replaces the passed element with this element
* @param {String/HTMLElement/Ext.dom.Element} el The element to replace.
* The id of the node, a DOM Node or an existing Element.
* @return {Ext.dom.Element} This element
*/
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.dom.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.dom.Element} This element
*/
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.core.DomHelper.insertBefore(me.dom, el);
}
delete Ext.cache[me.id];
Ext.removeNode(me.dom);
me.id = Ext.id(me.dom = el);
Ext.dom.Element.addToCache(me.isFlyweight ? new Ext.dom.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.dom.Element} The new child element
*/
createChild: function(config, insertBefore, returnDom) {
config = config || {tag: 'div'};
if (insertBefore) {
return Ext.core.DomHelper.insertBefore(insertBefore, config, returnDom !== true);
}
else {
return Ext.core.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} domNode (optional) True to return the raw DOM element instead of Ext.dom.Element
* @return {HTMLElement/Ext.dom.Element} The newly created wrapper element
*/
wrap: function(config, domNode) {
var dom = this.dom,
wrapper = this.self.create(config, domNode),
wrapperDom = (domNode) ? wrapper : wrapper.dom,
parentNode = dom.parentNode;
if (parentNode) {
parentNode.insertBefore(wrapperDom, dom);
}
wrapperDom.appendChild(dom);
return wrapper;
},
wrapAllChildren: function(config) {
var dom = this.dom,
children = dom.childNodes,
wrapper = this.self.create(config),
wrapperDom = wrapper.dom;
while (children.length > 0) {
wrapperDom.appendChild(dom.firstChild);
}
dom.appendChild(wrapperDom);
return wrapper;
},
unwrapAllChildren: function() {
var dom = this.dom,
children = dom.childNodes,
parentNode = dom.parentNode;
if (parentNode) {
while (children.length > 0) {
parentNode.insertBefore(dom, dom.firstChild);
}
this.destroy();
}
},
unwrap: function() {
var dom = this.dom,
parentNode = dom.parentNode,
grandparentNode;
if (parentNode) {
grandparentNode = parentNode.parentNode;
grandparentNode.insertBefore(dom, parentNode);
grandparentNode.removeChild(parentNode);
}
else {
grandparentNode = document.createDocumentFragment();
grandparentNode.appendChild(dom);
}
return this;
},
/**
* 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.dom.Element (defaults to false)
* @return {HTMLElement/Ext.dom.Element} The inserted node (or nearest related if more than 1 inserted)
*/
insertHtml: function(where, html, returnEl) {
var el = Ext.core.DomHelper.insertHtml(where, this.dom, html);
return returnEl ? Ext.get(el) : el;
}
});
/**
* @class Ext.dom.Element
*/
Ext.dom.Element.override({
/**
* 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(el) {
return this.getXY(el)[0];
},
/**
* 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(el) {
return this.getXY(el)[1];
},
/**
* 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 {Array} The XY position of the element
*/
getXY: function() {
var webkitConvert = window.webkitConvertPointFromNodeToPage;
if (webkitConvert) {
return function() {
var point = webkitConvert(this.dom, new WebKitPoint(0, 0));
return [point.x, point.y];
}
}
else return function() {
var rect = this.dom.getBoundingClientRect(),
round = Math.round;
return [round(rect.left + window.pageXOffset), round(rect.top + window.pageYOffset)];
}
}(),
/**
* 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 {Mixed} element The element to get the offsets from.
* @return {Array} 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.dom.Element} this
*/
setX: function(x) {
return this.setXY([x, this.getY()]);
},
/**
* 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.dom.Element} this
*/
setY: function(y) {
return this.setXY([this.getX(), y]);
},
/**
* 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 {Array} 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.dom.Element} this
*/
setXY: function(pos) {
var me = this;
if (arguments.length > 1) {
pos = [pos, arguments[1]];
}
// me.position();
var pts = me.translatePoints(pos),
style = me.dom.style;
for (pos in pts) {
if (!pts.hasOwnProperty(pos)) {
continue;
}
if (!isNaN(pts[pos])) style[pos] = pts[pos] + "px";
}
return me;
},
/**
* Gets the left X coordinate
* @return {Number}
*/
getLeft: function() {
return parseInt(this.getStyle('left'), 10) || 0;
},
/**
* Gets the right X coordinate of the element (element X position + element width)
* @return {Number}
*/
getRight: function() {
return parseInt(this.getStyle('right'), 10) || 0;
},
/**
* Gets the top Y coordinate
* @return {Number}
*/
getTop: function() {
return parseInt(this.getStyle('top'), 10) || 0;
},
/**
* Gets the bottom Y coordinate of the element (element Y position + element height)
* @return {Number}
*/
getBottom: function() {
return parseInt(this.getStyle('bottom'), 10) || 0;
},
/**
* Translates the passed page coordinates into left/top css values for this element
* @param {Number/Array} 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) {
y = isNaN(x[1]) ? y : x[1];
x = isNaN(x[0]) ? x : x[0];
var me = this,
relative = me.isStyle('position', 'relative'),
o = me.getXY(),
l = parseInt(me.getStyle('left'), 10),
t = parseInt(me.getStyle('top'), 10);
l = !isNaN(l) ? l : (relative ? 0 : me.dom.offsetLeft);
t = !isNaN(t) ? t : (relative ? 0 : me.dom.offsetTop);
return {left: (x - o[0] + l), top: (y - o[1] + t)};
},
/**
* Sets the element's box. Use getBox() on another element to get a box object.
* @param {Object} box The box to fill, for example:
*
* {
* left: ...,
* top: ...,
* width: ...,
* height: ...
* }
*
* @return {Ext.dom.Element} this
*/
setBox: function(box) {
var me = this,
width = box.width,
height = box.height,
top = box.top,
left = box.left;
if (left !== undefined) {
me.setLeft(left);
}
if (top !== undefined) {
me.setTop(top);
}
if (width !== undefined) {
me.setWidth(width);
}
if (height !== undefined) {
me.setHeight(height);
}
return this;
},
/**
* Return an object defining the area of this Element which can be passed to {@link #setBox} to
* set another Element's size/location to match this element.
* @param {Boolean} contentBox (optional) If true a box for the content of the element is returned.
* @param {Boolean} local (optional) If true the element's left and top are returned instead of page x/y.
* @return {Object} box An object in the format<pre><code>
{
x: &lt;Element's X position>,
y: &lt;Element's Y position>,
width: &lt;Element's width>,
height: &lt;Element's height>,
bottom: &lt;Element's lower bound>,
right: &lt;Element's rightmost bound>
}
</code></pre>
* The returned object may also be addressed as an Array where index 0 contains the X position
* and index 1 contains the Y position. So the result may also be used for {@link #setXY}
*/
getBox: function(contentBox, local) {
var me = this,
dom = me.dom,
width = dom.offsetWidth,
height = dom.offsetHeight,
xy, box, l, r, t, b;
if (!local) {
xy = me.getXY();
}
else if (contentBox) {
xy = [0, 0];
}
else {
xy = [parseInt(me.getStyle("left"), 10) || 0, parseInt(me.getStyle("top"), 10) || 0];
}
if (!contentBox) {
box = {
x: xy[0],
y: xy[1],
0: xy[0],
1: xy[1],
width: width,
height: height
};
}
else {
l = me.getBorderWidth.call(me, "l") + me.getPadding.call(me, "l");
r = me.getBorderWidth.call(me, "r") + me.getPadding.call(me, "r");
t = me.getBorderWidth.call(me, "t") + me.getPadding.call(me, "t");
b = me.getBorderWidth.call(me, "b") + me.getPadding.call(me, "b");
box = {
x: xy[0] + l,
y: xy[1] + t,
0: xy[0] + l,
1: xy[1] + t,
width: width - (l + r),
height: height - (t + b)
};
}
box.left = box.x;
box.top = box.y;
box.right = box.x + box.width;
box.bottom = box.y + box.height;
return box;
},
/**
* Return an object defining the area of this Element which can be passed to {@link #setBox} to
* set another Element's size/location to match this element.
* @param {Boolean} asRegion(optional) If true an Ext.util.Region will be returned
* @return {Object} box An object in the format<pre><code>
{
x: &lt;Element's X position>,
y: &lt;Element's Y position>,
width: &lt;Element's width>,
height: &lt;Element's height>,
bottom: &lt;Element's lower bound>,
right: &lt;Element's rightmost bound>
}
</code></pre>
* The returned object may also be addressed as an Array where index 0 contains the X position
* and index 1 contains the Y position. So the result may also be used for {@link #setXY}
*/
getPageBox: function(getRegion) {
var me = this,
el = me.dom,
w = el.offsetWidth,
h = el.offsetHeight,
xy = me.getXY(),
t = xy[1],
r = xy[0] + w,
b = xy[1] + h,
l = xy[0];
if (!el) {
return new Ext.util.Region();
}
if (getRegion) {
return new Ext.util.Region(t, r, b, l);
}
else {
return {
left: l,
top: t,
width: w,
height: h,
right: r,
bottom: b
};
}
}
});
/**
* @class Ext.dom.Element
*/
Ext.dom.Element.addMembers({
WIDTH: 'width',
HEIGHT: 'height',
MIN_WIDTH: 'min-width',
MIN_HEIGHT: 'min-height',
MAX_WIDTH: 'max-width',
MAX_HEIGHT: 'max-height',
TOP: 'top',
RIGHT: 'right',
BOTTOM: 'bottom',
LEFT: 'left',
/**
* @property VISIBILITY
* Visibility mode constant for use with {@link #setVisibilityMode}. Use visibility to hide element
*/
VISIBILITY: 1,
/**
* @property DISPLAY
* Visibility mode constant for use with {@link #setVisibilityMode}. Use display to hide element
*/
DISPLAY: 2,
/**
* @property OFFSETS
* Visibility mode constant for use with {@link #setVisibilityMode}. Use offsets to hide element
*/
OFFSETS: 3,
SEPARATOR: '-',
trimRe: /^\s+|\s+$/g,
wordsRe: /\w/g,
spacesRe: /\s+/,
styleSplitRe: /\s*(?::|;)\s*/,
transparentRe: /^(?:transparent|(?:rgba[(](?:\s*\d+\s*[,]){3}\s*0\s*[)]))$/i,
classNameSplitRegex: /[\s]+/,
borders: {
t: 'border-top-width',
r: 'border-right-width',
b: 'border-bottom-width',
l: 'border-left-width'
},
paddings: {
t: 'padding-top',
r: 'padding-right',
b: 'padding-bottom',
l: 'padding-left'
},
margins: {
t: 'margin-top',
r: 'margin-right',
b: 'margin-bottom',
l: 'margin-left'
},
/**
* @property {String} defaultUnit
* The default unit to append to CSS values where a unit isn't provided.
*/
defaultUnit: "px",
isSynchronized: false,
/**
* @private
*/
synchronize: function() {
var dom = this.dom,
hasClassMap = {},
className = dom.className,
classList, i, ln, name;
if (className.length > 0) {
classList = dom.className.split(this.classNameSplitRegex);
for (i = 0, ln = classList.length; i < ln; i++) {
name = classList[i];
hasClassMap[name] = true;
}
}
else {
classList = [];
}
this.classList = classList;
this.hasClassMap = hasClassMap;
this.isSynchronized = true;
return this;
},
/**
* Adds the given CSS class(es) to this Element
* @param {String} names The CSS class(es) to add to this element
* @param {String} prefix Optional prefix to prepend to each class
* @param {String} suffix Optional suffix to append to each class
*/
addCls: function(names, prefix, suffix) {
if (!names) {
return this;
}
if (!this.isSynchronized) {
this.synchronize();
}
var dom = this.dom,
map = this.hasClassMap,
classList = this.classList,
SEPARATOR = this.SEPARATOR,
i, ln, name;
prefix = prefix ? prefix + SEPARATOR : '';
suffix = suffix ? SEPARATOR + suffix : '';
if (typeof names == 'string') {
names = names.split(this.spacesRe);
}
for (i = 0, ln = names.length; i < ln; i++) {
name = prefix + names[i] + suffix;
if (!map[name]) {
map[name] = true;
classList.push(name);
}
}
dom.className = classList.join(' ');
return this;
},
/**
* Removes the given CSS class(es) from this Element
* @param {String} names The CSS class(es) to remove from this element
* @param {String} prefix Optional prefix to prepend to each class to be removed
* @param {String} suffix Optional suffix to append to each class to be removed
*/
removeCls: function(names, prefix, suffix) {
if (!names) {
return this;
}
if (!this.isSynchronized) {
this.synchronize();
}
if (!suffix) {
suffix = '';
}
var dom = this.dom,
map = this.hasClassMap,
classList = this.classList,
SEPARATOR = this.SEPARATOR,
i, ln, name;
prefix = prefix ? prefix + SEPARATOR : '';
suffix = suffix ? SEPARATOR + suffix : '';
if (typeof names == 'string') {
names = names.split(this.spacesRe);
}
for (i = 0, ln = names.length; i < ln; i++) {
name = prefix + names[i] + suffix;
if (map[name]) {
delete map[name];
Ext.Array.remove(classList, name);
}
}
dom.className = classList.join(' ');
return this;
},
/**
* 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.dom.Element} this
*/
replaceCls: function(oldName, newName, prefix, suffix) {
return this.removeCls(oldName, prefix, suffix).addCls(newName, prefix, suffix);
},
/**
* Checks if the specified CSS class exists on this element's DOM node.
* @param {String} className The CSS class to check for
* @return {Boolean} True if the class exists, else false
*/
hasCls: function(name) {
if (!this.isSynchronized) {
this.synchronize();
}
return this.hasClassMap.hasOwnProperty(name);
},
/**
* 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.dom.Element} this
*/
toggleCls: function(className) {
return this.hasCls(className) ? this.removeCls(className) : this.addCls(className);
},
/**
* Set the width of this Element.
* @param {Number/String} width The new width.
* @return {Ext.dom.Element} this
*/
setWidth: function(width) {
return this.setLengthValue(this.WIDTH, width);
},
/**
* Set the height of this Element.
* @param {Number/String} height The new height.
* @return {Ext.dom.Element} this
*/
setHeight: function(height) {
return this.setLengthValue(this.HEIGHT, height);
},
/**
* Set the size 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.
* - 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.
* @return {Ext.dom.Element} this
*/
setSize: function(width, height) {
if (Ext.isObject(width)) {
// in case of object from getSize()
height = width.height;
width = width.width;
}
this.setWidth(width);
this.setHeight(height);
return this;
},
/**
* Set the minimum width of this Element.
* @param {Number/String} width The new minimum width.
* @return {Ext.dom.Element} this
*/
setMinWidth: function(width) {
return this.setLengthValue(this.MIN_WIDTH, width);
},
/**
* Set the minimum height of this Element.
* @param {Number/String} height The new minimum height.
* @return {Ext.dom.Element} this
*/
setMinHeight: function(height) {
return this.setLengthValue(this.MIN_HEIGHT, height);
},
/**
* Set the maximum width of this Element.
* @param {Number/String} width The new maximum width.
* @return {Ext.dom.Element} this
*/
setMaxWidth: function(width) {
return this.setLengthValue(this.MAX_WIDTH, width);
},
/**
* Set the maximum height of this Element.
* @param {Number/String} height The new maximum height.
* @return {Ext.dom.Element} this
*/
setMaxHeight: function(height) {
return this.setLengthValue(this.MAX_HEIGHT, height);
},
/**
* Sets the element's top position directly using CSS style (instead of {@link #setY}).
* @param {String} top The top CSS property value
* @return {Ext.dom.Element} this
*/
setTop: function(top) {
return this.setLengthValue(this.TOP, top);
},
/**
* Sets the element's CSS right style.
* @param {String} right The right CSS property value
* @return {Ext.dom.Element} this
*/
setRight: function(right) {
return this.setLengthValue(this.RIGHT, right);
},
/**
* Sets the element's CSS bottom style.
* @param {String} bottom The bottom CSS property value
* @return {Ext.dom.Element} this
*/
setBottom: function(bottom) {
return this.setLengthValue(this.BOTTOM, bottom);
},
/**
* Sets the element's left position directly using CSS style (instead of {@link #setX}).
* @param {String} left The left CSS property value
* @return {Ext.dom.Element} this
*/
setLeft: function(left) {
return this.setLengthValue(this.LEFT, left);
},
setMargin: function(margin) {
var domStyle = this.dom.style;
if (margin || margin === 0) {
margin = this.self.unitizeBox((margin === true) ? 5 : margin);
domStyle.setProperty('margin', margin, 'important');
}
else {
domStyle.removeProperty('margin-top');
domStyle.removeProperty('margin-right');
domStyle.removeProperty('margin-bottom');
domStyle.removeProperty('margin-left');
}
},
setPadding: function(padding) {
var domStyle = this.dom.style;
if (padding || padding === 0) {
padding = this.self.unitizeBox((padding === true) ? 5 : padding);
domStyle.setProperty('padding', padding, 'important');
}
else {
domStyle.removeProperty('padding-top');
domStyle.removeProperty('padding-right');
domStyle.removeProperty('padding-bottom');
domStyle.removeProperty('padding-left');
}
},
setBorder: function(border) {
var domStyle = this.dom.style;
if (border || border === 0) {
border = this.self.unitizeBox((border === true) ? 1 : border);
domStyle.setProperty('border-width', border, 'important');
}
else {
domStyle.removeProperty('border-top-width');
domStyle.removeProperty('border-right-width');
domStyle.removeProperty('border-bottom-width');
domStyle.removeProperty('border-left-width');
}
},
setLengthValue: function(name, value) {
var domStyle = this.dom.style;
if (value === null) {
domStyle.removeProperty(name);
return this;
}
if (typeof value == 'number') {
value = value + 'px';
}
domStyle.setProperty(name, value, 'important');
return this;
},
/**
* 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
* @return {Ext.Element} this
*/
setVisible: function(visible) {
var mode = this.getVisibilityMode(),
method = visible ? 'removeCls' : 'addCls';
switch (mode) {
case this.VISIBILITY:
this.removeCls(['x-hidden-display', 'x-hidden-offsets']);
this[method]('x-hidden-visibility');
break;
case this.DISPLAY:
this.removeCls(['x-hidden-visibility', 'x-hidden-offsets']);
this[method]('x-hidden-display');
break;
case this.OFFSETS:
this.removeCls(['x-hidden-visibility', 'x-hidden-display']);
this[method]('x-hidden-offsets');
break;
}
return this;
},
getVisibilityMode: function() {
var dom = this.dom,
mode = Ext.dom.Element.data(dom, 'visibilityMode');
if (mode === undefined) {
Ext.dom.Element.data(dom, 'visibilityMode', mode = this.DISPLAY);
}
return mode;
},
/**
* Use this to change the visisbiliy mode between {@link #VISIBILITY}, {@link #DISPLAY} or {@link #OFFSETS}.
*/
setVisibilityMode: function(mode) {
this.self.data(this.dom, 'visibilityMode', mode);
return this;
},
/**
* Shows this element.
* Uses display mode to determine whether to use "display" or "visibility". See {@link #setVisible}.
*/
show: function() {
var dom = this.dom;
if (dom) {
dom.style.removeProperty('display');
}
},
/**
* Hides this element.
* Uses display mode to determine whether to use "display" or "visibility". See {@link #setVisible}.
*/
hide: function() {
var dom = this.dom,
domStyle = dom.style,
needsRedraw = Ext.os.is.iOS;
if (domStyle.getPropertyValue('display') !== 'none') {
// iOS sometimes has a long delay before redrawing elements with their CSS 'display' set to 'none'
// This force a redraw to make sure the element is hidden instantly
if (needsRedraw) {
domStyle.setProperty('display', 'none', 'important');
dom.offsetHeight;
domStyle.removeProperty('display');
dom.offsetHeight;
}
domStyle.setProperty('display', 'none', 'important');
}
},
setVisibility: function(isVisible) {
var domStyle = this.dom.style;
if (isVisible) {
domStyle.removeProperty('visibility');
}
else {
domStyle.setProperty('visibility', 'hidden', 'important');
}
},
/**
* This shared object is keyed by style name (e.g., 'margin-left' or 'marginLeft'). The
* values are objects with the following properties:
*
* * `name` (String) : The actual name to be presented to the DOM. This is typically the value
* returned by {@link #normalize}.
* * `get` (Function) : A hook function that will perform the get on this style. These
* functions receive "(dom, el)" arguments. The `dom` parameter is the DOM Element
* from which to get ths tyle. The `el` argument (may be null) is the Ext.Element.
* * `set` (Function) : A hook function that will perform the set on this style. These
* functions receive "(dom, value, el)" arguments. The `dom` parameter is the DOM Element
* from which to get ths tyle. The `value` parameter is the new value for the style. The
* `el` argument (may be null) is the Ext.Element.
*
* The `this` pointer is the object that contains `get` or `set`, which means that
* `this.name` can be accessed if needed. The hook functions are both optional.
* @private
* @markdown
*/
styleHooks: {},
// private
addStyles: function(sides, styles) {
var totalSize = 0,
sidesArr = sides.match(this.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;
},
/**
* Checks if the current value of a style is equal to a given value.
* @param {String} style property whose value is returned.
* @param {String} value to check against.
* @return {Boolean} true for when the current value equals the given value.
*/
isStyle: function(style, val) {
return this.getStyle(style) == val;
},
/**
* Normalizes currentStyle and computedStyle.
* @param {String} prop The style property whose value is returned.
* @return {String} The current value of the style property for this element.
*/
getStyle: function(prop) {
var me = this,
dom = me.dom,
hook = me.styleHooks[prop],
cs, result;
if (dom == document) {
return null;
}
if (!hook) {
me.styleHooks[prop] = hook = { name: this.self.normalize(prop) };
}
if (hook.get) {
return hook.get(dom, me);
}
cs = window.getComputedStyle(dom, '');
// why the dom.style lookup? It is not true that "style == computedStyle" as
// well as the fact that 0/false are valid answers...
result = (cs && cs[hook.name]); // || dom.style[hook.name];
// Webkit returns rgb values for transparent, how does this work n IE9+
// if (!supportsTransparentColor && result == 'rgba(0, 0, 0, 0)') {
// result = 'transparent';
// }
return result;
},
/**
* 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] The value to apply to the given property, or null if an object was passed.
* @return {Ext.dom.Element} this
*/
setStyle: function(prop, value) {
var me = this,
dom = me.dom,
hooks = me.styleHooks,
style = dom.style,
valueFrom = Ext.valueFrom,
name, hook;
// we don't promote the 2-arg form to object-form to avoid the overhead...
if (typeof prop == 'string') {
hook = hooks[prop];
if (!hook) {
hooks[prop] = hook = { name: Ext.dom.Element.normalize(prop) };
}
value = valueFrom(value, '');
if (hook.set) {
hook.set(dom, value, me);
} else {
style[hook.name] = value;
}
}
else {
for (name in prop) {
if (prop.hasOwnProperty(name)) {
hook = hooks[name];
if (!hook) {
hooks[name] = hook = { name: Ext.dom.Element.normalize(name) };
}
value = valueFrom(prop[name], '');
if (hook.set) {
hook.set(dom, value, me);
}
else {
style[hook.name] = value;
}
}
}
}
return me;
},
/**
* Returns the offset height of the element
* @param {Boolean} [contentHeight] true to get the height minus borders and padding
* @return {Number} The element's height
*/
getHeight: function(contentHeight) {
var dom = this.dom,
height = contentHeight ? (dom.clientHeight - this.getPadding("tb")) : dom.offsetHeight;
return height > 0 ? height : 0;
},
/**
* Returns the offset width of the element
* @param {Boolean} [contentWidth] true to get the width minus borders and padding
* @return {Number} The element's width
*/
getWidth: function(contentWidth) {
var dom = this.dom,
width = contentWidth ? (dom.clientWidth - this.getPadding("lr")) : dom.offsetWidth;
return width > 0 ? width : 0;
},
/**
* 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 **l**eft width + the border **r**ight width.
* @return {Number} The width of the sides passed added together
*/
getBorderWidth: function(side) {
return this.addStyles(side, this.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 **l**eft + the padding **r**ight.
* @return {Number} The padding of the sides passed added together
*/
getPadding: function(side) {
return this.addStyles(side, this.paddings);
},
/**
* 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.dom.Element} this
*/
applyStyles: function(styles) {
if (styles) {
var dom = this.dom,
styleType, i, len;
if (typeof styles == 'function') {
styles = styles.call();
}
styleType = typeof styles;
if (styleType == 'string') {
styles = Ext.util.Format.trim(styles).split(this.styleSplitRe);
for (i = 0, len = styles.length; i < len;) {
dom.style[Ext.dom.Element.normalize(styles[i++])] = styles[i++];
}
}
else if (styleType == 'object') {
this.setStyle(styles);
}
}
},
/**
* Returns the size of the element.
* @param {Boolean} [contentSize] true to get the width/size minus borders and padding
* @return {Object} An object containing the element's size:
* @return {Number} return.width
* @return {Number} return.height
*/
getSize: function(contentSize) {
var dom = this.dom;
return {
width: Math.max(0, contentSize ? (dom.clientWidth - this.getPadding("lr")) : dom.offsetWidth),
height: Math.max(0, contentSize ? (dom.clientHeight - this.getPadding("tb")) : dom.offsetHeight)
};
},
/**
* Forces the browser to repaint this element
* @return {Ext.dom.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;
},
/**
* 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] 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.dom.Element
*/
Ext.dom.Element.addMembers({
getParent: function() {
return Ext.get(this.dom.parentNode);
},
getFirstChild: function() {
return Ext.get(this.dom.firstElementChild);
},
/**
* Returns true if this element is an ancestor of the passed element
* @param {HTMLElement/String} element The element to check
* @return {Boolean} True if this element is an ancestor of el, else false
*/
contains: function(element) {
if (!element) {
return false;
}
var dom = Ext.getDom(element);
// we need el-contains-itself logic here because isAncestor does not do that:
return (dom === this.dom) || this.self.isAncestor(this.dom, dom);
},
/**
* 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.dom.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.dom.Element} The matching DOM node (or null if no match was found)
*/
up: function(simpleSelector, maxDepth) {
return this.findParentNode(simpleSelector, maxDepth, true);
},
select: function(selector, composite) {
return Ext.dom.Element.select(selector, this.dom, composite);
},
/**
* 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.dom.Element (defaults to false)
* @return {HTMLElement/Ext.dom.Element} The child Ext.dom.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.dom.Element (defaults to false)
* @return {HTMLElement/Ext.dom.Element} The child Ext.dom.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.dom.Element
* @return {Ext.dom.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.dom.Element
* @return {Ext.dom.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.dom.Element
* @return {Ext.dom.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.dom.Element
* @return {Ext.dom.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.dom.Element
* @return {Ext.dom.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;
},
isAncestor: function(element) {
return this.self.isAncestor.call(this.self, this.dom, element);
}
});
/**
* 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.dom.Element} and
* {@link Ext.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.define('Ext.dom.CompositeElementLite', {
alternateClassName: ['Ext.CompositeElementLite', 'Ext.CompositeElement'],
requires: ['Ext.dom.Element'],
statics: {
/**
* @private
* @static
* Copies all of the functions from Ext.dom.Element's prototype onto CompositeElementLite's prototype.
*/
importElementMethods: function() {
}
},
constructor: function(elements, root) {
/**
* @property {HTMLElement[]} elements
* 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.dom.CompositeElementLite, {
* nextAll: function() {
* var elements = this.elements, i, l = elements.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 = elements[i].nextSibling; n; n = n.nextSibling) {
* r[++ri] = n;
* }
* }
*
* // Add all found siblings to this Composite
* return this.add(r);
* }
* });
*
*/
this.elements = [];
this.add(elements, root);
this.el = new Ext.dom.Element.Fly();
},
isComposite: true,
// private
getElement: function(el) {
// Set the shared flyweight dom property to the current element
return this.el.attach(el).synchronize();
},
// 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.dom.CompositeElementLite} els Either an Array of DOM elements to add, or another Composite
* object who's elements should be added.
* @return {Ext.dom.CompositeElementLite} This Composite object.
*/
add: function(els, root) {
var elements = this.elements,
i, ln;
if (!els) {
return this;
}
if (typeof els == "string") {
els = Ext.dom.Element.selectorFunction(els, root);
}
else if (els.isComposite) {
els = els.elements;
}
else if (!Ext.isIterable(els)) {
els = [els];
}
for (i = 0, ln = els.length; i < ln; ++i) {
elements.push(this.transformElement(els[i]));
}
return this;
},
invoke: function(fn, args) {
var elements = this.elements,
ln = elements.length,
element,
i;
for (i = 0; i < ln; i++) {
element = elements[i];
if (element) {
Ext.dom.Element.prototype[fn].apply(this.getElement(element), args);
}
}
return this;
},
/**
* Returns a flyweight Element of the dom element object at the specified index
* @param {Number} index
* @return {Ext.dom.Element}
*/
item: function(index) {
var el = this.elements[index],
out = null;
if (el) {
out = this.getElement(el);
}
return out;
},
// fixes scope with flyweight
addListener: function(eventName, handler, scope, opt) {
var els = this.elements,
len = els.length,
i, e;
for (i = 0; i < len; i++) {
e = els[i];
if (e) {
Ext.EventManager.on(e, eventName, handler, scope || e, opt);
}
}
return this;
},
/**
* Calls the passed function for each element in this composite.
* @param {Function} fn The function to call.
* @param {Ext.dom.Element} fn.el The current Element in the iteration. **This is the flyweight
* (shared) Ext.dom.Element instance, so if you require a a reference to the dom node, use el.dom.**
* @param {Ext.dom.CompositeElementLite} fn.c This Composite object.
* @param {Number} fn.index 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.dom.CompositeElementLite} this
*/
each: function(fn, scope) {
var me = this,
els = me.elements,
len = els.length,
i, e;
for (i = 0; i < len; i++) {
e = els[i];
if (e) {
e = this.getElement(e);
if (fn.call(scope || e, e, me, i) === false) {
break;
}
}
}
return me;
},
/**
* Clears this Composite and adds the elements passed.
* @param {HTMLElement[]/Ext.dom.CompositeElementLite} els Either an array of DOM elements, or another Composite from which
* to fill this Composite.
* @return {Ext.dom.CompositeElementLite} this
*/
fill: function(els) {
var me = this;
me.elements = [];
me.add(els);
return me;
},
/**
* Filters this composite to only elements that match the passed selector.
* @param {String/Function} selector A string CSS selector or a comparison function. The comparison function will be
* called with the following arguments:
* @param {Ext.dom.Element} selector.el The current DOM element.
* @param {Number} selector.index The current index within the collection.
* @return {Ext.dom.CompositeElementLite} 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 {String/HTMLElement/Ext.Element/Number} el The id of an element, or an Ext.dom.Element, or an HtmlElement
* to find within the composite collection.
* @return {Number} The index of the passed Ext.dom.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] True to remove and replace the element in the document too.
* @return {Ext.dom.CompositeElementLite} 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 = [];
},
addElements: function(els, root) {
if (!els) {
return this;
}
if (typeof els == "string") {
els = Ext.dom.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.dom.Element}
*/
first: function() {
return this.item(0);
},
/**
* Returns the last Element
* @return {Ext.dom.Element}
*/
last: function() {
return this.item(this.getCount() - 1);
},
/**
* Returns true if this composite contains the passed element
* @param {String/HTMLElement/Ext.Element/Number} el 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] True to also remove the element from the document
* @return {Ext.dom.CompositeElementLite} this
*/
removeElement: function(keys, removeDom) {
var me = this,
elements = this.elements,
el;
Ext.each(keys, function(val) {
if ((el = (elements[val] || elements[val = me.indexOf(val)]))) {
if (removeDom) {
if (el.dom) {
el.remove();
}
else {
Ext.removeNode(el);
}
}
Ext.Array.erase(elements, val, 1);
}
});
return this;
}
}, function() {
var Element = Ext.dom.Element,
elementPrototype = Element.prototype,
prototype = this.prototype,
name;
for (name in elementPrototype) {
if (typeof elementPrototype[name] == 'function'){
(function(key) {
prototype[key] = prototype[key] || function() {
return this.invoke(key, arguments);
};
}).call(prototype, name);
}
}
prototype.on = prototype.addListener;
if (Ext.DomQuery){
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.dom.CompositeElementLite CompositeElementLite} object.
* @param {String/HTMLElement[]} selector The CSS selector or an array of elements
* @param {HTMLElement/String} [root] The root element of the query or id of the root
* @return {Ext.dom.CompositeElementLite}
* @member Ext.dom.Element
* @method select
*/
Element.select = function(selector, root) {
var elements;
if (typeof selector == "string") {
elements = Element.selectorFunction(selector, root);
}
else if (selector.length !== undefined) {
elements = selector;
}
else {
throw new Error("[Ext.select] Invalid selector specified: " + selector);
}
return new Ext.CompositeElementLite(elements);
};
/**
* @member Ext
* @method select
* @alias Ext.dom.Element#select
*/
Ext.select = function() {
return Element.select.apply(Element, arguments);
};
});
/*
This file is part of Sencha Touch 2
Copyright (c) 2012 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.
*/
/**
* @private
*
* <p>Provides a registry of all Components (instances of {@link Ext.Component} or any subclass
* thereof) on a page so that they can be easily accessed by {@link Ext.Component component}
* {@link Ext.Component#getId id} (see {@link #get}, or the convenience method {@link Ext#getCmp Ext.getCmp}).</p>
* <p>This object also provides a registry of available Component <i>classes</i>
* indexed by a mnemonic code known as the Component's `xtype`.
* The <code>xtype</code> provides a way to avoid instantiating child Components
* when creating a full, nested config object for a complete Ext page.</p>
* <p>A child Component may be specified simply as a <i>config object</i>
* as long as the correct `xtype` is specified so that if and when the Component
* needs rendering, the correct type can be looked up for lazy instantiation.</p>
* <p>For a list of all available `xtype`, see {@link Ext.Component}.</p>
*/
Ext.define('Ext.ComponentManager', {
alternateClassName: 'Ext.ComponentMgr',
singleton: true,
constructor: function() {
var map = {};
// The sole reason for this is just to support the old code of ComponentQuery
this.all = {
map: map,
getArray: function() {
var list = [],
id;
for (id in map) {
list.push(map[id]);
}
return list;
}
};
this.map = map;
},
/**
* Registers an item to be managed
* @param {Object} component The item to register
*/
register: function(component) {
var id = component.getId();
if (this.map[id]) {
Ext.Logger.warn('Registering a component with a id (`' + id + '`) which has already been used. Please ensure the existing component has been destroyed (`Ext.Component#destroy()`.');
}
this.map[component.getId()] = component;
},
/**
* Unregisters an item by removing it from this manager
* @param {Object} component The item to unregister
*/
unregister: function(component) {
delete this.map[component.getId()];
},
/**
* Checks if an item type is registered.
* @param {String} component The mnemonic string by which the class may be looked up
* @return {Boolean} Whether the type is registered.
*/
isRegistered : function(component){
return this.map[component] !== undefined;
},
/**
* 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.map[id];
},
/**
* Creates a new Component from the specified config object using the
* config object's xtype to determine the class to instantiate.
* @param {Object} config A configuration object for the Component you wish to create.
* @param {Function} defaultType (optional) The constructor to provide the default Component type if
* the config object does not contain a <code>xtype</code>. (Optional if the config contains a <code>xtype</code>).
* @return {Ext.Component} The newly instantiated Component.
*/
create: function(component, defaultType) {
if (component.isComponent) {
return component;
}
else if (Ext.isString(component)) {
return Ext.createByAlias('widget.' + component);
}
else {
var type = component.xtype || defaultType;
return Ext.createByAlias('widget.' + type, component);
}
},
registerType: Ext.emptyFn
});
/**
* @class Ext.ComponentQuery
* @extends Object
* @singleton
*
* 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
*
* 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#query},
* {@link Ext.Container#down} and {@link Ext.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, getter, getValue;
for (; i < length; i++) {
candidate = items[i];
getter = Ext.Class.getConfigNameMap(property).get;
if (candidate[getter]) {
getValue = candidate[getter]();
if (!value ? !!getValue : (String(getValue) === value)) {
result.push(candidate);
}
}
else if (candidate.config && candidate.config[property]) {
if (!value ? !!candidate.config[property] : (String(candidate.config[property]) === value)) {
result.push(candidate);
}
}
else 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.getId() === id || candidate.getItemId() === id) {
result.push(candidate);
}
}
return result;
},
// Filters the passed candidate array and returns only items which the named pseudo class matcher filters in
filterByPseudo = function(items, name, value) {
return cq.pseudos[name](items, value);
},
// Determines leading mode
// > for direct child, and ^ to switch to ownerCt axis
modeRe = /^(\s?([>\^])\s?|\s|$)/,
// Matches a token with possibly (true|false) appended for the "shallow" parameter
tokenRe = /^(#)?([\w\-]+|\*)(?:\((true|false)\))?/,
matchers = [{
// Checks for .xtype with possibly (true|false) appended for the "shallow" parameter
re: /^\.([\w\-]+)(?:\((true|false)\))?/,
method: filterByXType
},{
// checks for [attribute=value]
re: /^(?:[\[](?:@)?([\w\-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]])/,
method: filterByAttribute
}, {
// checks for #cmpItemId
re: /^#([\w\-]+)/,
method: filterById
}, {
// checks for :<pseudo_class>(<selector>)
re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/,
method: filterByPseudo
}, {
// checks for {<member_expression>}
re: /^(?:\{([^\}]+)\})/,
method: filterFnPattern
}];
cq.Query = Ext.extend(Object, {
constructor: function(cfg) {
cfg = cfg || {};
Ext.apply(this, cfg);
},
/**
* @private
* 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;
}
},
/**
* 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} root The Container within which to perform the query.
* If omitted, all Components within the document are included in the search.
*
* This parameter may also be an array of Components to filter according to the selector.</p>
* @returns {Ext.Component[]} The matched Components.
*
* @member Ext.ComponentQuery
*/
query: function(selector, root) {
var selectors = selector.split(','),
length = selectors.length,
i = 0,
results = [],
noDupResults = [],
dupMatcher = {},
query, resultsLn, cmp;
for (; i < length; i++) {
selector = Ext.String.trim(selectors[i]);
query = this.parse(selector);
// 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
}
// Exhausted all matches: It's an error
if (i === (length - 1)) {
Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"');
}
}
}
// 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
});
}
});
});
/**
* This class parses the XTemplate syntax and calls abstract methods to process the parts.
* @private
*/
Ext.define('Ext.XTemplateParser', {
constructor: function (config) {
Ext.apply(this, config);
},
/**
* @property {Number} level The 'for' loop context level. This is adjusted up by one
* prior to calling {@link #doFor} and down by one after calling the corresponding
* {@link #doEnd} that closes the loop. This will be 1 on the first {@link #doFor}
* call.
*/
/**
* This method is called to process a piece of raw text from the tpl.
* @param {String} text
* @method doText
*/
// doText: function (text)
/**
* This method is called to process expressions (like `{[expr]}`).
* @param {String} expr The body of the expression (inside "{[" and "]}").
* @method doExpr
*/
// doExpr: function (expr)
/**
* This method is called to process simple tags (like `{tag}`).
* @method doTag
*/
// doTag: function (tag)
/**
* This method is called to process `<tpl else>`.
* @method doElse
*/
// doElse: function ()
/**
* This method is called to process `{% text %}`.
* @param {String} text
* @method doEval
*/
// doEval: function (text)
/**
* This method is called to process `<tpl if="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
* @method doIf
*/
// doIf: function (action, actions)
/**
* This method is called to process `<tpl elseif="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
* @method doElseIf
*/
// doElseIf: function (action, actions)
/**
* This method is called to process `<tpl switch="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
* @method doSwitch
*/
// doSwitch: function (action, actions)
/**
* This method is called to process `<tpl case="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
* @method doCase
*/
// doCase: function (action, actions)
/**
* This method is called to process `<tpl default>`.
* @method doDefault
*/
// doDefault: function ()
/**
* This method is called to process `</tpl>`. It is given the action type that started
* the tpl and the set of additional actions.
* @param {String} type The type of action that is being ended.
* @param {Object} actions The other actions keyed by the attribute name (such as 'exec').
* @method doEnd
*/
// doEnd: function (type, actions)
/**
* This method is called to process `<tpl for="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name (such as 'exec').
* @method doFor
*/
// doFor: function (action, actions)
/**
* This method is called to process `<tpl exec="action">`. If there are other attributes,
* these are passed in the actions object.
* @param {String} action
* @param {Object} actions Other actions keyed by the attribute name.
* @method doExec
*/
// doExec: function (action, actions)
/**
* This method is called to process an empty `<tpl>`. This is unlikely to need to be
* implemented, so a default (do nothing) version is provided.
* @method
*/
doTpl: Ext.emptyFn,
parse: function (str) {
var me = this,
len = str.length,
aliases = { elseif: 'elif' },
topRe = me.topRe,
actionsRe = me.actionsRe,
index, stack, s, m, t, prev, frame, subMatch, begin, end, actions;
me.level = 0;
me.stack = stack = [];
for (index = 0; index < len; index = end) {
topRe.lastIndex = index;
m = topRe.exec(str);
if (!m) {
me.doText(str.substring(index, len));
break;
}
begin = m.index;
end = topRe.lastIndex;
if (index < begin) {
me.doText(str.substring(index, begin));
}
if (m[1]) {
end = str.indexOf('%}', begin+2);
me.doEval(str.substring(begin+2, end));
end += 2;
} else if (m[2]) {
end = str.indexOf(']}', begin+2);
me.doExpr(str.substring(begin+2, end));
end += 2;
} else if (m[3]) { // if ('{' token)
me.doTag(m[3]);
} else if (m[4]) { // content of a <tpl xxxxxx> tag
actions = null;
while ((subMatch = actionsRe.exec(m[4])) !== null) {
s = subMatch[2] || subMatch[3];
if (s) {
s = Ext.String.htmlDecode(s); // decode attr value
t = subMatch[1];
t = aliases[t] || t;
actions = actions || {};
prev = actions[t];
if (typeof prev == 'string') {
actions[t] = [prev, s];
} else if (prev) {
actions[t].push(s);
} else {
actions[t] = s;
}
}
}
if (!actions) {
if (me.elseRe.test(m[4])) {
me.doElse();
} else if (me.defaultRe.test(m[4])) {
me.doDefault();
} else {
me.doTpl();
stack.push({ type: 'tpl' });
}
}
else if (actions['if']) {
me.doIf(actions['if'], actions)
stack.push({ type: 'if' });
}
else if (actions['switch']) {
me.doSwitch(actions['switch'], actions)
stack.push({ type: 'switch' });
}
else if (actions['case']) {
me.doCase(actions['case'], actions);
}
else if (actions['elif']) {
me.doElseIf(actions['elif'], actions);
}
else if (actions['for']) {
++me.level;
me.doFor(actions['for'], actions);
stack.push({ type: 'for', actions: actions });
}
else if (actions.exec) {
me.doExec(actions.exec, actions);
stack.push({ type: 'exec', actions: actions });
}
/*
else {
// todo - error
}
/**/
} else {
frame = stack.pop();
me.doEnd(frame.type, frame.actions);
if (frame.type == 'for') {
--me.level;
}
}
}
},
// Internal regexes
topRe: /(?:(\{\%)|(\{\[)|\{([^{}]*)\})|(?:<tpl([^>]*)\>)|(?:<\/tpl>)/g,
actionsRe: /\s*(elif|elseif|if|for|exec|switch|case|eval)\s*\=\s*(?:(?:["]([^"]*)["])|(?:[']([^']*)[']))\s*/g,
defaultRe: /^\s*default\s*$/,
elseRe: /^\s*else\s*$/
});
/**
* @author Ed Spencer
* @private
*
* Represents a single action as {@link Ext.app.Application#dispatch dispatched} by an Application. This is typically
* generated as a result of a url change being matched by a Route, triggering Application's dispatch function.
*
* This is a private class and its functionality and existence may change in the future. Use at your own risk.
*
*/
Ext.define('Ext.app.Action', {
config: {
/**
* @cfg {Object} scope The scope in which the {@link #action} should be called
*/
scope: null,
/**
* @cfg {Ext.app.Application} application The Application that this Action is bound to
*/
application: null,
/**
* @cfg {Ext.app.Controller} controller The {@link Ext.app.Controller controller} whose {@link #action} should
* be called
*/
controller: null,
/**
* @cfg {String} action The name of the action on the {@link #controller} that should be called
*/
action: null,
/**
* @cfg {Array} args The set of arguments that will be passed to the controller's {@link #action}
*/
args: [],
/**
* @cfg {String} url The url that was decoded into the controller/action/args in this Action
*/
url: undefined,
data: {},
title: null,
/**
* @cfg {Array} beforeFilters The (optional) set of functions to call before the {@link #action} is called.
* This is usually handled directly by the Controller or Application when an Ext.app.Action instance is
* created, but is alterable before {@link #resume} is called.
* @accessor
*/
beforeFilters: [],
/**
* @private
* Keeps track of which before filter is currently being executed by {@link #resume}
*/
currentFilterIndex: -1
},
constructor: function(config) {
this.initConfig(config);
this.getUrl();
},
/**
* Starts execution of this Action by calling each of the {@link #beforeFilters} in turn (if any are specified),
* before calling the Controller {@link #action}. Same as calling {@link #resume}.
*/
execute: function() {
this.resume();
},
/**
* Resumes the execution of this Action (or starts it if it had not been started already). This iterates over all
* of the configured {@link #beforeFilters} and calls them. Each before filter is called with this Action as the
* sole argument, and is expected to call action.resume() in order to allow the next filter to be called, or if
* this is the final filter, the original {@link Ext.app.Controller Controller} function.
*/
resume: function() {
var index = this.getCurrentFilterIndex() + 1,
filters = this.getBeforeFilters(),
controller = this.getController(),
nextFilter = filters[index];
if (nextFilter) {
this.setCurrentFilterIndex(index);
nextFilter.call(controller, this);
} else {
controller[this.getAction()].apply(controller, this.getArgs());
}
},
/**
* @private
*/
applyUrl: function(url) {
if (url === null || url === undefined) {
url = this.urlEncode();
}
return url;
},
/**
* @private
* If the controller config is a string, swap it for a reference to the actuall controller instance
* @param {String} controller The controller name
*/
applyController: function(controller) {
var app = this.getApplication(),
profile = app.getCurrentProfile();
if (Ext.isString(controller)) {
controller = app.getController(controller, profile ? profile.getNamespace() : null);
}
return controller;
},
/**
* @private
*/
urlEncode: function() {
var controller = this.getController(),
splits;
if (controller instanceof Ext.app.Controller) {
splits = controller.$className.split('.');
controller = splits[splits.length - 1];
}
return controller + "/" + this.getAction();
}
});
/**
* @author Ed Spencer
* @private
*
* Represents a mapping between a url and a controller/action pair. May also contain additional params. This is a
* private internal class that should not need to be used by end-developer code. Its API and existence are subject to
* change so use at your own risk.
*
* For information on how to use routes we suggest reading the following guides:
*
* * <a href="#!/guide/history_support">Using History Support</a>
* * <a href="#!/guide/apps_intro">Intro to Applications</a>
* * <a href="#!/guide/controllers">Using Controllers</a>
*
*/
Ext.define('Ext.app.Route', {
config: {
/**
* @cfg {Object} conditions Optional set of conditions for each token in the url string. Each key should be one
* of the tokens, each value should be a regex that the token should accept. For example, if you have a Route
* with a url like "files/:fileName" and you want it to match urls like "files/someImage.jpg" then you can set
* these conditions to allow the :fileName token to accept strings containing a period ("."):
*
* conditions: {
* ':fileName': "[0-9a-zA-Z\.]+"
* }
*
*/
conditions: {},
/**
* @cfg {String} url The url regex to match against. Required
*/
url: null,
/**
* @cfg {String} controller The name of the Controller whose {@link #action} will be called if this route is
* matched
*/
controller: null,
/**
* @cfg {String} action The name of the action that will be called on the {@link #controller} if this route is
* matched
*/
action: null,
/**
* @private
* @cfg {Boolean} initialized Indicates whether or not this Route has been initialized. We don't initialize
* straight away so as to save unnecessary processing.
*/
initialized: false
},
constructor: function(config) {
this.initConfig(config);
},
/**
* Attempts to recognize a given url string and return controller/action pair for it
* @param {String} url The url to recognize
* @return {Object} The matched data, or false if no match
*/
recognize: function(url) {
if (!this.getInitialized()) {
this.initialize();
}
if (this.recognizes(url)) {
var matches = this.matchesFor(url),
args = url.match(this.matcherRegex);
args.shift();
return Ext.applyIf(matches, {
controller: this.getController(),
action : this.getAction(),
historyUrl: url,
args : args
});
}
},
/**
* @private
* Sets up the relevant regular expressions used to match against this route
*/
initialize: function() {
/*
* The regular expression we use to match a segment of a route mapping
* this will recognise segments starting with a colon,
* e.g. on 'namespace/:controller/:action', :controller and :action will be recognised
*/
this.paramMatchingRegex = new RegExp(/:([0-9A-Za-z\_]*)/g);
/*
* Converts a route string into an array of symbols starting with a colon. e.g.
* ":controller/:action/:id" => [':controller', ':action', ':id']
*/
this.paramsInMatchString = this.getUrl().match(this.paramMatchingRegex) || [];
this.matcherRegex = this.createMatcherRegex(this.getUrl());
this.setInitialized(true);
},
/**
* @private
* Returns true if this Route matches the given url string
* @param {String} url The url to test
* @return {Boolean} True if this Route recognizes the url
*/
recognizes: function(url) {
return this.matcherRegex.test(url);
},
/**
* @private
* Returns a hash of matching url segments for the given url.
* @param {String} url The url to extract matches for
* @return {Object} matching url segments
*/
matchesFor: function(url) {
var params = {},
keys = this.paramsInMatchString,
values = url.match(this.matcherRegex),
length = keys.length,
i;
//first value is the entire match so reject
values.shift();
for (i = 0; i < length; i++) {
params[keys[i].replace(":", "")] = values[i];
}
return params;
},
/**
* @private
* Returns an array of matching url segments for the given url.
* @param {String} url The url to extract matches for
* @return {Array} matching url segments
*/
argsFor: function(url) {
var args = [],
keys = this.paramsInMatchString,
values = url.match(this.matcherRegex),
length = keys.length,
i;
//first value is the entire match so reject
values.shift();
for (i = 0; i < length; i++) {
args.push(keys[i].replace(':', ""));
params[keys[i].replace(":", "")] = values[i];
}
return params;
},
/**
* @private
* Constructs a url for the given config object by replacing wildcard placeholders in the Route's url
* @param {Object} config The config object
* @return {String} The constructed url
*/
urlFor: function(config) {
var url = this.getUrl();
for (var key in config) {
url = url.replace(":" + key, config[key]);
}
return url;
},
/**
* @private
* Takes the configured url string including wildcards and returns a regex that can be used to match
* against a url
* @param {String} url The url string
* @return {RegExp} The matcher regex
*/
createMatcherRegex: function(url) {
/**
* Converts a route string into an array of symbols starting with a colon. e.g.
* ":controller/:action/:id" => [':controller', ':action', ':id']
*/
var paramsInMatchString = this.paramsInMatchString,
length = paramsInMatchString.length,
i, cond, matcher;
for (i = 0; i < length; i++) {
cond = this.getConditions()[paramsInMatchString[i]];
matcher = Ext.util.Format.format("({0})", cond || "[%a-zA-Z0-9\-\\_\\s,]+");
url = url.replace(new RegExp(paramsInMatchString[i]), matcher);
}
//we want to match the whole string, so include the anchors
return new RegExp("^" + url + "$");
}
});
/**
* @author Ed Spencer
* @private
*
* The Router is an ordered set of route definitions that decode a url into a controller function to execute. Each
* route defines a type of url to match, along with the controller function to call if it is matched. The Router is
* usually managed exclusively by an {@link Ext.app.Application Application}, which also uses a
* {@link Ext.app.History History} instance to find out when the browser's url has changed.
*
* Routes are almost always defined inside a {@link Ext.app.Controller Controller}, as opposed to on the Router itself.
* End-developers should not usually need to interact directly with the Router as the Application and Controller
* classes manage everything automatically. See the {@link Ext.app.Controller Controller documentation} for more
* information on specifying routes.
*/
Ext.define('Ext.app.Router', {
requires: ['Ext.app.Route'],
config: {
/**
* @cfg {Array} routes The set of routes contained within this Router. Read only
*/
routes: [],
/**
* @cfg {Object} defaults Default configuration options for each Route connected to this Router.
*/
defaults: {
action: 'index'
}
},
constructor: function(config) {
this.initConfig(config);
},
/**
* Connects a url-based route to a controller/action pair plus additional params
* @param {String} url The url to recognize
*/
connect: function(url, params) {
params = Ext.apply({url: url}, params || {}, this.getDefaults());
var route = Ext.create('Ext.app.Route', params);
this.getRoutes().push(route);
return route;
},
/**
* Recognizes a url string connected to the Router, return the controller/action pair plus any additional
* config associated with it
* @param {String} url The url to recognize
* @return {Object/undefined} If the url was recognized, the controller and action to call, else undefined
*/
recognize: function(url) {
var routes = this.getRoutes(),
length = routes.length,
i, result;
for (i = 0; i < length; i++) {
result = routes[i].recognize(url);
if (result !== undefined) {
return result;
}
}
return undefined;
},
/**
* Convenience method which just calls the supplied function with the Router instance. Example usage:
<pre><code>
Ext.Router.draw(function(map) {
map.connect('activate/:token', {controller: 'users', action: 'activate'});
map.connect('home', {controller: 'index', action: 'home'});
});
</code></pre>
* @param {Function} fn The fn to call
*/
draw: function(fn) {
fn.call(this, this);
},
/**
* @private
*/
clear: function() {
this.setRoutes([]);
}
}, function() {
});
/**
* @private
*/
Ext.define('Ext.behavior.Behavior', {
constructor: function(component) {
this.component = component;
component.on('destroy', 'onComponentDestroy', this);
},
onComponentDestroy: Ext.emptyFn
});
/**
* @author Ed Spencer
* @class Ext.data.Error
*
* <p>This is used when validating a record. The validate method will return an Ext.data.Errors collection
* containing Ext.data.Error instances. Each error has a field and a message.</p>
*
* <p>Usually this class does not need to be instantiated directly - instances are instead created
* automatically when {@link Ext.data.Model#validate validate} on a model instance.</p>
*/
Ext.define('Ext.data.Error', {
config: {
/**
* @cfg {String} field
* The name of the field this error belongs to.
*/
field: null,
/**
* @cfg {String} message
* The message containing the description of the error.
*/
message: ''
},
constructor: function(config) {
this.initConfig(config);
}
});
/**
* @aside guide ajax
* @singleton
*
* This class is used to create JsonP requests. JsonP is a mechanism that allows for making requests for data cross
* domain. More information is available [here](http://en.wikipedia.org/wiki/JSONP).
*
* ## Example
*
* @example preview
* Ext.Viewport.add({
* xtype: 'button',
* text: 'Make JsonP Request',
* centered: true,
* handler: function(button) {
* // Mask the viewport
* Ext.Viewport.mask();
*
* // Remove the button
* button.destroy();
*
* // Make the JsonP request
* Ext.data.JsonP.request({
* url: 'http://free.worldweatheronline.com/feed/weather.ashx',
* callbackKey: 'callback',
* params: {
* key: '23f6a0ab24185952101705',
* q: '94301', // Palo Alto
* format: 'json',
* num_of_days: 5
* },
* success: function(result) {
* // Unmask the viewport
* Ext.Viewport.unmask();
*
* // Get the weather data from the json object result
* var weather = result.data.weather;
* if (weather) {
* // Style the viewport html, and set the html of the max temperature
* Ext.Viewport.setStyleHtmlContent(true);
* Ext.Viewport.setHtml('The temperature in Palo Alto is <b>' + weather[0].tempMaxF + '° F<b/>');
* }
* }
* });
* }
* });
*
* See the {@link #request} method for more details on making a JsonP request.
*/
Ext.define('Ext.data.JsonP', {
alternateClassName: 'Ext.util.JSONP',
/* Begin Definitions */
singleton: true,
statics: {
requestCount: 0,
requests: {}
},
/* End Definitions */
/**
* @property {Number} [timeout=30000]
* A default timeout for any JsonP requests. If the request has not completed in this time the failure callback will
* be fired. The timeout is in ms. Defaults to 30000.
*/
timeout: 30000,
/**
* @property {Boolean} [disableCaching=true]
* True to add a unique cache-buster param to requests. Defaults to true.
*/
disableCaching: true,
/**
* @property {String} [disableCachingParam="_dc"]
* Change the parameter which is sent went disabling caching through a cache buster. Defaults to '_dc'.
*/
disableCachingParam: '_dc',
/**
* @property {String} [callbackKey="callback"]
* Specifies the GET parameter that will be sent to the server containing the function name to be executed when the
* request completes. Defaults to callback. Thus, a common request will be in the form of
* url?callback=Ext.data.JsonP.callback1
*/
callbackKey: 'callback',
/**
* Makes a JSONP request.
* @param {Object} options An object which may contain the following properties. Note that options will take
* priority over any defaults that are specified in the class.
*
* @param {String} options.url The URL to request.
* @param {Object} [options.params] An object containing a series of key value pairs that will be sent along with the request.
* @param {Number} [options.timeout] See {@link #timeout}
* @param {String} [options.callbackKey] See {@link #callbackKey}
* @param {String} [options.callbackName] See {@link #callbackKey}
* The function name to use for this request. By default this name will be auto-generated: Ext.data.JsonP.callback1,
* Ext.data.JsonP.callback2, etc. Setting this option to "my_name" will force the function name to be
* Ext.data.JsonP.my_name. Use this if you want deterministic behavior, but be careful - the callbackName should be
* different in each JsonP request that you make.
* @param {Boolean} [options.disableCaching] See {@link #disableCaching}
* @param {String} [options.disableCachingParam] See {@link #disableCachingParam}
* @param {Function} [options.success] A function to execute if the request succeeds.
* @param {Function} [options.failure] A function to execute if the request fails.
* @param {Function} [options.callback] A function to execute when the request completes, whether it is a success or failure.
* @param {Object} [options.scope] The scope in which to execute the callbacks: The "this" object for the
* callback function. Defaults to the browser window.
*
* @return {Object} request An object containing the request details.
*/
request: function(options){
options = Ext.apply({}, options);
if (!options.url) {
Ext.Logger.error('A url must be specified for a JSONP request.');
}
var me = this,
disableCaching = Ext.isDefined(options.disableCaching) ? options.disableCaching : me.disableCaching,
cacheParam = options.disableCachingParam || me.disableCachingParam,
id = ++me.statics().requestCount,
callbackName = options.callbackName || 'callback' + id,
callbackKey = options.callbackKey || me.callbackKey,
timeout = Ext.isDefined(options.timeout) ? options.timeout : me.timeout,
params = Ext.apply({}, options.params),
url = options.url,
name = Ext.isSandboxed ? Ext.getUniqueGlobalNamespace() : 'Ext',
request,
script;
params[callbackKey] = name + '.data.JsonP.' + callbackName;
if (disableCaching) {
params[cacheParam] = new Date().getTime();
}
script = me.createScript(url, params, options);
me.statics().requests[id] = request = {
url: url,
params: params,
script: script,
id: id,
scope: options.scope,
success: options.success,
failure: options.failure,
callback: options.callback,
callbackKey: callbackKey,
callbackName: callbackName
};
if (timeout > 0) {
request.timeout = setTimeout(Ext.bind(me.handleTimeout, me, [request]), timeout);
}
me.setupErrorHandling(request);
me[callbackName] = Ext.bind(me.handleResponse, me, [request], true);
me.loadScript(request);
return request;
},
/**
* Abort a request. If the request parameter is not specified all open requests will be aborted.
* @param {Object/String} request The request to abort
*/
abort: function(request){
var requests = this.statics().requests,
key;
if (request) {
if (!request.id) {
request = requests[request];
}
this.abort(request);
} else {
for (key in requests) {
if (requests.hasOwnProperty(key)) {
this.abort(requests[key]);
}
}
}
},
/**
* Sets up error handling for the script
* @private
* @param {Object} request The request
*/
setupErrorHandling: function(request){
request.script.onerror = Ext.bind(this.handleError, this, [request]);
},
/**
* Handles any aborts when loading the script
* @private
* @param {Object} request The request
*/
handleAbort: function(request){
request.errorType = 'abort';
this.handleResponse(null, request);
},
/**
* Handles any script errors when loading the script
* @private
* @param {Object} request The request
*/
handleError: function(request){
request.errorType = 'error';
this.handleResponse(null, request);
},
/**
* Cleans up anu script handling errors
* @private
* @param {Object} request The request
*/
cleanupErrorHandling: function(request){
request.script.onerror = null;
},
/**
* Handle any script timeouts
* @private
* @param {Object} request The request
*/
handleTimeout: function(request){
request.errorType = 'timeout';
this.handleResponse(null, request);
},
/**
* Handle a successful response
* @private
* @param {Object} result The result from the request
* @param {Object} request The request
*/
handleResponse: function(result, request){
var success = true;
if (request.timeout) {
clearTimeout(request.timeout);
}
delete this[request.callbackName];
delete this.statics()[request.id];
this.cleanupErrorHandling(request);
Ext.fly(request.script).destroy();
if (request.errorType) {
success = false;
Ext.callback(request.failure, request.scope, [request.errorType]);
} else {
Ext.callback(request.success, request.scope, [result]);
}
Ext.callback(request.callback, request.scope, [success, result, request.errorType]);
},
/**
* Create the script tag given the specified url, params and options. The options
* parameter is passed to allow an override to access it.
* @private
* @param {String} url The url of the request
* @param {Object} params Any extra params to be sent
* @param {Object} options The object passed to {@link #request}.
*/
createScript: function(url, params, options) {
var script = document.createElement('script');
script.setAttribute("src", Ext.urlAppend(url, Ext.Object.toQueryString(params)));
script.setAttribute("async", true);
script.setAttribute("type", "text/javascript");
return script;
},
/**
* Loads the script for the given request by appending it to the HEAD element. This is
* its own method so that users can override it (as well as {@link #createScript}).
* @private
* @param request The request object.
*/
loadScript: function (request) {
Ext.getHead().appendChild(request.script);
}
});
/**
* @author Ed Spencer
*
* Represents a single read or write operation performed by a {@link Ext.data.proxy.Proxy Proxy}. Operation objects are
* used to enable communication between Stores and Proxies. Application developers should rarely need to interact with
* Operation objects directly.
*
* Note that when you define an Operation directly, you need to specify at least the {@link #model} configuration.
*
* Several Operations can be batched together in a {@link Ext.data.Batch batch}.
*/
Ext.define('Ext.data.Operation', {
config: {
/**
* @cfg {Boolean} synchronous
* True if this Operation is to be executed synchronously. This property is inspected by a
* {@link Ext.data.Batch Batch} to see if a series of Operations can be executed in parallel or not.
* @accessor
* @private
*/
synchronous: true,
/**
* @cfg {String} action
* The action being performed by this Operation. Should be one of 'create', 'read', 'update' or 'destroy'.
* @accessor
*/
action: null,
/**
* @cfg {Ext.util.Filter[]} filters
* Optional array of filter objects. Only applies to 'read' actions.
* @accessor
* @private
*/
filters: null,
/**
* @cfg {Ext.util.Sorter[]} sorters
* Optional array of sorter objects. Only applies to 'read' actions.
* @accessor
* @private
*/
sorters: null,
/**
* @cfg {Ext.util.Grouper} grouper
* Optional grouping configuration. Only applies to 'read' actions where grouping is desired.
* @accessor
* @private
*/
grouper: null,
/**
* @cfg {Number} start
* The start index (offset), used in paging when running a 'read' action.
* @accessor
* @private
*/
start: null,
/**
* @cfg {Number} limit
* The number of records to load. Used on 'read' actions when paging is being used.
* @accessor
* @private
*/
limit: null,
/**
* @cfg {Ext.data.Batch} batch
* The batch that this Operation is a part of.
* @accessor
* @private
*/
batch: null,
/**
* @cfg {Function} callback
* Function to execute when operation completed.
* @cfg {Ext.data.Model[]} callback.records Array of records.
* @cfg {Ext.data.Operation} callback.operation The Operation itself.
* @cfg {Boolean} callback.success True when operation completed successfully.
* @accessor
* @private
*/
callback: null,
/**
* @cfg {Object} scope
* Scope for the {@link #callback} function.
* @accessor
* @private
*/
scope: null,
/**
* @cfg {Ext.data.ResultSet} resultSet
* The ResultSet for this operation.
* @accessor
*/
resultSet: null,
/**
* @cfg {Array} records
* The records associated to this operation. Before an operation starts, these
* are the records you are updating, creating, or destroying. After an operation
* is completed, a Proxy usually sets these records on the Operation to become
* the processed records. If you don't set these records on your operation in
* your proxy, then the getter will return the ones defined on the {@link #resultSet}
* @accessor
*/
records: null,
/**
* @cfg {Ext.data.Request} request
* The request used for this Operation. Operations don't usually care about Request and Response data, but in the
* ServerProxy and any of its subclasses they may be useful for further processing.
* @accessor
*/
request: null,
/**
* @cfg {Object} response
* The response that was gotten from the server if there was one.
* @accessor
*/
response: null,
/**
* @cfg {Boolean} withCredentials
* This field is necessary when using cross-origin resource sharing.
*/
withCredentials: null,
/**
* @cfg {Object} params
* The params send along with this operation. These usually apply to a Server proxy if you are
* creating your own custom proxy,
* @accessor
*/
params: null,
url: null,
page: null,
node: null,
/**
* @cfg {Ext.data.Model} model
* The Model that this Operation will be dealing with. This configuration is required when defining any Operation.
* Since Operations take care of creating, updating, destroying and reading records, it needs access to the Model.
*/
model: undefined,
addRecords: false
},
/**
* @property {Boolean} started
* Read-only property tracking the start status of this Operation. Use {@link #isStarted}.
* @private
*/
started: false,
/**
* @property {Boolean} running
* Read-only property tracking the run status of this Operation. Use {@link #isRunning}.
* @private
*/
running: false,
/**
* @property {Boolean} complete
* Read-only property tracking the completion status of this Operation. Use {@link #isComplete}.
* @private
*/
complete: false,
/**
* @property {Boolean} success
* Read-only property tracking whether the Operation was successful or not. This starts as undefined and is set to true
* or false by the Proxy that is executing the Operation. It is also set to false by {@link #setException}. Use
* {@link #wasSuccessful} to query success status.
* @private
*/
success: undefined,
/**
* @property {Boolean} exception
* Read-only property tracking the exception status of this Operation. Use {@link #hasException} and see {@link #getError}.
* @private
*/
exception: false,
/**
* @property {String/Object} error
* The error object passed when {@link #setException} was called. This could be any object or primitive.
* @private
*/
error: undefined,
/**
* Creates new Operation object.
* @param {Object} config (optional) Config object.
*/
constructor: function(config) {
this.initConfig(config);
},
applyModel: function(model) {
if (typeof model == 'string') {
model = Ext.data.ModelManager.getModel(model);
if (!model) {
Ext.Logger.error('Model with name ' + arguments[0] + ' doesnt exist.');
}
}
if (model && !model.prototype.isModel && Ext.isObject(model)) {
model = Ext.data.ModelManager.registerType(model.storeId || model.id || Ext.id(), model);
}
if (!model) {
Ext.Logger.warn('Unless you define your model using metadata, an Operation needs to have a model defined.');
}
return model;
},
getRecords: function() {
var resultSet = this.getResultSet();
return this._records || (resultSet ? resultSet.getRecords() : []);
},
/**
* Marks the Operation as started.
*/
setStarted: function() {
this.started = true;
this.running = true;
},
/**
* Marks the Operation as completed.
*/
setCompleted: function() {
this.complete = true;
this.running = false;
},
/**
* Marks the Operation as successful.
*/
setSuccessful: function() {
this.success = true;
},
/**
* Marks the Operation as having experienced an exception. Can be supplied with an option error message/object.
* @param {String/Object} error (optional) error string/object
*/
setException: function(error) {
this.exception = true;
this.success = false;
this.running = false;
this.error = error;
},
/**
* Returns true if this Operation encountered an exception (see also {@link #getError})
* @return {Boolean} True if there was an exception
*/
hasException: function() {
return this.exception === true;
},
/**
* Returns the error string or object that was set using {@link #setException}
* @return {String/Object} The error object
*/
getError: function() {
return this.error;
},
/**
* Returns true if the Operation has been started. Note that the Operation may have started AND completed, see
* {@link #isRunning} to test if the Operation is currently running.
* @return {Boolean} True if the Operation has started
*/
isStarted: function() {
return this.started === true;
},
/**
* Returns true if the Operation has been started but has not yet completed.
* @return {Boolean} True if the Operation is currently running
*/
isRunning: function() {
return this.running === true;
},
/**
* Returns true if the Operation has been completed
* @return {Boolean} True if the Operation is complete
*/
isComplete: function() {
return this.complete === true;
},
/**
* Returns true if the Operation has completed and was successful
* @return {Boolean} True if successful
*/
wasSuccessful: function() {
return this.isComplete() && this.success === true;
},
/**
* Checks whether this operation should cause writing to occur.
* @return {Boolean} Whether the operation should cause a write to occur.
*/
allowWrite: function() {
return this.getAction() != 'read';
},
process: function(action, resultSet, request, response) {
if (resultSet.getSuccess() !== false) {
this.setResponse(response);
this.setResultSet(resultSet);
this.setCompleted();
this.setSuccessful();
} else {
return false;
}
return this['process' + Ext.String.capitalize(action)].call(this, resultSet, request, response);
},
processRead: function(resultSet) {
var records = resultSet.getRecords(),
processedRecords = [],
Model = this.getModel(),
ln = records.length,
i, record;
for (i = 0; i < ln; i++) {
record = records[i];
processedRecords.push(new Model(record.data, record.id, record.node));
}
this.setRecords(processedRecords);
return true;
},
processCreate: function(resultSet) {
var updatedRecords = resultSet.getRecords(),
currentRecords = this.getRecords(),
ln = updatedRecords.length,
i, currentRecord, updatedRecord;
for (i = 0; i < ln; i++) {
updatedRecord = updatedRecords[i];
if (updatedRecord.clientId === null && currentRecords.length == 1 && updatedRecords.length == 1) {
currentRecord = currentRecords[i];
} else {
currentRecord = this.findCurrentRecord(updatedRecord.clientId);
}
if (currentRecord) {
this.updateRecord(currentRecord, updatedRecord);
}
else {
Ext.Logger.warn('Unable to match the record that came back from the server.');
}
}
return true;
},
processUpdate: function(resultSet) {
var updatedRecords = resultSet.getRecords(),
currentRecords = this.getRecords(),
ln = updatedRecords.length,
i, currentRecord, updatedRecord;
for (i = 0; i < ln; i++) {
updatedRecord = updatedRecords[i];
currentRecord = currentRecords[i];
if (currentRecord) {
this.updateRecord(currentRecord, updatedRecord);
}
else {
Ext.Logger.warn('Unable to match the updated record that came back from the server.');
}
}
return true;
},
processDestroy: function(resultSet) {
var updatedRecords = resultSet.getRecords(),
ln = updatedRecords.length,
i, currentRecord, updatedRecord;
for (i = 0; i < ln; i++) {
updatedRecord = updatedRecords[i];
currentRecord = this.findCurrentRecord(updatedRecord.id);
if (currentRecord) {
currentRecord.setIsErased(true);
currentRecord.notifyStores('afterErase', currentRecord);
}
else {
Ext.Logger.warn('Unable to match the destroyed record that came back from the server.');
}
}
},
findCurrentRecord: function(clientId) {
var currentRecords = this.getRecords(),
ln = currentRecords.length,
i, currentRecord;
for (i = 0; i < ln; i++) {
currentRecord = currentRecords[i];
if (currentRecord.getId() === clientId) {
return currentRecord;
}
}
},
updateRecord: function(currentRecord, updatedRecord) {
var recordData = updatedRecord.data,
recordId = updatedRecord.id;
currentRecord.beginEdit();
currentRecord.set(recordData);
if (recordId !== null) {
currentRecord.setId(recordId);
}
// We call endEdit with silent: true because the commit below already makes
// sure any store is notified of the record being updated.
currentRecord.endEdit(true);
currentRecord.commit();
}
});
/**
* @author Ed Spencer
*
* Simple class that represents a Request that will be made by any {@link Ext.data.proxy.Server} subclass.
* All this class does is standardize the representation of a Request as used by any ServerProxy subclass,
* it does not contain any actual logic or perform the request itself.
*/
Ext.define('Ext.data.Request', {
config: {
/**
* @cfg {String} action
* The name of the action this Request represents. Usually one of 'create', 'read', 'update' or 'destroy'.
*/
action: null,
/**
* @cfg {Object} params
* HTTP request params. The Proxy and its Writer have access to and can modify this object.
*/
params: null,
/**
* @cfg {String} method
* The HTTP method to use on this Request. Should be one of 'GET', 'POST', 'PUT' or 'DELETE'.
*/
method: 'GET',
/**
* @cfg {String} url
* The url to access on this Request.
*/
url: null,
/**
* @cfg {Ext.data.Operation} operation
* The operation this request belongs to.
*/
operation: null,
/**
* @cfg {Ext.data.proxy.Proxy} proxy
* The proxy this request belongs to.
*/
proxy: null,
/**
* @cfg {Boolean} disableCaching
* Wether or not to disable caching for this request.
* Defaults to false.
*/
disableCaching: false,
/**
* @cfg {Object} headers
* Some requests (like XMLHttpRequests) want to send additional server headers.
* This configuration can be set for those types of requests.
*/
headers: {},
/**
* @cfg {String} callbackKey
* Some requests (like JsonP) want to send an additional key that contains
* the name of the callback function.
*/
callbackKey: null,
/**
* @cfg {Ext.data.JsonP} jsonp
* JsonP requests return a handle that might be useful in the callback function.
*/
jsonP: null,
/**
* @cfg {Object} jsonData
* This is used by some write actions to attach data to the request without encoding it
* as a parameter.
*/
jsonData: null,
/**
* @cfg {Object} xmlData
* This is used by some write actions to attach data to the request without encoding it
* as a parameter, but instead sending it as XML.
*/
xmlData: null,
/**
* @cfg {Boolean} withCredentials
* This field is necessary when using cross-origin resource sharing.
*/
withCredentials: null,
callback: null,
scope: null,
timeout: 30000,
records: null,
// The following two configurations are only used by Ext.data.proxy.Direct and are just
// for being able to retrieve them after the request comes back from the server.
directFn: null,
args: null
},
/**
* Creates the Request object.
* @param {Object} [config] Config object.
*/
constructor: function(config) {
this.initConfig(config);
}
});
/**
* @author Ed Spencer
*
* Simple wrapper class that represents a set of records returned by a Proxy.
*/
Ext.define('Ext.data.ResultSet', {
config: {
/**
* @cfg {Boolean} loaded
* True if the records have already been loaded. This is only meaningful when dealing with
* SQL-backed proxies.
*/
loaded: true,
/**
* @cfg {Number} count
* The number of records in this ResultSet. Note that total may differ from this number.
*/
count: null,
/**
* @cfg {Number} total
* The total number of records reported by the data source. This ResultSet may form a subset of
* those records (see {@link #count}).
*/
total: null,
/**
* @cfg {Boolean} success
* True if the ResultSet loaded successfully, false if any errors were encountered.
*/
success: false,
/**
* @cfg {Ext.data.Model[]} records (required)
* The array of record instances.
*/
records: null
},
/**
* Creates the resultSet
* @param {Object} [config] Config object.
*/
constructor: function(config) {
this.initConfig(config);
},
applyCount: function(count) {
if (!count && count !== 0) {
return this.getRecords().length;
}
return count;
},
/**
* @private
* Make sure we set the right count when new records have been sent in
*/
updateRecords: function(records) {
this.setCount(records.length);
}
});
/**
* @class Ext.data.SortTypes
* This class defines a series of static methods that are used on a
* {@link Ext.data.Field} for performing sorting. The methods cast the
* underlying values into a data type that is appropriate for sorting on
* that particular field. If a {@link Ext.data.Field#type} is specified,
* the sortType will be set to a sane default if the sortType is not
* explicitly defined on the field. The sortType will make any necessary
* modifications to the value and return it.
* <ul>
* <li><b>asText</b> - Removes any tags and converts the value to a string</li>
* <li><b>asUCText</b> - Removes any tags and converts the value to an uppercase string</li>
* <li><b>asUCText</b> - Converts the value to an uppercase string</li>
* <li><b>asDate</b> - Converts the value into Unix epoch time</li>
* <li><b>asFloat</b> - Converts the value to a floating point number</li>
* <li><b>asInt</b> - Converts the value to an integer number</li>
* </ul>
* <p>
* It is also possible to create a custom sortType that can be used throughout
* an application.
* <pre><code>
Ext.apply(Ext.data.SortTypes, {
asPerson: function(person){
// expects an object with a first and last name property
return person.lastName.toUpperCase() + person.firstName.toLowerCase();
}
});
Ext.define('Employee', {
extend: 'Ext.data.Model',
config: {
fields: [{
name: 'person',
sortType: 'asPerson'
}, {
name: 'salary',
type: 'float' // sortType set to asFloat
}]
}
});
* </code></pre>
* </p>
* @singleton
* @docauthor Evan Trimboli <evan@sencha.com>
*/
Ext.define('Ext.data.SortTypes', {
singleton: true,
/**
* The regular expression used to strip tags
* @type {RegExp}
* @property
*/
stripTagsRE : /<\/?[^>]+>/gi,
/**
* Default sort that does nothing
* @param {Object} value The value being converted
* @return {Object} The comparison value
*/
none : function(value) {
return value;
},
/**
* Strips all HTML tags to sort on text only
* @param {Object} value The value being converted
* @return {String} The comparison value
*/
asText : function(value) {
return String(value).replace(this.stripTagsRE, "");
},
/**
* Strips all HTML tags to sort on text only - Case insensitive
* @param {Object} value The value being converted
* @return {String} The comparison value
*/
asUCText : function(value) {
return String(value).toUpperCase().replace(this.stripTagsRE, "");
},
/**
* Case insensitive string
* @param {Object} value The value being converted
* @return {String} The comparison value
*/
asUCString : function(value) {
return String(value).toUpperCase();
},
/**
* Date sorting
* @param {Object} value The value being converted
* @return {Number} The comparison value
*/
asDate : function(value) {
if (!value) {
return 0;
}
if (Ext.isDate(value)) {
return value.getTime();
}
return Date.parse(String(value));
},
/**
* Float sorting
* @param {Object} value The value being converted
* @return {Number} The comparison value
*/
asFloat : function(value) {
value = parseFloat(String(value).replace(/,/g, ""));
return isNaN(value) ? 0 : value;
},
/**
* Integer sorting
* @param {Object} value The value being converted
* @return {Number} The comparison value
*/
asInt : function(value) {
value = parseInt(String(value).replace(/,/g, ""), 10);
return isNaN(value) ? 0 : value;
}
});
/**
* @class Ext.data.Types
* <p>This is s static class containing the system-supplied data types which may be given to a {@link Ext.data.Field Field}.<p/>
* <p>The properties in this class are used as type indicators in the {@link Ext.data.Field Field} class, so to
* test whether a Field is of a certain type, compare the {@link Ext.data.Field#type type} property against properties
* of this class.</p>
* <p>Developers may add their own application-specific data types to this class. Definition names must be UPPERCASE.
* each type definition must contain three properties:</p>
* <div class="mdetail-params"><ul>
* <li><code>convert</code> : <i>Function</i><div class="sub-desc">A function to convert raw data values from a data block into the data
* to be stored in the Field. The function is passed the collowing parameters:
* <div class="mdetail-params"><ul>
* <li><b>v</b> : Mixed<div class="sub-desc">The data value as read by the Reader, if undefined will use
* the configured <tt>{@link Ext.data.Field#defaultValue defaultValue}</tt>.</div></li>
* <li><b>rec</b> : Mixed<div class="sub-desc">The data object containing the row as read by the Reader.
* Depending on the Reader type, this could be an Array ({@link Ext.data.reader.Array ArrayReader}), an object
* ({@link Ext.data.reader.Json JsonReader}), or an XML element.</div></li>
* </ul></div></div></li>
* <li><code>sortType</code> : <i>Function</i> <div class="sub-desc">A function to convert the stored data into comparable form, as defined by {@link Ext.data.SortTypes}.</div></li>
* <li><code>type</code> : <i>String</i> <div class="sub-desc">A textual data type name.</div></li>
* </ul></div>
* <p>For example, to create a VELatLong field (See the Microsoft Bing Mapping API) containing the latitude/longitude value of a datapoint on a map from a JsonReader data block
* which contained the properties <code>lat</code> and <code>long</code>, you would define a new data type like this:</p>
*<pre><code>
// Add a new Field data type which stores a VELatLong object in the Record.
Ext.data.Types.VELATLONG = {
convert: function(v, data) {
return new VELatLong(data.lat, data.long);
},
sortType: function(v) {
return v.Latitude; // When sorting, order by latitude
},
type: 'VELatLong'
};
</code></pre>
* <p>Then, when declaring a Model, use <pre><code>
var types = Ext.data.Types; // allow shorthand type access
Ext.define('Unit', {
extend: 'Ext.data.Model',
config: {
fields: [
{ name: 'unitName', mapping: 'UnitName' },
{ name: 'curSpeed', mapping: 'CurSpeed', type: types.INT },
{ name: 'latitude', mapping: 'lat', type: types.FLOAT },
{ name: 'position', type: types.VELATLONG }
]
}
});
</code></pre>
* @singleton
*/
Ext.define('Ext.data.Types', {
singleton: true,
requires: ['Ext.data.SortTypes'],
/**
* @property {RegExp} stripRe
* A regular expression for stripping non-numeric characters from a numeric value. Defaults to <tt>/[\$,%]/g</tt>.
* This should be overridden for localization.
*/
stripRe: /[\$,%]/g,
dashesRe: /-/g,
iso8601TestRe: /\d\dT\d\d/,
iso8601SplitRe: /[- :T\.Z\+]/
}, function() {
var Types = this,
sortTypes = Ext.data.SortTypes;
Ext.apply(Types, {
/**
* @property {Object} AUTO
* This data type means that no conversion is applied to the raw data before it is placed into a Record.
*/
AUTO: {
convert: function(value) {
return value;
},
sortType: sortTypes.none,
type: 'auto'
},
/**
* @property {Object} STRING
* This data type means that the raw data is converted into a String before it is placed into a Record.
*/
STRING: {
convert: function(value) {
// 'this' is the actual field that calls this convert method
return (value === undefined || value === null)
? (this.getAllowNull() ? null : '')
: String(value);
},
sortType: sortTypes.asUCString,
type: 'string'
},
/**
* @property {Object} INT
* This data type means that the raw data is converted into an integer before it is placed into a Record.
* <p>The synonym <code>INTEGER</code> is equivalent.</p>
*/
INT: {
convert: function(value) {
return (value !== undefined && value !== null && value !== '')
? ((typeof value === 'number')
? parseInt(value, 10)
: parseInt(String(value).replace(Types.stripRe, ''), 10)
)
: (this.getAllowNull() ? null : 0);
},
sortType: sortTypes.none,
type: 'int'
},
/**
* @property {Object} FLOAT
* This data type means that the raw data is converted into a number before it is placed into a Record.
* <p>The synonym <code>NUMBER</code> is equivalent.</p>
*/
FLOAT: {
convert: function(value) {
return (value !== undefined && value !== null && value !== '')
? ((typeof value === 'number')
? value
: parseFloat(String(value).replace(Types.stripRe, ''), 10)
)
: (this.getAllowNull() ? null : 0);
},
sortType: sortTypes.none,
type: 'float'
},
/**
* @property {Object} BOOL
* <p>This data type means that the raw data is converted into a boolean before it is placed into
* a Record. The string "true" and the number 1 are converted to boolean <code>true</code>.</p>
* <p>The synonym <code>BOOLEAN</code> is equivalent.</p>
*/
BOOL: {
convert: function(value) {
if ((value === undefined || value === null || value === '') && this.getAllowNull()) {
return null;
}
return value === true || value === 'true' || value == 1;
},
sortType: sortTypes.none,
type: 'bool'
},
/**
* @property {Object} DATE
* This data type means that the raw data is converted into a Date before it is placed into a Record.
* The date format is specified in the constructor of the {@link Ext.data.Field} to which this type is
* being applied.
*/
DATE: {
convert: function(value) {
var dateFormat = this.getDateFormat(),
parsed;
if (!value) {
return null;
}
if (Ext.isDate(value)) {
return value;
}
if (dateFormat) {
if (dateFormat == 'timestamp') {
return new Date(value*1000);
}
if (dateFormat == 'time') {
return new Date(parseInt(value, 10));
}
return Ext.Date.parse(value, dateFormat);
}
parsed = new Date(Date.parse(value));
if (isNaN(parsed)) {
// Dates with ISO 8601 format are not well supported by mobile devices, this can work around the issue.
if (Types.iso8601TestRe.test(value)) {
parsed = value.split(Types.iso8601SplitRe);
parsed = new Date(parsed[0], parsed[1]-1, parsed[2], parsed[3], parsed[4], parsed[5]);
}
if (isNaN(parsed)) {
// Dates with the format "2012-01-20" fail, but "2012/01/20" work in some browsers. We'll try and
// get around that.
parsed = new Date(Date.parse(value.replace(this.dashesRe, "/")));
if (isNaN(parsed)) {
Ext.Logger.warn("Cannot parse the passed value (" + value + ") into a valid date");
}
}
}
return isNaN(parsed) ? null : parsed;
},
sortType: sortTypes.asDate,
type: 'date'
}
});
Ext.apply(Types, {
/**
* @property {Object} BOOLEAN
* <p>This data type means that the raw data is converted into a boolean before it is placed into
* a Record. The string "true" and the number 1 are converted to boolean <code>true</code>.</p>
* <p>The synonym <code>BOOL</code> is equivalent.</p>
*/
BOOLEAN: this.BOOL,
/**
* @property {Object} INTEGER
* This data type means that the raw data is converted into an integer before it is placed into a Record.
* <p>The synonym <code>INT</code> is equivalent.</p>
*/
INTEGER: this.INT,
/**
* @property {Object} NUMBER
* This data type means that the raw data is converted into a number before it is placed into a Record.
* <p>The synonym <code>FLOAT</code> is equivalent.</p>
*/
NUMBER: this.FLOAT
});
});
/**
* @author Ed Spencer
* @aside guide models
*
* This singleton contains a set of validation functions that can be used to validate any type of data. They are most
* often used in {@link Ext.data.Model Models}, where they are automatically set up and executed.
*/
Ext.define('Ext.data.Validations', {
alternateClassName: 'Ext.data.validations',
singleton: true,
config: {
/**
* @property {String} presenceMessage
* The default error message used when a presence validation fails.
*/
presenceMessage: 'must be present',
/**
* @property {String} lengthMessage
* The default error message used when a length validation fails.
*/
lengthMessage: 'is the wrong length',
/**
* @property {Boolean} formatMessage
* The default error message used when a format validation fails.
*/
formatMessage: 'is the wrong format',
/**
* @property {String} inclusionMessage
* The default error message used when an inclusion validation fails.
*/
inclusionMessage: 'is not included in the list of acceptable values',
/**
* @property {String} exclusionMessage
* The default error message used when an exclusion validation fails.
*/
exclusionMessage: 'is not an acceptable value',
/**
* @property {String} emailMessage
* The default error message used when an email validation fails
*/
emailMessage: 'is not a valid email address'
},
constructor: function(config) {
this.initConfig(config);
},
/**
* Returns the configured error message for any of the validation types.
* @param {String} type The type of validation you want to get the error message for.
*/
getMessage: function(type) {
var getterFn = this['get' + type[0].toUpperCase() + type.slice(1) + 'Message'];
if (getterFn) {
return getterFn.call(this);
}
return '';
},
/**
* The regular expression used to validate email addresses
* @property emailRe
* @type RegExp
*/
emailRe: /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/,
/**
* Validates that the given value is present.
* For example:
*
* validations: [{type: 'presence', field: 'age'}]
*
* @param {Object} config Config object
* @param {Object} value The value to validate
* @return {Boolean} True if validation passed
*/
presence: function(config, value) {
if (value === undefined) {
value = config;
}
//we need an additional check for zero here because zero is an acceptable form of present data
return !!value || value === 0;
},
/**
* Returns true if the given value is between the configured min and max values.
* For example:
*
* validations: [{type: 'length', field: 'name', min: 2}]
*
* @param {Object} config Config object
* @param {String} value The value to validate
* @return {Boolean} True if the value passes validation
*/
length: function(config, value) {
if (value === undefined || value === null) {
return false;
}
var length = value.length,
min = config.min,
max = config.max;
if ((min && length < min) || (max && length > max)) {
return false;
} else {
return true;
}
},
/**
* Validates that an email string is in the correct format
* @param {Object} config Config object
* @param {String} email The email address
* @return {Boolean} True if the value passes validation
*/
email: function(config, email) {
return Ext.data.validations.emailRe.test(email);
},
/**
* Returns true if the given value passes validation against the configured `matcher` regex.
* For example:
*
* validations: [{type: 'format', field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}]
*
* @param {Object} config Config object
* @param {String} value The value to validate
* @return {Boolean} True if the value passes the format validation
*/
format: function(config, value) {
return !!(config.matcher && config.matcher.test(value));
},
/**
* Validates that the given value is present in the configured `list`.
* For example:
*
* validations: [{type: 'inclusion', field: 'gender', list: ['Male', 'Female']}]
*
* @param {Object} config Config object
* @param {String} value The value to validate
* @return {Boolean} True if the value is present in the list
*/
inclusion: function(config, value) {
return config.list && Ext.Array.indexOf(config.list,value) != -1;
},
/**
* Validates that the given value is present in the configured `list`.
* For example:
*
* validations: [{type: 'exclusion', field: 'username', list: ['Admin', 'Operator']}]
*
* @param {Object} config Config object
* @param {String} value The value to validate
* @return {Boolean} True if the value is not present in the list
*/
exclusion: function(config, value) {
return config.list && Ext.Array.indexOf(config.list,value) == -1;
}
});
/**
* @author Tommy Maintz
*
* This class is the simple default id generator for Model instances.
*
* An example of a configured simple generator would be:
*
* Ext.define('MyApp.data.MyModel', {
* extend: 'Ext.data.Model',
* config: {
* identifier: {
* type: 'simple',
* prefix: 'ID_'
* }
* }
* });
* // assign id's of ID_1, ID_2, ID_3, etc.
*
*/
Ext.define('Ext.data.identifier.Simple', {
alias: 'data.identifier.simple',
statics: {
AUTO_ID: 1
},
config: {
prefix: 'ext-record-'
},
constructor: function(config) {
this.initConfig(config);
},
generate: function(record) {
return this._prefix + this.self.AUTO_ID++;
}
});
/**
* @author Tommy Maintz
*
* This class generates UUID's according to RFC 4122. This class has a default id property.
* This means that a single instance is shared unless the id property is overridden. Thus,
* two {@link Ext.data.Model} instances configured like the following share one generator:
*
* Ext.define('MyApp.data.MyModelX', {
* extend: 'Ext.data.Model',
* config: {
* identifier: 'uuid'
* }
* });
*
* Ext.define('MyApp.data.MyModelY', {
* extend: 'Ext.data.Model',
* config: {
* identifier: 'uuid'
* }
* });
*
* This allows all models using this class to share a commonly configured instance.
*
* # Using Version 1 ("Sequential") UUID's
*
* If a server can provide a proper timestamp and a "cryptographic quality random number"
* (as described in RFC 4122), the shared instance can be configured as follows:
*
* Ext.data.identifier.Uuid.Global.reconfigure({
* version: 1,
* clockSeq: clock, // 14 random bits
* salt: salt, // 48 secure random bits (the Node field)
* timestamp: ts // timestamp per Section 4.1.4
* });
*
* // or these values can be split into 32-bit chunks:
*
* Ext.data.identifier.Uuid.Global.reconfigure({
* version: 1,
* clockSeq: clock,
* salt: { lo: saltLow32, hi: saltHigh32 },
* timestamp: { lo: timestampLow32, hi: timestamptHigh32 }
* });
*
* This approach improves the generator's uniqueness by providing a valid timestamp and
* higher quality random data. Version 1 UUID's should not be used unless this information
* can be provided by a server and care should be taken to avoid caching of this data.
*
* See http://www.ietf.org/rfc/rfc4122.txt for details.
*/
Ext.define('Ext.data.identifier.Uuid', {
extend: 'Ext.data.identifier.Simple',
alias: 'data.identifier.uuid',
config: {
/**
* The id for this generator instance. By default all model instances share the same
* UUID generator instance. By specifying an id other then 'uuid', a unique generator instance
* will be created for the Model.
*/
id: undefined,
/**
* @property {Number/Object} salt
* When created, this value is a 48-bit number. For computation, this value is split
* into 32-bit parts and stored in an object with `hi` and `lo` properties.
*/
salt: null,
/**
* @property {Number/Object} timestamp
* When created, this value is a 60-bit number. For computation, this value is split
* into 32-bit parts and stored in an object with `hi` and `lo` properties.
*/
timestamp: null,
/**
* @cfg {Number} version
* The Version of UUID. Supported values are:
*
* * 1 : Time-based, "sequential" UUID.
* * 4 : Pseudo-random UUID.
*
* The default is 4.
*/
version: 4
},
applyId: function(id) {
if (id === undefined) {
return Ext.data.identifier.Uuid.Global;
}
return id;
},
constructor: function() {
var me = this;
me.callParent(arguments);
me.parts = [];
me.init();
},
/**
* Reconfigures this generator given new config properties.
*/
reconfigure: function(config) {
this.setConfig(config);
this.init();
},
generate: function () {
var me = this,
parts = me.parts,
version = me.getVersion(),
salt = me.getSalt(),
time = me.getTimestamp();
/*
The magic decoder ring (derived from RFC 4122 Section 4.2.2):
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_low |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_mid | ver | time_hi |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|res| clock_hi | clock_low | salt 0 |M| salt 1 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| salt (2-5) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
time_mid clock_hi (low 6 bits)
time_low | time_hi |clock_lo
| | | || salt[0]
| | | || | salt[1..5]
v v v vv v v
0badf00d-aced-1def-b123-dfad0badbeef
^ ^ ^
version | multicast (low bit)
|
reserved (upper 2 bits)
*/
parts[0] = me.toHex(time.lo, 8);
parts[1] = me.toHex(time.hi & 0xFFFF, 4);
parts[2] = me.toHex(((time.hi >>> 16) & 0xFFF) | (version << 12), 4);
parts[3] = me.toHex(0x80 | ((me.clockSeq >>> 8) & 0x3F), 2) +
me.toHex(me.clockSeq & 0xFF, 2);
parts[4] = me.toHex(salt.hi, 4) + me.toHex(salt.lo, 8);
if (version == 4) {
me.init(); // just regenerate all the random values...
} else {
// sequentially increment the timestamp...
++time.lo;
if (time.lo >= me.twoPow32) { // if (overflow)
time.lo = 0;
++time.hi;
}
}
return parts.join('-').toLowerCase();
},
/**
* @private
*/
init: function () {
var me = this,
salt = me.getSalt(),
time = me.getTimestamp();
if (me.getVersion() == 4) {
// See RFC 4122 (Secion 4.4)
// o If the state was unavailable (e.g., non-existent or corrupted),
// or the saved node ID is different than the current node ID,
// generate a random clock sequence value.
me.clockSeq = me.rand(0, me.twoPow14-1);
if (!salt) {
salt = {};
me.setSalt(salt);
}
if (!time) {
time = {};
me.setTimestamp(time);
}
// See RFC 4122 (Secion 4.4)
salt.lo = me.rand(0, me.twoPow32-1);
salt.hi = me.rand(0, me.twoPow16-1);
time.lo = me.rand(0, me.twoPow32-1);
time.hi = me.rand(0, me.twoPow28-1);
} else {
// this is run only once per-instance
me.setSalt(me.split(me.getSalt()));
me.setTimestamp(me.split(me.getTimestamp()));
// Set multicast bit: "the least significant bit of the first octet of the
// node ID" (nodeId = salt for this implementation):
me.getSalt().hi |= 0x100;
}
},
/**
* Some private values used in methods on this class.
* @private
*/
twoPow14: Math.pow(2, 14),
twoPow16: Math.pow(2, 16),
twoPow28: Math.pow(2, 28),
twoPow32: Math.pow(2, 32),
/**
* Converts a value into a hexadecimal value. Also allows for a maximum length
* of the returned value.
* @param value
* @param length
* @private
*/
toHex: function(value, length) {
var ret = value.toString(16);
if (ret.length > length) {
ret = ret.substring(ret.length - length); // right-most digits
} else if (ret.length < length) {
ret = Ext.String.leftPad(ret, length, '0');
}
return ret;
},
/**
* Generates a random value with between a low and high.
* @param lo
* @param hi
* @private
*/
rand: function(lo, hi) {
var v = Math.random() * (hi - lo + 1);
return Math.floor(v) + lo;
},
/**
* Splits a number into a low and high value.
* @param bignum
* @private
*/
split: function(bignum) {
if (typeof(bignum) == 'number') {
var hi = Math.floor(bignum / this.twoPow32);
return {
lo: Math.floor(bignum - hi * this.twoPow32),
hi: hi
};
}
return bignum;
}
}, function() {
this.Global = new this({
id: 'uuid'
});
});
/**
* @author Ed Spencer
*
* Base Writer class used by most subclasses of {@link Ext.data.proxy.Server}. This class is
* responsible for taking a set of {@link Ext.data.Operation} objects and a {@link Ext.data.Request}
* object and modifying that request based on the Operations.
*
* For example a Ext.data.writer.Json would format the Operations and their {@link Ext.data.Model}
* instances based on the config options passed to the JsonWriter's constructor.
*
* Writers are not needed for any kind of local storage - whether via a
* {@link Ext.data.proxy.WebStorage Web Storage proxy} (see {@link Ext.data.proxy.LocalStorage localStorage})
* or just in memory via a {@link Ext.data.proxy.Memory MemoryProxy}.
*/
Ext.define('Ext.data.writer.Writer', {
alias: 'writer.base',
alternateClassName: ['Ext.data.DataWriter', 'Ext.data.Writer'],
config: {
/**
* @cfg {Boolean} writeAllFields True to write all fields from the record to the server. If set to false it
* will only send the fields that were modified. Defaults to <tt>true</tt>. Note that any fields that have
* {@link Ext.data.Field#persist} set to false will still be ignored.
*/
writeAllFields: true,
/**
* @cfg {String} nameProperty This property is used to read the key for each value that will be sent to the server.
* For example:
* <pre><code>
Ext.define('Person', {
extend: 'Ext.data.Model',
fields: [{
name: 'first',
mapping: 'firstName'
}, {
name: 'last',
mapping: 'lastName'
}, {
name: 'age'
}]
});
new Ext.data.writer.Writer({
writeAllFields: true,
nameProperty: 'mapping'
});
// This will be sent to the server
{
firstName: 'first name value',
lastName: 'last name value',
age: 1
}
* </code></pre>
* Defaults to <tt>name</tt>. If the value is not present, the field name will always be used.
*/
nameProperty: 'name'
},
/**
* Creates new Writer.
* @param {Object} config (optional) Config object.
*/
constructor: function(config) {
this.initConfig(config);
},
/**
* Prepares a Proxy's Ext.data.Request object
* @param {Ext.data.Request} request The request object
* @return {Ext.data.Request} The modified request object
*/
write: function(request) {
var operation = request.getOperation(),
records = operation.getRecords() || [],
len = records.length,
i = 0,
data = [];
for (; i < len; i++) {
data.push(this.getRecordData(records[i]));
}
return this.writeRecords(request, data);
},
writeDate: function(field, date) {
var dateFormat = field.dateFormat || 'timestamp';
switch (dateFormat) {
case 'timestamp':
return date.getTime()/1000;
case 'time':
return date.getTime();
default:
return Ext.Date.format(date, dateFormat);
}
},
/**
* Formats the data for each record before sending it to the server. This
* method should be overridden to format the data in a way that differs from the default.
* @param {Object} record The record that we are writing to the server.
* @return {Object} An object literal of name/value keys to be written to the server.
* By default this method returns the data property on the record.
*/
getRecordData: function(record) {
var isPhantom = record.phantom === true,
writeAll = this.getWriteAllFields() || isPhantom,
nameProperty = this.getNameProperty(),
fields = record.getFields(),
data = {},
changes, name, field, key, value, fieldConfig;
if (writeAll) {
fields.each(function(field) {
fieldConfig = field.config;
if (fieldConfig.persist) {
name = fieldConfig[nameProperty] || fieldConfig.name;
value = record.get(fieldConfig.name);
if (fieldConfig.type.type == 'date') {
value = this.writeDate(fieldConfig, value);
}
data[name] = value;
}
}, this);
} else {
// Only write the changes
changes = record.getChanges();
for (key in changes) {
if (changes.hasOwnProperty(key)) {
field = fields.get(key);
fieldConfig = field.config;
if (fieldConfig.persist) {
name = fieldConfig[nameProperty] || field.name;
value = changes[key];
if (fieldConfig.type.type == 'date') {
value = this.writeDate(fieldConfig, value);
}
data[name] = value;
}
}
}
if (!isPhantom) {
// always include the id for non phantoms
data[record.getIdProperty()] = record.getId();
}
}
return data;
}
// Convert old properties in data into a config object
});
/**
* @author Ed Spencer
* @class Ext.data.writer.Xml
This class is used to write {@link Ext.data.Model} data to the server in an XML format.
The {@link #documentRoot} property is used to specify the root element in the XML document.
The {@link #record} option is used to specify the element name for each record that will make
up the XML document.
* @markdown
*/
Ext.define('Ext.data.writer.Xml', {
/* Begin Definitions */
extend: 'Ext.data.writer.Writer',
alternateClassName: 'Ext.data.XmlWriter',
alias: 'writer.xml',
/* End Definitions */
config: {
/**
* @cfg {String} documentRoot The name of the root element of the document. Defaults to <tt>'xmlData'</tt>.
* If there is more than 1 record and the root is not specified, the default document root will still be used
* to ensure a valid XML document is created.
*/
documentRoot: 'xmlData',
/**
* @cfg {String} defaultDocumentRoot The root to be used if {@link #documentRoot} is empty and a root is required
* to form a valid XML document.
*/
defaultDocumentRoot: 'xmlData',
/**
* @cfg {String} header A header to use in the XML document (such as setting the encoding or version).
* Defaults to <tt>''</tt>.
*/
header: '',
/**
* @cfg {String} record The name of the node to use for each record. Defaults to <tt>'record'</tt>.
*/
record: 'record'
},
//inherit docs
writeRecords: function(request, data) {
var me = this,
xml = [],
i = 0,
len = data.length,
root = me.getDocumentRoot(),
record = me.getRecord(),
needsRoot = data.length !== 1,
item,
key;
// may not exist
xml.push(me.getHeader() || '');
if (!root && needsRoot) {
root = me.getDefaultDocumentRoot();
}
if (root) {
xml.push('<', root, '>');
}
for (; i < len; ++i) {
item = data[i];
xml.push('<', record, '>');
for (key in item) {
if (item.hasOwnProperty(key)) {
xml.push('<', key, '>', item[key], '</', key, '>');
}
}
xml.push('</', record, '>');
}
if (root) {
xml.push('</', root, '>');
}
request.setXmlData(xml.join(''));
return request;
}
});
/**
* Small utility class used internally to represent a Direct method.
* @class Ext.direct.RemotingMethod
* @private
*/
Ext.define('Ext.direct.RemotingMethod', {
config: {
name: null,
params: null,
formHandler: null,
len: null,
ordered: true
},
constructor: function(config) {
this.initConfig(config);
},
applyParams: function(params) {
if (Ext.isNumber(params)) {
this.setLen(params);
} else if (Ext.isArray(params)) {
this.setOrdered(false);
var ln = params.length,
ret = [],
i, param, name;
for (i = 0; i < ln; i++) {
param = params[i];
name = Ext.isObject(param) ? param.name : param;
ret.push(name);
}
return ret;
}
},
getArgs: function(params, paramOrder, paramsAsHash) {
var args = [],
i, ln;
if (this.getOrdered()) {
if (this.getLen() > 0) {
// If a paramOrder was specified, add the params into the argument list in that order.
if (paramOrder) {
for (i = 0, ln = paramOrder.length; i < ln; i++) {
args.push(params[paramOrder[i]]);
}
} else if (paramsAsHash) {
// If paramsAsHash was specified, add all the params as a single object argument.
args.push(params);
}
}
} else {
args.push(params);
}
return args;
},
/**
* Takes the arguments for the Direct function and splits the arguments
* from the scope and the callback.
* @param {Array} args The arguments passed to the direct call
* @return {Object} An object with 3 properties, args, callback & scope.
*/
getCallData: function(args) {
var me = this,
data = null,
len = me.getLen(),
params = me.getParams(),
callback, scope, name;
if (me.getOrdered()) {
callback = args[len];
scope = args[len + 1];
if (len !== 0) {
data = args.slice(0, len);
}
} else {
data = Ext.apply({}, args[0]);
callback = args[1];
scope = args[2];
for (name in data) {
if (data.hasOwnProperty(name)) {
if (!Ext.Array.contains(params, name)) {
delete data[name];
}
}
}
}
return {
data: data,
callback: callback,
scope: scope
};
}
});
/**
* Supporting Class for Ext.Direct (not intended to be used directly).
*/
Ext.define('Ext.direct.Transaction', {
alias: 'direct.transaction',
alternateClassName: 'Ext.Direct.Transaction',
statics: {
TRANSACTION_ID: 0
},
config: {
id: undefined,
provider: null,
retryCount: 0,
args: null,
action: null,
method: null,
data: null,
callback: null,
form: null
},
constructor: function(config) {
this.initConfig(config);
},
applyId: function(id) {
if (id === undefined) {
id = ++this.self.TRANSACTION_ID;
}
return id;
},
updateId: function(id) {
this.id = this.tid = id;
},
getTid: function() {
return this.tid;
},
send: function(){
this.getProvider().queueTransaction(this);
},
retry: function(){
this.setRetryCount(this.getRetryCount() + 1);
this.send();
}
});
/**
* 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.dom.Element}. 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.define('Ext.dom.CompositeElement', {
alternateClassName: 'Ext.CompositeElement',
extend: 'Ext.dom.CompositeElementLite',
// 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);
}
}, function() {
Ext.dom.Element.select = function(selector, unique, root) {
var elements;
if (typeof selector == "string") {
elements = Ext.dom.Element.selectorFunction(selector, root);
}
else if (selector.length !== undefined) {
elements = selector;
}
else {
throw new Error("[Ext.select] Invalid selector specified: " + selector);
}
return (unique === true) ? new Ext.CompositeElement(elements) : new Ext.CompositeElementLite(elements);
};
});
/**
* @private
*/
Ext.define('Ext.event.Controller', {
isFiring: false,
listenerStack: null,
constructor: function(info) {
this.firingListeners = [];
this.firingArguments = [];
this.setInfo(info);
return this;
},
setInfo: function(info) {
this.info = info;
},
getInfo: function() {
return this.info;
},
setListenerStacks: function(listenerStacks) {
this.listenerStacks = listenerStacks;
},
fire: function(args, action) {
var listenerStacks = this.listenerStacks,
firingListeners = this.firingListeners,
firingArguments = this.firingArguments,
push = firingListeners.push,
ln = listenerStacks.length,
listeners, beforeListeners, currentListeners, afterListeners,
isActionBefore = false,
isActionAfter = false,
i;
firingListeners.length = 0;
if (action) {
if (action.order !== 'after') {
isActionBefore = true;
}
else {
isActionAfter = true;
}
}
if (ln === 1) {
listeners = listenerStacks[0].listeners;
beforeListeners = listeners.before;
currentListeners = listeners.current;
afterListeners = listeners.after;
if (beforeListeners.length > 0) {
push.apply(firingListeners, beforeListeners);
}
if (isActionBefore) {
push.call(firingListeners, action);
}
if (currentListeners.length > 0) {
push.apply(firingListeners, currentListeners);
}
if (isActionAfter) {
push.call(firingListeners, action);
}
if (afterListeners.length > 0) {
push.apply(firingListeners, afterListeners);
}
}
else {
for (i = 0; i < ln; i++) {
beforeListeners = listenerStacks[i].listeners.before;
if (beforeListeners.length > 0) {
push.apply(firingListeners, beforeListeners);
}
}
if (isActionBefore) {
push.call(firingListeners, action);
}
for (i = 0; i < ln; i++) {
currentListeners = listenerStacks[i].listeners.current;
if (currentListeners.length > 0) {
push.apply(firingListeners, currentListeners);
}
}
if (isActionAfter) {
push.call(firingListeners, action);
}
for (i = 0; i < ln; i++) {
afterListeners = listenerStacks[i].listeners.after;
if (afterListeners.length > 0) {
push.apply(firingListeners, afterListeners);
}
}
}
if (firingListeners.length === 0) {
return this;
}
if (!args) {
args = [];
}
firingArguments.length = 0;
firingArguments.push.apply(firingArguments, args);
// Backwards compatibility
firingArguments.push(null, this);
this.doFire();
return this;
},
doFire: function() {
var firingListeners = this.firingListeners,
firingArguments = this.firingArguments,
optionsArgumentIndex = firingArguments.length - 2,
i, ln, listener, options, fn, firingFn,
boundFn, isLateBinding, scope, args, result;
this.isPausing = false;
this.isPaused = false;
this.isStopped = false;
this.isFiring = true;
for (i = 0,ln = firingListeners.length; i < ln; i++) {
listener = firingListeners[i];
options = listener.options;
fn = listener.fn;
firingFn = listener.firingFn;
boundFn = listener.boundFn;
isLateBinding = listener.isLateBinding;
scope = listener.scope;
// Re-bind the callback if it has changed since the last time it's bound (overridden)
if (isLateBinding && boundFn && boundFn !== scope[fn]) {
boundFn = false;
firingFn = false;
}
if (!boundFn) {
if (isLateBinding) {
boundFn = scope[fn];
if (!boundFn) {
continue;
}
}
else {
boundFn = fn;
}
listener.boundFn = boundFn;
}
if (!firingFn) {
firingFn = boundFn;
if (options.buffer) {
firingFn = Ext.Function.createBuffered(firingFn, options.buffer, scope);
}
if (options.delay) {
firingFn = Ext.Function.createDelayed(firingFn, options.delay, scope);
}
listener.firingFn = firingFn;
}
firingArguments[optionsArgumentIndex] = options;
args = firingArguments;
if (options.args) {
args = options.args.concat(args);
}
if (options.single === true) {
listener.stack.remove(fn, scope, listener.order);
}
result = firingFn.apply(scope, args);
if (result === false) {
this.stop();
}
if (this.isStopped) {
break;
}
if (this.isPausing) {
this.isPaused = true;
firingListeners.splice(0, i + 1);
return;
}
}
this.isFiring = false;
this.listenerStacks = null;
firingListeners.length = 0;
firingArguments.length = 0;
this.connectingController = null;
},
connect: function(controller) {
this.connectingController = controller;
},
resume: function() {
var connectingController = this.connectingController;
this.isPausing = false;
if (this.isPaused && this.firingListeners.length > 0) {
this.isPaused = false;
this.doFire();
}
if (connectingController) {
connectingController.resume();
}
return this;
},
isInterrupted: function() {
return this.isStopped || this.isPaused;
},
stop: function() {
var connectingController = this.connectingController;
this.isStopped = true;
if (connectingController) {
this.connectingController = null;
connectingController.stop();
}
this.isFiring = false;
this.listenerStacks = null;
return this;
},
pause: function() {
var connectingController = this.connectingController;
this.isPausing = true;
if (connectingController) {
connectingController.pause();
}
return this;
}
});
// Using @mixins to include all members of Ext.event.Touch
// into here to keep documentation simpler
/**
* @mixins Ext.event.Touch
*
* Just as {@link Ext.dom.Element} wraps around a native DOM node, {@link Ext.event.Event} wraps the browser's native
* event-object normalizing cross-browser differences such as mechanisms to stop event-propagation along with a method
* to prevent default actions from taking place.
*
* Here is a simple example of how you use it:
*
* @example preview
* Ext.Viewport.add({
* layout: 'fit',
* items: [
* {
* docked: 'top',
* xtype: 'toolbar',
* title: 'Ext.event.Event example!'
* },
* {
* id: 'logger',
* styleHtmlContent: true,
* html: 'Tap somewhere!',
* padding: 5
* }
* ]
* });
*
* Ext.Viewport.element.on({
* tap: function(e, node) {
* var string = '';
*
* string += 'You tapped at: <strong>{ x: ' + e.pageX + ', y: ' + e.pageY + ' }</strong> <i>(e.pageX & e.pageY)</i>';
* string += '<hr />';
* string += 'The HTMLElement you tapped has the className of: <strong>' + e.target.className + '</strong> <i>(e.target)</i>';
* string += '<hr />';
* string += 'The HTMLElement which has the listener has a className of: <strong>' + e.getTarget().className + '</strong> <i>(e.getTarget())</i>';
*
* Ext.getCmp('logger').setHtml(string);
* }
* });
*
* ## Recgonisers
*
* Sencha Touch includes a bunch of default event recognisers to know when a user taps, swipes, etc.
*
* For a full list of default recognisers, and more information, please view the {@link Ext.event.recognizer.Recognizer} documentation
*/
Ext.define('Ext.event.Event', {
alternateClassName: 'Ext.EventObject',
isStopped: false,
set: function(name, value) {
if (arguments.length === 1 && typeof name != 'string') {
var info = name;
for (name in info) {
if (info.hasOwnProperty(name)) {
this[name] = info[name];
}
}
}
else {
this[name] = info[name];
}
},
/**
* Stop the event (preventDefault and stopPropagation)
*/
stopEvent: function() {
return this.stopPropagation();
},
/**
* Cancels bubbling of the event.
*/
stopPropagation: function() {
this.isStopped = true;
return this;
}
});
/**
* @private
*/
Ext.define('Ext.event.ListenerStack', {
currentOrder: 'current',
length: 0,
constructor: function() {
this.listeners = {
before: [],
current: [],
after: []
};
this.lateBindingMap = {};
return this;
},
add: function(fn, scope, options, order) {
var lateBindingMap = this.lateBindingMap,
listeners = this.getAll(order),
i = listeners.length,
bindingMap, listener, id;
if (typeof fn == 'string' && scope.isIdentifiable) {
id = scope.getId();
bindingMap = lateBindingMap[id];
if (bindingMap) {
if (bindingMap[fn]) {
return false;
}
else {
bindingMap[fn] = true;
}
}
else {
lateBindingMap[id] = bindingMap = {};
bindingMap[fn] = true;
}
}
else {
if (i > 0) {
while (i--) {
listener = listeners[i];
if (listener.fn === fn && listener.scope === scope) {
listener.options = options;
return false;
}
}
}
}
listener = this.create(fn, scope, options, order);
if (options && options.prepend) {
delete options.prepend;
listeners.unshift(listener);
}
else {
listeners.push(listener);
}
this.length++;
return true;
},
getAt: function(index, order) {
return this.getAll(order)[index];
},
getAll: function(order) {
if (!order) {
order = this.currentOrder;
}
return this.listeners[order];
},
count: function(order) {
return this.getAll(order).length;
},
create: function(fn, scope, options, order) {
return {
stack: this,
fn: fn,
firingFn: false,
boundFn: false,
isLateBinding: typeof fn == 'string',
scope: scope,
options: options || {},
order: order
};
},
remove: function(fn, scope, order) {
var listeners = this.getAll(order),
i = listeners.length,
isRemoved = false,
lateBindingMap = this.lateBindingMap,
listener, id;
if (i > 0) {
// Start from the end index, faster than looping from the
// beginning for "single" listeners,
// which are normally LIFO
while (i--) {
listener = listeners[i];
if (listener.fn === fn && listener.scope === scope) {
listeners.splice(i, 1);
isRemoved = true;
this.length--;
if (typeof fn == 'string' && scope.isIdentifiable) {
id = scope.getId();
if (lateBindingMap[id] && lateBindingMap[id][fn]) {
delete lateBindingMap[id][fn];
}
}
break;
}
}
}
return isRemoved;
}
});
/**
* @private
*/
Ext.define('Ext.event.publisher.Publisher', {
targetType: '',
idSelectorRegex: /^#([\w\-]+)$/i,
constructor: function() {
var handledEvents = this.handledEvents,
handledEventsMap,
i, ln, event;
handledEventsMap = this.handledEventsMap = {};
for (i = 0,ln = handledEvents.length; i < ln; i++) {
event = handledEvents[i];
handledEventsMap[event] = true;
}
this.subscribers = {};
return this;
},
handles: function(eventName) {
var map = this.handledEventsMap;
return !!map[eventName] || !!map['*'] || eventName === '*';
},
getHandledEvents: function() {
return this.handledEvents;
},
setDispatcher: function(dispatcher) {
this.dispatcher = dispatcher;
},
subscribe: function() {
return false;
},
unsubscribe: function() {
return false;
},
unsubscribeAll: function() {
delete this.subscribers;
this.subscribers = {};
return this;
},
notify: function() {
return false;
},
getTargetType: function() {
return this.targetType;
},
dispatch: function(target, eventName, args) {
this.dispatcher.doDispatchEvent(this.targetType, target, eventName, args);
}
});
/**
* A base class for all event recognisers in Sencha Touch.
*
* Sencha Touch, by default, includes various different {@link Ext.event.recognizer.Recognizer} subclasses to recognise
* events happening in your application.
*
* ## Default recognisers
*
* * {@link Ext.event.recognizer.Tap}
* * {@link Ext.event.recognizer.DoubleTap}
* * {@link Ext.event.recognizer.LongPress}
* * {@link Ext.event.recognizer.Drag}
* * {@link Ext.event.recognizer.HorizontalSwipe}
* * {@link Ext.event.recognizer.Pinch}
* * {@link Ext.event.recognizer.Rotate}
*
* ## Additional recognisers
*
* * {@link Ext.event.recognizer.VerticalSwipe}
*
* If you want to create custom recognisers, or disable recognisers in your Sencha Touch application, please refer to the
* documentation in {@link Ext#setup}.
*
* @private
*/
Ext.define('Ext.event.recognizer.Recognizer', {
mixins: ['Ext.mixin.Identifiable'],
handledEvents: [],
config: {
onRecognized: Ext.emptyFn,
onFailed: Ext.emptyFn,
callbackScope: null
},
constructor: function(config) {
this.initConfig(config);
return this;
},
getHandledEvents: function() {
return this.handledEvents;
},
onStart: Ext.emptyFn,
onEnd: Ext.emptyFn,
fail: function() {
this.getOnFailed().apply(this.getCallbackScope(), arguments);
return false;
},
fire: function() {
this.getOnRecognized().apply(this.getCallbackScope(), arguments);
}
});
/**
* @private
*/
Ext.define('Ext.event.recognizer.Touch', {
extend: 'Ext.event.recognizer.Recognizer',
onTouchStart: Ext.emptyFn,
onTouchMove: Ext.emptyFn,
onTouchEnd: Ext.emptyFn
});
/**
* @private
*/
Ext.define('Ext.fx.State', {
isAnimatable: {
'background-color' : true,
'background-image' : true,
'background-position': true,
'border-bottom-color': true,
'border-bottom-width': true,
'border-color' : true,
'border-left-color' : true,
'border-left-width' : true,
'border-right-color' : true,
'border-right-width' : true,
'border-spacing' : true,
'border-top-color' : true,
'border-top-width' : true,
'border-width' : true,
'bottom' : true,
'color' : true,
'crop' : true,
'font-size' : true,
'font-weight' : true,
'height' : true,
'left' : true,
'letter-spacing' : true,
'line-height' : true,
'margin-bottom' : true,
'margin-left' : true,
'margin-right' : true,
'margin-top' : true,
'max-height' : true,
'max-width' : true,
'min-height' : true,
'min-width' : true,
'opacity' : true,
'outline-color' : true,
'outline-offset' : true,
'outline-width' : true,
'padding-bottom' : true,
'padding-left' : true,
'padding-right' : true,
'padding-top' : true,
'right' : true,
'text-indent' : true,
'text-shadow' : true,
'top' : true,
'vertical-align' : true,
'visibility' : true,
'width' : true,
'word-spacing' : true,
'z-index' : true,
'zoom' : true,
'transform' : true
},
constructor: function(data) {
this.data = {};
this.set(data);
},
setConfig: function(data) {
this.set(data);
return this;
},
setRaw: function(data) {
this.data = data;
return this;
},
clear: function() {
return this.setRaw({});
},
setTransform: function(name, value) {
var data = this.data,
isArray = Ext.isArray(value),
transform = data.transform,
ln, key;
if (!transform) {
transform = data.transform = {
translateX: 0,
translateY: 0,
translateZ: 0,
scaleX: 1,
scaleY: 1,
scaleZ: 1,
rotate: 0,
rotateX: 0,
rotateY: 0,
rotateZ: 0,
skewX: 0,
skewY: 0
};
}
if (typeof name == 'string') {
switch (name) {
case 'translate':
if (isArray) {
ln = value.length;
if (ln == 0) { break; }
transform.translateX = value[0];
if (ln == 1) { break; }
transform.translateY = value[1];
if (ln == 2) { break; }
transform.translateZ = value[2];
}
else {
transform.translateX = value;
}
break;
case 'rotate':
if (isArray) {
ln = value.length;
if (ln == 0) { break; }
transform.rotateX = value[0];
if (ln == 1) { break; }
transform.rotateY = value[1];
if (ln == 2) { break; }
transform.rotateZ = value[2];
}
else {
transform.rotate = value;
}
break;
case 'scale':
if (isArray) {
ln = value.length;
if (ln == 0) { break; }
transform.scaleX = value[0];
if (ln == 1) { break; }
transform.scaleY = value[1];
if (ln == 2) { break; }
transform.scaleZ = value[2];
}
else {
transform.scaleX = value;
transform.scaleY = value;
}
break;
case 'skew':
if (isArray) {
ln = value.length;
if (ln == 0) { break; }
transform.skewX = value[0];
if (ln == 1) { break; }
transform.skewY = value[1];
}
else {
transform.skewX = value;
}
break;
default:
transform[name] = value;
}
}
else {
for (key in name) {
if (name.hasOwnProperty(key)) {
value = name[key];
this.setTransform(key, value);
}
}
}
},
set: function(name, value) {
var data = this.data,
key;
if (typeof name != 'string') {
for (key in name) {
value = name[key];
if (key === 'transform') {
this.setTransform(value);
}
else {
data[key] = value;
}
}
}
else {
if (name === 'transform') {
this.setTransform(value);
}
else {
data[name] = value;
}
}
return this;
},
unset: function(name) {
var data = this.data;
if (data.hasOwnProperty(name)) {
delete data[name];
}
return this;
},
getData: function() {
return this.data;
}
});
/**
* @private
*/
Ext.define('Ext.fx.easing.Abstract', {
config: {
startTime: 0,
startValue: 0
},
isEasing: true,
isEnded: false,
constructor: function(config) {
this.initConfig(config);
return this;
},
applyStartTime: function(startTime) {
if (!startTime) {
startTime = Ext.Date.now();
}
return startTime;
},
updateStartTime: function(startTime) {
this.reset();
},
reset: function() {
this.isEnded = false;
},
getValue: Ext.emptyFn
});
/**
* @private
*/
Ext.define('Ext.fx.easing.Bounce', {
extend: 'Ext.fx.easing.Abstract',
config: {
springTension: 0.3,
acceleration: 30,
startVelocity: 0
},
getValue: function() {
var deltaTime = Ext.Date.now() - this.getStartTime(),
theta = (deltaTime / this.getAcceleration()),
powTime = theta * Math.pow(Math.E, -this.getSpringTension() * theta);
return this.getStartValue() + (this.getStartVelocity() * powTime);
}
});
/**
* @private
*/
Ext.define('Ext.fx.easing.Linear', {
extend: 'Ext.fx.easing.Abstract',
alias: 'easing.linear',
config: {
duration: 0,
endValue: 0
},
updateStartValue: function(startValue) {
this.distance = this.getEndValue() - startValue;
},
updateEndValue: function(endValue) {
this.distance = endValue - this.getStartValue();
},
getValue: function() {
var deltaTime = Ext.Date.now() - this.getStartTime(),
duration = this.getDuration();
if (deltaTime > duration) {
this.isEnded = true;
return this.getEndValue();
}
else {
return this.getStartValue() + ((deltaTime / duration) * this.distance);
}
}
});
/**
* @private
*/
Ext.define('Ext.fx.easing.Momentum', {
extend: 'Ext.fx.easing.Abstract',
config: {
acceleration: 30,
friction: 0,
startVelocity: 0
},
alpha: 0,
updateFriction: function(friction) {
var theta = Math.log(1 - (friction / 10));
this.theta = theta;
this.alpha = theta / this.getAcceleration();
},
updateStartVelocity: function(velocity) {
this.velocity = velocity * this.getAcceleration();
},
updateAcceleration: function(acceleration) {
this.velocity = this.getStartVelocity() * acceleration;
this.alpha = this.theta / acceleration;
},
getValue: function() {
return this.getStartValue() - this.velocity * (1 - this.getFrictionFactor()) / this.theta;
},
getFrictionFactor: function() {
var deltaTime = Ext.Date.now() - this.getStartTime();
return Math.exp(deltaTime * this.alpha);
},
getVelocity: function() {
return this.getFrictionFactor() * this.velocity;
}
});
Ext.define('Ext.log.Base', {
config: {},
constructor: function(config) {
this.initConfig(config);
return this;
}
});
/**
* @class Ext.Logger
* Logs messages to help with debugging.
*
* ## Example
*
* Ext.Logger.deprecate('This method is no longer supported.');
*
* @singleton
*/
(function() {
var Logger = Ext.define('Ext.log.Logger', {
extend: 'Ext.log.Base',
statics: {
defaultPriority: 'info',
priorities: {
/**
* @method verbose
* Convenience method for {@link #log} with priority 'verbose'
*/
verbose: 0,
/**
* @method info
* Convenience method for {@link #log} with priority 'info'
*/
info: 1,
/**
* @method deprecate
* Convenience method for {@link #log} with priority 'deprecate'
*/
deprecate: 2,
/**
* @method warn
* Convenience method for {@link #log} with priority 'warn'
*/
warn: 3,
/**
* @method error
* Convenience method for {@link #log} with priority 'error'
*/
error: 4
}
},
config: {
enabled: true,
minPriority: 'deprecate',
writers: {}
},
/**
* Logs a message to help with debugging
* @param {String} message Message to log
* @param {Number} priority Priority of the log message
*/
log: function(message, priority, callerId) {
if (!this.getEnabled()) {
return this;
}
var statics = Logger,
priorities = statics.priorities,
priorityValue = priorities[priority],
caller = this.log.caller,
callerDisplayName = '',
writers = this.getWriters(),
event, i, originalCaller;
if (!priority) {
priority = 'info';
}
if (priorities[this.getMinPriority()] > priorityValue) {
return this;
}
if (!callerId) {
callerId = 1;
}
if (Ext.isArray(message)) {
message = message.join(" ");
}
else {
message = String(message);
}
if (typeof callerId == 'number') {
i = callerId;
do {
i--;
caller = caller.caller;
if (!caller) {
break;
}
if (!originalCaller) {
originalCaller = caller.caller;
}
if (i <= 0 && caller.displayName) {
break;
}
}
while (caller !== originalCaller);
callerDisplayName = Ext.getDisplayName(caller);
}
else {
caller = caller.caller;
callerDisplayName = Ext.getDisplayName(callerId) + '#' + caller.$name;
}
event = {
time: Ext.Date.now(),
priority: priorityValue,
priorityName: priority,
message: message,
caller: caller,
callerDisplayName: callerDisplayName
};
for (i in writers) {
if (writers.hasOwnProperty(i)) {
writers[i].write(Ext.merge({}, event));
}
}
return this;
}
}, function() {
Ext.Object.each(this.priorities, function(priority) {
this.override(priority, function(message, callerId) {
if (!callerId) {
callerId = 1;
}
if (typeof callerId == 'number') {
callerId += 1;
}
this.log(message, priority, callerId);
});
}, this);
});
})();
Ext.define('Ext.log.filter.Filter', {
extend: 'Ext.log.Base',
accept: function(event) {
return true;
}
});
Ext.define('Ext.log.filter.Priority', {
extend: 'Ext.log.filter.Filter',
config: {
minPriority: 1
},
accept: function(event) {
return event.priority >= this.getMinPriority();
}
});
Ext.define('Ext.log.formatter.Formatter', {
extend: 'Ext.log.Base',
config: {
messageFormat: "{message}"
},
format: function(event) {
return this.substitute(this.getMessageFormat(), event);
},
substitute: function(template, data) {
var name, value;
for (name in data) {
if (data.hasOwnProperty(name)) {
value = data[name];
template = template.replace(new RegExp("\\{" + name + "\\}", "g"), value);
}
}
return template;
}
});
Ext.define('Ext.log.writer.Writer', {
extend: 'Ext.log.Base',
requires: ['Ext.log.formatter.Formatter'],
config: {
formatter: null,
filters: {}
},
constructor: function() {
this.activeFilters = [];
return this.callParent(arguments);
},
updateFilters: function(filters) {
var activeFilters = this.activeFilters,
i, filter;
activeFilters.length = 0;
for (i in filters) {
if (filters.hasOwnProperty(i)) {
filter = filters[i];
activeFilters.push(filter);
}
}
},
write: function(event) {
var filters = this.activeFilters,
formatter = this.getFormatter(),
i, ln, filter;
for (i = 0,ln = filters.length; i < ln; i++) {
filter = filters[i];
if (!filters[i].accept(event)) {
return this;
}
}
if (formatter) {
event.message = formatter.format(event);
}
this.doWrite(event);
return this;
},
// @private
doWrite: Ext.emptyFn
});
/**
* Base class for all mixins.
* @private
*/
Ext.define('Ext.mixin.Mixin', {
onClassExtended: function(cls, data) {
var mixinConfig = data.mixinConfig,
parentClassMixinConfig,
beforeHooks, afterHooks;
if (mixinConfig) {
parentClassMixinConfig = cls.superclass.mixinConfig;
if (parentClassMixinConfig) {
mixinConfig = data.mixinConfig = Ext.merge({}, parentClassMixinConfig, mixinConfig);
}
data.mixinId = mixinConfig.id;
beforeHooks = mixinConfig.beforeHooks;
afterHooks = mixinConfig.hooks || mixinConfig.afterHooks;
if (beforeHooks || afterHooks) {
Ext.Function.interceptBefore(data, 'onClassMixedIn', function(targetClass) {
var mixin = this.prototype;
if (beforeHooks) {
Ext.Object.each(beforeHooks, function(from, to) {
targetClass.override(to, function() {
if (mixin[from].apply(this, arguments) !== false) {
return this.callOverridden(arguments);
}
});
});
}
if (afterHooks) {
Ext.Object.each(afterHooks, function(from, to) {
targetClass.override(to, function() {
var ret = this.callOverridden(arguments);
mixin[from].apply(this, arguments);
return ret;
});
});
}
});
}
}
}
});
/**
* Tracks what records are currently selected in a databound widget. This class is mixed in to {@link Ext.dataview.DataView} and
* all subclasses.
* @private
*/
Ext.define('Ext.mixin.Selectable', {
extend: 'Ext.mixin.Mixin',
mixinConfig: {
id: 'selectable',
hooks: {
updateStore: 'updateStore'
}
},
/**
* @event beforeselectionchange
* Fires before an item is selected
* @param {Ext.mixin.Selectable} this
* @preventable selectionchange
* @deprecated 2.0.0 Please listen to the {@link #selectionchange} event with an order of `before` instead.
*/
/**
* @event selectionchange
* Fires when a selection changes
* @param {Ext.mixin.Selectable} this
* @param {Ext.data.Model[]} records The records whose selection has changed
*/
config: {
/**
* @cfg {Boolean} disableSelection <p><tt>true</tt> to disable selection.
* This configuration will lock the selection model that the DataView uses.</p>
* @accessor
*/
disableSelection: null,
/**
* @cfg {String} mode
* Modes of selection.
* Valid values are SINGLE, SIMPLE, and MULTI. Defaults to 'SINGLE'
* @accessor
*/
mode: 'SINGLE',
/**
* @cfg {Boolean} allowDeselect
* Allow users to deselect a record in a DataView, List or Grid. Only applicable when the Selectable's mode is
* 'SINGLE'. Defaults to false.
* @accessor
*/
allowDeselect: false,
/**
* @cfg {Ext.data.Model} lastSelected
* @private
* @accessor
*/
lastSelected: null,
/**
* @cfg {Ext.data.Model} lastFocused
* @private
* @accessor
*/
lastFocused: null,
/**
* @cfg {Boolean} deselectOnContainerClick True to deselect current selection when the container body is
* clicked. Defaults to true
* @accessor
*/
deselectOnContainerClick: true
},
modes: {
SINGLE: true,
SIMPLE: true,
MULTI: true
},
selectableEventHooks: {
addrecords: 'onSelectionStoreAdd',
removerecords: 'onSelectionStoreRemove',
updaterecord: 'onSelectionStoreUpdate',
load: 'refreshSelection',
refresh: 'refreshSelection'
},
constructor: function() {
this.selected = new Ext.util.MixedCollection();
this.callParent(arguments);
},
/**
* @private
*/
applyMode: function(mode) {
mode = mode ? mode.toUpperCase() : 'SINGLE';
// set to mode specified unless it doesnt exist, in that case
// use single.
return this.modes[mode] ? mode : 'SINGLE';
},
/**
* @private
*/
updateStore: function(newStore, oldStore) {
var me = this,
bindEvents = Ext.apply({}, me.selectableEventHooks, { scope: me });
if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) {
if (oldStore.autoDestroy) {
oldStore.destroy();
}
else {
oldStore.un(bindEvents);
}
}
if (newStore) {
newStore.on(bindEvents);
me.refreshSelection();
}
},
/**
* Selects all records.
* @param {Boolean} silent True to suppress all select events.
*/
selectAll: function(silent) {
var me = this,
selections = me.getStore().getRange(),
ln = selections.length,
i = 0;
for (; i < ln; i++) {
me.select(selections[i], true, silent);
}
},
/**
* Deselects all records.
*/
deselectAll: function(supress) {
var me = this,
selections = me.getStore().getRange();
me.deselect(selections, supress);
me.selected.clear();
me.setLastSelected(null);
me.setLastFocused(null);
},
// Provides differentiation of logic between MULTI, SIMPLE and SINGLE
// selection modes.
selectWithEvent: function(record) {
var me = this,
isSelected = me.isSelected(record);
switch (me.getMode()) {
case 'MULTI':
case 'SIMPLE':
if (isSelected) {
me.deselect(record);
}
else {
me.select(record, true);
}
break;
case 'SINGLE':
if (me.getAllowDeselect() && isSelected) {
// if allowDeselect is on and this record isSelected, deselect it
me.deselect(record);
} else {
// select the record and do NOT maintain existing selections
me.select(record, false);
}
break;
}
},
/**
* Selects a range of rows if the selection model {@link Ext.mixin.Selectable#getDisableSelection is not locked}.
* All rows in between startRow and endRow are also selected.
* @param {Number} startRow The index of the first row in the range
* @param {Number} endRow The index of the last row in the range
* @param {Boolean} keepExisting (optional) True to retain existing selections
*/
selectRange: function(startRecord, endRecord, keepExisting, dir) {
var me = this,
store = me.getStore(),
startRow = store.indexOf(startRecord),
endRow = store.indexOf(endRecord),
selectedCount = 0,
tmp, dontDeselect, i;
if (me.getDisableSelection()) {
return;
}
// swap values
if (startRow > endRow) {
tmp = endRow;
endRow = startRow;
startRow = tmp;
}
for (i = startRow; i <= endRow; i++) {
if (me.isSelected(store.getAt(i))) {
selectedCount++;
}
}
if (!dir) {
dontDeselect = -1;
}
else {
dontDeselect = (dir == 'up') ? startRow : endRow;
}
for (i = startRow; i <= endRow; i++) {
if (selectedCount == (endRow - startRow + 1)) {
if (i != dontDeselect) {
me.deselect(i, true);
}
} else {
me.select(i, true);
}
}
},
/**
* Adds the given records to the currently selected set
* @param {Ext.data.Model/Array/Number} records The records to select
* @param {Boolean} keepExisting If true, the existing selection will be added to (if not, the old selection is replaced)
* @param {Boolean} suppressEvent If true, the 'select' event will not be fired
*/
select: function(records, keepExisting, suppressEvent) {
var me = this,
record;
if (me.getDisableSelection()) {
return;
}
if (typeof records === "number") {
records = [me.getStore().getAt(records)];
}
if (!records) {
return;
}
if (me.getMode() == "SINGLE" && records) {
record = records.length ? records[0] : records;
me.doSingleSelect(record, suppressEvent);
} else {
me.doMultiSelect(records, keepExisting, suppressEvent);
}
},
/**
* Selects a single record
* @private
*/
doSingleSelect: function(record, suppressEvent) {
var me = this,
selected = me.selected;
if (me.getDisableSelection()) {
return;
}
// already selected.
// should we also check beforeselect?
if (me.isSelected(record)) {
return;
}
if (selected.getCount() > 0) {
me.deselect(me.getLastSelected(), suppressEvent);
}
selected.add(record);
me.setLastSelected(record);
me.onItemSelect(record, suppressEvent);
me.setLastFocused(record);
me.fireSelectionChange(!suppressEvent);
},
/**
* Selects a set of multiple records
* @private
*/
doMultiSelect: function(records, keepExisting, suppressEvent) {
if (records === null || this.getDisableSelection()) {
return;
}
records = !Ext.isArray(records) ? [records] : records;
var me = this,
selected = me.selected,
ln = records.length,
change = false,
i = 0,
record;
if (!keepExisting && selected.getCount() > 0) {
change = true;
me.deselect(me.getSelection(), true);
}
for (; i < ln; i++) {
record = records[i];
if (keepExisting && me.isSelected(record)) {
continue;
}
change = true;
me.setLastSelected(record);
selected.add(record);
if (!suppressEvent) {
me.setLastFocused(record);
}
me.onItemSelect(record, suppressEvent);
}
this.fireSelectionChange(change && !suppressEvent);
},
/**
* Deselects the given record(s). If many records are currently selected, it will only deselect those you pass in.
* @param {Number/Array/Ext.data.Model} records The record(s) to deselect. Can also be a number to reference by index
* @param {Boolean} suppressEvent If true the deselect event will not be fired
*/
deselect: function(records, suppressEvent) {
var me = this;
if (me.getDisableSelection()) {
return;
}
records = Ext.isArray(records) ? records : [records];
var selected = me.selected,
change = false,
i = 0,
store = me.getStore(),
ln = records.length,
record;
for (; i < ln; i++) {
record = records[i];
if (typeof record === 'number') {
record = store.getAt(record);
}
if (selected.remove(record)) {
if (me.getLastSelected() == record) {
me.setLastSelected(selected.last());
}
change = true;
}
if (record) {
me.onItemDeselect(record, suppressEvent);
}
}
me.fireSelectionChange(change && !suppressEvent);
},
/**
* Sets a record as the last focused record. This does NOT mean
* that the record has been selected.
* @param {Ext.data.Record} newRecord
* @param {Ext.data.Record} oldRecord
*/
updateLastFocused: function(newRecord, oldRecord) {
this.onLastFocusChanged(oldRecord, newRecord);
},
fireSelectionChange: function(fireEvent) {
var me = this;
if (fireEvent) {
me.fireEvent('selectionchange', me, me.getSelection());
}
},
/**
* Returns an array of the currently selected records.
* @return {Array} An array of selected records
*/
getSelection: function() {
return this.selected.getRange();
},
/**
* Returns <tt>true</tt> if the specified row is selected.
* @param {Ext.data.Model/Number} record The record or index of the record to check
* @return {Boolean}
*/
isSelected: function(record) {
record = Ext.isNumber(record) ? this.getStore().getAt(record) : record;
return this.selected.indexOf(record) !== -1;
},
/**
* Returns true if there is a selected record.
* @return {Boolean}
*/
hasSelection: function() {
return this.selected.getCount() > 0;
},
/**
* @private
*/
refreshSelection: function() {
var me = this,
selections = me.getSelection();
me.deselectAll(true);
if (selections.length) {
me.select(selections, false, true);
}
},
// when a store is cleared remove all selections
// (if there were any)
onSelectionStoreClear: function() {
var me = this,
selected = me.selected;
if (selected.getCount() > 0) {
selected.clear();
me.setLastSelected(null);
me.setLastFocused(null);
me.fireSelectionChange(true);
}
},
// prune records from the SelectionModel if
// they were selected at the time they were
// removed.
onSelectionStoreRemove: function(store, record) {
var me = this,
selected = me.selected;
if (me.getDisableSelection()) {
return;
}
if (selected.remove(record)) {
if (me.getLastSelected() == record) {
me.setLastSelected(null);
}
if (me.getLastFocused() == record) {
me.setLastFocused(null);
}
me.fireSelectionChange(true);
}
},
/**
* Returns the number of selections.
* @return {Number}
*/
getSelectionCount: function() {
return this.selected.getCount();
},
onSelectionStoreAdd: Ext.emptyFn,
onSelectionStoreUpdate: Ext.emptyFn,
onItemSelect: Ext.emptyFn,
onItemDeselect: Ext.emptyFn,
onLastFocusChanged: Ext.emptyFn,
onEditorKey: Ext.emptyFn
}, function() {
/**
* Selects a record instance by record instance or index.
* @member Ext.mixin.Selectable
* @method doSelect
* @param {Ext.data.Model/Number} records An array of records or an index
* @param {Boolean} keepExisting
* @param {Boolean} suppressEvent Set to false to not fire a select event
* @deprecated 2.0.0 Please use {@link #select} instead.
*/
/**
* Deselects a record instance by record instance or index.
* @member Ext.mixin.Selectable
* @method doDeselect
* @param {Ext.data.Model/Number} records An array of records or an index
* @param {Boolean} suppressEvent Set to false to not fire a deselect event
* @deprecated 2.0.0 Please use {@link #deselect} instead.
*/
/**
* Returns the selection mode currently used by this Selectable
* @member Ext.mixin.Selectable
* @method getSelectionMode
* @return {String} The current mode
* @deprecated 2.0.0 Please use {@link #getMode} instead.
*/
/**
* Returns the array of previously selected items
* @member Ext.mixin.Selectable
* @method getLastSelected
* @return {Array} The previous selection
* @deprecated 2.0.0
*/
/**
* Returns true if the Selectable is currently locked
* @member Ext.mixin.Selectable
* @method isLocked
* @return {Boolean} True if currently locked
* @deprecated 2.0.0 Please use {@link #getDisableSelection} instead.
*/
/**
* This was an internal function accidentally exposed in 1.x and now deprecated. Calling it has no effect
* @member Ext.mixin.Selectable
* @method setLastFocused
* @deprecated 2.0.0
*/
/**
* Deselects any currently selected records and clears all stored selections
* @member Ext.mixin.Selectable
* @method clearSelections
* @deprecated 2.0.0 Please use {@link #deselectAll} instead.
*/
/**
* Returns the number of selections.
* @member Ext.mixin.Selectable
* @method getCount
* @return {Number}
* @deprecated 2.0.0 Please use {@link #getSelectionCount} instead.
*/
/**
* @cfg {Boolean} locked
* @inheritdoc Ext.mixin.Selectable#disableSelection
* @deprecated 2.0.0 Please use {@link #disableSelection} instead.
*/
});
/**
* A Traversable mixin.
* @private
*/
Ext.define('Ext.mixin.Traversable', {
extend: 'Ext.mixin.Mixin',
mixinConfig: {
id: 'traversable'
},
setParent: function(parent) {
this.parent = parent;
return this;
},
/**
* @member Ext.Component
* Returns `true` if this component has a parent.
* @return {Boolean} `true` if this component has a parent.
*/
hasParent: function() {
return Boolean(this.parent);
},
/**
* @member Ext.Component
* Returns the parent of this component, if it has one.
* @return {Ext.Component} The parent of this component.
*/
getParent: function() {
return this.parent;
},
getAncestors: function() {
var ancestors = [],
parent = this.getParent();
while (parent) {
ancestors.push(parent);
parent = parent.getParent();
}
return ancestors;
},
getAncestorIds: function() {
var ancestorIds = [],
parent = this.getParent();
while (parent) {
ancestorIds.push(parent.getId());
parent = parent.getParent();
}
return ancestorIds;
}
});
/**
* 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.
*
* Using {@link Ext.util.DelayedTask} is very simple:
*
* //create the delayed task instance with our callback
* var task = Ext.create('Ext.util.DelayedTask', function() {
* console.log('callback!');
* });
*
* task.delay(1500); //the callback function will now be called after 1500ms
*
* task.cancel(); //the callback function will never be called now, unless we call delay() again
*
* ## Example
*
* @example
* //create a textfield where we can listen to text
* var field = Ext.create('Ext.field.Text', {
* xtype: 'textfield',
* label: 'Length: 0'
* });
*
* //add the textfield into a fieldset
* Ext.Viewport.add({
* xtype: 'formpanel',
* items: [{
* xtype: 'fieldset',
* items: [field],
* instructions: 'Type into the field and watch the count go up after 500ms.'
* }]
* });
*
* //create our delayed task with a function that returns the fields length as the fields label
* var task = Ext.create('Ext.util.DelayedTask', function() {
* field.setLabel('Length: ' + field.getValue().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.
* field.on('keyup', function() {
* task.delay(500);
* });
*
* @constructor
* The parameters to this constructor serve as defaults and are not required.
* @param {Function} fn The default function to call.
* @param {Object} scope 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 The default Array of arguments.
*/
Ext.define('Ext.util.DelayedTask', {
config: {
interval: null,
delay: null,
fn: null,
scope: null,
args: null
},
constructor: function(fn, scope, args) {
var config = {
fn: fn,
scope: scope,
args: args
};
this.initConfig(config);
},
/**
* Cancels any pending timeout and queues a new one.
* @param {Number} delay The milliseconds to delay
* @param {Function} newFn Overrides the original function passed when instantiated.
* @param {Object} newScope Overrides the original `scope` passed when instantiated. Remember that if no scope
* is specified, `this` will refer to the browser window.
* @param {Array} newArgs Overrides the original `args` passed when instantiated.
*/
delay: function(delay, newFn, newScope, newArgs) {
var me = this;
//cancel any existing queued functions
me.cancel();
//set all the new configurations
me.setConfig({
delay: delay,
fn: newFn,
scope: newScope,
args: newArgs
});
//create the callback method for this delayed task
var call = function() {
me.getFn().apply(me.getScope(), me.getArgs() || []);
me.cancel();
};
me.setInterval(setInterval(call, me.getDelay()));
},
/**
* Cancel the last queued timeout
*/
cancel: function() {
this.setInterval(null);
},
/**
* @private
* Clears the old interval
*/
updateInterval: function(newInterval, oldInterval) {
if (oldInterval) {
clearInterval(oldInterval);
}
},
/**
* @private
* Changes the value into an array if it isn't one.
*/
applyArgs: function(config) {
if (!Ext.isArray(config)) {
config = [config];
}
return config;
}
});
/**
* Represents a filter that can be applied to a {@link Ext.util.MixedCollection MixedCollection}. Can either simply
* filter on a property/value pair or pass in a filter function with custom logic. Filters are always used in the
* context of MixedCollections, though {@link Ext.data.Store Store}s frequently create them when filtering and searching
* on their records. Example usage:
*
* // Set up a fictional MixedCollection containing a few people to filter on
* var allNames = new Ext.util.MixedCollection();
* allNames.addAll([
* { id: 1, name: 'Ed', age: 25 },
* { id: 2, name: 'Jamie', age: 37 },
* { id: 3, name: 'Abe', age: 32 },
* { id: 4, name: 'Aaron', age: 26 },
* { id: 5, name: 'David', age: 32 }
* ]);
*
* var ageFilter = new Ext.util.Filter({
* property: 'age',
* value : 32
* });
*
* var longNameFilter = new Ext.util.Filter({
* filterFn: function(item) {
* return item.name.length > 4;
* }
* });
*
* // a new MixedCollection with the 3 names longer than 4 characters
* var longNames = allNames.filter(longNameFilter);
*
* // a new MixedCollection with the 2 people of age 24:
* var youngFolk = allNames.filter(ageFilter);
*/
Ext.define('Ext.util.Filter', {
isFilter: true,
config: {
/**
* @cfg {String} [property=null]
* The property to filter on. Required unless a `filter` is passed
*/
property: null,
/**
* @cfg {RegExp/Mixed} [value=null]
* The value you want to match against. Can be a regular expression which will be used as matcher or any other
* value.
*/
value: null,
/**
* @cfg {Function} filterFn
* A custom filter function which is passed each item in the {@link Ext.util.MixedCollection} in turn. Should
* return true to accept each item or false to reject it
*/
filterFn: Ext.emptyFn,
/**
* @cfg {Boolean} [anyMatch=false]
* True to allow any match - no regex start/end line anchors will be added.
*/
anyMatch: false,
/**
* @cfg {Boolean} [exactMatch=false]
* True to force exact match (^ and $ characters added to the regex). Ignored if anyMatch is true.
*/
exactMatch: false,
/**
* @cfg {Boolean} [caseSensitive=false]
* True to make the regex case sensitive (adds 'i' switch to regex).
*/
caseSensitive: false,
/**
* @cfg {String} [root=null]
* Optional root property. This is mostly useful when filtering a Store, in which case we set the root to 'data'
* to make the filter pull the {@link #property} out of the data object of each item
*/
root: null,
/**
* @cfg {String} id
* An optional id this filter can be keyed by in Collections. If no id is specified it will generate an id by
* first trying a combination of property-value, and if none if these were specified (like when having a
* filterFn) it will generate a random id.
*/
id: undefined,
/**
* @cfg {Object} [scope=null]
* The scope in which to run the filterFn
*/
scope: null
},
applyId: function(id) {
if (!id) {
if (this.getProperty()) {
id = this.getProperty() + '-' + String(this.getValue());
}
if (!id) {
id = Ext.id(null, 'ext-filter-');
}
}
return id;
},
/**
* Creates new Filter.
* @param {Object} config Config object
*/
constructor: function(config) {
this.initConfig(config);
},
applyFilterFn: function(filterFn) {
if (filterFn === Ext.emptyFn) {
filterFn = this.getInitialConfig('filter');
if (filterFn) {
return filterFn;
}
var value = this.getValue();
if (!this.getProperty() && !value && value !== 0) {
Ext.Logger.error('A Filter requires either a property and value, or a filterFn to be set');
return Ext.emptyFn;
}
else {
return this.createFilterFn();
}
}
return filterFn;
},
/**
* @private
* Creates a filter function for the configured property/value/anyMatch/caseSensitive options for this Filter
*/
createFilterFn: function() {
var me = this,
matcher = me.createValueMatcher();
return function(item) {
var root = me.getRoot(),
property = me.getProperty();
if (root) {
item = item[root];
}
return matcher.test(item[property]);
};
},
/**
* @private
* Returns a regular expression based on the given value and matching options
*/
createValueMatcher: function() {
var me = this,
value = me.getValue(),
anyMatch = me.getAnyMatch(),
exactMatch = me.getExactMatch(),
caseSensitive = me.getCaseSensitive(),
escapeRe = Ext.String.escapeRegex;
if (value === null || value === undefined || !value.exec) { // not a regex
value = String(value);
if (anyMatch === true) {
value = escapeRe(value);
} else {
value = '^' + escapeRe(value);
if (exactMatch === true) {
value += '$';
}
}
value = new RegExp(value, caseSensitive ? '' : 'i');
}
return value;
}
});
/**
* Represents a 2D point with x and y properties, useful for comparison and instantiation
* from an event:
*
* var point = Ext.util.Point.fromEvent(e);
*
*/
Ext.define('Ext.util.Point', {
radianToDegreeConstant: 180 / Math.PI,
statics: {
/**
* Returns a new instance of Ext.util.Point based on the pageX / pageY values of the given event
* @static
* @param {Event} e The event
* @return Ext.util.Point
*/
fromEvent: function(e) {
var changedTouches = e.changedTouches,
touch = (changedTouches && changedTouches.length > 0) ? changedTouches[0] : e;
return this.fromTouch(touch);
},
/**
* Returns a new instance of Ext.util.Point based on the pageX / pageY values of the given touch
* @static
* @param {Event} touch
* @return Ext.util.Point
*/
fromTouch: function(touch) {
return new this(touch.pageX, touch.pageY);
},
/**
* Returns a new point from an object that has 'x' and 'y' properties, if that object is not an instance
* of Ext.util.Point. Otherwise, returns the given point itself.
* @param object
*/
from: function(object) {
if (!object) {
return new this(0, 0);
}
if (!(object instanceof this)) {
return new this(object.x, object.y);
}
return object;
}
},
/**
* Creates point on 2D plane.
* @param {Number} [x=0] X coordinate.
* @param {Number} [y=0] Y coordinate.
*/
constructor: function(x, y) {
if (typeof x == 'undefined') {
x = 0;
}
if (typeof y == 'undefined') {
y = 0;
}
this.x = x;
this.y = y;
return this;
},
/**
* Copy a new instance of this point
* @return {Ext.util.Point} the new point
*/
clone: function() {
return new this.self(this.x, this.y);
},
/**
* Clones this Point.
* @deprecated 2.0.0 Please use {@link #clone} instead
*/
copy: function() {
return this.clone.apply(this, arguments);
},
/**
* Copy the x and y values of another point / object to this point itself
* @param {Ext.util.Point/Object} point
* @return {Ext.util.Point} this This point
*/
copyFrom: function(point) {
this.x = point.x;
this.y = point.y;
return this;
},
/**
* Returns a human-eye-friendly string that represents this point,
* useful for debugging.
* @return {String} For example `Point[12,8]`
*/
toString: function() {
return "Point[" + this.x + "," + this.y + "]";
},
/**
* Compare this point and another point
* @param {Ext.util.Point/Object} The point to compare with, either an instance
* of Ext.util.Point or an object with x and y properties
* @return {Boolean} Returns whether they are equivalent
*/
equals: function(point) {
return (this.x === point.x && this.y === point.y);
},
/**
* Whether the given point is not away from this point within the given threshold amount
* @param {Ext.util.Point/Object} The point to check with, either an instance
* of Ext.util.Point or an object with x and y properties
* @param {Object/Number} threshold Can be either an object with x and y properties or a number
* @return {Boolean}
*/
isCloseTo: function(point, threshold) {
if (typeof threshold == 'number') {
threshold = {x: threshold};
threshold.y = threshold.x;
}
var x = point.x,
y = point.y,
thresholdX = threshold.x,
thresholdY = threshold.y;
return (this.x <= x + thresholdX && this.x >= x - thresholdX &&
this.y <= y + thresholdY && this.y >= y - thresholdY);
},
/**
* Returns true if this point is close to another one.
* @deprecated 2.0.0 Please use {@link #isCloseTo} instead
*/
isWithin: function() {
return this.isCloseTo.apply(this, arguments);
},
/**
* Translate this point by the given amounts
* @param {Number} x Amount to translate in the x-axis
* @param {Number} y Amount to translate in the y-axis
* @return {Boolean}
*/
translate: function(x, y) {
this.x += x;
this.y += y;
return this;
},
/**
* Compare this point with another point when the x and y values of both points are rounded. E.g:
* [100.3,199.8] will equals to [100, 200]
* @param {Ext.util.Point/Object} The point to compare with, either an instance
* of Ext.util.Point or an object with x and y properties
* @return {Boolean}
*/
roundedEquals: function(point) {
return (Math.round(this.x) === Math.round(point.x) &&
Math.round(this.y) === Math.round(point.y));
},
getDistanceTo: function(point) {
var deltaX = this.x - point.x,
deltaY = this.y - point.y;
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
},
getAngleTo: function(point) {
var deltaX = this.x - point.x,
deltaY = this.y - point.y;
return Math.atan2(deltaY, deltaX) * this.radianToDegreeConstant;
}
});
/**
* Represents a rectangular region and provides a number of utility methods
* to compare regions.
*/
Ext.define('Ext.util.Region', {
statics: {
/**
* @static
* Retrieves an Ext.util.Region for a particular element.
* @param {String/HTMLElement/Ext.Element} el The element or its ID.
* @return {Ext.util.Region} region
*/
getRegion: function(el) {
return Ext.fly(el).getPageBox(true);
},
/**
* @static
* Creates new Region from an object:
*
* Ext.util.Region.from({top: 0, right: 5, bottom: 3, left: -1});
* // the above is equivalent to:
* new Ext.util.Region(0, 5, 3, -1);
*
* @param {Object} o An object with top, right, bottom, left properties
* @return {Ext.util.Region} region The region constructed based on the passed object
*/
from: function(o) {
return new this(o.top, o.right, o.bottom, o.left);
}
},
/**
* Creates new Region.
* @param {Number} top Top
* @param {Number} right Right
* @param {Number} bottom Bottom
* @param {Number} left Left
*/
constructor: function(t, r, b, l) {
var me = this;
me.top = t;
me[1] = t;
me.right = r;
me.bottom = b;
me.left = l;
me[0] = l;
},
/**
* Checks if this region completely contains the region that is passed in.
* @param {Ext.util.Region} region
*/
contains: function(region) {
var me = this;
return (region.left >= me.left &&
region.right <= me.right &&
region.top >= me.top &&
region.bottom <= me.bottom);
},
/**
* Checks if this region intersects the region passed in.
* @param {Ext.util.Region} region
* @return {Ext.util.Region/Boolean} Returns the intersected region or false if there is no intersection.
*/
intersect: function(region) {
var me = this,
t = Math.max(me.top, region.top),
r = Math.min(me.right, region.right),
b = Math.min(me.bottom, region.bottom),
l = Math.max(me.left, region.left);
if (b > t && r > l) {
return new Ext.util.Region(t, r, b, l);
}
else {
return false;
}
},
/**
* Returns the smallest region that contains the current AND targetRegion.
* @param {Ext.util.Region} region
*/
union: function(region) {
var me = this,
t = Math.min(me.top, region.top),
r = Math.max(me.right, region.right),
b = Math.max(me.bottom, region.bottom),
l = Math.min(me.left, region.left);
return new Ext.util.Region(t, r, b, l);
},
/**
* Modifies the current region to be constrained to the targetRegion.
* @param {Ext.util.Region} targetRegion
*/
constrainTo: function(r) {
var me = this,
constrain = Ext.util.Numbers.constrain;
me.top = constrain(me.top, r.top, r.bottom);
me.bottom = constrain(me.bottom, r.top, r.bottom);
me.left = constrain(me.left, r.left, r.right);
me.right = constrain(me.right, r.left, r.right);
return me;
},
/**
* Modifies the current region to be adjusted by offsets.
* @param {Number} top top offset
* @param {Number} right right offset
* @param {Number} bottom bottom offset
* @param {Number} left left offset
*/
adjust: function(t, r, b, l) {
var me = this;
me.top += t;
me.left += l;
me.right += r;
me.bottom += b;
return me;
},
/**
* Get the offset amount of a point outside the region
* @param {String} axis optional
* @param {Ext.util.Point} p the point
* @return {Ext.util.Region}
*/
getOutOfBoundOffset: function(axis, p) {
if (!Ext.isObject(axis)) {
if (axis == 'x') {
return this.getOutOfBoundOffsetX(p);
} else {
return this.getOutOfBoundOffsetY(p);
}
} else {
p = axis;
var d = new Ext.util.Offset();
d.x = this.getOutOfBoundOffsetX(p.x);
d.y = this.getOutOfBoundOffsetY(p.y);
return d;
}
},
/**
* Get the offset amount on the x-axis
* @param {Number} p the offset
* @return {Number}
*/
getOutOfBoundOffsetX: function(p) {
if (p <= this.left) {
return this.left - p;
} else if (p >= this.right) {
return this.right - p;
}
return 0;
},
/**
* Get the offset amount on the y-axis
* @param {Number} p the offset
* @return {Number}
*/
getOutOfBoundOffsetY: function(p) {
if (p <= this.top) {
return this.top - p;
} else if (p >= this.bottom) {
return this.bottom - p;
}
return 0;
},
/**
* Check whether the point / offset is out of bound
* @param {String} axis optional
* @param {Ext.util.Point/Number} p the point / offset
* @return {Boolean}
*/
isOutOfBound: function(axis, p) {
if (!Ext.isObject(axis)) {
if (axis == 'x') {
return this.isOutOfBoundX(p);
} else {
return this.isOutOfBoundY(p);
}
} else {
p = axis;
return (this.isOutOfBoundX(p.x) || this.isOutOfBoundY(p.y));
}
},
/**
* Check whether the offset is out of bound in the x-axis
* @param {Number} p the offset
* @return {Boolean}
*/
isOutOfBoundX: function(p) {
return (p < this.left || p > this.right);
},
/**
* Check whether the offset is out of bound in the y-axis
* @param {Number} p the offset
* @return {Boolean}
*/
isOutOfBoundY: function(p) {
return (p < this.top || p > this.bottom);
},
/*
* Restrict a point within the region by a certain factor.
* @param {String} axis Optional
* @param {Ext.util.Point/Ext.util.Offset/Object} p
* @param {Number} factor
* @return {Ext.util.Point/Ext.util.Offset/Object/Number}
*/
restrict: function(axis, p, factor) {
if (Ext.isObject(axis)) {
var newP;
factor = p;
p = axis;
if (p.copy) {
newP = p.copy();
}
else {
newP = {
x: p.x,
y: p.y
};
}
newP.x = this.restrictX(p.x, factor);
newP.y = this.restrictY(p.y, factor);
return newP;
} else {
if (axis == 'x') {
return this.restrictX(p, factor);
} else {
return this.restrictY(p, factor);
}
}
},
/*
* Restrict an offset within the region by a certain factor, on the x-axis
* @param {Number} p
* @param {Number} factor The factor, optional, defaults to 1
* @return
*/
restrictX: function(p, factor) {
if (!factor) {
factor = 1;
}
if (p <= this.left) {
p -= (p - this.left) * factor;
}
else if (p >= this.right) {
p -= (p - this.right) * factor;
}
return p;
},
/*
* Restrict an offset within the region by a certain factor, on the y-axis
* @param {Number} p
* @param {Number} factor The factor, optional, defaults to 1
*/
restrictY: function(p, factor) {
if (!factor) {
factor = 1;
}
if (p <= this.top) {
p -= (p - this.top) * factor;
}
else if (p >= this.bottom) {
p -= (p - this.bottom) * factor;
}
return p;
},
/*
* Get the width / height of this region
* @return {Object} an object with width and height properties
*/
getSize: function() {
return {
width: this.right - this.left,
height: this.bottom - this.top
};
},
/**
* Copy a new instance
* @return {Ext.util.Region}
*/
copy: function() {
return new Ext.util.Region(this.top, this.right, this.bottom, this.left);
},
/**
* Dump this to an eye-friendly string, great for debugging
* @return {String} For example `Region[0,1,3,2]`
*/
toString: function() {
return "Region[" + this.top + "," + this.right + "," + this.bottom + "," + this.left + "]";
},
/**
* Translate this region by the given offset amount
* @param {Object} offset
* @return {Ext.util.Region} this This Region
*/
translateBy: function(offset) {
this.left += offset.x;
this.right += offset.x;
this.top += offset.y;
this.bottom += offset.y;
return this;
},
/**
* Round all the properties of this region
* @return {Ext.util.Region} this This Region
*/
round: function() {
this.top = Math.round(this.top);
this.right = Math.round(this.right);
this.bottom = Math.round(this.bottom);
this.left = Math.round(this.left);
return this;
},
/**
* Check whether this region is equivalent to the given region
* @param {Ext.util.Region} region The region to compare with
* @return {Boolean}
*/
equals: function(region) {
return (this.top == region.top && this.right == region.right && this.bottom == region.bottom && this.left == region.left)
}
});
/**
* Represents a single sorter that can be used as part of the sorters configuration in Ext.mixin.Sortable.
*
* A common place for Sorters to be used are {@link Ext.data.Store Stores}. For example:
*
* @example miniphone
* var store = Ext.create('Ext.data.Store', {
* fields: ['firstName', 'lastName'],
* sorters: 'lastName',
*
* data: [
* { firstName: 'Tommy', lastName: 'Maintz' },
* { firstName: 'Rob', lastName: 'Dougan' },
* { firstName: 'Ed', lastName: 'Spencer'},
* { firstName: 'Jamie', lastName: 'Avins' },
* { firstName: 'Nick', lastName: 'Poulden'}
* ]
* });
*
* Ext.create('Ext.List', {
* fullscreen: true,
* itemTpl: '<div class="contact">{firstName} <strong>{lastName}</strong></div>',
* store: store
* });
*
* In the next example, we specify a custom sorter function:
*
* @example miniphone
* var store = Ext.create('Ext.data.Store', {
* fields: ['person'],
* sorters: [
* {
* // Sort by first letter of last name, in descending order
* sorterFn: function(record1, record2) {
* var name1 = record1.data.person.name.split('-')[1].substr(0, 1),
* name2 = record2.data.person.name.split('-')[1].substr(0, 1);
*
* return name1 > name2 ? 1 : (name1 == name2 ? 0 : -1);
* },
* direction: 'DESC'
* }
* ],
* data: [
* { person: { name: 'Tommy-Maintz' } },
* { person: { name: 'Rob-Dougan' } },
* { person: { name: 'Ed-Spencer' } },
* { person: { name: 'Nick-Poulden' } },
* { person: { name: 'Jamie-Avins' } }
* ]
* });
*
* Ext.create('Ext.List', {
* fullscreen: true,
* itemTpl: '{person.name}',
* store: store
* });
*/
Ext.define('Ext.util.Sorter', {
isSorter: true,
config: {
/**
* @cfg {String} property The property to sort by. Required unless `sorterFn` is provided
*/
property: null,
/**
* @cfg {Function} sorterFn A specific sorter function to execute. Can be passed instead of {@link #property}.
* This function should compare the two passed arguments, returning -1, 0 or 1 depending on if item 1 should be
* sorted before, at the same level, or after item 2.
*
* sorterFn: function(person1, person2) {
* return (person1.age > person2.age) ? 1 : (person1.age == person2.age ? 0 : -1);
* }
*/
sorterFn: null,
/**
* @cfg {String} root Optional root property. This is mostly useful when sorting a Store, in which case we set the
* root to 'data' to make the filter pull the {@link #property} out of the data object of each item
*/
root: null,
/**
* @cfg {Function} transform A function that will be run on each value before
* it is compared in the sorter. The function will receive a single argument,
* the value.
*/
transform: null,
/**
* @cfg {String} direction The direction to sort by.
*/
direction: "ASC",
/**
* @cfg {Mixed} id An optional id this sorter can be keyed by in Collections. If
* no id is specified it will use the property name used in this Sorter. If no
* property is specified, e.g. when adding a custom sorter function we will generate
* a random id.
*/
id: undefined
},
constructor: function(config) {
this.initConfig(config);
},
applySorterFn: function(sorterFn) {
if (!sorterFn && !this.getProperty()) {
Ext.Logger.error("A Sorter requires either a property or a sorterFn.");
}
return sorterFn;
},
applyProperty: function(property) {
if (!property && !this.getSorterFn()) {
Ext.Logger.error("A Sorter requires either a property or a sorterFn.");
}
return property;
},
applyId: function(id) {
if (!id) {
id = this.getProperty();
if (!id) {
id = Ext.id(null, 'ext-sorter-');
}
}
return id;
},
/**
* @private
* Creates and returns a function which sorts an array by the given property and direction
* @return {Function} A function which sorts by the property/direction combination provided
*/
createSortFunction: function(sorterFn) {
var me = this,
modifier = me.getDirection().toUpperCase() == "DESC" ? -1 : 1;
//create a comparison function. Takes 2 objects, returns 1 if object 1 is greater,
//-1 if object 2 is greater or 0 if they are equal
return function(o1, o2) {
return modifier * sorterFn.call(me, o1, o2);
};
},
/**
* @private
* Basic default sorter function that just compares the defined property of each object
*/
defaultSortFn: function(item1, item2) {
var me = this,
transform = me._transform,
root = me._root,
value1, value2,
property = me._property;
if (root !== null) {
item1 = item1[root];
item2 = item2[root];
}
value1 = item1[property];
value2 = item2[property];
if (transform) {
value1 = transform(value1);
value2 = transform(value2);
}
return value1 > value2 ? 1 : (value1 < value2 ? -1 : 0);
},
updateDirection: function() {
this.updateSortFn();
},
updateSortFn: function() {
this.sort = this.createSortFunction(this.getSorterFn() || this.defaultSortFn);
},
/**
* Toggles the direction of this Sorter. Note that when you call this function,
* the Collection this Sorter is part of does not get refreshed automatically.
*/
toggle: function() {
this.setDirection(Ext.String.toggle(this.getDirection(), "ASC", "DESC"));
}
});
/**
* <p>General purpose inflector class that {@link #pluralize pluralizes}, {@link #singularize singularizes} and
* {@link #ordinalize ordinalizes} words. Sample usage:</p>
*
<pre><code>
//turning singular words into plurals
Ext.util.Inflector.pluralize('word'); //'words'
Ext.util.Inflector.pluralize('person'); //'people'
Ext.util.Inflector.pluralize('sheep'); //'sheep'
//turning plurals into singulars
Ext.util.Inflector.singularize('words'); //'word'
Ext.util.Inflector.singularize('people'); //'person'
Ext.util.Inflector.singularize('sheep'); //'sheep'
//ordinalizing numbers
Ext.util.Inflector.ordinalize(11); //"11th"
Ext.util.Inflector.ordinalize(21); //"21th"
Ext.util.Inflector.ordinalize(1043); //"1043rd"
</code></pre>
*
* <p><u>Customization</u></p>
*
* <p>The Inflector comes with a default set of US English pluralization rules. These can be augmented with additional
* rules if the default rules do not meet your application's requirements, or swapped out entirely for other languages.
* Here is how we might add a rule that pluralizes "ox" to "oxen":</p>
*
<pre><code>
Ext.util.Inflector.plural(/^(ox)$/i, "$1en");
</code></pre>
*
* <p>Each rule consists of two items - a regular expression that matches one or more rules, and a replacement string.
* In this case, the regular expression will only match the string "ox", and will replace that match with "oxen".
* Here's how we could add the inverse rule:</p>
*
<pre><code>
Ext.util.Inflector.singular(/^(ox)en$/i, "$1");
</code></pre>
*
* <p>Note that the ox/oxen rules are present by default.</p>
*/
Ext.define('Ext.util.Inflector', {
/* Begin Definitions */
singleton: true,
/* End Definitions */
/**
* @private
* The registered plural tuples. Each item in the array should contain two items - the first must be a regular
* expression that matchers the singular form of a word, the second must be a String that replaces the matched
* part of the regular expression. This is managed by the {@link #plural} method.
* @property plurals
* @type Array
*/
plurals: [
[(/(quiz)$/i), "$1zes" ],
[(/^(ox)$/i), "$1en" ],
[(/([m|l])ouse$/i), "$1ice" ],
[(/(matr|vert|ind)ix|ex$/i), "$1ices" ],
[(/(x|ch|ss|sh)$/i), "$1es" ],
[(/([^aeiouy]|qu)y$/i), "$1ies" ],
[(/(hive)$/i), "$1s" ],
[(/(?:([^f])fe|([lr])f)$/i), "$1$2ves"],
[(/sis$/i), "ses" ],
[(/([ti])um$/i), "$1a" ],
[(/(buffal|tomat|potat)o$/i), "$1oes" ],
[(/(bu)s$/i), "$1ses" ],
[(/(alias|status|sex)$/i), "$1es" ],
[(/(octop|vir)us$/i), "$1i" ],
[(/(ax|test)is$/i), "$1es" ],
[(/^person$/), "people" ],
[(/^man$/), "men" ],
[(/^(child)$/), "$1ren" ],
[(/s$/i), "s" ],
[(/$/), "s" ]
],
/**
* @private
* The set of registered singular matchers. Each item in the array should contain two items - the first must be a
* regular expression that matches the plural form of a word, the second must be a String that replaces the
* matched part of the regular expression. This is managed by the {@link #singular} method.
* @property singulars
* @type Array
*/
singulars: [
[(/(quiz)zes$/i), "$1" ],
[(/(matr)ices$/i), "$1ix" ],
[(/(vert|ind)ices$/i), "$1ex" ],
[(/^(ox)en/i), "$1" ],
[(/(alias|status)es$/i), "$1" ],
[(/(octop|vir)i$/i), "$1us" ],
[(/(cris|ax|test)es$/i), "$1is" ],
[(/(shoe)s$/i), "$1" ],
[(/(o)es$/i), "$1" ],
[(/(bus)es$/i), "$1" ],
[(/([m|l])ice$/i), "$1ouse" ],
[(/(x|ch|ss|sh)es$/i), "$1" ],
[(/(m)ovies$/i), "$1ovie" ],
[(/(s)eries$/i), "$1eries"],
[(/([^aeiouy]|qu)ies$/i), "$1y" ],
[(/([lr])ves$/i), "$1f" ],
[(/(tive)s$/i), "$1" ],
[(/(hive)s$/i), "$1" ],
[(/([^f])ves$/i), "$1fe" ],
[(/(^analy)ses$/i), "$1sis" ],
[(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i), "$1$2sis"],
[(/([ti])a$/i), "$1um" ],
[(/(n)ews$/i), "$1ews" ],
[(/people$/i), "person" ],
[(/s$/i), "" ]
],
/**
* @private
* The registered uncountable words
* @property uncountable
* @type Array
*/
uncountable: [
"sheep",
"fish",
"series",
"species",
"money",
"rice",
"information",
"equipment",
"grass",
"mud",
"offspring",
"deer",
"means"
],
/**
* Adds a new singularization rule to the Inflector. See the intro docs for more information
* @param {RegExp} matcher The matcher regex
* @param {String} replacer The replacement string, which can reference matches from the matcher argument
*/
singular: function(matcher, replacer) {
this.singulars.unshift([matcher, replacer]);
},
/**
* Adds a new pluralization rule to the Inflector. See the intro docs for more information
* @param {RegExp} matcher The matcher regex
* @param {String} replacer The replacement string, which can reference matches from the matcher argument
*/
plural: function(matcher, replacer) {
this.plurals.unshift([matcher, replacer]);
},
/**
* Removes all registered singularization rules
*/
clearSingulars: function() {
this.singulars = [];
},
/**
* Removes all registered pluralization rules
*/
clearPlurals: function() {
this.plurals = [];
},
/**
* Returns true if the given word is transnumeral (the word is its own singular and plural form - e.g. sheep, fish)
* @param {String} word The word to test
* @return {Boolean} True if the word is transnumeral
*/
isTransnumeral: function(word) {
return Ext.Array.indexOf(this.uncountable, word) != -1;
},
/**
* Returns the pluralized form of a word (e.g. Ext.util.Inflector.pluralize('word') returns 'words')
* @param {String} word The word to pluralize
* @return {String} The pluralized form of the word
*/
pluralize: function(word) {
if (this.isTransnumeral(word)) {
return word;
}
var plurals = this.plurals,
length = plurals.length,
tuple, regex, i;
for (i = 0; i < length; i++) {
tuple = plurals[i];
regex = tuple[0];
if (regex == word || (regex.test && regex.test(word))) {
return word.replace(regex, tuple[1]);
}
}
return word;
},
/**
* Returns the singularized form of a word (e.g. Ext.util.Inflector.singularize('words') returns 'word')
* @param {String} word The word to singularize
* @return {String} The singularized form of the word
*/
singularize: function(word) {
if (this.isTransnumeral(word)) {
return word;
}
var singulars = this.singulars,
length = singulars.length,
tuple, regex, i;
for (i = 0; i < length; i++) {
tuple = singulars[i];
regex = tuple[0];
if (regex == word || (regex.test && regex.test(word))) {
return word.replace(regex, tuple[1]);
}
}
return word;
},
/**
* Returns the correct {@link Ext.data.Model Model} name for a given string. Mostly used internally by the data
* package
* @param {String} word The word to classify
* @return {String} The classified version of the word
*/
classify: function(word) {
return Ext.String.capitalize(this.singularize(word));
},
/**
* Ordinalizes a given number by adding a prefix such as 'st', 'nd', 'rd' or 'th' based on the last digit of the
* number. 21 -> 21st, 22 -> 22nd, 23 -> 23rd, 24 -> 24th etc
* @param {Number} number The number to ordinalize
* @return {String} The ordinalized number
*/
ordinalize: function(number) {
var parsed = parseInt(number, 10),
mod10 = parsed % 10,
mod100 = parsed % 100;
//11 through 13 are a special case
if (11 <= mod100 && mod100 <= 13) {
return number + "th";
} else {
switch(mod10) {
case 1 : return number + "st";
case 2 : return number + "nd";
case 3 : return number + "rd";
default: return number + "th";
}
}
}
}, function() {
//aside from the rules above, there are a number of words that have irregular pluralization so we add them here
var irregulars = {
alumnus: 'alumni',
cactus : 'cacti',
focus : 'foci',
nucleus: 'nuclei',
radius: 'radii',
stimulus: 'stimuli',
ellipsis: 'ellipses',
paralysis: 'paralyses',
oasis: 'oases',
appendix: 'appendices',
index: 'indexes',
beau: 'beaux',
bureau: 'bureaux',
tableau: 'tableaux',
woman: 'women',
child: 'children',
man: 'men',
corpus: 'corpora',
criterion: 'criteria',
curriculum: 'curricula',
genus: 'genera',
memorandum: 'memoranda',
phenomenon: 'phenomena',
foot: 'feet',
goose: 'geese',
tooth: 'teeth',
antenna: 'antennae',
formula: 'formulae',
nebula: 'nebulae',
vertebra: 'vertebrae',
vita: 'vitae'
},
singular;
for (singular in irregulars) {
this.plural(singular, irregulars[singular]);
this.singular(irregulars[singular], singular);
}
});
/**
* Ext.Anim is used to excute simple animations defined in {@link Ext.anims}. The {@link #run} method can take any of the
* properties defined below.
*
* Ext.Anim.run(this, 'fade', {
* out: false,
* autoClear: true
* });
*
* When using {@link Ext.Anim#run}, ensure you require {@link Ext.Anim} in your application. Either do this using {@link Ext#require}:
*
* Ext.requires('Ext.Anim');
*
* when using {@link Ext#setup}:
*
* Ext.setup({
* requires: ['Ext.Anim'],
* onReady: function() {
* //do something
* }
* });
*
* or when using {@link Ext#application}:
*
* Ext.application({
* requires: ['Ext.Anim'],
* launch: function() {
* //do something
* }
* });
*
* @singleton
*/
Ext.Anim = Ext.extend(Object, {
isAnim: true,
/**
* @cfg {Boolean} disableAnimations
* True to disable animations.
*/
disableAnimations: false,
defaultConfig: {
/**
* @cfg {Object} from
* An object of CSS values which the animation begins with. If you define a CSS property here, you must also
* define it in the {@link #to} config.
*/
from: {},
/**
* @cfg {Object} to
* An object of CSS values which the animation ends with. If you define a CSS property here, you must also
* define it in the {@link #from} config.
*/
to: {},
/**
* @cfg {Number} duration
* Time in milliseconds for the animation to last.
*/
duration: 250,
/**
* @cfg {Number} delay Time to delay before starting the animation.
*/
delay: 0,
/**
* @cfg {String} easing
* Valid values are 'ease', 'linear', ease-in', 'ease-out', 'ease-in-out' or a cubic-bezier curve as defined by CSS.
*/
easing: 'ease-in-out',
/**
* @cfg {Boolean} autoClear
* True to remove all custom CSS defined in the {@link #to} config when the animation is over.
*/
autoClear: true,
/**
* @cfg {Boolean} out
* True if you want the animation to slide out of the screen.
*/
out: true,
/**
* @cfg {String} direction
* Valid values are 'left', 'right', 'up', 'down' and null.
*/
direction: null,
/**
* @cfg {Boolean} reverse
* True to reverse the animation direction. For example, if the animation direction was set to 'left', it would
* then use 'right'.
*/
reverse: false
},
/**
* @cfg {Function} before
* Code to execute before starting the animation.
*/
/**
* @cfg {Function} after
* Code to execute after the animation ends.
*/
/**
* @cfg {Object} scope
* Scope to run the {@link #before} function in.
*/
opposites: {
'left': 'right',
'right': 'left',
'up': 'down',
'down': 'up'
},
constructor: function(config) {
config = Ext.apply({}, config || {}, this.defaultConfig);
this.config = config;
Ext.Anim.superclass.constructor.call(this);
this.running = [];
},
initConfig: function(el, runConfig) {
var me = this,
config = Ext.apply({}, runConfig || {}, me.config);
config.el = el = Ext.get(el);
if (config.reverse && me.opposites[config.direction]) {
config.direction = me.opposites[config.direction];
}
if (me.config.before) {
me.config.before.call(config, el, config);
}
if (runConfig.before) {
runConfig.before.call(config.scope || config, el, config);
}
return config;
},
run: function(el, config) {
el = Ext.get(el);
config = config || {};
var me = this,
style = el.dom.style,
property,
after = config.after;
if (me.running[el.id]) {
me.onTransitionEnd(null, el, {
config: config,
after: after
});
}
config = this.initConfig(el, config);
if (this.disableAnimations) {
for (property in config.to) {
if (!config.to.hasOwnProperty(property)) {
continue;
}
style[property] = config.to[property];
}
this.onTransitionEnd(null, el, {
config: config,
after: after
});
return me;
}
el.un('transitionend', me.onTransitionEnd, me);
style.webkitTransitionDuration = '0ms';
for (property in config.from) {
if (!config.from.hasOwnProperty(property)) {
continue;
}
style[property] = config.from[property];
}
setTimeout(function() {
// If this element has been destroyed since the timeout started, do nothing
if (!el.dom) {
return;
}
// If this is a 3d animation we have to set the perspective on the parent
if (config.is3d === true) {
el.parent().setStyle({
// See https://sencha.jira.com/browse/TOUCH-1498
'-webkit-perspective': '1200',
'-webkit-transform-style': 'preserve-3d'
});
}
style.webkitTransitionDuration = config.duration + 'ms';
style.webkitTransitionProperty = 'all';
style.webkitTransitionTimingFunction = config.easing;
// Bind our listener that fires after the animation ends
el.on('transitionend', me.onTransitionEnd, me, {
single: true,
config: config,
after: after
});
for (property in config.to) {
if (!config.to.hasOwnProperty(property)) {
continue;
}
style[property] = config.to[property];
}
}, config.delay || 5);
me.running[el.id] = config;
return me;
},
onTransitionEnd: function(ev, el, o) {
el = Ext.get(el);
if (this.running[el.id] === undefined) {
return;
}
var style = el.dom.style,
config = o.config,
me = this,
property;
if (config.autoClear) {
for (property in config.to) {
if (!config.to.hasOwnProperty(property) || config[property] === false) {
continue;
}
style[property] = '';
}
}
style.webkitTransitionDuration = null;
style.webkitTransitionProperty = null;
style.webkitTransitionTimingFunction = null;
if (config.is3d) {
el.parent().setStyle({
'-webkit-perspective': '',
'-webkit-transform-style': ''
});
}
if (me.config.after) {
me.config.after.call(config, el, config);
}
if (o.after) {
o.after.call(config.scope || me, el, config);
}
delete me.running[el.id];
}
});
Ext.Anim.seed = 1000;
/**
* Used to run an animation on a specific element. Use the config argument to customize the animation
* @param {Ext.Element/HTMLElement} el The element to animate
* @param {String} anim The animation type, defined in {@link Ext.anims}
* @param {Object} config The config object for the animation
* @method run
*/
Ext.Anim.run = function(el, anim, config) {
if (el.isComponent) {
el = el.element;
}
config = config || {};
if (anim.isAnim) {
anim.run(el, config);
}
else {
if (Ext.isObject(anim)) {
if (config.before && anim.before) {
config.before = Ext.createInterceptor(config.before, anim.before, anim.scope);
}
if (config.after && anim.after) {
config.after = Ext.createInterceptor(config.after, anim.after, anim.scope);
}
config = Ext.apply({}, config, anim);
anim = anim.type;
}
if (!Ext.anims[anim]) {
throw anim + ' is not a valid animation type.';
}
else {
// add el check to make sure dom exists.
if (el && el.dom) {
Ext.anims[anim].run(el, config);
}
}
}
};
/**
* @class Ext.anims
* <p>Defines different types of animations. <strong>flip, cube, wipe</strong> animations do not work on Android.</p>
* <p>Please refer to {@link Ext.Anim} on how to use animations.</p>
* @singleton
*/
Ext.anims = {
/**
* Fade Animation
*/
fade: new Ext.Anim({
type: 'fade',
before: function(el) {
var fromOpacity = 1,
toOpacity = 1,
curZ = el.getStyle('z-index') == 'auto' ? 0 : el.getStyle('z-index'),
zIndex = curZ;
if (this.out) {
toOpacity = 0;
} else {
zIndex = Math.abs(curZ) + 1;
fromOpacity = 0;
}
this.from = {
'opacity': fromOpacity,
'z-index': zIndex
};
this.to = {
'opacity': toOpacity,
'z-index': zIndex
};
}
}),
/**
* Slide Animation
*/
slide: new Ext.Anim({
direction: 'left',
cover: false,
reveal: false,
opacity: false,
'z-index': false,
before: function(el) {
var currentZIndex = el.getStyle('z-index') == 'auto' ? 0 : el.getStyle('z-index'),
currentOpacity = el.getStyle('opacity'),
zIndex = currentZIndex + 1,
out = this.out,
direction = this.direction,
toX = 0,
toY = 0,
fromX = 0,
fromY = 0,
elH = el.getHeight(),
elW = el.getWidth();
if (direction == 'left' || direction == 'right') {
if (out) {
toX = -elW;
}
else {
fromX = elW;
}
}
else if (direction == 'up' || direction == 'down') {
if (out) {
toY = -elH;
}
else {
fromY = elH;
}
}
if (direction == 'right' || direction == 'down') {
toY *= -1;
toX *= -1;
fromY *= -1;
fromX *= -1;
}
if (this.cover && out) {
toX = 0;
toY = 0;
zIndex = currentZIndex;
}
else if (this.reveal && !out) {
fromX = 0;
fromY = 0;
zIndex = currentZIndex;
}
this.from = {
'-webkit-transform': 'translate3d(' + fromX + 'px, ' + fromY + 'px, 0)',
'z-index': zIndex,
'opacity': currentOpacity - 0.01
};
this.to = {
'-webkit-transform': 'translate3d(' + toX + 'px, ' + toY + 'px, 0)',
'z-index': zIndex,
'opacity': currentOpacity
};
}
}),
/**
* Pop Animation
*/
pop: new Ext.Anim({
scaleOnExit: true,
before: function(el) {
var fromScale = 1,
toScale = 1,
fromOpacity = 1,
toOpacity = 1,
curZ = el.getStyle('z-index') == 'auto' ? 0 : el.getStyle('z-index'),
fromZ = curZ,
toZ = curZ;
if (!this.out) {
fromScale = 0.01;
fromZ = curZ + 1;
toZ = curZ + 1;
fromOpacity = 0;
}
else {
if (this.scaleOnExit) {
toScale = 0.01;
toOpacity = 0;
} else {
toOpacity = 0.8;
}
}
this.from = {
'-webkit-transform': 'scale(' + fromScale + ')',
'-webkit-transform-origin': '50% 50%',
'opacity': fromOpacity,
'z-index': fromZ
};
this.to = {
'-webkit-transform': 'scale(' + toScale + ')',
'-webkit-transform-origin': '50% 50%',
'opacity': toOpacity,
'z-index': toZ
};
}
}),
/**
* Flip Animation
*/
flip: new Ext.Anim({
is3d: true,
direction: 'left',
before: function(el) {
var rotateProp = 'Y',
fromScale = 1,
toScale = 1,
fromRotate = 0,
toRotate = 0;
if (this.out) {
toRotate = -180;
toScale = 0.8;
}
else {
fromRotate = 180;
fromScale = 0.8;
}
if (this.direction == 'up' || this.direction == 'down') {
rotateProp = 'X';
}
if (this.direction == 'right' || this.direction == 'left') {
toRotate *= -1;
fromRotate *= -1;
}
this.from = {
'-webkit-transform': 'rotate' + rotateProp + '(' + fromRotate + 'deg) scale(' + fromScale + ')',
'-webkit-backface-visibility': 'hidden'
};
this.to = {
'-webkit-transform': 'rotate' + rotateProp + '(' + toRotate + 'deg) scale(' + toScale + ')',
'-webkit-backface-visibility': 'hidden'
};
}
}),
/**
* Cube Animation
*/
cube: new Ext.Anim({
is3d: true,
direction: 'left',
style: 'outer',
before: function(el) {
var origin = '0% 0%',
fromRotate = 0,
toRotate = 0,
rotateProp = 'Y',
fromZ = 0,
toZ = 0,
elW = el.getWidth(),
elH = el.getHeight(),
showTranslateZ = true,
fromTranslate = ' translateX(0)',
toTranslate = '';
if (this.direction == 'left' || this.direction == 'right') {
if (this.out) {
origin = '100% 100%';
toZ = elW;
toRotate = -90;
} else {
origin = '0% 0%';
fromZ = elW;
fromRotate = 90;
}
} else if (this.direction == 'up' || this.direction == 'down') {
rotateProp = 'X';
if (this.out) {
origin = '100% 100%';
toZ = elH;
toRotate = 90;
} else {
origin = '0% 0%';
fromZ = elH;
fromRotate = -90;
}
}
if (this.direction == 'down' || this.direction == 'right') {
fromRotate *= -1;
toRotate *= -1;
origin = (origin == '0% 0%') ? '100% 100%': '0% 0%';
}
if (this.style == 'inner') {
fromZ *= -1;
toZ *= -1;
fromRotate *= -1;
toRotate *= -1;
if (!this.out) {
toTranslate = ' translateX(0px)';
origin = '0% 50%';
} else {
toTranslate = fromTranslate;
origin = '100% 50%';
}
}
this.from = {
'-webkit-transform': 'rotate' + rotateProp + '(' + fromRotate + 'deg)' + (showTranslateZ ? ' translateZ(' + fromZ + 'px)': '') + fromTranslate,
'-webkit-transform-origin': origin
};
this.to = {
'-webkit-transform': 'rotate' + rotateProp + '(' + toRotate + 'deg) translateZ(' + toZ + 'px)' + toTranslate,
'-webkit-transform-origin': origin
};
},
duration: 250
}),
/**
* Wipe Animation.
* <p>Because of the amount of calculations involved, this animation is best used on small display
* changes or specifically for phone environments. Does not currently accept any parameters.</p>
*/
wipe: new Ext.Anim({
before: function(el) {
var curZ = el.getStyle('z-index'),
zIndex,
mask = '';
if (!this.out) {
zIndex = curZ + 1;
mask = '-webkit-gradient(linear, left bottom, right bottom, from(transparent), to(#000), color-stop(66%, #000), color-stop(33%, transparent))';
this.from = {
'-webkit-mask-image': mask,
'-webkit-mask-size': el.getWidth() * 3 + 'px ' + el.getHeight() + 'px',
'z-index': zIndex,
'-webkit-mask-position-x': 0
};
this.to = {
'-webkit-mask-image': mask,
'-webkit-mask-size': el.getWidth() * 3 + 'px ' + el.getHeight() + 'px',
'z-index': zIndex,
'-webkit-mask-position-x': -el.getWidth() * 2 + 'px'
};
}
},
duration: 500
})
};
/**
* This class compiles the XTemplate syntax into a function object. The function is used
* like so:
*
* function (out, values, parent, xindex, xcount) {
* // out is the output array to store results
* // values, parent, xindex and xcount have their historical meaning
* }
*
* @markdown
* @private
*/
Ext.define('Ext.XTemplateCompiler', {
extend: 'Ext.XTemplateParser',
// Chrome really likes "new Function" to realize the code block (as in it is
// 2x-3x faster to call it than using eval), but Firefox chokes on it badly.
// IE and Opera are also fine with the "new Function" technique.
useEval: Ext.isGecko,
useFormat: true,
propNameRe: /^[\w\d\$]*$/,
compile: function (tpl) {
var me = this,
code = me.generate(tpl);
// When using "new Function", we have to pass our "Ext" variable to it in order to
// support sandboxing. If we did not, the generated function would use the global
// "Ext", not the "Ext" from our sandbox (scope chain).
//
return me.useEval ? me.evalTpl(code) : (new Function('Ext', code))(Ext);
},
generate: function (tpl) {
var me = this;
me.body = [
'var c0=values, p0=parent, n0=xcount, i0=xindex;\n'
];
me.funcs = [
'var fm=Ext.util.Format;' // note: Ext here is properly sandboxed
];
me.switches = [];
me.parse(tpl);
me.funcs.push(
(me.useEval ? '$=' : 'return') + ' function (' + me.fnArgs + ') {',
me.body.join(''),
'}'
);
var code = me.funcs.join('\n');
return code;
},
//-----------------------------------
// XTemplateParser callouts
doText: function (text) {
text = text.replace(this.aposRe, "\\'");
text = text.replace(this.newLineRe, '\\n');
this.body.push('out.push(\'', text, '\')\n');
},
doExpr: function (expr) {
this.body.push('out.push(String(', expr, '))\n');
},
doTag: function (tag) {
this.doExpr(this.parseTag(tag));
},
doElse: function () {
this.body.push('} else {\n');
},
doEval: function (text) {
this.body.push(text, '\n');
},
doIf: function (action, actions) {
var me = this;
// If it's just a propName, use it directly in the if
if (me.propNameRe.test(action)) {
me.body.push('if (', me.parseTag(action), ') {\n');
}
// Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
else {
me.body.push('if (', me.addFn(action), me.callFn, ') {\n');
}
if (actions.exec) {
me.doExec(actions.exec);
}
},
doElseIf: function (action, actions) {
var me = this;
// If it's just a propName, use it directly in the else if
if (me.propNameRe.test(action)) {
me.body.push('} else if (', me.parseTag(action), ') {\n');
}
// Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
else {
me.body.push('} else if (', me.addFn(action), me.callFn, ') {\n');
}
if (actions.exec) {
me.doExec(actions.exec);
}
},
doSwitch: function (action) {
var me = this;
// If it's just a propName, use it directly in the switch
if (me.propNameRe.test(action)) {
me.body.push('switch (', me.parseTag(action), ') {\n');
}
// Otherwise, it must be an expression, and needs to be returned from an fn which uses with(values)
else {
me.body.push('switch (', me.addFn(action), me.callFn, ') {\n');
}
me.switches.push(0);
},
doCase: function (action) {
var me = this,
cases = Ext.isArray(action) ? action : [action],
n = me.switches.length - 1,
match, i;
if (me.switches[n]) {
me.body.push('break;\n');
} else {
me.switches[n]++;
}
for (i = 0, n = cases.length; i < n; ++i) {
match = me.intRe.exec(cases[i]);
cases[i] = match ? match[1] : ("'" + cases[i].replace(me.aposRe,"\\'") + "'");
}
me.body.push('case ', cases.join(': case '), ':\n');
},
doDefault: function () {
var me = this,
n = me.switches.length - 1;
if (me.switches[n]) {
me.body.push('break;\n');
} else {
me.switches[n]++;
}
me.body.push('default:\n');
},
doEnd: function (type, actions) {
var me = this,
L = me.level-1;
if (type == 'for') {
/*
To exit a for loop we must restore the outer loop's context. The code looks
like this (which goes with that produced by doFor:
for (...) { // the part generated by doFor
... // the body of the for loop
// ... any tpl for exec statement goes here...
}
parent = p1;
values = r2;
xcount = n1;
xindex = i1
*/
if (actions.exec) {
me.doExec(actions.exec);
}
me.body.push('}\n');
me.body.push('parent=p',L,';values=r',L+1,';xcount=n',L,';xindex=i',L,'\n');
} else if (type == 'if' || type == 'switch') {
me.body.push('}\n');
}
},
doFor: function (action, actions) {
var me = this,
s = me.addFn(action),
L = me.level,
up = L-1;
/*
We are trying to produce a block of code that looks like below. We use the nesting
level to uniquely name the control variables.
var c2 = f5.call(this, out, values, parent, xindex, xcount),
// c2 is the context object for the for loop
a2 = Ext.isArray(c2),
// a2 is the isArray result for the context
p2 = (parent=c1),
// p2 is the parent context (of the outer for loop)
r2 = values
// r2 is the values object to
// i2 is the loop index and n2 is the number (xcount) of this for loop
for (var i2 = 0, n2 = a2 ? c2.length : (c2 ? 1 : 0), xcount = n2; i2 < n2; ++i2) {
values=a2?c2[i2]:c2 // adjust special vars to inner scope
xindex=i2+1 // xindex is 1-based
The body of the loop is whatever comes between the tpl and /tpl statements (which
is handled by doEnd).
*/
me.body.push('var c',L,'=',s,me.callFn,', a',L,'=Ext.isArray(c',L,'),p',L,'=(parent=c',up,'),r',L,'=values\n',
'for (var i',L,'=0,n',L,'=a',L,'?c',L,'.length:(c',L,'?1:0), xcount=n',L,';i',L,'<n'+L+';++i',L,'){\n',
'values=a',L,'?c',L,'[i',L,']:c',L,'\n',
'xindex=i',L,'+1\n');
},
doExec: function (action, actions) {
var me = this,
name = 'f' + me.funcs.length;
me.funcs.push('function ' + name + '(' + me.fnArgs + ') {',
' try { with(values) {',
' ' + action,
' }} catch(e) {}',
'}');
me.body.push(name + me.callFn + '\n');
},
//-----------------------------------
// Internal
addFn: function (body) {
var me = this,
name = 'f' + me.funcs.length;
if (body === '.') {
me.funcs.push('function ' + name + '(' + me.fnArgs + ') {',
' return values',
'}');
} else if (body === '..') {
me.funcs.push('function ' + name + '(' + me.fnArgs + ') {',
' return parent',
'}');
} else {
me.funcs.push('function ' + name + '(' + me.fnArgs + ') {',
' try { with(values) {',
' return(' + body + ')',
' }} catch(e) {}',
'}');
}
return name;
},
parseTag: function (tag) {
var m = this.tagRe.exec(tag),
name = m[1],
format = m[2],
args = m[3],
math = m[4],
v;
// name = "." - Just use the values object.
if (name == '.') {
// filter to not include arrays/objects/nulls
v = 'Ext.Array.indexOf(["string", "number", "boolean"], typeof values) > -1 || Ext.isDate(values) ? values : ""';
}
// name = "#" - Use the xindex
else if (name == '#') {
v = 'xindex';
}
else if (name.substr(0, 7) == "parent.") {
v = name;
}
// name has a . in it - Use object literal notation, starting from values
else if ((name.indexOf('.') !== -1) && (name.indexOf('-') === -1)) {
v = "values." + name;
}
// name is a property of values
else {
v = "values['" + name + "']";
}
if (math) {
v = '(' + v + math + ')';
}
if (format && this.useFormat) {
args = args ? ',' + args : "";
if (format.substr(0, 5) != "this.") {
format = "fm." + format + '(';
} else {
format += '(';
}
} else {
args = '';
format = "(" + v + " === undefined ? '' : ";
}
return format + v + args + ')';
},
// @private
evalTpl: function ($) {
// We have to use eval to realize the code block and capture the inner func we also
// don't want a deep scope chain. We only do this in Firefox and it is also unhappy
// with eval containing a return statement, so instead we assign to "$" and return
// that. Because we use "eval", we are automatically sandboxed properly.
eval($);
return $;
},
newLineRe: /\r\n|\r|\n/g,
aposRe: /[']/g,
intRe: /^\s*(\d+)\s*$/,
tagRe: /([\w-\.\#]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?(\s?[\+\-\*\/]\s?[\d\.\+\-\*\/\(\)]+)?/
}, function () {
var proto = this.prototype;
proto.fnArgs = 'out,values,parent,xindex,xcount';
proto.callFn = '.call(this,' + proto.fnArgs + ')';
});
/**
* @author Ed Spencer
* @aside guide models
*
* Fields are used to define what a Model is. They aren't instantiated directly - instead, when we create a class that
* extends {@link Ext.data.Model}, it will automatically create a Field instance for each field configured in a {@link
* Ext.data.Model Model}. For example, we might set up a model like this:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* 'name', 'email',
* {name: 'age', type: 'int'},
* {name: 'gender', type: 'string', defaultValue: 'Unknown'}
* ]
* }
* });
*
* Four fields will have been created for the User Model - name, email, age and gender. Note that we specified a couple
* of different formats here; if we only pass in the string name of the field (as with name and email), the field is set
* up with the 'auto' type. It's as if we'd done this instead:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* {name: 'name', type: 'auto'},
* {name: 'email', type: 'auto'},
* {name: 'age', type: 'int'},
* {name: 'gender', type: 'string', defaultValue: 'Unknown'}
* ]
* }
* });
*
* # Types and conversion
*
* The {@link #type} is important - it's used to automatically convert data passed to the field into the correct format.
* In our example above, the name and email fields used the 'auto' type and will just accept anything that is passed
* into them. The 'age' field had an 'int' type however, so if we passed 25.4 this would be rounded to 25.
*
* Sometimes a simple type isn't enough, or we want to perform some processing when we load a Field's data. We can do
* this using a {@link #convert} function. Here, we're going to create a new field based on another:
*
* Ext.define('User', {
* extend: 'Ext.data.Model',
* config: {
* fields: [
* 'name', 'email',
* {name: 'age', type: 'int'},
* {name: 'gender', type: 'string', defaultValue: 'Unknown'},
*
* {
* name: 'firstName',
* convert: function(value, record) {
* var fullName = record.get('name'),
* splits = fullName.split(" "),
* firstName = splits[0];
*
* return firstName;
* }
* }
* ]
* }
* });
*
* Now when we create a new User, the firstName is populated automatically based on the name:
*
* var ed = Ext.create('User', {name: 'Ed Spencer'});
*
* console.log(ed.get('firstName')); //logs 'Ed', based on our convert function
*
* In fact, if we log out all of the data inside ed, we'll see this:
*
* console.log(ed.data);
*
* //outputs this:
* {
* age: 0,
* email: "",
* firstName: "Ed",
* gender: "Unknown",
* name: "Ed Spencer"
* }
*
* The age field has been given a default of zero because we made it an int type. As an auto field, email has defaulted
* to an empty string. When we registered the User model we set gender's {@link #defaultValue} to 'Unknown' so we see
* that now. Let's correct that and satisfy ourselves that the types work as we expect:
*
* ed.set('gender', 'Male');
* ed.get('gender'); //returns 'Male'
*
* ed.set('age', 25.4);
* ed.get('age'); //returns 25 - we wanted an int, not a float, so no decimal places allowed
*/
Ext.define('Ext.data.Field', {
requires: ['Ext.data.Types', 'Ext.data.SortTypes'],
alias: 'data.field',
isField: true,
config: {
/**
* @cfg {String} name
*
* The name by which the field is referenced within the Model. This is referenced by, for example, the `dataIndex`
* property in column definition objects passed to Ext.grid.property.HeaderContainer.
*
* Note: In the simplest case, if no properties other than `name` are required, a field definition may consist of
* just a String for the field name.
*/
name: null,
/**
* @cfg {String/Object} type
*
* The data type for automatic conversion from received data to the *stored* value if
* `{@link Ext.data.Field#convert convert}` has not been specified. This may be specified as a string value.
* Possible values are
*
* - auto (Default, implies no conversion)
* - string
* - int
* - float
* - boolean
* - date
*
* This may also be specified by referencing a member of the {@link Ext.data.Types} class.
*
* Developers may create their own application-specific data types by defining new members of the {@link
* Ext.data.Types} class.
*/
type: 'auto',
/**
* @cfg {Function} convert
*
* A function which converts the value provided by the Reader into an object that will be stored in the Model.
* It is passed the following parameters:
*
* - **v** : Mixed
*
* The data value as read by the Reader, if undefined will use the configured `{@link Ext.data.Field#defaultValue
* defaultValue}`.
*
* - **rec** : Ext.data.Model
*
* The data object containing the Model as read so far by the Reader. Note that the Model may not be fully populated
* at this point as the fields are read in the order that they are defined in your
* {@link Ext.data.Model#cfg-fields fields} array.
*
* Example of convert functions:
*
* function fullName(v, record) {
* return record.name.last + ', ' + record.name.first;
* }
*
* function location(v, record) {
* return !record.city ? '' : (record.city + ', ' + record.state);
* }
*
* Ext.define('Dude', {
* extend: 'Ext.data.Model',
* fields: [
* {name: 'fullname', convert: fullName},
* {name: 'firstname', mapping: 'name.first'},
* {name: 'lastname', mapping: 'name.last'},
* {name: 'city', defaultValue: 'homeless'},
* 'state',
* {name: 'location', convert: location}
* ]
* });
*
* // create the data store
* var store = Ext.create('Ext.data.Store', {
* reader: {
* type: 'json',
* model: 'Dude',
* idProperty: 'key',
* rootProperty: 'daRoot',
* totalProperty: 'total'
* }
* });
*
* var myData = [
* { key: 1,
* name: { first: 'Fat', last: 'Albert' }
* // notice no city, state provided in data2 object
* },
* { key: 2,
* name: { first: 'Barney', last: 'Rubble' },
* city: 'Bedrock', state: 'Stoneridge'
* },
* { key: 3,
* name: { first: 'Cliff', last: 'Claven' },
* city: 'Boston', state: 'MA'
* }
* ];
*/
convert: undefined,
/**
* @cfg {String} dateFormat
*
* Used when converting received data into a Date when the {@link #type} is specified as `"date"`.
*
* A format string for the {@link Ext.Date#parse Ext.Date.parse} function, or "timestamp" if the value provided by
* the Reader is a UNIX timestamp, or "time" if the value provided by the Reader is a javascript millisecond
* timestamp. See {@link Ext.Date}.
*/
dateFormat: null,
/**
* @cfg {Boolean} allowNull
*
* Use when converting received data into a boolean, string or number type (either int or float). If the value cannot be
* parsed, null will be used if allowNull is true, otherwise the value will be 0. Defaults to true.
*/
allowNull: true,
/**
* @cfg {Object} defaultValue
*
* The default value used **when a Model is being created by a {@link Ext.data.reader.Reader Reader}**
* when the item referenced by the `{@link Ext.data.Field#mapping mapping}` does not exist in the data object
* (i.e. undefined). Defaults to "".
*/
defaultValue: undefined,
/**
* @cfg {String/Number} mapping
*
* (Optional) A path expression for use by the {@link Ext.data.reader.Reader} implementation that is creating the
* {@link Ext.data.Model Model} to extract the Field value from the data object. If the path expression is the same
* as the field name, the mapping may be omitted.
*
* The form of the mapping expression depends on the Reader being used.
*
* - {@link Ext.data.reader.Json}
*
* The mapping is a string containing the javascript expression to reference the data from an element of the data2
* item's {@link Ext.data.reader.Json#rootProperty rootProperty} Array. Defaults to the field name.
*
* - {@link Ext.data.reader.Xml}
*
* The mapping is an {@link Ext.DomQuery} path to the data item relative to the DOM element that represents the
* {@link Ext.data.reader.Xml#record record}. Defaults to the field name.
*
* - {@link Ext.data.reader.Array}
*
* The mapping is a number indicating the Array index of the field's value. Defaults to the field specification's
* Array position.
*
* If a more complex value extraction strategy is required, then configure the Field with a {@link #convert}
* function. This is passed the whole row object, and may interrogate it in whatever way is necessary in order to
* return the desired data.
*/
mapping: null,
/**
* @cfg {Function} sortType
*
* A function which converts a Field's value to a comparable value in order to ensure correct sort ordering.
* Predefined functions are provided in {@link Ext.data.SortTypes}. A custom sort example:
*
* // current sort after sort we want
* // +-+------+ +-+------+
* // |1|First | |1|First |
* // |2|Last | |3|Second|
* // |3|Second| |2|Last |
* // +-+------+ +-+------+
*
* sortType: function(value) {
* switch (value.toLowerCase()) // native toLowerCase():
* {
* case 'first': return 1;
* case 'second': return 2;
* default: return 3;
* }
* }
*/
sortType : undefined,
/**
* @cfg {String} sortDir
*
* Initial direction to sort (`"ASC"` or `"DESC"`). Defaults to `"ASC"`.
*/
sortDir : "ASC",
/**
* @cfg {Boolean} allowBlank
* @private
*
* Used for validating a {@link Ext.data.Model model}. Defaults to true. An empty value here will cause
* {@link Ext.data.Model}.{@link Ext.data.Model#isValid isValid} to evaluate to false.
*/
allowBlank : true,
/**
* @cfg {Boolean} persist
*
* False to exclude this field from being synchronized with the server or localstorage.
* This option is useful when model fields are used to keep state on the client but do
* not need to be persisted to the server. Defaults to true.
*/
persist: true,
// Used in LocalStorage stuff
encode: null,
decode: null
},
constructor : function(config) {
// This adds support for just passing a string used as the field name
if (Ext.isString(config)) {
config = {name: config};
}
this.initConfig(config);
},
applyType: function(type) {
var types = Ext.data.Types,
autoType = types.AUTO;
if (type) {
if (Ext.isString(type)) {
return types[type.toUpperCase()] || autoType;
} else {
// At this point we expect an actual type
return type;
}
}
return autoType;
},
updateType: function(newType, oldType) {
var convert = this.getConvert();
if (oldType && convert === oldType.convert) {
this.setConvert(newType.convert);
}
},
applySortType: function(sortType) {
var sortTypes = Ext.data.SortTypes,
type = this.getType(),
defaultSortType = type.sortType;
if (sortType) {
if (Ext.isString(sortType)) {
return sortTypes[sortType] || defaultSortType;
} else {
// At this point we expect a function
return sortType;
}
}
return defaultSortType;
},
applyConvert: function(convert) {
var defaultConvert = this.getType().convert;
if (convert && convert !== defaultConvert) {
this._hasCustomConvert = true;
return convert;
} else {
this._hasCustomConvert = false;
return defaultConvert;
}
},
hasCustomConvert: function() {
return this._hasCustomConvert;
}
});
/**
* @author Tommy Maintz
*
* This class is a sequential id generator. A simple use of this class would be like so:
*
* Ext.define('MyApp.data.MyModel', {
* extend: 'Ext.data.Model',
* config: {
* identifier: 'sequential'
* }
* });
* // assign id's of 1, 2, 3, etc.
*
* An example of a configured generator would be:
*
* Ext.define('MyApp.data.MyModel', {
* extend: 'Ext.data.Model',
* config: {
* identifier: {
* type: 'sequential',
* prefix: 'ID_',
* seed: 1000
* }
* }
* });
* // assign id's of ID_1000, ID_1001, ID_1002, etc.
*
*/
Ext.define('Ext.data.identifier.Sequential', {
extend: 'Ext.data.identifier.Simple',
alias: 'data.identifier.sequential',
config: {
/**
* @cfg {String} prefix
* The string to place in front of the sequential number for each generated id. The
* default is blank.
*/
prefix: '',
/**
* @cfg {Number} seed
* The number at which to start generating sequential id's. The default is 1.
*/
seed: 1
},
constructor: function() {
var me = this;
me.callParent(arguments);
me.parts = [me.getPrefix(), ''];
},
generate: function(record) {
var me = this,
parts = me.parts,
seed = me.getSeed() + 1;
me.setSeed(seed);
parts[1] = seed;
return parts.join('');
}
});
/**
* This class is used to write {@link Ext.data.Model} data to the server in a JSON format.
* The {@link #allowSingle} configuration can be set to false to force the records to always be
* encoded in an array, even if there is only a single record being sent.
*/
Ext.define('Ext.data.writer.Json', {
extend: 'Ext.data.writer.Writer',
alternateClassName: 'Ext.data.JsonWriter',
alias: 'writer.json',
config: {
/**
* @cfg {String} root
* The key under which the records in this Writer will be placed. If you specify {@link #encode} to be true,
* we default this to 'records'.
*
* Example generated request, using root: 'records':
*
* {'records': [{name: 'my record'}, {name: 'another record'}]}
*
*/
root: undefined,
/**
* @cfg {Boolean} encode
* True to use Ext.encode() on the data before sending. The encode option should only be set to true when a
* {@link #root} is defined, because the values will be sent as part of the request parameters as opposed to
* a raw post. The root will be the name of the parameter sent to the server.
*/
encode: false,
/**
* @cfg {Boolean} allowSingle
* False to ensure that records are always wrapped in an array, even if there is only one record being sent.
* When there is more than one record, they will always be encoded into an array.
*
* Example:
*
* // with allowSingle: true
* "root": {
* "first": "Mark",
* "last": "Corrigan"
* }
*
* // with allowSingle: false
* "root": [{
* "first": "Mark",
* "last": "Corrigan"
* }]
*/
allowSingle: true,
encodeRequest: false
},
applyRoot: function(root) {
if (!root && (this.getEncode() || this.getEncodeRequest())) {
root = 'data';
}
return root;
},
//inherit docs
writeRecords: function(request, data) {
var root = this.getRoot(),
params = request.getParams(),
allowSingle = this.getAllowSingle(),
jsonData;
if (this.getAllowSingle() && data && data.length == 1) {
// convert to single object format
data = data[0];
}
if (this.getEncodeRequest()) {
jsonData = request.getJsonData() || {};
if (data && (data.length || (allowSingle && Ext.isObject(data)))) {
jsonData[root] = data;
}
request.setJsonData(Ext.apply(jsonData, params || {}));
request.setParams(null);
request.setMethod('POST');
return request;
}
if (!data || !(data.length || (allowSingle && Ext.isObject(data)))) {
return request;
}
if (this.getEncode()) {
if (root) {
// sending as a param, need to encode
params[root] = Ext.encode(data);
} else {
Ext.Logger.error('Must specify a root when using encode');
}
} else {
// send as jsonData
jsonData = request.getJsonData() || {};
if (root) {
jsonData[root] = data;
} else {
jsonData = data;
}
request.setJsonData(jsonData);
}
return request;
}
});
/*
* @allowSingle: true
* @encodeRequest: false
* Url: update.json?param1=test
* {'field1': 'test': 'field2': 'test'}
*
* @allowSingle: false
* @encodeRequest: false
* Url: update.json?param1=test
* [{'field1': 'test', 'field2': 'test'}]
*
* @allowSingle: true
* @root: 'data'
* @encodeRequest: true
* Url: update.json
* {
* 'param1': 'test',
* 'data': {'field1': 'test', 'field2': 'test'}
* }
*
* @allowSingle: false
* @root: 'data'
* @encodeRequest: true
* Url: update.json
* {
* 'param1': 'test',
* 'data': [{'field1': 'test', 'field2': 'test'}]
* }
*
* @allowSingle: true
* @root: data
* @encodeRequest: false
* Url: update.json
* param1=test&data={'field1': 'test', 'field2': 'test'}
*
* @allowSingle: false
* @root: data
* @encodeRequest: false
* @ncode: true
* Url: update.json
* param1=test&data=[{'field1': 'test', 'field2': 'test'}]
*
* @allowSingle: true
* @root: data
* @encodeRequest: false
* Url: update.json?param1=test&data={'field1': 'test', 'field2': 'test'}
*
* @allowSingle: false
* @root: data
* @encodeRequest: false
* Url: update.json?param1=test&data=[{'field1': 'test', 'field2': 'test'}]
*/
/**
* @private
*/
Ext.define('Ext.event.Dispatcher', {
requires: [
'Ext.event.ListenerStack',
'Ext.event.Controller'
],
statics: {
getInstance: function() {
if (!this.instance) {
this.instance = new this();
}
return this.instance;
},
setInstance: function(instance) {
this.instance = instance;
return this;
}
},
config: {
publishers: {}
},
wildcard: '*',
constructor: function(config) {
this.listenerStacks = {};
this.activePublishers = {};
this.publishersCache = {};
this.noActivePublishers = [];
this.controller = null;
this.initConfig(config);
return this;
},
getListenerStack: function(targetType, target, eventName, createIfNotExist) {
var listenerStacks = this.listenerStacks,
map = listenerStacks[targetType],
listenerStack;
createIfNotExist = Boolean(createIfNotExist);
if (!map) {
if (createIfNotExist) {
listenerStacks[targetType] = map = {};
}
else {
return null;
}
}
map = map[target];
if (!map) {
if (createIfNotExist) {
listenerStacks[targetType][target] = map = {};
}
else {
return null;
}
}
listenerStack = map[eventName];
if (!listenerStack) {
if (createIfNotExist) {
map[eventName] = listenerStack = new Ext.event.ListenerStack();
}
else {
return null;
}
}
return listenerStack;
},
getController: function(targetType, target, eventName, connectedController) {
var controller = this.controller,
info = {
targetType: targetType,
target: target,
eventName: eventName
};
if (!controller) {
this.controller = controller = new Ext.event.Controller();
}
if (controller.isFiring) {
controller = new Ext.event.Controller();
}
controller.setInfo(info);
if (connectedController && controller !== connectedController) {
controller.connect(connectedController);
}
return controller;
},
applyPublishers: function(publishers) {
var i, publisher;
this.publishersCache = {};
for (i in publishers) {
if (publishers.hasOwnProperty(i)) {
publisher = publishers[i];
this.registerPublisher(publisher);
}
}
return publishers;
},
registerPublisher: function(publisher) {
var activePublishers = this.activePublishers,
targetType = publisher.getTargetType(),
publishers = activePublishers[targetType];
if (!publishers) {
activePublishers[targetType] = publishers = [];
}
publishers.push(publisher);
publisher.setDispatcher(this);
return this;
},
getCachedActivePublishers: function(targetType, eventName) {
var cache = this.publishersCache,
publishers;
if ((publishers = cache[targetType]) && (publishers = publishers[eventName])) {
return publishers;
}
return null;
},
cacheActivePublishers: function(targetType, eventName, publishers) {
var cache = this.publishersCache;
if (!cache[targetType]) {
cache[targetType] = {};
}
cache[targetType][eventName] = publishers;
return publishers;
},
getActivePublishers: function(targetType, eventName) {
var publishers, activePublishers,
i, ln, publisher;
if ((publishers = this.getCachedActivePublishers(targetType, eventName))) {
return publishers;
}
activePublishers = this.activePublishers[targetType];
if (activePublishers) {
publishers = [];
for (i = 0,ln = activePublishers.length; i < ln; i++) {
publisher = activePublishers[i];
if (publisher.handles(eventName)) {
publishers.push(publisher);
}
}
}
else {
publishers = this.noActivePublishers;
}
return this.cacheActivePublishers(targetType, eventName, publishers);
},
hasListener: function(targetType, target, eventName) {
var listenerStack = this.getListenerStack(targetType, target, eventName);
if (listenerStack) {
return listenerStack.count() > 0;
}
return false;
},
addListener: function(targetType, target, eventName) {
var publishers = this.getActivePublishers(targetType, eventName),
ln = publishers.length,
i;
if (ln > 0) {
for (i = 0; i < ln; i++) {
publishers[i].subscribe(target, eventName);
}
}
return this.doAddListener.apply(this, arguments);
},
doAddListener: function(targetType, target, eventName, fn, scope, options, order) {
var listenerStack = this.getListenerStack(targetType, target, eventName, true);
return listenerStack.add(fn, scope, options, order);
},
removeListener: function(targetType, target, eventName) {
var publishers = this.getActivePublishers(targetType, eventName),
ln = publishers.length,
i;
if (ln > 0) {
for (i = 0; i < ln; i++) {
publishers[i].unsubscribe(target, eventName);
}
}
return this.doRemoveListener.apply(this, arguments);
},
doRemoveListener: function(targetType, target, eventName, fn, scope, order) {
var listenerStack = this.getListenerStack(targetType, target, eventName);
if (listenerStack === null) {
return false;
}
return listenerStack.remove(fn, scope, order);
},
clearListeners: function(targetType, target, eventName) {
var listenerStacks = this.listenerStacks,
ln = arguments.length,
stacks, publishers, i, publisherGroup;
if (ln === 3) {
if (listenerStacks[targetType] && listenerStacks[targetType][target]) {
this.removeListener(targetType, target, eventName);
delete listenerStacks[targetType][target][eventName];
}
}
else if (ln === 2) {
if (listenerStacks[targetType]) {
stacks = listenerStacks[targetType][target];
if (stacks) {
for (eventName in stacks) {
if (stacks.hasOwnProperty(eventName)) {
publishers = this.getActivePublishers(targetType, eventName);
for (i = 0,ln = publishers.length; i < ln; i++) {
publishers[i].unsubscribe(target, eventName, true);
}
}
}
delete listenerStacks[targetType][target];
}
}
}
else if (ln === 1) {
publishers = this.activePublishers[targetType];
for (i = 0,ln = publishers.length; i < ln; i++) {
publishers[i].unsubscribeAll();
}
delete listenerStacks[targetType];
}
else {
publishers = this.activePublishers;
for (targetType in publishers) {
if (publishers.hasOwnProperty(targetType)) {
publisherGroup = publishers[targetType];
for (i = 0,ln = publisherGroup.length; i < ln; i++) {
publisherGroup[i].unsubscribeAll();
}
}
}
delete this.listenerStacks;
this.listenerStacks = {};
}
return this;
},
dispatchEvent: function(targetType, target, eventName) {
var publishers = this.getActivePublishers(targetType, eventName),
ln = publishers.length,
i;
if (ln > 0) {
for (i = 0; i < ln; i++) {
publishers[i].notify(target, eventName);
}
}
return this.doDispatchEvent.apply(this, arguments);
},
doDispatchEvent: function(targetType, target, eventName, args, action, connectedController) {
var listenerStack = this.getListenerStack(targetType, target, eventName),
wildcardStacks = this.getWildcardListenerStacks(targetType, target, eventName),
controller;
if ((listenerStack === null || listenerStack.length == 0)) {
if (wildcardStacks.length == 0 && !action) {
return;
}
}
else {
wildcardStacks.push(listenerStack);
}
controller = this.getController(targetType, target, eventName, connectedController);
controller.setListenerStacks(wildcardStacks);
controller.fire(args, action);
return !controller.isInterrupted();
},
getWildcardListenerStacks: function(targetType, target, eventName) {
var stacks = [],
wildcard = this.wildcard,
isEventNameNotWildcard = eventName !== wildcard,
isTargetNotWildcard = target !== wildcard,
stack;
if (isEventNameNotWildcard && (stack = this.getListenerStack(targetType, target, wildcard))) {
stacks.push(stack);
}
if (isTargetNotWildcard && (stack = this.getListenerStack(targetType, wildcard, eventName))) {
stacks.push(stack);
}
return stacks;
}
});
/**
* @private
* @extends Object
* DOM event. This class really extends Ext.event.Event, but for documentation
* purposes it's members are listed inside Ext.event.Event.
*/
Ext.define('Ext.event.Dom', {
extend: 'Ext.event.Event',
constructor: function(event) {
var target = event.target,
touches;
if (target && target.nodeType !== 1) {
target = target.parentNode;
}
touches = event.changedTouches;
if (touches) {
touches = touches[0];
this.pageX = touches.pageX;
this.pageY = touches.pageY;
}
else {
this.pageX = event.pageX;
this.pageY = event.pageY;
}
this.browserEvent = this.event = event;
this.target = this.delegatedTarget = target;
this.type = event.type;
this.timeStamp = this.time = event.timeStamp;
if (typeof this.time != 'number') {
this.time = new Date(this.time).getTime();
}
return this;
},
/**
* @property {Number} distance
* The distance of the event.
*
* **This is only available when the event type is `swipe` and `pinch`**
*/
/**
* @property {HTMLElement} target
* The target HTMLElement for this event. For example; if you are listening to a tap event and you tap on a `<div>` element,
* this will return that `<div>` element.
*/
/**
* @property {Number} pageX The browsers x coordinate of the event.
*/
/**
* @property {Number} pageY The browsers y coordinate of the event.
*/
stopEvent: function() {
this.preventDefault();
return this.callParent();
},
/**
* Prevents the browsers default handling of the event.
*/
preventDefault: function() {
this.browserEvent.preventDefault();
},
/**
* Gets the x coordinate of the event.
* @deprecated 2.0 Please use {@link #pageX} property directly.
*/
getPageX: function() {
return this.browserEvent.pageX;
},
/**
* Gets the y coordinate of the event.
* @deprecated 2.0 Please use {@link #pageX} property directly.
*/
getPageY: function() {
return this.browserEvent.pageY;
},
/**
* Gets the X and Y coordinates of the event.
* @deprecated 2.0 Please use the {@link #pageX} and {@link #pageY} properties directly.
*/
getXY: function() {
if (!this.xy) {
this.xy = [this.getPageX(), this.getPageY()];
}
return this.xy;
},
/**
* Gets the target for the event. Unlike {@link #target}, this returns the main element for your event. So if you are
* listening to a tap event on Ext.Viewport.element, and you tap on an inner element of Ext.Viewport.element, this will
* return Ext.Viewport.element.
*
* If you want the element you tapped on, then use {@link #target}.
*
* @param {String} selector (optional) A simple selector to filter the target or look for an ancestor of the target
* @param {Number/Mixed} 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 (arguments.length === 0) {
return this.delegatedTarget;
}
return selector ? Ext.fly(this.target).findParent(selector, maxDepth, returnEl) : (returnEl ? Ext.get(this.target) : this.target);
},
/**
* Returns the time of the event.
* @return {Date}
*/
getTime: function() {
return this.time;
},
setDelegatedTarget: function(target) {
this.delegatedTarget = target;
},
makeUnpreventable: function() {
this.browserEvent.preventDefault = Ext.emptyFn;
}
});
/**
* @private
* Touch event.
*/
Ext.define('Ext.event.Touch', {
extend: 'Ext.event.Dom',
requires: [
'Ext.util.Point'
],
constructor: function(event, info) {
if (info) {
this.set(info);
}
this.touchesMap = {};
this.changedTouches = this.cloneTouches(event.changedTouches);
this.touches = this.cloneTouches(event.touches);
this.targetTouches = this.cloneTouches(event.targetTouches);
return this.callParent([event]);
},
clone: function() {
return new this.self(this);
},
setTargets: function(targetsMap) {
this.doSetTargets(this.changedTouches, targetsMap);
this.doSetTargets(this.touches, targetsMap);
this.doSetTargets(this.targetTouches, targetsMap);
},
doSetTargets: function(touches, targetsMap) {
var i, ln, touch, identifier, targets;
for (i = 0,ln = touches.length; i < ln; i++) {
touch = touches[i];
identifier = touch.identifier;
targets = targetsMap[identifier];
if (targets) {
touch.targets = targets;
}
}
},
cloneTouches: function(touches) {
var map = this.touchesMap,
clone = [],
lastIdentifier = null,
i, ln, touch, identifier;
for (i = 0,ln = touches.length; i < ln; i++) {
touch = touches[i];
identifier = touch.identifier;
// A quick fix for a bug found in Bada 1.0 where all touches have
// idenfitier of 0
if (lastIdentifier !== null && identifier === lastIdentifier) {
identifier++;
}
lastIdentifier = identifier;
if (!map[identifier]) {
map[identifier] = {
pageX: touch.pageX,
pageY: touch.pageY,
identifier: identifier,
target: touch.target,
timeStamp: touch.timeStamp,
point: Ext.util.Point.fromTouch(touch),
targets: touch.targets
};
}
clone[i] = map[identifier];
}
return clone;
}
});
/**
* @private
*/
Ext.define('Ext.event.publisher.ComponentPaint', {
extend: 'Ext.event.publisher.Publisher',
targetType: 'component',
handledEvents: ['painted', 'erased'],
eventNames: {
painted: 'painted',
erased: 'erased'
},
constructor: function() {
this.callParent(arguments);
this.hiddenQueue = {};
this.renderedQueue = {};
},
getSubscribers: function(eventName, createIfNotExist) {
var subscribers = this.subscribers;
if (!subscribers.hasOwnProperty(eventName)) {
if (!createIfNotExist) {
return null;
}
subscribers[eventName] = {
$length: 0
};
}
return subscribers[eventName];
},
setDispatcher: function(dispatcher) {
var targetType = this.targetType;
dispatcher.doAddListener(targetType, '*', 'renderedchange', 'onBeforeComponentRenderedChange', this, null, 'before');
dispatcher.doAddListener(targetType, '*', 'hiddenchange', 'onBeforeComponentHiddenChange', this, null, 'before');
dispatcher.doAddListener(targetType, '*', 'renderedchange', 'onComponentRenderedChange', this, null, 'after');
dispatcher.doAddListener(targetType, '*', 'hiddenchange', 'onComponentHiddenChange', this, null, 'after');
return this.callParent(arguments);
},
subscribe: function(target, eventName) {
var match = target.match(this.idSelectorRegex),
subscribers,
id;
if (!match) {
return false;
}
id = match[1];
subscribers = this.getSubscribers(eventName, true);
if (subscribers.hasOwnProperty(id)) {
subscribers[id]++;
return true;
}
subscribers[id] = 1;
subscribers.$length++;
return true;
},
unsubscribe: function(target, eventName, all) {
var match = target.match(this.idSelectorRegex),
subscribers,
id;
if (!match || !(subscribers = this.getSubscribers(eventName))) {
return false;
}
id = match[1];
if (!subscribers.hasOwnProperty(id) || (!all && --subscribers[id] > 0)) {
return true;
}
delete subscribers[id];
if (--subscribers.$length === 0) {
delete this.subscribers[eventName];
}
return true;
},
onBeforeComponentRenderedChange: function(container, component, rendered) {
var eventNames = this.eventNames,
eventName = rendered ? eventNames.painted : eventNames.erased,
subscribers = this.getSubscribers(eventName),
queue;
if (subscribers && subscribers.$length > 0) {
this.renderedQueue[component.getId()] = queue = [];
this.publish(subscribers, component, eventName, queue);
}
},
onBeforeComponentHiddenChange: function(component, hidden) {
var eventNames = this.eventNames,
eventName = hidden ? eventNames.erased : eventNames.painted,
subscribers = this.getSubscribers(eventName),
queue;
if (subscribers && subscribers.$length > 0) {
this.hiddenQueue[component.getId()] = queue = [];
this.publish(subscribers, component, eventName, queue);
}
},
onComponentRenderedChange: function(container, component) {
var renderedQueue = this.renderedQueue,
id = component.getId(),
queue;
if (!renderedQueue.hasOwnProperty(id)) {
return;
}
queue = renderedQueue[id];
delete renderedQueue[id];
if (queue.length > 0) {
this.dispatchQueue(queue);
}
},
onComponentHiddenChange: function(component) {
var hiddenQueue = this.hiddenQueue,
id = component.getId(),
queue;
if (!hiddenQueue.hasOwnProperty(id)) {
return;
}
queue = hiddenQueue[id];
delete hiddenQueue[id];
if (queue.length > 0) {
this.dispatchQueue(queue);
}
},
dispatchQueue: function(dispatchingQueue) {
var dispatcher = this.dispatcher,
targetType = this.targetType,
eventNames = this.eventNames,
queue = dispatchingQueue.slice(),
ln = queue.length,
i, item, component, eventName, isPainted;
dispatchingQueue.length = 0;
if (ln > 0) {
for (i = 0; i < ln; i++) {
item = queue[i];
component = item.component;
eventName = item.eventName;
isPainted = component.isPainted();
if ((eventName === eventNames.painted && isPainted) || eventName === eventNames.erased && !isPainted) {
dispatcher.doDispatchEvent(targetType, '#' + item.id, eventName, [component]);
}
}
queue.length = 0;
}
},
publish: function(subscribers, component, eventName, dispatchingQueue) {
var id = component.getId(),
needsDispatching = false,
eventNames, items, i, ln, isPainted;
if (subscribers[id]) {
eventNames = this.eventNames;
isPainted = component.isPainted();
if ((eventName === eventNames.painted && !isPainted) || eventName === eventNames.erased && isPainted) {
needsDispatching = true;
}
else {
return this;
}
}
if (component.isContainer) {
items = component.getItems().items;
for (i = 0,ln = items.length; i < ln; i++) {
this.publish(subscribers, items[i], eventName, dispatchingQueue);
}
}
else if (component.isDecorator) {
this.publish(subscribers, component.getComponent(), eventName, dispatchingQueue);
}
if (needsDispatching) {
dispatchingQueue.push({
id: id,
eventName: eventName,
component: component
});
}
}
});
/**
* @private
*/
Ext.define('Ext.event.publisher.Dom', {
extend: 'Ext.event.publisher.Publisher',
requires: [
'Ext.env.Browser',
'Ext.Element',
'Ext.event.Dom'
],
targetType: 'element',
idOrClassSelectorRegex: /^([#|\.])([\w\-]+)$/,
handledEvents: ['click', 'focus', 'blur',
'mousemove', 'mousedown', 'mouseup', 'mouseover', 'mouseout',
'keyup', 'keydown', 'keypress', 'submit',
'transitionend', 'animationstart', 'animationend'],
classNameSplitRegex: /\s+/,
SELECTOR_ALL: '*',
constructor: function() {
var eventNames = this.getHandledEvents(),
eventNameMap = {},
i, ln, eventName, vendorEventName;
this.doBubbleEventsMap = {
'click': true,
'submit': true,
'mousedown': true,
'mousemove': true,
'mouseup': true,
'mouseover': true,
'mouseout': true,
'transitionend': true
};
this.onEvent = Ext.Function.bind(this.onEvent, this);
for (i = 0,ln = eventNames.length; i < ln; i++) {
eventName = eventNames[i];
vendorEventName = this.getVendorEventName(eventName);
eventNameMap[vendorEventName] = eventName;
this.attachListener(vendorEventName);
}
this.eventNameMap = eventNameMap;
return this.callParent();
},
getSubscribers: function(eventName) {
var subscribers = this.subscribers,
eventSubscribers = subscribers[eventName];
if (!eventSubscribers) {
eventSubscribers = subscribers[eventName] = {
id: {
$length: 0
},
className: {
$length: 0
},
selector: [],
all: 0,
$length: 0
}
}
return eventSubscribers;
},
getVendorEventName: function(eventName) {
if (eventName === 'transitionend') {
eventName = Ext.browser.getVendorProperyName('transitionEnd');
}
else if (eventName === 'animationstart') {
eventName = Ext.browser.getVendorProperyName('animationStart');
}
else if (eventName === 'animationend') {
eventName = Ext.browser.getVendorProperyName('animationEnd');
}
return eventName;
},
attachListener: function(eventName) {
document.addEventListener(eventName, this.onEvent, !this.doesEventBubble(eventName));
return this;
},
removeListener: function(eventName) {
document.removeEventListener(eventName, this.onEvent, !this.doesEventBubble(eventName));
return this;
},
doesEventBubble: function(eventName) {
return !!this.doBubbleEventsMap[eventName];
},
subscribe: function(target, eventName) {
if (!this.handles(eventName)) {
return false;
}
var idOrClassSelectorMatch = target.match(this.idOrClassSelectorRegex),
subscribers = this.getSubscribers(eventName),
idSubscribers = subscribers.id,
classNameSubscribers = subscribers.className,
selectorSubscribers = subscribers.selector,
type, value;
if (idOrClassSelectorMatch !== null) {
type = idOrClassSelectorMatch[1];
value = idOrClassSelectorMatch[2];
if (type === '#') {
if (idSubscribers.hasOwnProperty(value)) {
idSubscribers[value]++;
return true;
}
idSubscribers[value] = 1;
idSubscribers.$length++;
}
else {
if (classNameSubscribers.hasOwnProperty(value)) {
classNameSubscribers[value]++;
return true;
}
classNameSubscribers[value] = 1;
classNameSubscribers.$length++;
}
}
else {
if (target === this.SELECTOR_ALL) {
subscribers.all++;
}
else {
if (selectorSubscribers.hasOwnProperty(target)) {
selectorSubscribers[target]++;
return true;
}
selectorSubscribers[target] = 1;
selectorSubscribers.push(target);
}
}
subscribers.$length++;
return true;
},
unsubscribe: function(target, eventName, all) {
if (!this.handles(eventName)) {
return false;
}
var idOrClassSelectorMatch = target.match(this.idOrClassSelectorRegex),
subscribers = this.getSubscribers(eventName),
idSubscribers = subscribers.id,
classNameSubscribers = subscribers.className,
selectorSubscribers = subscribers.selector,
type, value;
all = Boolean(all);
if (idOrClassSelectorMatch !== null) {
type = idOrClassSelectorMatch[1];
value = idOrClassSelectorMatch[2];
if (type === '#') {
if (!idSubscribers.hasOwnProperty(value) || (!all && --idSubscribers[value] > 0)) {
return true;
}
delete idSubscribers[value];
idSubscribers.$length--;
}
else {
if (!classNameSubscribers.hasOwnProperty(value) || (!all && --classNameSubscribers[value] > 0)) {
return true;
}
delete classNameSubscribers[value];
classNameSubscribers.$length--;
}
}
else {
if (target === this.SELECTOR_ALL) {
if (all) {
subscribers.all = 0;
}
else {
subscribers.all--;
}
}
else {
if (!selectorSubscribers.hasOwnProperty(target) || (!all && --selectorSubscribers[target] > 0)) {
return true;
}
delete selectorSubscribers[target];
Ext.Array.remove(selectorSubscribers, target);
}
}
subscribers.$length--;
return true;
},
getElementTarget: function(target) {
if (target.nodeType !== 1) {
target = target.parentNode;
if (!target || target.nodeType !== 1) {
return null;
}
}
return target;
},
getBubblingTargets: function(target) {
var targets = [];
if (!target) {
return targets;
}
do {
targets[targets.length] = target;
target = target.parentNode;
} while (target && target.nodeType === 1);
return targets;
},
dispatch: function(target, eventName, args) {
args.push(args[0].target);
this.callParent(arguments);
},
publish: function(eventName, targets, event) {
var subscribers = this.getSubscribers(eventName),
wildcardSubscribers;
if (subscribers.$length === 0 || !this.doPublish(subscribers, eventName, targets, event)) {
wildcardSubscribers = this.getSubscribers('*');
if (wildcardSubscribers.$length > 0) {
this.doPublish(wildcardSubscribers, eventName, targets, event);
}
}
return this;
},
doPublish: function(subscribers, eventName, targets, event) {
var idSubscribers = subscribers.id,
classNameSubscribers = subscribers.className,
selectorSubscribers = subscribers.selector,
hasIdSubscribers = idSubscribers.$length > 0,
hasClassNameSubscribers = classNameSubscribers.$length > 0,
hasSelectorSubscribers = selectorSubscribers.length > 0,
hasAllSubscribers = subscribers.all > 0,
isClassNameHandled = {},
args = [event],
hasDispatched = false,
classNameSplitRegex = this.classNameSplitRegex,
i, ln, j, subLn, target, id, className, classNames, selector;
for (i = 0,ln = targets.length; i < ln; i++) {
target = targets[i];
event.setDelegatedTarget(target);
if (hasIdSubscribers) {
id = target.id;
if (id) {
if (idSubscribers.hasOwnProperty(id)) {
hasDispatched = true;
this.dispatch('#' + id, eventName, args);
}
}
}
if (hasClassNameSubscribers) {
className = target.className;
if (className) {
classNames = className.split(classNameSplitRegex);
for (j = 0,subLn = classNames.length; j < subLn; j++) {
className = classNames[j];
if (!isClassNameHandled[className]) {
isClassNameHandled[className] = true;
if (classNameSubscribers.hasOwnProperty(className)) {
hasDispatched = true;
this.dispatch('.' + className, eventName, args);
}
}
}
}
}
// Stop propagation
if (event.isStopped) {
return hasDispatched;
}
}
if (hasAllSubscribers && !hasDispatched) {
event.setDelegatedTarget(event.browserEvent.target);
hasDispatched = true;
this.dispatch(this.SELECTOR_ALL, eventName, args);
if (event.isStopped) {
return hasDispatched;
}
}
if (hasSelectorSubscribers) {
for (j = 0,subLn = targets.length; j < subLn; j++) {
target = targets[j];
for (i = 0,ln = selectorSubscribers.length; i < ln; i++) {
selector = selectorSubscribers[i];
if (this.matchesSelector(target, selector)) {
event.setDelegatedTarget(target);
hasDispatched = true;
this.dispatch(selector, eventName, args);
}
if (event.isStopped) {
return hasDispatched;
}
}
}
}
return hasDispatched;
},
matchesSelector: function(element, selector) {
if ('webkitMatchesSelector' in element) {
return element.webkitMatchesSelector(selector);
}
return Ext.DomQuery.is(element, selector);
},
onEvent: function(e) {
var eventName = this.eventNameMap[e.type];
if (!eventName || this.getSubscribersCount(eventName) === 0) {
return;
}
var target = this.getElementTarget(e.target),
targets;
if (!target) {
return;
}
if (this.doesEventBubble(eventName)) {
targets = this.getBubblingTargets(target);
}
else {
targets = [target];
}
this.publish(eventName, targets, new Ext.event.Dom(e));
},
hasSubscriber: function(target, eventName) {
if (!this.handles(eventName)) {
return false;
}
var match = target.match(this.idOrClassSelectorRegex),
subscribers = this.getSubscribers(eventName),
type, value;
if (match !== null) {
type = match[1];
value = match[2];
if (type === '#') {
return subscribers.id.hasOwnProperty(value);
}
else {
return subscribers.className.hasOwnProperty(value);
}
}
else {
return (subscribers.selector.hasOwnProperty(target) && Ext.Array.indexOf(subscribers.selector, target) !== -1);
}
return false;
},
getSubscribersCount: function(eventName) {
if (!this.handles(eventName)) {
return 0;
}
return this.getSubscribers(eventName).$length + this.getSubscribers('*').$length;
}
});
/**
* @private
*/
Ext.define('Ext.event.publisher.TouchGesture', {
extend: 'Ext.event.publisher.Dom',
requires: [
'Ext.util.Point',
'Ext.event.Touch'
],
handledEvents: ['touchstart', 'touchmove', 'touchend', 'touchcancel'],
moveEventName: 'touchmove',
config: {
moveThrottle: 1,
buffering: {
enabled: false,
interval: 10
},
recognizers: {}
},
currentTouchesCount: 0,
constructor: function(config) {
this.processEvents = Ext.Function.bind(this.processEvents, this);
this.eventProcessors = {
touchstart: this.onTouchStart,
touchmove: this.onTouchMove,
touchend: this.onTouchEnd,
touchcancel: this.onTouchEnd
};
this.eventToRecognizerMap = {};
this.activeRecognizers = [];
this.currentRecognizers = [];
this.currentTargets = {};
this.currentTouches = {};
this.buffer = [];
this.initConfig(config);
return this.callParent();
},
applyBuffering: function(buffering) {
if (buffering.enabled === true) {
this.bufferTimer = setInterval(this.processEvents, buffering.interval);
}
else {
clearInterval(this.bufferTimer);
}
return buffering;
},
applyRecognizers: function(recognizers) {
var i, recognizer;
for (i in recognizers) {
if (recognizers.hasOwnProperty(i)) {
recognizer = recognizers[i];
if (recognizer) {
this.registerRecognizer(recognizer);
}
}
}
return recognizers;
},
handles: function(eventName) {
return this.callParent(arguments) || this.eventToRecognizerMap.hasOwnProperty(eventName);
},
doesEventBubble: function() {
// All touch events bubble
return true;
},
eventLogs: [],
onEvent: function(e) {
var buffering = this.getBuffering();
e = new Ext.event.Touch(e);
if (buffering.enabled) {
this.buffer.push(e);
}
else {
this.processEvent(e);
}
},
processEvents: function() {
var buffer = this.buffer,
ln = buffer.length,
moveEvents = [],
events, event, i;
if (ln > 0) {
events = buffer.slice(0);
buffer.length = 0;
for (i = 0; i < ln; i++) {
event = events[i];
if (event.type === this.moveEventName) {
moveEvents.push(event);
}
else {
if (moveEvents.length > 0) {
this.processEvent(this.mergeEvents(moveEvents));
moveEvents.length = 0;
}
this.processEvent(event);
}
}
if (moveEvents.length > 0) {
this.processEvent(this.mergeEvents(moveEvents));
moveEvents.length = 0;
}
}
},
mergeEvents: function(events) {
var changedTouchesLists = [],
ln = events.length,
i, event, targetEvent;
targetEvent = events[ln - 1];
if (ln === 1) {
return targetEvent;
}
for (i = 0; i < ln; i++) {
event = events[i];
changedTouchesLists.push(event.changedTouches);
}
targetEvent.changedTouches = this.mergeTouchLists(changedTouchesLists);
return targetEvent;
},
mergeTouchLists: function(touchLists) {
var touches = {},
list = [],
i, ln, touchList, j, subLn, touch, identifier;
for (i = 0,ln = touchLists.length; i < ln; i++) {
touchList = touchLists[i];
for (j = 0,subLn = touchList.length; j < subLn; j++) {
touch = touchList[j];
identifier = touch.identifier;
touches[identifier] = touch;
}
}
for (identifier in touches) {
if (touches.hasOwnProperty(identifier)) {
list.push(touches[identifier]);
}
}
return list;
},
registerRecognizer: function(recognizer) {
var map = this.eventToRecognizerMap,
activeRecognizers = this.activeRecognizers,
handledEvents = recognizer.getHandledEvents(),
i, ln, eventName;
recognizer.setOnRecognized(this.onRecognized);
recognizer.setCallbackScope(this);
for (i = 0,ln = handledEvents.length; i < ln; i++) {
eventName = handledEvents[i];
map[eventName] = recognizer;
}
activeRecognizers.push(recognizer);
return this;
},
onRecognized: function(eventName, e, touches, info) {
var targetGroups = [],
ln = touches.length,
targets, i, touch;
if (ln === 1) {
return this.publish(eventName, touches[0].targets, e, info);
}
for (i = 0; i < ln; i++) {
touch = touches[i];
targetGroups.push(touch.targets);
}
targets = this.getCommonTargets(targetGroups);
this.publish(eventName, targets, e, info);
},
publish: function(eventName, targets, event, info) {
event.set(info);
return this.callParent([eventName, targets, event]);
},
getCommonTargets: function(targetGroups) {
var firstTargetGroup = targetGroups[0],
ln = targetGroups.length;
if (ln === 1) {
return firstTargetGroup;
}
var commonTargets = [],
i = 1,
target, targets, j;
while (true) {
target = firstTargetGroup[firstTargetGroup.length - i];
if (!target) {
return commonTargets;
}
for (j = 1; j < ln; j++) {
targets = targetGroups[j];
if (targets[targets.length - i] !== target) {
return commonTargets;
}
}
commonTargets.unshift(target);
i++;
}
return commonTargets;
},
invokeRecognizers: function(methodName, e) {
var recognizers = this.activeRecognizers,
ln = recognizers.length,
i, recognizer;
if (methodName === 'onStart') {
for (i = 0; i < ln; i++) {
recognizers[i].isActive = true;
}
}
for (i = 0; i < ln; i++) {
recognizer = recognizers[i];
if (recognizer.isActive && recognizer[methodName].call(recognizer, e) === false) {
recognizer.isActive = false;
}
}
},
getActiveRecognizers: function() {
return this.activeRecognizers;
},
processEvent: function(e) {
this.eventProcessors[e.type].call(this, e);
},
onTouchStart: function(e) {
var currentTargets = this.currentTargets,
currentTouches = this.currentTouches,
currentTouchesCount = this.currentTouchesCount,
changedTouches = e.changedTouches,
touches = e.touches,
touchesLn = touches.length,
currentIdentifiers = {},
ln = changedTouches.length,
i, touch, identifier, fakeEndEvent;
currentTouchesCount += ln;
if (currentTouchesCount > touchesLn) {
for (i = 0; i < touchesLn; i++) {
touch = touches[i];
identifier = touch.identifier;
currentIdentifiers[identifier] = true;
}
for (identifier in currentTouches) {
if (currentTouches.hasOwnProperty(identifier)) {
if (!currentIdentifiers[identifier]) {
currentTouchesCount--;
fakeEndEvent = e.clone();
touch = currentTouches[identifier];
touch.targets = this.getBubblingTargets(this.getElementTarget(touch.target));
fakeEndEvent.changedTouches = [touch];
this.onTouchEnd(fakeEndEvent);
}
}
}
// Fix for a bug found in Motorola Droid X (Gingerbread) and similar
// where there are 2 touchstarts but just one touchend
if (currentTouchesCount > touchesLn) {
return;
}
}
for (i = 0; i < ln; i++) {
touch = changedTouches[i];
identifier = touch.identifier;
if (!currentTouches.hasOwnProperty(identifier)) {
this.currentTouchesCount++;
}
currentTouches[identifier] = touch;
currentTargets[identifier] = this.getBubblingTargets(this.getElementTarget(touch.target));
}
e.setTargets(currentTargets);
for (i = 0; i < ln; i++) {
touch = changedTouches[i];
this.publish('touchstart', touch.targets, e, {touch: touch});
}
if (!this.isStarted) {
this.isStarted = true;
this.invokeRecognizers('onStart', e);
}
this.invokeRecognizers('onTouchStart', e);
},
onTouchMove: function(e) {
if (!this.isStarted) {
return;
}
var currentTargets = this.currentTargets,
currentTouches = this.currentTouches,
moveThrottle = this.getMoveThrottle(),
changedTouches = e.changedTouches,
stillTouchesCount = 0,
i, ln, touch, point, oldPoint, identifier;
e.setTargets(currentTargets);
for (i = 0,ln = changedTouches.length; i < ln; i++) {
touch = changedTouches[i];
identifier = touch.identifier;
point = touch.point;
oldPoint = currentTouches[identifier].point;
if (moveThrottle && point.isCloseTo(oldPoint, moveThrottle)) {
stillTouchesCount++;
continue;
}
currentTouches[identifier] = touch;
this.publish('touchmove', touch.targets, e, {touch: touch});
}
if (stillTouchesCount < ln) {
this.invokeRecognizers('onTouchMove', e);
}
},
onTouchEnd: function(e) {
if (!this.isStarted) {
return;
}
var currentTargets = this.currentTargets,
currentTouches = this.currentTouches,
changedTouches = e.changedTouches,
ln = changedTouches.length,
isEnded, identifier, i, touch;
e.setTargets(currentTargets);
this.currentTouchesCount -= ln;
isEnded = (this.currentTouchesCount === 0);
if (isEnded) {
this.isStarted = false;
}
for (i = 0; i < ln; i++) {
touch = changedTouches[i];
identifier = touch.identifier;
delete currentTouches[identifier];
delete currentTargets[identifier];
this.publish('touchend', touch.targets, e, {touch: touch});
}
this.invokeRecognizers('onTouchEnd', e);
if (isEnded) {
this.invokeRecognizers('onEnd', e);
}
}
}, function() {
if (!Ext.feature.has.Touch) {
this.override({
moveEventName: 'mousemove',
map: {
mouseToTouch: {
mousedown: 'touchstart',
mousemove: 'touchmove',
mouseup: 'touchend'
},
touchToMouse: {
touchstart: 'mousedown',
touchmove: 'mousemove',
touchend: 'mouseup'
}
},
attachListener: function(eventName) {
eventName = this.map.touchToMouse[eventName];
if (!eventName) {
return;
}
return this.callOverridden([eventName]);
},
lastEventType: null,
onEvent: function(e) {
if ('button' in e && e.button !== 0) {
return;
}
var type = e.type,
touchList = [e];
// Temporary fix for a recent Chrome bugs where events don't seem to bubble up to document
// when the element is being animated
// with webkit-transition (2 mousedowns without any mouseup)
if (type === 'mousedown' && this.lastEventType && this.lastEventType !== 'mouseup') {
var fixedEvent = document.createEvent("MouseEvent");
fixedEvent.initMouseEvent('mouseup', e.bubbles, e.cancelable,
document.defaultView, e.detail, e.screenX, e.screenY, e.clientX,
e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.metaKey,
e.button, e.relatedTarget);
this.onEvent(fixedEvent);
}
if (type !== 'mousemove') {
this.lastEventType = type;
}
e.identifier = 1;
e.touches = (type !== 'mouseup') ? touchList : [];
e.targetTouches = (type !== 'mouseup') ? touchList : [];
e.changedTouches = touchList;
return this.callOverridden([e]);
},
processEvent: function(e) {
this.eventProcessors[this.map.mouseToTouch[e.type]].call(this, e);
}
});
}
});
/**
* @private
*/
Ext.define('Ext.event.recognizer.MultiTouch', {
extend: 'Ext.event.recognizer.Touch',
requiredTouchesCount: 2,
isTracking: false,
isStarted: false,
onTouchStart: function(e) {
var requiredTouchesCount = this.requiredTouchesCount,
touches = e.touches,
touchesCount = touches.length;
if (touchesCount === requiredTouchesCount) {
this.start(e);
}
else if (touchesCount > requiredTouchesCount) {
this.end(e);
}
},
onTouchEnd: function(e) {
this.end(e);
},
start: function() {
if (!this.isTracking) {
this.isTracking = true;
this.isStarted = false;
}
},
end: function(e) {
if (this.isTracking) {
this.isTracking = false;
if (this.isStarted) {
this.isStarted = false;
this.fireEnd(e);
}
}
}
});
/**
* A event recogniser which knows when you pinch.
*
* @private
*/
Ext.define('Ext.event.recognizer.Pinch', {
extend: 'Ext.event.recognizer.MultiTouch',
requiredTouchesCount: 2,
handledEvents: ['pinchstart', 'pinch', 'pinchend'],
/**
* @member Ext.dom.Element
* @event pinchstart
* Fired once when a pinch has started.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @member Ext.dom.Element
* @event pinch
* Fires continuously when there is pinching (the touch must move for this to be fired).
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @member Ext.dom.Element
* @event pinchend
* Fires when a pinch has ended.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @property {Number} scale
* The scape of a pinch event.
*
* **This is only available when the event type is `pinch`**
* @member Ext.event.Event
*/
startDistance: 0,
lastTouches: null,
onTouchMove: function(e) {
if (!this.isTracking) {
return;
}
var touches = Array.prototype.slice.call(e.touches),
firstPoint, secondPoint, distance;
firstPoint = touches[0].point;
secondPoint = touches[1].point;
distance = firstPoint.getDistanceTo(secondPoint);
if (distance === 0) {
return;
}
if (!this.isStarted) {
this.isStarted = true;
this.startDistance = distance;
this.fire('pinchstart', e, touches, {
touches: touches,
distance: distance,
scale: 1
});
}
else {
this.fire('pinch', e, touches, {
touches: touches,
distance: distance,
scale: distance / this.startDistance
});
}
this.lastTouches = touches;
},
fireEnd: function(e) {
this.fire('pinchend', e, this.lastTouches);
},
fail: function() {
return this.callParent(arguments);
}
});
/**
* A simple event recogniser which knows when you rotate.
*
* @private
*/
Ext.define('Ext.event.recognizer.Rotate', {
extend: 'Ext.event.recognizer.MultiTouch',
requiredTouchesCount: 2,
handledEvents: ['rotatestart', 'rotate', 'rotateend'],
/**
* @member Ext.dom.Element
* @event rotatestart
* Fired once when a rotation has started.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @member Ext.dom.Element
* @event rotate
* Fires continuously when there is rotation (the touch must move for this to be fired).
* When listening to this, ensure you know about the {@link Ext.event.Event#angle} and {@link Ext.event.Event#rotation}
* properties in the `event` object.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @member Ext.dom.Element
* @event rotateend
* Fires when a rotation event has ended.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @property {Number} angle
* The angle of the rotation.
*
* **This is only available when the event type is `rotate`**
* @member Ext.event.Event
*/
/**
* @property {Number} rotation
* A amount of rotation, since the start of the event.
*
* **This is only available when the event type is `rotate`**
* @member Ext.event.Event
*/
startAngle: 0,
lastTouches: null,
lastAngle: null,
onTouchMove: function(e) {
if (!this.isTracking) {
return;
}
var touches = Array.prototype.slice.call(e.touches),
lastAngle = this.lastAngle,
firstPoint, secondPoint, angle, nextAngle, previousAngle, diff;
firstPoint = touches[0].point;
secondPoint = touches[1].point;
angle = firstPoint.getAngleTo(secondPoint);
if (lastAngle !== null) {
diff = Math.abs(lastAngle - angle);
nextAngle = angle + 360;
previousAngle = angle - 360;
if (Math.abs(nextAngle - lastAngle) < diff) {
angle = nextAngle;
}
else if (Math.abs(previousAngle - lastAngle) < diff) {
angle = previousAngle;
}
}
this.lastAngle = angle;
if (!this.isStarted) {
this.isStarted = true;
this.startAngle = angle;
this.fire('rotatestart', e, touches, {
touches: touches,
angle: angle,
rotation: 0
});
}
else {
this.fire('rotate', e, touches, {
touches: touches,
angle: angle,
rotation: angle - this.startAngle
});
}
this.lastTouches = touches;
},
fireEnd: function(e) {
this.lastAngle = null;
this.fire('rotateend', e, this.lastTouches);
}
});
/**
* @private
*/
Ext.define('Ext.event.recognizer.SingleTouch', {
extend: 'Ext.event.recognizer.Touch',
inheritableStatics: {
NOT_SINGLE_TOUCH: 0x01,
TOUCH_MOVED: 0x02
},
onTouchStart: function(e) {
if (e.touches.length > 1) {
return this.fail(this.self.NOT_SINGLE_TOUCH);
}
}
});
/**
* A simple event recogniser which knows when you double tap.
*
* @private
*/
Ext.define('Ext.event.recognizer.DoubleTap', {
extend: 'Ext.event.recognizer.SingleTouch',
config: {
maxDuration: 300
},
handledEvents: ['singletap', 'doubletap'],
/**
* @member Ext.dom.Element
* @event singletap
* Fires when there is a single tap.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @member Ext.dom.Element
* @event doubletap
* Fires when there is a double tap.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
singleTapTimer: null,
onTouchStart: function(e) {
if (this.callParent(arguments) === false) {
return false;
}
this.startTime = e.time;
clearTimeout(this.singleTapTimer);
},
onTouchMove: function() {
return this.fail(this.self.TOUCH_MOVED);
},
onEnd: function(e) {
var me = this,
maxDuration = this.getMaxDuration(),
touch = e.changedTouches[0],
time = e.time,
lastTapTime = this.lastTapTime,
duration;
this.lastTapTime = time;
if (lastTapTime) {
duration = time - lastTapTime;
if (duration <= maxDuration) {
this.lastTapTime = 0;
this.fire('doubletap', e, [touch], {
touch: touch,
duration: duration
});
return;
}
}
if (time - this.startTime > maxDuration) {
this.fireSingleTap(e, touch);
}
else {
this.singleTapTimer = setTimeout(function() {
me.fireSingleTap(e, touch);
}, maxDuration);
}
},
fireSingleTap: function(e, touch) {
this.fire('singletap', e, [touch], {
touch: touch
});
}
});
/**
* A simple event recogniser which knows when you drag.
*
* @private
*/
Ext.define('Ext.event.recognizer.Drag', {
extend: 'Ext.event.recognizer.SingleTouch',
isStarted: false,
startPoint: null,
previousPoint: null,
lastPoint: null,
handledEvents: ['dragstart', 'drag', 'dragend'],
/**
* @member Ext.dom.Element
* @event dragstart
* Fired once when a drag has started.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @member Ext.dom.Element
* @event drag
* Fires continuously when there is dragging (the touch must move for this to be fired).
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @member Ext.dom.Element
* @event dragend
* Fires when a drag has ended.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
onTouchStart: function(e) {
var startTouches,
startTouch;
if (this.callParent(arguments) === false) {
if (this.isStarted && this.lastMoveEvent !== null) {
this.onTouchEnd(this.lastMoveEvent);
}
return false;
}
this.startTouches = startTouches = e.changedTouches;
this.startTouch = startTouch = startTouches[0];
this.startPoint = startTouch.point;
},
onTouchMove: function(e) {
var touches = e.changedTouches,
touch = touches[0],
point = touch.point,
time = e.time;
if (this.lastPoint) {
this.previousPoint = this.lastPoint;
}
if (this.lastTime) {
this.previousTime = this.lastTime;
}
this.lastTime = time;
this.lastPoint = point;
this.lastMoveEvent = e;
if (!this.isStarted) {
this.isStarted = true;
this.startTime = time;
this.previousTime = time;
this.previousPoint = this.startPoint;
this.fire('dragstart', e, this.startTouches, this.getInfo(e, this.startTouch));
}
else {
this.fire('drag', e, touches, this.getInfo(e, touch));
}
},
onTouchEnd: function(e) {
if (this.isStarted) {
var touches = e.changedTouches,
touch = touches[0],
point = touch.point;
this.isStarted = false;
this.lastPoint = point;
this.fire('dragend', e, touches, this.getInfo(e, touch));
this.startTime = 0;
this.previousTime = 0;
this.lastTime = 0;
this.startPoint = null;
this.previousPoint = null;
this.lastPoint = null;
this.lastMoveEvent = null;
}
},
getInfo: function(e, touch) {
var time = e.time,
startPoint = this.startPoint,
previousPoint = this.previousPoint,
startTime = this.startTime,
previousTime = this.previousTime,
point = this.lastPoint,
deltaX = point.x - startPoint.x,
deltaY = point.y - startPoint.y,
info = {
touch: touch,
startX: startPoint.x,
startY: startPoint.y,
previousX: previousPoint.x,
previousY: previousPoint.y,
pageX: point.x,
pageY: point.y,
deltaX: deltaX,
deltaY: deltaY,
absDeltaX: Math.abs(deltaX),
absDeltaY: Math.abs(deltaY),
previousDeltaX: point.x - previousPoint.x,
previousDeltaY: point.y - previousPoint.y,
time: time,
startTime: startTime,
previousTime: previousTime,
deltaTime: time - startTime,
previousDeltaTime: time - previousTime
};
return info;
}
});
/**
* A event recogniser which knows when you tap and hold for more than 1 second.
*
* @private
*/
Ext.define('Ext.event.recognizer.LongPress', {
extend: 'Ext.event.recognizer.SingleTouch',
inheritableStatics: {
DURATION_NOT_ENOUGH: 0x20
},
config: {
minDuration: 1000
},
handledEvents: ['longpress'],
/**
* @member Ext.dom.Element
* @event longpress
* Fires when you touch and hold still for more than 1 second.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @member Ext.dom.Element
* @event taphold
* @inheritdoc Ext.dom.Element#longpress
* @deprecated 2.0.0 Please add listener to 'longpress' event instead
*/
fireLongPress: function(e) {
var touch = e.changedTouches[0];
this.fire('longpress', e, [touch], {
touch: touch,
duration: this.getMinDuration()
});
this.isLongPress = true;
},
onTouchStart: function(e) {
var me = this;
if (this.callParent(arguments) === false) {
return false;
}
this.isLongPress = false;
this.timer = setTimeout(function() {
me.fireLongPress(e);
}, this.getMinDuration());
},
onTouchMove: function() {
return this.fail(this.self.TOUCH_MOVED);
},
onTouchEnd: function() {
if (!this.isLongPress) {
return this.fail(this.self.DURATION_NOT_ENOUGH);
}
},
fail: function() {
clearTimeout(this.timer);
return this.callParent(arguments);
}
}, function() {
});
/**
* A simple event recogniser which knows when you tap.
*
* @private
*/
Ext.define('Ext.event.recognizer.Tap', {
handledEvents: ['tap'],
/**
* @member Ext.dom.Element
* @event tap
* Fires when you tap
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @member Ext.dom.Element
* @event touchstart
* Fires when touch starts.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @member Ext.dom.Element
* @event tapstart
* @inheritdoc Ext.dom.Element#touchstart
* @deprecated 2.0.0 Please add listener to 'touchstart' event instead
*/
/**
* @member Ext.dom.Element
* @event touchmove
* Fires when movement while touching.
* @param {Ext.event.Event} event The {@link Ext.event.Event} event encapsulating the DOM event.
* @param {HTMLElement} node The target of the event.
* @param {Object} options The options object passed to Ext.mixin.Observable.addListener.
*/
/**
* @member Ext.dom.Element
* @event tapcancel
* @inheritdoc Ext.dom.Element#touchmove
* @deprecated 2.0.0 Please add listener to 'touchmove' event instead
*/
extend: 'Ext.event.recognizer.SingleTouch',
onTouchMove: function() {
return this.fail(this.self.TOUCH_MOVED);
},
onTouchEnd: function(e) {
var touch = e.changedTouches[0];
this.fire('tap', e, [touch]);
}
}, function() {
});
/**
* @class Ext.Date
* @mixins Ext.DateExtras
* A set of useful static methods to deal with date.
*
* **Please note:** Unless you require `Ext.DateExtras`, only the {@link #now} method will be available. You **MUST**
* require `Ext.DateExtras` to use the other methods available below.
*
* Usage with {@link Ext#setup}:
*
* Ext.setup({
* requires: 'Ext.DateExtras',
* onReady: function() {
* var date = new Date();
* alert(Ext.Date.format(date, 'j/d/Y'));
* }
* });
*
* The date parsing and formatting syntax contains a subset of
* <a href="http://www.php.net/date">PHP's date() function</a>, and the formats that are
* supported will provide results equivalent to their PHP versions.
*
* The following is a list of all currently supported formats:
* <pre class="">
Format Description Example returned values
------ ----------------------------------------------------------------------- -----------------------
d Day of the month, 2 digits with leading zeros 01 to 31
D A short textual representation of the day of the week Mon to Sun
j Day of the month without leading zeros 1 to 31
l A full textual representation of the day of the week Sunday to Saturday
N ISO-8601 numeric representation of the day of the week 1 (for Monday) through 7 (for Sunday)
S English ordinal suffix for the day of the month, 2 characters st, nd, rd or th. Works well with j
w Numeric representation of the day of the week 0 (for Sunday) to 6 (for Saturday)
z The day of the year (starting from 0) 0 to 364 (365 in leap years)
W ISO-8601 week number of year, weeks starting on Monday 01 to 53
F A full textual representation of a month, such as January or March January to December
m Numeric representation of a month, with leading zeros 01 to 12
M A short textual representation of a month Jan to Dec
n Numeric representation of a month, without leading zeros 1 to 12
t Number of days in the given month 28 to 31
L Whether it&#39;s a leap year 1 if it is a leap year, 0 otherwise.
o ISO-8601 year number (identical to (Y), but if the ISO week number (W) Examples: 1998 or 2004
belongs to the previous or next year, that year is used instead)
Y A full numeric representation of a year, 4 digits Examples: 1999 or 2003
y A two digit representation of a year Examples: 99 or 03
a Lowercase Ante meridiem and Post meridiem am or pm
A Uppercase Ante meridiem and Post meridiem AM or PM
g 12-hour format of an hour without leading zeros 1 to 12
G 24-hour format of an hour without leading zeros 0 to 23
h 12-hour format of an hour with leading zeros 01 to 12
H 24-hour format of an hour with leading zeros 00 to 23
i Minutes, with leading zeros 00 to 59
s Seconds, with leading zeros 00 to 59
u Decimal fraction of a second Examples:
(minimum 1 digit, arbitrary number of digits allowed) 001 (i.e. 0.001s) or
100 (i.e. 0.100s) or
999 (i.e. 0.999s) or
999876543210 (i.e. 0.999876543210s)
O Difference to Greenwich time (GMT) in hours and minutes Example: +1030
P Difference to Greenwich time (GMT) with colon between hours and minutes Example: -08:00
T Timezone abbreviation of the machine running the code Examples: EST, MDT, PDT ...
Z Timezone offset in seconds (negative if west of UTC, positive if east) -43200 to 50400
c ISO 8601 date
Notes: Examples:
1) If unspecified, the month / day defaults to the current month / day, 1991 or
the time defaults to midnight, while the timezone defaults to the 1992-10 or
browser's timezone. If a time is specified, it must include both hours 1993-09-20 or
and minutes. The "T" delimiter, seconds, milliseconds and timezone 1994-08-19T16:20+01:00 or
are optional. 1995-07-18T17:21:28-02:00 or
2) The decimal fraction of a second, if specified, must contain at 1996-06-17T18:22:29.98765+03:00 or
least 1 digit (there is no limit to the maximum number 1997-05-16T19:23:30,12345-0400 or
of digits allowed), and may be delimited by either a '.' or a ',' 1998-04-15T20:24:31.2468Z or
Refer to the examples on the right for the various levels of 1999-03-14T20:24:32Z or
date-time granularity which are supported, or see 2000-02-13T21:25:33
http://www.w3.org/TR/NOTE-datetime for more info. 2001-01-12 22:26:34
U Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) 1193432466 or -2138434463
MS Microsoft AJAX serialized dates \/Date(1238606590509)\/ (i.e. UTC milliseconds since epoch) or
\/Date(1238606590509+0800)\/
</pre>
*
* Example usage (note that you must escape format specifiers with '\\' to render them as character literals):
* <pre><code>
// Sample date:
// 'Wed Jan 10 2007 15:05:01 GMT-0600 (Central Standard Time)'
var dt = new Date('1/10/2007 03:05:01 PM GMT-0600');
console.log(Ext.Date.format(dt, 'Y-m-d')); // 2007-01-10
console.log(Ext.Date.format(dt, 'F j, Y, g:i a')); // January 10, 2007, 3:05 pm
console.log(Ext.Date.format(dt, 'l, \\t\\he jS \\of F Y h:i:s A')); // Wednesday, the 10th of January 2007 03:05:01 PM
</code></pre>
*
* Here are some standard date/time patterns that you might find helpful. They
* are not part of the source of Ext.Date, but to use them you can simply copy this
* block of code into any script that is included after Ext.Date and they will also become
* globally available on the Date object. Feel free to add or remove patterns as needed in your code.
* <pre><code>
Ext.Date.patterns = {
ISO8601Long:"Y-m-d H:i:s",
ISO8601Short:"Y-m-d",
ShortDate: "n/j/Y",
LongDate: "l, F d, Y",
FullDateTime: "l, F d, Y g:i:s A",
MonthDay: "F d",
ShortTime: "g:i A",
LongTime: "g:i:s A",
SortableDateTime: "Y-m-d\\TH:i:s",
UniversalSortableDateTime: "Y-m-d H:i:sO",
YearMonth: "F, Y"
};
</code></pre>
*
* Example usage:
* <pre><code>
var dt = new Date();
console.log(Ext.Date.format(dt, Ext.Date.patterns.ShortDate));
</code></pre>
* <p>Developer-written, custom formats may be used by supplying both a formatting and a parsing function
* which perform to specialized requirements. The functions are stored in {@link #parseFunctions} and {@link #formatFunctions}.</p>
* @singleton
*/
/*
* Most of the date-formatting functions below are the excellent work of Baron Schwartz.
* (see http://www.xaprb.com/blog/2005/12/12/javascript-closures-for-runtime-efficiency/)
* They generate precompiled functions from format patterns instead of parsing and
* processing each pattern every time a date is formatted. These functions are available
* on every Date object.
*/
(function() {
// create private copy of Ext's Ext.util.Format.format() method
// - to remove unnecessary dependency
// - to resolve namespace conflict with MS-Ajax's implementation
function xf(format) {
var args = Array.prototype.slice.call(arguments, 1);
return format.replace(/\{(\d+)\}/g, function(m, i) {
return args[i];
});
}
/**
* Extra methods to be mixed into Ext.Date.
*
* Require this class to get Ext.Date with all the methods listed below.
*
* Using Ext.setup:
*
* Ext.setup({
* requires: 'Ext.DateExtras',
* onReady: function() {
* var date = new Date();
* alert(Ext.Date.format(date, 'j/d/Y'));
* }
* });
*
* Using Ext.application:
*
* Ext.application({
* requires: 'Ext.DateExtras',
* launch: function() {
* var date = new Date();
* alert(Ext.Date.format(date, 'j/d/Y'));
* }
* });
*
* @singleton
*/
Ext.DateExtras = {
/**
* Returns the current timestamp
* @return {Date} The current timestamp
* @method
*/
now: Date.now || function() {
return +new Date();
},
/**
* Returns the number of milliseconds between two dates
* @param {Date} dateA The first date
* @param {Date} dateB (optional) The second date, defaults to now
* @return {Number} The difference in milliseconds
*/
getElapsed: function(dateA, dateB) {
return Math.abs(dateA - (dateB || new Date()));
},
/**
* Global flag which determines if strict date parsing should be used.
* Strict date parsing will not roll-over invalid dates, which is the
* default behaviour of javascript Date objects.
* (see {@link #parse} for more information)
* Defaults to <tt>false</tt>.
* @type Boolean
*/
useStrict: false,
// private
formatCodeToRegex: function(character, currentGroup) {
// Note: currentGroup - position in regex result array (see notes for Ext.Date.parseCodes below)
var p = utilDate.parseCodes[character];
if (p) {
p = typeof p == 'function'? p() : p;
utilDate.parseCodes[character] = p; // reassign function result to prevent repeated execution
}
return p ? Ext.applyIf({
c: p.c ? xf(p.c, currentGroup || "{0}") : p.c
}, p) : {
g: 0,
c: null,
s: Ext.String.escapeRegex(character) // treat unrecognised characters as literals
};
},
/**
* <p>An object hash in which each property is a date parsing function. The property name is the
* format string which that function parses.</p>
* <p>This object is automatically populated with date parsing functions as
* date formats are requested for Ext standard formatting strings.</p>
* <p>Custom parsing functions may be inserted into this object, keyed by a name which from then on
* may be used as a format string to {@link #parse}.<p>
* <p>Example:</p><pre><code>
Ext.Date.parseFunctions['x-date-format'] = myDateParser;
</code></pre>
* <p>A parsing function should return a Date object, and is passed the following parameters:<div class="mdetail-params"><ul>
* <li><code>date</code> : String<div class="sub-desc">The date string to parse.</div></li>
* <li><code>strict</code> : Boolean<div class="sub-desc">True to validate date strings while parsing
* (i.e. prevent javascript Date "rollover") (The default must be false).
* Invalid date strings should return null when parsed.</div></li>
* </ul></div></p>
* <p>To enable Dates to also be <i>formatted</i> according to that format, a corresponding
* formatting function must be placed into the {@link #formatFunctions} property.
* @property parseFunctions
* @type Object
*/
parseFunctions: {
"MS": function(input, strict) {
// note: the timezone offset is ignored since the MS Ajax server sends
// a UTC milliseconds-since-Unix-epoch value (negative values are allowed)
var re = new RegExp('\\/Date\\(([-+])?(\\d+)(?:[+-]\\d{4})?\\)\\/');
var r = (input || '').match(re);
return r? new Date(((r[1] || '') + r[2]) * 1) : null;
}
},
parseRegexes: [],
/**
* <p>An object hash in which each property is a date formatting function. The property name is the
* format string which corresponds to the produced formatted date string.</p>
* <p>This object is automatically populated with date formatting functions as
* date formats are requested for Ext standard formatting strings.</p>
* <p>Custom formatting functions may be inserted into this object, keyed by a name which from then on
* may be used as a format string to {@link #format}. Example:</p><pre><code>
Ext.Date.formatFunctions['x-date-format'] = myDateFormatter;
</code></pre>
* <p>A formatting function should return a string representation of the passed Date object, and is passed the following parameters:<div class="mdetail-params"><ul>
* <li><code>date</code> : Date<div class="sub-desc">The Date to format.</div></li>
* </ul></div></p>
* <p>To enable date strings to also be <i>parsed</i> according to that format, a corresponding
* parsing function must be placed into the {@link #parseFunctions} property.
* @property formatFunctions
* @type Object
*/
formatFunctions: {
"MS": function() {
// UTC milliseconds since Unix epoch (MS-AJAX serialized date format (MRSF))
return '\\/Date(' + this.getTime() + ')\\/';
}
},
y2kYear : 50,
/**
* Date interval constant
* @type String
*/
MILLI : "ms",
/**
* Date interval constant
* @type String
*/
SECOND : "s",
/**
* Date interval constant
* @type String
*/
MINUTE : "mi",
/** Date interval constant
* @type String
*/
HOUR : "h",
/**
* Date interval constant
* @type String
*/
DAY : "d",
/**
* Date interval constant
* @type String
*/
MONTH : "mo",
/**
* Date interval constant
* @type String
*/
YEAR : "y",
/**
* <p>An object hash containing default date values used during date parsing.</p>
* <p>The following properties are available:<div class="mdetail-params"><ul>
* <li><code>y</code> : Number<div class="sub-desc">The default year value. (defaults to undefined)</div></li>
* <li><code>m</code> : Number<div class="sub-desc">The default 1-based month value. (defaults to undefined)</div></li>
* <li><code>d</code> : Number<div class="sub-desc">The default day value. (defaults to undefined)</div></li>
* <li><code>h</code> : Number<div class="sub-desc">The default hour value. (defaults to undefined)</div></li>
* <li><code>i</code> : Number<div class="sub-desc">The default minute value. (defaults to undefined)</div></li>
* <li><code>s</code> : Number<div class="sub-desc">The default second value. (defaults to undefined)</div></li>
* <li><code>ms</code> : Number<div class="sub-desc">The default millisecond value. (defaults to undefined)</div></li>
* </ul></div></p>
* <p>Override these properties to customize the default date values used by the {@link #parse} method.</p>
* <p><b>Note: In countries which experience Daylight Saving Time (i.e. DST), the <tt>h</tt>, <tt>i</tt>, <tt>s</tt>
* and <tt>ms</tt> properties may coincide with the exact time in which DST takes effect.
* It is the responsiblity of the developer to account for this.</b></p>
* Example Usage:
* <pre><code>
// set default day value to the first day of the month
Ext.Date.defaults.d = 1;
// parse a February date string containing only year and month values.
// setting the default day value to 1 prevents weird date rollover issues
// when attempting to parse the following date string on, for example, March 31st 2009.
Ext.Date.parse('2009-02', 'Y-m'); // returns a Date object representing February 1st 2009
</code></pre>
* @property defaults
* @type Object
*/
defaults: {},
/**
* An array of textual day names.
* Override these values for international dates.
* Example:
* <pre><code>
Ext.Date.dayNames = [
'SundayInYourLang',
'MondayInYourLang',
...
];
</code></pre>
* @type Array
*/
dayNames : [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
/**
* An array of textual month names.
* Override these values for international dates.
* Example:
* <pre><code>
Ext.Date.monthNames = [
'JanInYourLang',
'FebInYourLang',
...
];
</code></pre>
* @type Array
*/
monthNames : [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
],
/**
* An object hash of zero-based javascript month numbers (with short month names as keys. note: keys are case-sensitive).
* Override these values for international dates.
* Example:
* <pre><code>
Ext.Date.monthNumbers = {
'ShortJanNameInYourLang':0,
'ShortFebNameInYourLang':1,
...
};
</code></pre>
* @type Object
*/
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
},
/**
* <p>The date format string that the {@link Ext.util.Format#date} function uses.
* See {@link Ext.Date} for details.</p>
* <p>This defaults to <code>m/d/Y</code>, but may be overridden in a locale file.</p>
* @property defaultFormat
* @type String
*/
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()];
},
/**
* The base format-code to formatting-function hashmap used by the {@link #format} method.
* Formatting functions are strings (or functions which return strings) which
* will return the appropriate value when evaluated in the context of the Date object
* from which the {@link #format} method is called.
* Add to / override these mappings for custom date formatting.
* Note: Ext.Date.format() treats characters as literals if an appropriate mapping cannot be found.
* Example:
* <pre><code>
Ext.Date.formatCodes.x = "Ext.util.Format.leftPad(this.getDate(), 2, '0')";
console.log(Ext.Date.format(new Date(), 'X'); // returns the current day of the month
</code></pre>
* @type Object
*/
formatCodes : {
d: "Ext.String.leftPad(this.getDate(), 2, '0')",
D: "Ext.Date.getShortDayName(this.getDay())", // get localised short day name
j: "this.getDate()",
l: "Ext.Date.dayNames[this.getDay()]",
N: "(this.getDay() ? this.getDay() : 7)",
S: "Ext.Date.getSuffix(this)",
w: "this.getDay()",
z: "Ext.Date.getDayOfYear(this)",
W: "Ext.String.leftPad(Ext.Date.getWeekOfYear(this), 2, '0')",
F: "Ext.Date.monthNames[this.getMonth()]",
m: "Ext.String.leftPad(this.getMonth() + 1, 2, '0')",
M: "Ext.Date.getShortMonthName(this.getMonth())", // get localised short month name
n: "(this.getMonth() + 1)",
t: "Ext.Date.getDaysInMonth(this)",
L: "(Ext.Date.isLeapYear(this) ? 1 : 0)",
o: "(this.getFullYear() + (Ext.Date.getWeekOfYear(this) == 1 && this.getMonth() > 0 ? +1 : (Ext.Date.getWeekOfYear(this) >= 52 && this.getMonth() < 11 ? -1 : 0)))",
Y: "Ext.String.leftPad(this.getFullYear(), 4, '0')",
y: "('' + this.getFullYear()).substring(2, 4)",
a: "(this.getHours() < 12 ? 'am' : 'pm')",
A: "(this.getHours() < 12 ? 'AM' : 'PM')",
g: "((this.getHours() % 12) ? this.getHours() % 12 : 12)",
G: "this.getHours()",
h: "Ext.String.leftPad((this.getHours() % 12) ? this.getHours() % 12 : 12, 2, '0')",
H: "Ext.String.leftPad(this.getHours(), 2, '0')",
i: "Ext.String.leftPad(this.getMinutes(), 2, '0')",
s: "Ext.String.leftPad(this.getSeconds(), 2, '0')",
u: "Ext.String.leftPad(this.getMilliseconds(), 3, '0')",
O: "Ext.Date.getGMTOffset(this)",
P: "Ext.Date.getGMTOffset(this, true)",
T: "Ext.Date.getTimezone(this)",
Z: "(this.getTimezoneOffset() * -60)",
c: function() { // ISO-8601 -- GMT format
for (var c = "Y-m-dTH:i:sP", code = [], i = 0, l = c.length; i < l; ++i) {
var e = c.charAt(i);
code.push(e == "T" ? "'T'" : utilDate.getFormatCode(e)); // treat T as a character literal
}
return code.join(" + ");
},
/*
c: function() { // ISO-8601 -- UTC format
return [
"this.getUTCFullYear()", "'-'",
"Ext.util.Format.leftPad(this.getUTCMonth() + 1, 2, '0')", "'-'",
"Ext.util.Format.leftPad(this.getUTCDate(), 2, '0')",
"'T'",
"Ext.util.Format.leftPad(this.getUTCHours(), 2, '0')", "':'",
"Ext.util.Format.leftPad(this.getUTCMinutes(), 2, '0')", "':'",
"Ext.util.Format.leftPad(this.getUTCSeconds(), 2, '0')",
"'Z'"
].join(" + ");
},
*/
U: "Math.round(this.getTime() / 1000)"
},
/**
* Checks if the passed Date parameters will cause a javascript Date "rollover".
* @param {Number} year 4-digit year
* @param {Number} month 1-based month-of-year
* @param {Number} day Day of month
* @param {Number} hour (optional) Hour
* @param {Number} minute (optional) Minute
* @param {Number} second (optional) Second
* @param {Number} millisecond (optional) Millisecond
* @return {Boolean} true if the passed parameters do not cause a Date "rollover", false otherwise.
*/
isValid : function(y, m, d, h, i, s, ms) {
// setup defaults
h = h || 0;
i = i || 0;
s = s || 0;
ms = ms || 0;
// Special handling for year < 100
var dt = utilDate.add(new Date(y < 100 ? 100 : y, m - 1, d, h, i, s, ms), utilDate.YEAR, y < 100 ? y - 100 : 0);
return y == dt.getFullYear() &&
m == dt.getMonth() + 1 &&
d == dt.getDate() &&
h == dt.getHours() &&
i == dt.getMinutes() &&
s == dt.getSeconds() &&
ms == dt.getMilliseconds();
},
/**
* Parses the passed string using the specified date format.
* Note that this function expects normal calendar dates, meaning that months are 1-based (i.e. 1 = January).
* The {@link #defaults} hash will be used for any date value (i.e. year, month, day, hour, minute, second or millisecond)
* which cannot be found in the passed string. If a corresponding default date value has not been specified in the {@link #defaults} hash,
* the current date's year, month, day or DST-adjusted zero-hour time value will be used instead.
* Keep in mind that the input date string must precisely match the specified format string
* in order for the parse operation to be successful (failed parse operations return a null value).
* <p>Example:</p><pre><code>
//dt = Fri May 25 2007 (current date)
var dt = new Date();
//dt = Thu May 25 2006 (today&#39;s month/day in 2006)
dt = Ext.Date.parse("2006", "Y");
//dt = Sun Jan 15 2006 (all date parts specified)
dt = Ext.Date.parse("2006-01-15", "Y-m-d");
//dt = Sun Jan 15 2006 15:20:01
dt = Ext.Date.parse("2006-01-15 3:20:01 PM", "Y-m-d g:i:s A");
// attempt to parse Sun Feb 29 2006 03:20:01 in strict mode
dt = Ext.Date.parse("2006-02-29 03:20:01", "Y-m-d H:i:s", true); // returns null
</code></pre>
* @param {String} input The raw date string.
* @param {String} format The expected date string format.
* @param {Boolean} strict (optional) True to validate date strings while parsing (i.e. prevents javascript Date "rollover")
(defaults to false). Invalid date strings will return null when parsed.
* @return {Date} The parsed Date.
*/
parse : function(input, format, strict) {
var p = utilDate.parseFunctions;
if (p[format] == null) {
utilDate.createParser(format);
}
return p[format](input, Ext.isDefined(strict) ? strict : utilDate.useStrict);
},
// Backwards compat
parseDate: function(input, format, strict){
return utilDate.parse(input, format, strict);
},
// private
getFormatCode : function(character) {
var f = utilDate.formatCodes[character];
if (f) {
f = typeof f == 'function'? f() : f;
utilDate.formatCodes[character] = f; // reassign function result to prevent repeated execution
}
// note: unknown characters are treated as literals
return f || ("'" + Ext.String.escape(character) + "'");
},
// private
createFormat : function(format) {
var code = [],
special = false,
ch = '';
for (var i = 0; i < format.length; ++i) {
ch = format.charAt(i);
if (!special && ch == "\\") {
special = true;
} else if (special) {
special = false;
code.push("'" + Ext.String.escape(ch) + "'");
} else {
code.push(utilDate.getFormatCode(ch));
}
}
utilDate.formatFunctions[format] = Ext.functionFactory("return " + code.join('+'));
},
// private
createParser : (function() {
var code = [
"var dt, y, m, d, h, i, s, ms, o, z, zz, u, v,",
"def = Ext.Date.defaults,",
"results = String(input).match(Ext.Date.parseRegexes[{0}]);", // either null, or an array of matched strings
"if(results){",
"{1}",
"if(u != null){", // i.e. unix time is defined
"v = new Date(u * 1000);", // give top priority to UNIX time
"}else{",
// create Date object representing midnight of the current day;
// this will provide us with our date defaults
// (note: clearTime() handles Daylight Saving Time automatically)
"dt = Ext.Date.clearTime(new Date);",
// date calculations (note: these calculations create a dependency on Ext.Number.from())
"y = Ext.Number.from(y, Ext.Number.from(def.y, dt.getFullYear()));",
"m = Ext.Number.from(m, Ext.Number.from(def.m - 1, dt.getMonth()));",
"d = Ext.Number.from(d, Ext.Number.from(def.d, dt.getDate()));",
// time calculations (note: these calculations create a dependency on Ext.Number.from())
"h = Ext.Number.from(h, Ext.Number.from(def.h, dt.getHours()));",
"i = Ext.Number.from(i, Ext.Number.from(def.i, dt.getMinutes()));",
"s = Ext.Number.from(s, Ext.Number.from(def.s, dt.getSeconds()));",
"ms = Ext.Number.from(ms, Ext.Number.from(def.ms, dt.getMilliseconds()));",
"if(z >= 0 && y >= 0){",
// both the year and zero-based day of year are defined and >= 0.
// these 2 values alone provide sufficient info to create a full date object
// create Date object representing January 1st for the given year
// handle years < 100 appropriately
"v = Ext.Date.add(new Date(y < 100 ? 100 : y, 0, 1, h, i, s, ms), Ext.Date.YEAR, y < 100 ? y - 100 : 0);",
// then add day of year, checking for Date "rollover" if necessary
"v = !strict? v : (strict === true && (z <= 364 || (Ext.Date.isLeapYear(v) && z <= 365))? Ext.Date.add(v, Ext.Date.DAY, z) : null);",
"}else if(strict === true && !Ext.Date.isValid(y, m + 1, d, h, i, s, ms)){", // check for Date "rollover"
"v = null;", // invalid date, so return null
"}else{",
// plain old Date object
// handle years < 100 properly
"v = Ext.Date.add(new Date(y < 100 ? 100 : y, m, d, h, i, s, ms), Ext.Date.YEAR, y < 100 ? y - 100 : 0);",
"}",
"}",
"}",
"if(v){",
// favour UTC offset over GMT offset
"if(zz != null){",
// reset to UTC, then add offset
"v = Ext.Date.add(v, Ext.Date.SECOND, -v.getTimezoneOffset() * 60 - zz);",
"}else if(o){",
// reset to GMT, then add offset
"v = Ext.Date.add(v, Ext.Date.MINUTE, -v.getTimezoneOffset() + (sn == '+'? -1 : 1) * (hr * 60 + mn));",
"}",
"}",
"return v;"
].join('\n');
return function(format) {
var regexNum = utilDate.parseRegexes.length,
currentGroup = 1,
calc = [],
regex = [],
special = false,
ch = "";
for (var i = 0; i < format.length; ++i) {
ch = format.charAt(i);
if (!special && ch == "\\") {
special = true;
} else if (special) {
special = false;
regex.push(Ext.String.escape(ch));
} else {
var obj = utilDate.formatCodeToRegex(ch, currentGroup);
currentGroup += obj.g;
regex.push(obj.s);
if (obj.g && obj.c) {
calc.push(obj.c);
}
}
}
utilDate.parseRegexes[regexNum] = new RegExp("^" + regex.join('') + "$", 'i');
utilDate.parseFunctions[format] = Ext.functionFactory("input", "strict", xf(code, regexNum, calc.join('')));
};
})(),
// private
parseCodes : {
/*
* Notes:
* g = {Number} calculation group (0 or 1. only group 1 contributes to date calculations.)
* c = {String} calculation method (required for group 1. null for group 0. {0} = currentGroup - position in regex result array)
* s = {String} regex pattern. all matches are stored in results[], and are accessible by the calculation mapped to 'c'
*/
d: {
g:1,
c:"d = parseInt(results[{0}], 10);\n",
s:"(\\d{2})" // day of month with leading zeroes (01 - 31)
},
j: {
g:1,
c:"d = parseInt(results[{0}], 10);\n",
s:"(\\d{1,2})" // day of month without leading zeroes (1 - 31)
},
D: function() {
for (var a = [], i = 0; i < 7; a.push(utilDate.getShortDayName(i)), ++i); // get localised short day names
return {
g:0,
c:null,
s:"(?:" + a.join("|") +")"
};
},
l: function() {
return {
g:0,
c:null,
s:"(?:" + utilDate.dayNames.join("|") + ")"
};
},
N: {
g:0,
c:null,
s:"[1-7]" // ISO-8601 day number (1 (monday) - 7 (sunday))
},
S: {
g:0,
c:null,
s:"(?:st|nd|rd|th)"
},
w: {
g:0,
c:null,
s:"[0-6]" // javascript day number (0 (sunday) - 6 (saturday))
},
z: {
g:1,
c:"z = parseInt(results[{0}], 10);\n",
s:"(\\d{1,3})" // day of the year (0 - 364 (365 in leap years))
},
W: {
g:0,
c:null,
s:"(?:\\d{2})" // ISO-8601 week number (with leading zero)
},
F: function() {
return {
g:1,
c:"m = parseInt(Ext.Date.getMonthNumber(results[{0}]), 10);\n", // get localised month number
s:"(" + utilDate.monthNames.join("|") + ")"
};
},
M: function() {
for (var a = [], i = 0; i < 12; a.push(utilDate.getShortMonthName(i)), ++i); // get localised short month names
return Ext.applyIf({
s:"(" + a.join("|") + ")"
}, utilDate.formatCodeToRegex("F"));
},
m: {
g:1,
c:"m = parseInt(results[{0}], 10) - 1;\n",
s:"(\\d{2})" // month number with leading zeros (01 - 12)
},
n: {
g:1,
c:"m = parseInt(results[{0}], 10) - 1;\n",
s:"(\\d{1,2})" // month number without leading zeros (1 - 12)
},
t: {
g:0,
c:null,
s:"(?:\\d{2})" // no. of days in the month (28 - 31)
},
L: {
g:0,
c:null,
s:"(?:1|0)"
},
o: function() {
return utilDate.formatCodeToRegex("Y");
},
Y: {
g:1,
c:"y = parseInt(results[{0}], 10);\n",
s:"(\\d{4})" // 4-digit year
},
y: {
g:1,
c:"var ty = parseInt(results[{0}], 10);\n"
+ "y = ty > Ext.Date.y2kYear ? 1900 + ty : 2000 + ty;\n", // 2-digit year
s:"(\\d{1,2})"
},
/*
* In the am/pm parsing routines, we allow both upper and lower case
* even though it doesn't exactly match the spec. It gives much more flexibility
* in being able to specify case insensitive regexes.
*/
a: {
g:1,
c:"if (/(am)/i.test(results[{0}])) {\n"
+ "if (!h || h == 12) { h = 0; }\n"
+ "} else { if (!h || h < 12) { h = (h || 0) + 12; }}",
s:"(am|pm|AM|PM)"
},
A: {
g:1,
c:"if (/(am)/i.test(results[{0}])) {\n"
+ "if (!h || h == 12) { h = 0; }\n"
+ "} else { if (!h || h < 12) { h = (h || 0) + 12; }}",
s:"(AM|PM|am|pm)"
},
g: function() {
return utilDate.formatCodeToRegex("G");
},
G: {
g:1,
c:"h = parseInt(results[{0}], 10);\n",
s:"(\\d{1,2})" // 24-hr format of an hour without leading zeroes (0 - 23)
},
h: function() {
return utilDate.formatCodeToRegex("H");
},
H: {
g:1,
c:"h = parseInt(results[{0}], 10);\n",
s:"(\\d{2})" // 24-hr format of an hour with leading zeroes (00 - 23)
},
i: {
g:1,
c:"i = parseInt(results[{0}], 10);\n",
s:"(\\d{2})" // minutes with leading zeros (00 - 59)
},
s: {
g:1,
c:"s = parseInt(results[{0}], 10);\n",
s:"(\\d{2})" // seconds with leading zeros (00 - 59)
},
u: {
g:1,
c:"ms = results[{0}]; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n",
s:"(\\d+)" // decimal fraction of a second (minimum = 1 digit, maximum = unlimited)
},
O: {
g:1,
c:[
"o = results[{0}];",
"var sn = o.substring(0,1),", // get + / - sign
"hr = o.substring(1,3)*1 + Math.floor(o.substring(3,5) / 60),", // get hours (performs minutes-to-hour conversion also, just in case)
"mn = o.substring(3,5) % 60;", // get minutes
"o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + Ext.String.leftPad(hr, 2, '0') + Ext.String.leftPad(mn, 2, '0')) : null;\n" // -12hrs <= GMT offset <= 14hrs
].join("\n"),
s: "([+\-]\\d{4})" // GMT offset in hrs and mins
},
P: {
g:1,
c:[
"o = results[{0}];",
"var sn = o.substring(0,1),", // get + / - sign
"hr = o.substring(1,3)*1 + Math.floor(o.substring(4,6) / 60),", // get hours (performs minutes-to-hour conversion also, just in case)
"mn = o.substring(4,6) % 60;", // get minutes
"o = ((-12 <= (hr*60 + mn)/60) && ((hr*60 + mn)/60 <= 14))? (sn + Ext.String.leftPad(hr, 2, '0') + Ext.String.leftPad(mn, 2, '0')) : null;\n" // -12hrs <= GMT offset <= 14hrs
].join("\n"),
s: "([+\-]\\d{2}:\\d{2})" // GMT offset in hrs and mins (with colon separator)
},
T: {
g:0,
c:null,
s:"[A-Z]{1,4}" // timezone abbrev. may be between 1 - 4 chars
},
Z: {
g:1,
c:"zz = results[{0}] * 1;\n" // -43200 <= UTC offset <= 50400
+ "zz = (-43200 <= zz && zz <= 50400)? zz : null;\n",
s:"([+\-]?\\d{1,5})" // leading '+' sign is optional for UTC offset
},
c: function() {
var calc = [],
arr = [
utilDate.formatCodeToRegex("Y", 1), // year
utilDate.formatCodeToRegex("m", 2), // month
utilDate.formatCodeToRegex("d", 3), // day
utilDate.formatCodeToRegex("h", 4), // hour
utilDate.formatCodeToRegex("i", 5), // minute
utilDate.formatCodeToRegex("s", 6), // second
{c:"ms = results[7] || '0'; ms = parseInt(ms, 10)/Math.pow(10, ms.length - 3);\n"}, // decimal fraction of a second (minimum = 1 digit, maximum = unlimited)
{c:[ // allow either "Z" (i.e. UTC) or "-0530" or "+08:00" (i.e. UTC offset) timezone delimiters. assumes local timezone if no timezone is specified
"if(results[8]) {", // timezone specified
"if(results[8] == 'Z'){",
"zz = 0;", // UTC
"}else if (results[8].indexOf(':') > -1){",
utilDate.formatCodeToRegex("P", 8).c, // timezone offset with colon separator
"}else{",
utilDate.formatCodeToRegex("O", 8).c, // timezone offset without colon separator
"}",
"}"
].join('\n')}
];
for (var i = 0, l = arr.length; i < l; ++i) {
calc.push(arr[i].c);
}
return {
g:1,
c:calc.join(""),
s:[
arr[0].s, // year (required)
"(?:", "-", arr[1].s, // month (optional)
"(?:", "-", arr[2].s, // day (optional)
"(?:",
"(?:T| )?", // time delimiter -- either a "T" or a single blank space
arr[3].s, ":", arr[4].s, // hour AND minute, delimited by a single colon (optional). MUST be preceded by either a "T" or a single blank space
"(?::", arr[5].s, ")?", // seconds (optional)
"(?:(?:\\.|,)(\\d+))?", // decimal fraction of a second (e.g. ",12345" or ".98765") (optional)
"(Z|(?:[-+]\\d{2}(?::)?\\d{2}))?", // "Z" (UTC) or "-0530" (UTC offset without colon delimiter) or "+08:00" (UTC offset with colon delimiter) (optional)
")?",
")?",
")?"
].join("")
};
},
U: {
g:1,
c:"u = parseInt(results[{0}], 10);\n",
s:"(-?\\d+)" // leading minus sign indicates seconds before UNIX epoch
}
},
//Old Ext.Date prototype methods.
// private
dateFormat: function(date, format) {
return utilDate.format(date, format);
},
/**
* Formats a date given the supplied format string.
* @param {Date} date The date to format
* @param {String} format The format string
* @return {String} The formatted date
*/
format: function(date, format) {
if (utilDate.formatFunctions[format] == null) {
utilDate.createFormat(format);
}
var result = utilDate.formatFunctions[format].call(date);
return result + '';
},
/**
* Get the timezone abbreviation of the current date (equivalent to the format specifier 'T').
*
* Note: The date string returned by the javascript Date object's toString() method varies
* between browsers (e.g. FF vs IE) and system region settings (e.g. IE in Asia vs IE in America).
* For a given date string e.g. "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula Standard Time)",
* getTimezone() first tries to get the timezone abbreviation from between a pair of parentheses
* (which may or may not be present), failing which it proceeds to get the timezone abbreviation
* from the GMT offset portion of the date string.
* @param {Date} date The date
* @return {String} The abbreviated timezone name (e.g. 'CST', 'PDT', 'EDT', 'MPST' ...).
*/
getTimezone : function(date) {
// the following list shows the differences between date strings from different browsers on a WinXP SP2 machine from an Asian locale:
//
// Opera : "Thu, 25 Oct 2007 22:53:45 GMT+0800" -- shortest (weirdest) date string of the lot
// Safari : "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula Standard Time)" -- value in parentheses always gives the correct timezone (same as FF)
// FF : "Thu Oct 25 2007 22:55:35 GMT+0800 (Malay Peninsula Standard Time)" -- value in parentheses always gives the correct timezone
// IE : "Thu Oct 25 22:54:35 UTC+0800 2007" -- (Asian system setting) look for 3-4 letter timezone abbrev
// IE : "Thu Oct 25 17:06:37 PDT 2007" -- (American system setting) look for 3-4 letter timezone abbrev
//
// this crazy regex attempts to guess the correct timezone abbreviation despite these differences.
// step 1: (?:\((.*)\) -- find timezone in parentheses
// step 2: ([A-Z]{1,4})(?:[\-+][0-9]{4})?(?: -?\d+)?) -- if nothing was found in step 1, find timezone from timezone offset portion of date string
// step 3: remove all non uppercase characters found in step 1 and 2
return date.toString().replace(/^.* (?:\((.*)\)|([A-Z]{1,4})(?:[\-+][0-9]{4})?(?: -?\d+)?)$/, "$1$2").replace(/[^A-Z]/g, "");
},
/**
* Get the offset from GMT of the current date (equivalent to the format specifier 'O').
* @param {Date} date The date
* @param {Boolean} colon (optional) true to separate the hours and minutes with a colon (defaults to false).
* @return {String} The 4-character offset string prefixed with + or - (e.g. '-0600').
*/
getGMTOffset : function(date, colon) {
var offset = date.getTimezoneOffset();
return (offset > 0 ? "-" : "+")
+ Ext.String.leftPad(Math.floor(Math.abs(offset) / 60), 2, "0")
+ (colon ? ":" : "")
+ Ext.String.leftPad(Math.abs(offset % 60), 2, "0");
},
/**
* Get the numeric day number of the year, adjusted for leap year.
* @param {Date} date The date
* @return {Number} 0 to 364 (365 in leap years).
*/
getDayOfYear: function(date) {
var num = 0,
d = Ext.Date.clone(date),
m = date.getMonth(),
i;
for (i = 0, d.setDate(1), d.setMonth(0); i < m; d.setMonth(++i)) {
num += utilDate.getDaysInMonth(d);
}
return num + date.getDate() - 1;
},
/**
* Get the numeric ISO-8601 week number of the year.
* (equivalent to the format specifier 'W', but without a leading zero).
* @param {Date} date The date
* @return {Number} 1 to 53
* @method
*/
getWeekOfYear : (function() {
// adapted from http://www.merlyn.demon.co.uk/weekcalc.htm
var ms1d = 864e5, // milliseconds in a day
ms7d = 7 * ms1d; // milliseconds in a week
return function(date) { // return a closure so constants get calculated only once
var DC3 = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate() + 3) / ms1d, // an Absolute Day Number
AWN = Math.floor(DC3 / 7), // an Absolute Week Number
Wyr = new Date(AWN * ms7d).getUTCFullYear();
return AWN - Math.floor(Date.UTC(Wyr, 0, 7) / ms7d) + 1;
};
})(),
/**
* Checks if the current date falls within a leap year.
* @param {Date} date The date
* @return {Boolean} True if the current date falls within a leap year, false otherwise.
*/
isLeapYear : function(date) {
var year = date.getFullYear();
return !!((year & 3) == 0 && (year % 100 || (year % 400 == 0 && year)));
},
/**
* Get the first day of the current month, adjusted for leap year. The returned value
* is the numeric day index within the week (0-6) which can be used in conjunction with
* the {@link #monthNames} array to retrieve the textual day name.
* Example:
* <pre><code>
var dt = new Date('1/10/2007'),
firstDay = Ext.Date.getFirstDayOfMonth(dt);
console.log(Ext.Date.dayNames[firstDay]); //output: 'Monday'
* </code></pre>
* @param {Date} date The date
* @return {Number} The day number (0-6).
*/
getFirstDayOfMonth : function(date) {
var day = (date.getDay() - (date.getDate() - 1)) % 7;
return (day < 0) ? (day + 7) : day;
},
/**
* Get the last day of the current month, adjusted for leap year. The returned value
* is the numeric day index within the week (0-6) which can be used in conjunction with
* the {@link #monthNames} array to retrieve the textual day name.
* Example:
* <pre><code>
var dt = new Date('1/10/2007'),
lastDay = Ext.Date.getLastDayOfMonth(dt);
console.log(Ext.Date.dayNames[lastDay]); //output: 'Wednesday'
* </code></pre>
* @param {Date} date The date
* @return {Number} The day number (0-6).
*/
getLastDayOfMonth : function(date) {
return utilDate.getLastDateOfMonth(date).getDay();
},
/**
* Get the date of the first day of the month in which this date resides.
* @param {Date} date The date
* @return {Date}
*/
getFirstDateOfMonth : function(date) {
return new Date(date.getFullYear(), date.getMonth(), 1);
},
/**
* Get the date of the last day of the month in which this date resides.
* @param {Date} date The date
* @return {Date}
*/
getLastDateOfMonth : function(date) {
return new Date(date.getFullYear(), date.getMonth(), utilDate.getDaysInMonth(date));
},
/**
* Get the number of days in the current month, adjusted for leap year.
* @param {Date} date The date
* @return {Number} The number of days in the month.
* @method
*/
getDaysInMonth: (function() {
var daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return function(date) { // return a closure for efficiency
var m = date.getMonth();
return m == 1 && utilDate.isLeapYear(date) ? 29 : daysInMonth[m];
};
})(),
/**
* Get the English ordinal suffix of the current day (equivalent to the format specifier 'S').
* @param {Date} date The date
* @return {String} 'st, 'nd', 'rd' or 'th'.
*/
getSuffix : function(date) {
switch (date.getDate()) {
case 1:
case 21:
case 31:
return "st";
case 2:
case 22:
return "nd";
case 3:
case 23:
return "rd";
default:
return "th";
}
},
/**
* Creates and returns a new Date instance with the exact same date value as the called instance.
* Dates are copied and passed by reference, so if a copied date variable is modified later, the original
* variable will also be changed. When the intention is to create a new variable that will not
* modify the original instance, you should create a clone.
*
* Example of correctly cloning a date:
* <pre><code>
//wrong way:
var orig = new Date('10/1/2006');
var copy = orig;
copy.setDate(5);
console.log(orig); //returns 'Thu Oct 05 2006'!
//correct way:
var orig = new Date('10/1/2006'),
copy = Ext.Date.clone(orig);
copy.setDate(5);
console.log(orig); //returns 'Thu Oct 01 2006'
* </code></pre>
* @param {Date} date The date
* @return {Date} The new Date instance.
*/
clone : function(date) {
return new Date(date.getTime());
},
/**
* Checks if the current date is affected by Daylight Saving Time (DST).
* @param {Date} date The date
* @return {Boolean} True if the current date is affected by DST.
*/
isDST : function(date) {
// adapted from http://sencha.com/forum/showthread.php?p=247172#post247172
// courtesy of @geoffrey.mcgill
return new Date(date.getFullYear(), 0, 1).getTimezoneOffset() != date.getTimezoneOffset();
},
/**
* Attempts to clear all time information from this Date by setting the time to midnight of the same day,
* automatically adjusting for Daylight Saving Time (DST) where applicable.
* (note: DST timezone information for the browser's host operating system is assumed to be up-to-date)
* @param {Date} date The date
* @param {Boolean} clone true to create a clone of this date, clear the time and return it (defaults to false).
* @return {Date} this or the clone.
*/
clearTime : function(date, clone) {
if (clone) {
return Ext.Date.clearTime(Ext.Date.clone(date));
}
// get current date before clearing time
var d = date.getDate();
// clear time
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
if (date.getDate() != d) { // account for DST (i.e. day of month changed when setting hour = 0)
// note: DST adjustments are assumed to occur in multiples of 1 hour (this is almost always the case)
// refer to http://www.timeanddate.com/time/aboutdst.html for the (rare) exceptions to this rule
// increment hour until cloned date == current date
for (var hr = 1, c = utilDate.add(date, Ext.Date.HOUR, hr); c.getDate() != d; hr++, c = utilDate.add(date, Ext.Date.HOUR, hr));
date.setDate(d);
date.setHours(c.getHours());
}
return date;
},
/**
* Provides a convenient method for performing basic date arithmetic. This method
* does not modify the Date instance being called - it creates and returns
* a new Date instance containing the resulting date value.
*
* Examples:
* <pre><code>
// Basic usage:
var dt = Ext.Date.add(new Date('10/29/2006'), Ext.Date.DAY, 5);
console.log(dt); //returns 'Fri Nov 03 2006 00:00:00'
// Negative values will be subtracted:
var dt2 = Ext.Date.add(new Date('10/1/2006'), Ext.Date.DAY, -5);
console.log(dt2); //returns 'Tue Sep 26 2006 00:00:00'
* </code></pre>
*
* @param {Date} date The date to modify
* @param {String} interval A valid date interval enum value.
* @param {Number} value The amount to add to the current date.
* @return {Date} The new Date instance.
*/
add : function(date, interval, value) {
var d = Ext.Date.clone(date),
Date = Ext.Date;
if (!interval || value === 0) return d;
switch(interval.toLowerCase()) {
case Ext.Date.MILLI:
d.setMilliseconds(d.getMilliseconds() + value);
break;
case Ext.Date.SECOND:
d.setSeconds(d.getSeconds() + value);
break;
case Ext.Date.MINUTE:
d.setMinutes(d.getMinutes() + value);
break;
case Ext.Date.HOUR:
d.setHours(d.getHours() + value);
break;
case Ext.Date.DAY:
d.setDate(d.getDate() + value);
break;
case Ext.Date.MONTH:
var day = date.getDate();
if (day > 28) {
day = Math.min(day, Ext.Date.getLastDateOfMonth(Ext.Date.add(Ext.Date.getFirstDateOfMonth(date), 'mo', value)).getDate());
}
d.setDate(day);
d.setMonth(date.getMonth() + value);
break;
case Ext.Date.YEAR:
d.setFullYear(date.getFullYear() + value);
break;
}
return d;
},
/**
* Checks if a date falls on or between the given start and end dates.
* @param {Date} date The date to check
* @param {Date} start Start date
* @param {Date} end End date
* @return {Boolean} true if this date falls on or between the given start and end dates.
*/
between : function(date, start, end) {
var t = date.getTime();
return start.getTime() <= t && t <= end.getTime();
}
};
var utilDate = Ext.DateExtras;
Ext.apply(Ext.Date, utilDate);
})();
/**
* @private
*/
Ext.define('Ext.fx.Easing', {
requires: ['Ext.fx.easing.Linear'],
constructor: function(easing) {
return Ext.factory(easing, Ext.fx.easing.Linear, null, 'easing');
}
});
/**
* @private
*
* This easing is typically used for {@link Ext.scroll.Scroller}. It's a combination of
* {@link Ext.fx.easing.Momentum} and {@link Ext.fx.easing.Bounce}, which emulates deceleration when the animated element
* is still within its boundary, then bouncing back (snapping) when it's out-of-bound.
*/
Ext.define('Ext.fx.easing.BoundMomentum', {
extend: 'Ext.fx.easing.Abstract',
requires: [
'Ext.fx.easing.Momentum',
'Ext.fx.easing.Bounce'
],
config: {
/**
* @cfg {Object} momentum
* A valid config object for {@link Ext.fx.easing.Momentum}
* @accessor
*/
momentum: null,
/**
* @cfg {Object} bounce
* A valid config object for {@link Ext.fx.easing.Bounce}
* @accessor
*/
bounce: null,
minMomentumValue: 0,
maxMomentumValue: 0,
/**
* @cfg {Number} minVelocity
* The minimum velocity to end this easing
* @accessor
*/
minVelocity: 0.01,
/**
* @cfg {Number} startVelocity
* The start velocity
* @accessor
*/
startVelocity: 0
},
applyMomentum: function(config, currentEasing) {
return Ext.factory(config, Ext.fx.easing.Momentum, currentEasing);
},
applyBounce: function(config, currentEasing) {
return Ext.factory(config, Ext.fx.easing.Bounce, currentEasing);
},
updateStartTime: function(startTime) {
this.getMomentum().setStartTime(startTime);
this.callParent(arguments);
},
updateStartVelocity: function(startVelocity) {
this.getMomentum().setStartVelocity(startVelocity);
},
updateStartValue: function(startValue) {
this.getMomentum().setStartValue(startValue);
},
reset: function() {
this.lastValue = null;
this.isBouncingBack = false;
this.isOutOfBound = false;
return this.callParent(arguments);
},
getValue: function() {
var momentum = this.getMomentum(),
bounce = this.getBounce(),
startVelocity = momentum.getStartVelocity(),
direction = startVelocity > 0 ? 1 : -1,
minValue = this.getMinMomentumValue(),
maxValue = this.getMaxMomentumValue(),
boundedValue = (direction == 1) ? maxValue : minValue,
lastValue = this.lastValue,
value, velocity;
if (startVelocity === 0) {
return this.getStartValue();
}
if (!this.isOutOfBound) {
value = momentum.getValue();
velocity = momentum.getVelocity();
if (Math.abs(velocity) < this.getMinVelocity()) {
this.isEnded = true;
}
if (value >= minValue && value <= maxValue) {
return value;
}
this.isOutOfBound = true;
bounce.setStartTime(Ext.Date.now())
.setStartVelocity(velocity)
.setStartValue(boundedValue);
}
value = bounce.getValue();
if (!this.isEnded) {
if (!this.isBouncingBack) {
if (lastValue !== null) {
if ((direction == 1 && value < lastValue) || (direction == -1 && value > lastValue)) {
this.isBouncingBack = true;
}
}
}
else {
if (Math.round(value) == boundedValue) {
this.isEnded = true;
}
}
}
this.lastValue = value;
return value;
}
});
/**
* @private
*/
Ext.define('Ext.fx.easing.EaseIn', {
extend: 'Ext.fx.easing.Linear',
alias: 'easing.ease-in',
config: {
exponent: 4,
duration: 1500
},
getValue: function() {
var deltaTime = Ext.Date.now() - this.getStartTime(),
duration = this.getDuration(),
startValue = this.getStartValue(),
endValue = this.getEndValue(),
distance = this.distance,
theta = deltaTime / duration,
thetaEnd = Math.pow(theta, this.getExponent()),
currentValue = startValue + (thetaEnd * distance);
if (deltaTime >= duration) {
this.isEnded = true;
return endValue;
}
return currentValue;
}
});
/**
* @private
*/
Ext.define('Ext.fx.easing.EaseOut', {
extend: 'Ext.fx.easing.Linear',
alias: 'easing.ease-out',
config: {
exponent: 4,
duration: 1500
},
getValue: function() {
var deltaTime = Ext.Date.now() - this.getStartTime(),
duration = this.getDuration(),
startValue = this.getStartValue(),
endValue = this.getEndValue(),
distance = this.distance,
theta = deltaTime / duration,
thetaC = 1 - theta,
thetaEnd = 1 - Math.pow(thetaC, this.getExponent()),
currentValue = startValue + (thetaEnd * distance);
if (deltaTime >= duration) {
this.isEnded = true;
return endValue;
}
return currentValue;
}
});
Ext.define('Ext.log.formatter.Default', {
extend: 'Ext.log.formatter.Formatter',
config: {
messageFormat: "[{priorityName}][{callerDisplayName}] {message}"
},
format: function(event) {
var event = Ext.merge({}, event, {
priorityName: event.priorityName.toUpperCase()
});
return this.callParent([event]);
}
});
Ext.define('Ext.log.formatter.Identity', {
extend: 'Ext.log.formatter.Default',
config: {
messageFormat: "[{osIdentity}][{browserIdentity}][{timestamp}][{priorityName}][{callerDisplayName}] {message}"
},
format: function(event) {
event.timestamp = Ext.Date.toString();
event.browserIdentity = Ext.browser.name + ' ' + Ext.browser.version;
event.osIdentity = Ext.os.name + ' ' + Ext.os.version;
return this.callParent(arguments);
}
});
Ext.define('Ext.log.writer.Console', {
extend: 'Ext.log.writer.Writer',
config: {
throwOnErrors: true,
throwOnWarnings: false
},
doWrite: function(event) {
var message = event.message,
priority = event.priorityName,
consoleMethod;
if (priority === 'error' && this.getThrowOnErrors()) {
throw new Error(message);
}
if (typeof console !== 'undefined') {
consoleMethod = priority;
if (consoleMethod === 'deprecate') {
consoleMethod = 'warn';
}
if (consoleMethod === 'warn' && this.getThrowOnWarnings()) {
throw new Error(message);
}
if (!(consoleMethod in console)) {
consoleMethod = 'log';
}
console[consoleMethod](message);
}
}
});
Ext.define('Ext.log.writer.DocumentTitle', {
extend: 'Ext.log.writer.Writer',
doWrite: function(event) {
var message = event.message;
document.title = message;
}
});
/**
* @private
*/
Ext.define('Ext.mixin.Filterable', {
extend: 'Ext.mixin.Mixin',
requires: [
'Ext.util.Filter'
],
mixinConfig: {
id: 'filterable'
},
config: {
/**
* @cfg {Array} filters
* An array with filters. A filter can be an instance of Ext.util.Filter,
* an object representing an Ext.util.Filter configuration, or a filter function.
*/
filters: null,
/**
* @cfg {String} filterRoot
* The root inside each item in which the properties exist that we want to filter on.
* This is useful for filtering records in which the data exists inside a 'data' property.
*/
filterRoot: null
},
/**
* @property {Boolean} dirtyFilterFn
* A flag indicating wether the currently cashed filter function is still valid. Read-only.
*/
dirtyFilterFn: false,
/**
* @property currentSortFn
* This is the cached sorting function which is a generated function that calls all the
* configured sorters in the correct order. This is a read-only property.
*/
filterFn: null,
/**
* @property {Boolean} filtered
* A read-only flag indicating if this object is filtered
*/
filtered: false,
applyFilters: function(filters, collection) {
if (!collection) {
collection = this.createFiltersCollection();
}
collection.clear();
this.filtered = false;
this.dirtyFilterFn = true;
if (filters) {
this.addFilters(filters);
}
return collection;
},
createFiltersCollection: function() {
this._filters = Ext.create('Ext.util.Collection', function(filter) {
return filter.getId();
});
return this._filters;
},
/**
* This method adds a filter.
* @param {Ext.util.Sorter/Function/Object} filter Can be an instance of Ext.util.Filter,
* an object representing an Ext.util.Filter configuration, or a filter function.
*/
addFilter: function(filter) {
this.addFilters([filter]);
},
/**
* This method adds all the filters in a passed array.
* @param {Array} filters An array with filters. A filter can be an instance of Ext.util.Filter,
* an object representing an Ext.util.Filter configuration, or a filter function.
*/
addFilters: function(filters) {
var currentFilters = this.getFilters();
return this.insertFilters(currentFilters ? currentFilters.length : 0, filters);
},
/**
* This method adds a filter at a given index.
* @param {Number} index The index at which to insert the filter.
* @param {Ext.util.Sorter/Function/Object} filter Can be an instance of Ext.util.Filter,
* an object representing an Ext.util.Filter configuration, or a filter function.
*/
insertFilter: function(index, filter) {
return this.insertFilters(index, [filter]);
},
/**
* This method inserts all the filters in the passed array at the given index.
* @param {Number} index The index at which to insert the filters.
* @param {Array} filters Each filter can be an instance of Ext.util.Filter,
* an object representing an Ext.util.Filter configuration, or a filter function.
*/
insertFilters: function(index, filters) {
// We begin by making sure we are dealing with an array of sorters
if (!Ext.isArray(filters)) {
filters = [filters];
}
var ln = filters.length,
filterRoot = this.getFilterRoot(),
currentFilters = this.getFilters(),
newFilters = [],
filterConfig, i, filter;
if (!currentFilters) {
currentFilters = this.createFiltersCollection();
}
// We first have to convert every sorter into a proper Sorter instance
for (i = 0; i < ln; i++) {
filter = filters[i];
filterConfig = {
root: filterRoot
};
if (Ext.isFunction(filter)) {
filterConfig.filterFn = filter;
}
// If we are dealing with an object, we assume its a Sorter configuration. In this case
// we create an instance of Sorter passing this configuration.
else if (Ext.isObject(filter)) {
if (!filter.isFilter) {
if (filter.fn) {
filter.filterFn = filter.fn;
delete filter.fn;
}
filterConfig = Ext.apply(filterConfig, filter);
}
else {
newFilters.push(filter);
if (!filter.getRoot()) {
filter.setRoot(filterRoot);
}
continue;
}
}
// Finally we get to the point where it has to be invalid
else {
Ext.Logger.warn('Invalid filter specified:', filter);
}
// If a sorter config was created, make it an instance
filter = Ext.create('Ext.util.Filter', filterConfig);
newFilters.push(filter);
}
// Now lets add the newly created sorters.
for (i = 0, ln = newFilters.length; i < ln; i++) {
currentFilters.insert(index + i, newFilters[i]);
}
this.dirtyFilterFn = true;
if (currentFilters.length) {
this.filtered = true;
}
return currentFilters;
},
/**
* This method removes all the filters in a passed array.
* @param {Array} filters Each value in the array can be a string (property name),
* function (sorterFn), an object containing a property and value keys or
* {@link Ext.util.Sorter Sorter} instance.
*/
removeFilters: function(filters) {
// We begin by making sure we are dealing with an array of sorters
if (!Ext.isArray(filters)) {
filters = [filters];
}
var ln = filters.length,
currentFilters = this.getFilters(),
i, filter;
for (i = 0; i < ln; i++) {
filter = filters[i];
if (typeof filter === 'string') {
currentFilters.each(function(item) {
if (item.getProperty() === filter) {
currentFilters.remove(item);
}
});
}
else if (typeof filter === 'function') {
currentFilters.each(function(item) {
if (item.getFilterFn() === filter) {
currentFilters.remove(item);
}
});
}
else {
if (filter.isFilter) {
currentFilters.remove(filter);
}
else if (filter.property !== undefined && filter.value !== undefined) {
currentFilters.each(function(item) {
if (item.getProperty() === filter.property && item.getValue() === filter.value) {
currentFilters.remove(item);
}
});
}
}
}
if (!currentFilters.length) {
this.filtered = false;
}
},
/**
* This updates the cached sortFn based on the current sorters.
* @return {Function} sortFn The generated sort function.
* @private
*/
updateFilterFn: function() {
var filters = this.getFilters().items;
this.filterFn = function(item) {
var isMatch = true,
length = filters.length,
i;
for (i = 0; i < length; i++) {
var filter = filters[i],
fn = filter.getFilterFn(),
scope = filter.getScope() || this;
isMatch = isMatch && fn.call(scope, item);
}
return isMatch;
};
this.dirtyFilterFn = false;
return this.filterFn;
},
/**
* This method will sort an array based on the currently configured {@link Ext.data.Store#sorters sorters}.
* @param {Array} data The array you want to have sorted
* @return {Array} The array you passed after it is sorted
*/
filter: function(data) {
return this.getFilters().length ? Ext.Array.filter(data, this.getFilterFn()) : data;
},
isFiltered: function(item) {
return this.getFilters().length ? !this.getFilterFn()(item) : false;
},
/**
* Returns an up to date sort function.
* @return {Function} sortFn The sort function.
*/
getFilterFn: function() {
if (this.dirtyFilterFn) {
return this.updateFilterFn();
}
return this.filterFn;
}
});
/**
* Mixin that provides a common interface for publishing events. Classes using this mixin can use the {@link #fireEvent}
* and {@link #fireAction} methods to notify listeners of events on the class.
*
* Classes can also define a {@link #listeners} config to add an event hanler to the current object. See
* {@link #addListener} for more details.
*
* ## Example
*
* Ext.define('Employee', {
* mixins: ['Ext.mixin.Observable'],
*
* config: {
* fullName: ''
* },
*
* constructor: function(config) {
* this.initConfig(config); // We need to initialize the config options when the class is instantiated
* },
*
* quitJob: function() {
* this.fireEvent('quit');
* }
* });
*
* var newEmployee = Ext.create('Employee', {
*
* fullName: 'Ed Spencer',
*
* listeners: {
* quit: function() { // This function will be called when the 'quit' event is fired
* // By default, "this" will be the object that fired the event.
* console.log(this.getFullName() + " has quit!");
* }
* }
* });
*
* newEmployee.quitJob(); // Will log 'Ed Spencer has quit!'
*
* @aside guide events
*/
Ext.define('Ext.mixin.Observable', {
requires: ['Ext.event.Dispatcher'],
extend: 'Ext.mixin.Mixin',
mixins: ['Ext.mixin.Identifiable'],
mixinConfig: {
id: 'observable',
hooks: {
destroy: 'destroy'
}
},
alternateClassName: 'Ext.util.Observable',
// @private
isObservable: true,
observableType: 'observable',
validIdRegex: /^([\w\-]+)$/,
observableIdPrefix: '#',
listenerOptionsRegex: /^(?:delegate|single|delay|buffer|args|prepend)$/,
config: {
/**
* @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.
*
* See the [Event guide](#!/guide/events) for more
*
* **Note** it is bad practice to specify a listeners config when you are defining a class using Ext.define.
* Instead, only specify listeners when you are instantiating your class with Ext.create.
* @accessor
*/
listeners: null,
/**
* @cfg {String/String[]} bubbleEvents The event name to bubble, or an Array of event names.
* @accessor
*/
bubbleEvents: null
},
constructor: function(config) {
this.initConfig(config);
},
applyListeners: function(listeners) {
if (listeners) {
this.addListener(listeners);
}
},
applyBubbleEvents: function(bubbleEvents) {
if (bubbleEvents) {
this.enableBubble(bubbleEvents);
}
},
getOptimizedObservableId: function() {
return this.observableId;
},
getObservableId: function() {
if (!this.observableId) {
var id = this.getUniqueId();
if (!id.match(this.validIdRegex)) {
Ext.Logger.error("Invalid unique id of '" + id + "' for this object", this);
}
this.observableId = this.observableIdPrefix + id;
this.getObservableId = this.getOptimizedObservableId;
}
return this.observableId;
},
getOptimizedEventDispatcher: function() {
return this.eventDispatcher;
},
getEventDispatcher: function() {
if (!this.eventDispatcher) {
this.eventDispatcher = Ext.event.Dispatcher.getInstance();
this.getEventDispatcher = this.getOptimizedEventDispatcher;
this.getListeners();
this.getBubbleEvents();
}
return this.eventDispatcher;
},
getManagedListeners: function(object, eventName) {
var id = object.getUniqueId(),
managedListeners = this.managedListeners;
if (!managedListeners) {
this.managedListeners = managedListeners = {};
}
if (!managedListeners[id]) {
managedListeners[id] = {};
object.doAddListener('destroy', 'clearManagedListeners', this, {
single: true,
args: [object]
});
}
if (!managedListeners[id][eventName]) {
managedListeners[id][eventName] = [];
}
return managedListeners[id][eventName];
},
getUsedSelectors: function() {
var selectors = this.usedSelectors;
if (!selectors) {
selectors = this.usedSelectors = [];
selectors.$map = {};
}
return selectors;
},
/**
* 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 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 args = Array.prototype.slice.call(arguments, 1);
return this.doFireEvent(eventName, args);
},
/**
* Fires the specified event with the passed parameters and execute a function (action)
* at the end if there are no listeners that return false.
*
* @param {String} eventName The name of the event to fire.
* @param {Array} args Arguments to pass to handers
* @param {Function} fn Action
* @param {Object} scope scope of fn
*/
fireAction: function(eventName, args, fn, scope, options, order) {
var fnType = typeof fn,
action;
if (args === undefined) {
args = [];
}
if (fnType != 'undefined') {
action = {
fn: fn,
isLateBinding: fnType == 'string',
scope: scope || this,
options: options || {},
order: order
};
}
return this.doFireEvent(eventName, args, action);
},
doFireEvent: function(eventName, args, action, connectedController) {
if (this.eventFiringSuspended) {
return;
}
var id = this.getObservableId(),
dispatcher = this.getEventDispatcher();
return dispatcher.dispatchEvent(this.observableType, id, eventName, args, action, connectedController);
},
/**
* @private
* @param name
* @param fn
* @param scope
* @param options
*/
doAddListener: function(name, fn, scope, options, order) {
var isManaged = (scope && scope !== this && scope.isIdentifiable),
usedSelectors = this.getUsedSelectors(),
usedSelectorsMap = usedSelectors.$map,
selector = this.getObservableId(),
isAdded, managedListeners, delegate;
if (!options) {
options = {};
}
if (!scope) {
scope = this;
}
if (options.delegate) {
delegate = options.delegate;
// See https://sencha.jira.com/browse/TOUCH-1579
selector += ' ' + delegate;
}
if (!(selector in usedSelectorsMap)) {
usedSelectorsMap[selector] = true;
usedSelectors.push(selector);
}
isAdded = this.addDispatcherListener(selector, name, fn, scope, options, order);
if (isAdded && isManaged) {
managedListeners = this.getManagedListeners(scope, name);
managedListeners.push({
delegate: delegate,
scope: scope,
fn: fn,
order: order
});
}
return isAdded;
},
addDispatcherListener: function(selector, name, fn, scope, options, order) {
return this.getEventDispatcher().addListener(this.observableType, selector, name, fn, scope, options, order);
},
doRemoveListener: function(name, fn, scope, options, order) {
var isManaged = (scope && scope !== this && scope.isIdentifiable),
selector = this.getObservableId(),
isRemoved,
managedListeners, i, ln, listener, delegate;
if (options && options.delegate) {
delegate = options.delegate;
// See https://sencha.jira.com/browse/TOUCH-1579
selector += ' ' + delegate;
}
if (!scope) {
scope = this;
}
isRemoved = this.removeDispatcherListener(selector, name, fn, scope, order);
if (isRemoved && isManaged) {
managedListeners = this.getManagedListeners(scope, name);
for (i = 0,ln = managedListeners.length; i < ln; i++) {
listener = managedListeners[i];
if (listener.fn === fn && listener.scope === scope && listener.delegate === delegate && listener.order === order) {
managedListeners.splice(i, 1);
break;
}
}
}
return isRemoved;
},
removeDispatcherListener: function(selector, name, fn, scope, order) {
return this.getEventDispatcher().removeListener(this.observableType, selector, name, fn, scope, order);
},
clearManagedListeners: function(object) {
var managedListeners = this.managedListeners,
id, namedListeners, listeners, eventName, i, ln, listener, options;
if (!managedListeners) {
return this;
}
if (object) {
if (typeof object != 'string') {
id = object.getUniqueId();
}
else {
id = object;
}
namedListeners = managedListeners[id];
for (eventName in namedListeners) {
if (namedListeners.hasOwnProperty(eventName)) {
listeners = namedListeners[eventName];
for (i = 0,ln = listeners.length; i < ln; i++) {
listener = listeners[i];
options = {};
if (listener.delegate) {
options.delegate = listener.delegate;
}
if (this.doRemoveListener(eventName, listener.fn, listener.scope, options, listener.order)) {
i--;
ln--;
}
}
}
}
delete managedListeners[id];
return this;
}
for (id in managedListeners) {
if (managedListeners.hasOwnProperty(id)) {
this.clearManagedListeners(id);
}
}
},
/**
* @private
* @param operation
* @param eventName
* @param fn
* @param scope
* @param options
* @param order
*/
changeListener: function(actionFn, eventName, fn, scope, options, order) {
var eventNames,
listeners,
listenerOptionsRegex,
actualOptions,
name, value, i, ln, listener, valueType;
if (typeof fn != 'undefined') {
// Support for array format to add multiple listeners
if (typeof eventName != 'string') {
for (i = 0,ln = eventName.length; i < ln; i++) {
name = eventName[i];
actionFn.call(this, name, fn, scope, options, order);
}
return this;
}
actionFn.call(this, eventName, fn, scope, options, order);
}
else if (Ext.isArray(eventName)) {
listeners = eventName;
for (i = 0,ln = listeners.length; i < ln; i++) {
listener = listeners[i];
actionFn.call(this, listener.event, listener.fn, listener.scope, listener, listener.order);
}
}
else {
listenerOptionsRegex = this.listenerOptionsRegex;
options = eventName;
eventNames = [];
listeners = [];
actualOptions = {};
for (name in options) {
value = options[name];
if (name === 'scope') {
scope = value;
continue;
}
else if (name === 'order') {
order = value;
continue;
}
if (!listenerOptionsRegex.test(name)) {
valueType = typeof value;
if (valueType != 'string' && valueType != 'function') {
actionFn.call(this, name, value.fn, value.scope || scope, value, value.order || order);
continue;
}
eventNames.push(name);
listeners.push(value);
}
else {
actualOptions[name] = value;
}
}
for (i = 0,ln = eventNames.length; i < ln; i++) {
actionFn.call(this, eventNames[i], listeners[i], scope, actualOptions, order);
}
}
},
/**
* Appends an event handler to this object. You can review the available handlers by looking at the 'events'
* section of the documentation for the component you are working with.
*
* ## Combining Options
*
* Using the options argument, it is possible to combine different types of listeners:
*
* A delayed, one-time listener:
*
* container.on('tap', this.handleTap, 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:
*
* container.on({
* tap : this.onTap,
* swipe: this.onSwipe,
*
* scope: this // Important. Ensure "this" is correct during handler execution
* });
*
* One can also specify options for each event handler separately:
*
* container.on({
* tap : { fn: this.onTap, scope: this, single: true },
* swipe: { fn: button.onSwipe, scope: button }
* });
*
* See the [Events Guide](#!/guide/events) for more.
*
* @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.
*
* 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.
*
* - **order** : String
*
* The order of when the listener should be added into the listener queue.
*
* If you set an order of `before` and the event you are listening to is preventable, you can return `false` and it will stop the event.
*
* Available options are `before`, `current` and `after`. Defaults to `current`.
*
* - **buffer** : Number
*
* Causes the handler to be 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.
*
* - **delegate** : String
*
* Uses {@link Ext.ComponentQuery} to delegate events to a specified query selector within this item.
*
* // Create a container with a two children; a button and a toolbar
* var container = Ext.create('Ext.Container', {
* items: [
* {
* xtype: 'toolbar',
* docked: 'top',
* title: 'My Toolbar'
* },
* {
* xtype: 'button',
* text: 'My Button'
* }
* ]
* });
*
* container.on({
* // Ext.Buttons have an xtype of 'button', so we use that are a selector for our delegate
* delegate: 'button',
*
* tap: function() {
* alert('Button tapped!');
* }
* });
*
* @param {String} [order='current'] The order of when the listener should be added into the listener queue.
* Possible values are `before`, `current` and `after`.
*/
addListener: function(eventName, fn, scope, options, order) {
return this.changeListener(this.doAddListener, eventName, fn, scope, options, order);
},
/**
* Appends a before-event handler. Returning `false` from the handler will stop the event.
*
* Same as {@link #addListener} with `order` set to `'before'`.
*
* @param {String} eventName The name of the event to listen for.
* @param {Function} fn The method the event invokes.
* @param {Object} [scope] The scope for `fn`.
* @param {Object} [options] An object containing handler configuration.
*/
addBeforeListener: function(eventName, fn, scope, options) {
return this.addListener(eventName, fn, scope, options, 'before');
},
/**
* Appends an after-event handler.
*
* Same as {@link #addListener} with `order` set to `'after'`.
*
* @param {String} eventName The name of the event to listen for.
* @param {Function} fn The method the event invokes.
* @param {Object} [scope] The scope for `fn`.
* @param {Object} [options] An object containing handler configuration.
*/
addAfterListener: function(eventName, fn, scope, options) {
return this.addListener(eventName, fn, scope, options, 'after');
},
/**
* 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] 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.
* @param {Object} [options] Extra options object. See {@link #addListener} for details.
* @param {String} [order='current'] The order of the listener to remove.
* Possible values are `before`, `current` and `after`.
*/
removeListener: function(eventName, fn, scope, options, order) {
return this.changeListener(this.doRemoveListener, eventName, fn, scope, options, order);
},
/**
* Removes a before-event handler.
*
* Same as {@link #removeListener} with `order` set to `'before'`.
*
* @param {String} eventName The name of the event the handler was associated with.
* @param {Function} fn The handler to remove.
* @param {Object} [scope] The scope originally specified for `fn`.
* @param {Object} [options] Extra options object.
*/
removeBeforeListener: function(eventName, fn, scope, options) {
return this.removeListener(eventName, fn, scope, options, 'before');
},
/**
* Removes a before-event handler.
*
* Same as {@link #removeListener} with `order` set to `'after'`.
*
* @param {String} eventName The name of the event the handler was associated with.
* @param {Function} fn The handler to remove.
* @param {Object} [scope] The scope originally specified for `fn`.
* @param {Object} [options] Extra options object.
*/
removeAfterListener: function(eventName, fn, scope, options) {
return this.removeListener(eventName, fn, scope, options, 'after');
},
/**
* Removes all listeners for this object.
*/
clearListeners: function() {
var usedSelectors = this.getUsedSelectors(),
dispatcher = this.getEventDispatcher(),
i, ln, selector;
for (i = 0,ln = usedSelectors.length; i < ln; i++) {
selector = usedSelectors[i];
dispatcher.clearListeners(this.observableType, selector);
}
},
/**
* 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(eventName) {
return this.getEventDispatcher().hasListener(this.observableType, this.getObservableId(), eventName);
},
/**
* 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.eventFiringSuspended = true;
},
/**
* 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() {
this.eventFiringSuspended = false;
},
/**
* Relays selected events from the specified Observable as if the events were fired by <code><b>this</b></code>.
* @param {Object} object The Observable whose events this object is to relay.
* @param {String/Array/Object} events Array of event names to relay.
*/
relayEvents: function(object, events, prefix) {
var i, ln, oldName, newName;
if (typeof prefix == 'undefined') {
prefix = '';
}
if (typeof events == 'string') {
events = [events];
}
if (Ext.isArray(events)) {
for (i = 0,ln = events.length; i < ln; i++) {
oldName = events[i];
newName = prefix + oldName;
object.addListener(oldName, this.createEventRelayer(newName), this);
}
}
else {
for (oldName in events) {
if (events.hasOwnProperty(oldName)) {
newName = prefix + events[oldName];
object.addListener(oldName, this.createEventRelayer(newName), this);
}
}
}
return this;
},
/**
* @private
* @param args
* @param fn
*/
relayEvent: function(args, fn, scope, options, order) {
var fnType = typeof fn,
controller = args[args.length - 1],
eventName = controller.getInfo().eventName,
action;
args = Array.prototype.slice.call(args, 0, -2);
args[0] = this;
if (fnType != 'undefined') {
action = {
fn: fn,
scope: scope || this,
options: options || {},
order: order,
isLateBinding: fnType == 'string'
};
}
return this.doFireEvent(eventName, args, action, controller);
},
/**
* @private
* Creates an event handling function which refires the event from this object as the passed event name.
* @param newName
* @returns {Function}
*/
createEventRelayer: function(newName){
return function() {
return this.doFireEvent(newName, Array.prototype.slice.call(arguments, 0, -2));
}
},
/**
* 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.
*
* @param {String/String[]} events The event name to bubble, or an Array of event names.
*/
enableBubble: function(events) {
var isBubblingEnabled = this.isBubblingEnabled,
i, ln, name;
if (!isBubblingEnabled) {
isBubblingEnabled = this.isBubblingEnabled = {};
}
if (typeof events == 'string') {
events = Ext.Array.clone(arguments);
}
for (i = 0,ln = events.length; i < ln; i++) {
name = events[i];
if (!isBubblingEnabled[name]) {
isBubblingEnabled[name] = true;
this.addListener(name, this.createEventBubbler(name), this);
}
}
},
createEventBubbler: function(name) {
return function doBubbleEvent() {
var bubbleTarget = ('getBubbleTarget' in this) ? this.getBubbleTarget() : null;
if (bubbleTarget && bubbleTarget !== this && bubbleTarget.isObservable) {
bubbleTarget.fireAction(name, Array.prototype.slice.call(arguments, 0, -2), doBubbleEvent, bubbleTarget, null, 'after');
}
}
},
getBubbleTarget: function() {
return false;
},
destroy: function() {
if (this.observableId) {
this.fireEvent('destroy', this);
this.clearListeners();
this.clearManagedListeners();
}
},
addEvents: Ext.emptyFn
}, function() {
this.createAlias({
/**
* @method
* Alias for {@link #addListener}.
* @inheritdoc Ext.mixin.Observable#addListener
*/
on: 'addListener',
/**
* @method
* Alias for {@link #removeListener}.
* @inheritdoc Ext.mixin.Observable#removeListener
*/
un: 'removeListener',
/**
* @method
* Alias for {@link #addBeforeListener}.
* @inheritdoc Ext.mixin.Observable#addBeforeListener
*/
onBefore: 'addBeforeListener',
/**
* @method
* Alias for {@link #addAfterListener}.
* @inheritdoc Ext.mixin.Observable#addAfterListener
*/
onAfter: 'addAfterListener',
/**
* @method
* Alias for {@link #removeBeforeListener}.
* @inheritdoc Ext.mixin.Observable#removeBeforeListener
*/
unBefore: 'removeBeforeListener',
/**
* @method
* Alias for {@link #removeAfterListener}.
* @inheritdoc Ext.mixin.Observable#removeAfterListener
*/
unAfter: 'removeAfterListener'
});
});
/**
* @private
*/
Ext.define('Ext.Evented', {
alternateClassName: 'Ext.EventedBase',
mixins: ['Ext.mixin.Observable'],
statics: {
generateSetter: function(nameMap) {
var internalName = nameMap.internal,
applyName = nameMap.apply,
changeEventName = nameMap.changeEvent,
doSetName = nameMap.doSet;
return function(value) {
var initialized = this.initialized,
oldValue = this[internalName],
applier = this[applyName];
if (applier) {
value = applier.call(this, value, oldValue);
if (typeof value == 'undefined') {
return this;
}
}
if (value !== oldValue) {
if (initialized) {
this.fireAction(changeEventName, [this, value, oldValue], this.doSet, this, {
nameMap: nameMap
});
}
else {
this[internalName] = value;
this[doSetName].call(this, value, oldValue);
}
}
return this;
}
}
},
initialized: false,
constructor: function(config) {
this.initialConfig = config;
this.initialize();
},
initialize: function() {
this.initConfig(this.initialConfig);
this.initialized = true;
},
doSet: function(me, value, oldValue, options) {
var nameMap = options.nameMap;
me[nameMap.internal] = value;
me[nameMap.doSet].call(this, value, oldValue);
},
onClassExtended: function(Class, data) {
if (!data.hasOwnProperty('eventedConfig')) {
return;
}
var ExtClass = Ext.Class,
config = data.config,
eventedConfig = data.eventedConfig,
name, nameMap;
data.config = (config) ? Ext.applyIf(config, eventedConfig) : eventedConfig;
/*
* These are generated setters for eventedConfig
*
* If the component is initialized, it invokes fireAction to fire the event as well,
* which indicate something has changed. Otherwise, it just executes the action
* (happens during initialization)
*
* This is helpful when we only want the event to be fired for subsequent changes.
* Also it's a major performance improvement for instantiation when fired events
* are mostly useless since there's no listeners
*/
for (name in eventedConfig) {
if (eventedConfig.hasOwnProperty(name)) {
nameMap = ExtClass.getConfigNameMap(name);
data[nameMap.set] = this.generateSetter(nameMap);
}
}
}
});
/**
* @private
* This is the abstract class for {@link Ext.Component}.
*
* This should never be overriden.
*/
Ext.define('Ext.AbstractComponent', {
extend: 'Ext.Evented',
onClassExtended: function(Class, members) {
if (!members.hasOwnProperty('cachedConfig')) {
return;
}
var prototype = Class.prototype,
config = members.config,
cachedConfig = members.cachedConfig,
cachedConfigList = prototype.cachedConfigList,
hasCachedConfig = prototype.hasCachedConfig,
name, value;
delete members.cachedConfig;
prototype.cachedConfigList = cachedConfigList = (cachedConfigList) ? cachedConfigList.slice() : [];
prototype.hasCachedConfig = hasCachedConfig = (hasCachedConfig) ? Ext.Object.chain(hasCachedConfig) : {};
if (!config) {
members.config = config = {};
}
for (name in cachedConfig) {
if (cachedConfig.hasOwnProperty(name)) {
value = cachedConfig[name];
if (!hasCachedConfig[name]) {
hasCachedConfig[name] = true;
cachedConfigList.push(name);
}
config[name] = value;
}
}
},
getElementConfig: Ext.emptyFn,
referenceAttributeName: 'reference',
referenceSelector: '[reference]',
/**
* @private
* Significantly improve instantiation time for Component with multiple references
* Ext.Element instance of the reference domNode is only created the very first time
* it's ever used
*/
addReferenceNode: function(name, domNode) {
Ext.Object.defineProperty(this, name, {
get: function() {
var reference;
delete this[name];
this[name] = reference = new Ext.Element(domNode);
return reference;
},
configurable: true
});
},
initElement: function() {
var prototype = this.self.prototype,
id = this.getId(),
referenceList = [],
cleanAttributes = true,
referenceAttributeName = this.referenceAttributeName,
needsOptimization = false,
renderTemplate, renderElement, element,
referenceNodes, i, ln, referenceNode, reference,
configNameCache, defaultConfig, cachedConfigList, initConfigList, initConfigMap, configList,
elements, name, nameMap, internalName;
if (prototype.hasOwnProperty('renderTemplate')) {
renderTemplate = this.renderTemplate.cloneNode(true);
renderElement = renderTemplate.firstChild;
}
else {
cleanAttributes = false;
needsOptimization = true;
renderTemplate = document.createDocumentFragment();
renderElement = Ext.Element.create(this.getElementConfig(), true);
renderTemplate.appendChild(renderElement);
}
referenceNodes = renderTemplate.querySelectorAll(this.referenceSelector);
for (i = 0,ln = referenceNodes.length; i < ln; i++) {
referenceNode = referenceNodes[i];
reference = referenceNode.getAttribute(referenceAttributeName);
if (cleanAttributes) {
referenceNode.removeAttribute(referenceAttributeName);
}
if (reference == 'element') {
referenceNode.id = id;
this.element = element = new Ext.Element(referenceNode);
}
else {
this.addReferenceNode(reference, referenceNode);
}
referenceList.push(reference);
}
this.referenceList = referenceList;
if (!this.innerElement) {
this.innerElement = element;
}
if (renderElement === element.dom) {
this.renderElement = element;
}
else {
this.addReferenceNode('renderElement', renderElement);
}
// This happens only *once* per class, during the very first instantiation
// to optimize renderTemplate based on cachedConfig
if (needsOptimization) {
configNameCache = Ext.Class.configNameCache;
defaultConfig = this.config;
cachedConfigList = this.cachedConfigList;
initConfigList = this.initConfigList;
initConfigMap = this.initConfigMap;
configList = [];
for (i = 0,ln = cachedConfigList.length; i < ln; i++) {
name = cachedConfigList[i];
nameMap = configNameCache[name];
if (initConfigMap[name]) {
initConfigMap[name] = false;
Ext.Array.remove(initConfigList, name);
}
if (defaultConfig[name] !== null) {
configList.push(name);
this[nameMap.get] = this[nameMap.initGet];
}
}
for (i = 0,ln = configList.length; i < ln; i++) {
name = configList[i];
nameMap = configNameCache[name];
internalName = nameMap.internal;
this[internalName] = null;
this[nameMap.set].call(this, defaultConfig[name]);
delete this[nameMap.get];
prototype[internalName] = this[internalName];
}
renderElement = this.renderElement.dom;
prototype.renderTemplate = renderTemplate = document.createDocumentFragment();
renderTemplate.appendChild(renderElement.cloneNode(true));
elements = renderTemplate.querySelectorAll('[id]');
for (i = 0,ln = elements.length; i < ln; i++) {
element = elements[i];
element.removeAttribute('id');
}
for (i = 0,ln = referenceList.length; i < ln; i++) {
reference = referenceList[i];
this[reference].dom.removeAttribute('reference');
}
}
return this;
}
});
/**
* @author Ed Spencer
*
* @aside guide controllers
* @aside guide apps_intro
* @aside guide history_support
* @aside video mvc-part-1
* @aside video mvc-part-2
*
* Controllers are responsible for responding to events that occur within your app. If your app contains a Logout
* {@link Ext.Button button} that your user can tap on, a Controller would listen to the Button's tap event and take
* the appropriate action. It allows the View classes to handle the display of data and the Model classes to handle the
* loading and saving of data - the Controller is the glue that binds them together.
*
* ## Relation to Ext.app.Application
*
* Controllers exist within the context of an {@link Ext.app.Application Application}. An Application usually consists
* of a number of Controllers, each of which handle a specific part of the app. For example, an Application that
* handles the orders for an online shopping site might have controllers for Orders, Customers and Products.
*
* All of the Controllers that an Application uses are specified in the Application's
* {@link Ext.app.Application#controllers} config. The Application automatically instantiates each Controller and keeps
* references to each, so it is unusual to need to instantiate Controllers directly. By convention each Controller is
* named after the thing (usually the Model) that it deals with primarily, usually in the plural - for example if your
* app is called 'MyApp' and you have a Controller that manages Products, convention is to create a
* MyApp.controller.Products class in the file app/controller/Products.js.
*
* ## Refs and Control
*
* The centerpiece of Controllers is the twin configurations {@link #refs} and {@link #cfg-control}. These are used to
* easily gain references to Components inside your app and to take action on them based on events that they fire.
* Let's look at {@link #refs} first:
*
* ### Refs
*
* Refs leverage the powerful {@link Ext.ComponentQuery ComponentQuery} syntax to easily locate Components on your
* page. We can define as many refs as we like for each Controller, for example here we define a ref called 'nav' that
* finds a Component on the page with the ID 'mainNav'. We then use that ref in the addLogoutButton beneath it:
*
* Ext.define('MyApp.controller.Main', {
* extend: 'Ext.app.Controller',
*
* config: {
* refs: {
* nav: '#mainNav'
* }
* },
*
* addLogoutButton: function() {
* this.getNav().add({
* text: 'Logout'
* });
* }
* });
*
* Usually, a ref is just a key/value pair - the key ('nav' in this case) is the name of the reference that will be
* generated, the value ('#mainNav' in this case) is the {@link Ext.ComponentQuery ComponentQuery} selector that will
* be used to find the Component.
*
* Underneath that, we have created a simple function called addLogoutButton which uses this ref via its generated
* 'getNav' function. These getter functions are generated based on the refs you define and always follow the same
* format - 'get' followed by the capitalized ref name. In this case we're treating the nav reference as though it's a
* {@link Ext.Toolbar Toolbar}, and adding a Logout button to it when our function is called. This ref would recognize
* a Toolbar like this:
*
* Ext.create('Ext.Toolbar', {
* id: 'mainNav',
*
* items: [
* {
* text: 'Some Button'
* }
* ]
* });
*
* Assuming this Toolbar has already been created by the time we run our 'addLogoutButton' function (we'll see how that
* is invoked later), it will get the second button added to it.
*
* ### Advanced Refs
*
* Refs can also be passed a couple of additional options, beyond name and selector. These are autoCreate and xtype,
* which are almost always used together:
*
* Ext.define('MyApp.controller.Main', {
* extend: 'Ext.app.Controller',
*
* config: {
* refs: {
* nav: '#mainNav',
*
* infoPanel: {
* selector: 'tabpanel panel[name=fish] infopanel',
* xtype: 'infopanel',
* autoCreate: true
* }
* }
* }
* });
*
* We've added a second ref to our Controller. Again the name is the key, 'infoPanel' in this case, but this time we've
* passed an object as the value instead. This time we've used a slightly more complex selector query - in this example
* imagine that your app contains a {@link Ext.tab.Panel tab panel} and that one of the items in the tab panel has been
* given the name 'fish'. Our selector matches any Component with the xtype 'infopanel' inside that tab panel item.
*
* The difference here is that if that infopanel does not exist already inside the 'fish' panel, it will be
* automatically created when you call this.getInfoPanel inside your Controller. The Controller is able to do this
* because we provided the xtype to instantiate with in the event that the selector did not return anything.
*
* ### Control
*
* The sister config to {@link #refs} is {@link #cfg-control}. {@link #cfg-control Control} is the means by which your listen
* to events fired by Components and have your Controller react in some way. Control accepts both ComponentQuery
* selectors and refs as its keys, and listener objects as values - for example:
*
* Ext.define('MyApp.controller.Main', {
* extend: 'Ext.app.Controller',
*
* config: {
* control: {
* loginButton: {
* tap: 'doLogin'
* },
* 'button[action=logout]': {
* tap: 'doLogout'
* }
* },
*
* refs: {
* loginButton: 'button[action=login]'
* }
* },
*
* doLogin: function() {
* //called whenever the Login button is tapped
* },
*
* doLogout: function() {
* //called whenever any Button with action=logout is tapped
* }
* });
*
* Here we have set up two control declarations - one for our loginButton ref and the other for any Button on the page
* that has been given the action 'logout'. For each declaration we passed in a single event handler - in each case
* listening for the 'tap' event, specifying the action that should be called when that Button fires the tap event.
* Note that we specified the 'doLogin' and 'doLogout' methods as strings inside the control block - this is important.
*
* You can listen to as many events as you like in each control declaration, and mix and match ComponentQuery selectors
* and refs as the keys.
*
* ## Routes
*
* As of Sencha Touch 2, Controllers can now directly specify which routes they are interested in. This enables us to
* provide history support within our app, as well as the ability to deeply link to any part of the application that we
* provide a route for.
*
* For example, let's say we have a Controller responsible for logging in and viewing user profiles, and want to make
* those screens accessible via urls. We could achieve that like this:
*
* Ext.define('MyApp.controller.Users', {
* extend: 'Ext.app.Controller',
*
* config: {
* routes: {
* 'login': 'showLogin',
* 'user/:id': 'showUserById'
* },
*
* refs: {
* main: '#mainTabPanel'
* }
* },
*
* //uses our 'main' ref above to add a loginpanel to our main TabPanel (note that
* //'loginpanel' is a custom xtype created for this application)
* showLogin: function() {
* this.getMain().add({
* xtype: 'loginpanel'
* });
* },
*
* //Loads the User then adds a 'userprofile' view to the main TabPanel
* showUserById: function(id) {
* MyApp.model.User.load(id, {
* scope: this,
* success: function(user) {
* this.getMain().add({
* xtype: 'userprofile',
* user: user
* });
* }
* });
* }
* });
*
* The routes we specified above simply map the contents of the browser address bar to a Controller function to call
* when that route is matched. The routes can be simple text like the login route, which matches against
* http://myapp.com/#login, or contain wildcards like the 'user/:id' route, which matches urls like
* http://myapp.com/#user/123. Whenever the address changes the Controller automatically calls the function specified.
*
* Note that in the showUserById function we had to first load the User instance. Whenever you use a route, the
* function that is called by that route is completely responsible for loading its data and restoring state. This is
* because your user could either send that url to another person or simply refresh the page, which we wipe clear any
* cached data you had already loaded. There is a more thorough discussion of restoring state with routes in the
* application architecture guides.
*
* ## Advanced Usage
*
* See <a href="#!/guide/controllers">the Controllers guide</a> for advanced Controller usage including before filters
* and customizing for different devices.
*/
Ext.define('Ext.app.Controller', {
mixins: {
observable: "Ext.mixin.Observable"
},
config: {
/**
* @cfg {Object} refs A collection of named {@link Ext.ComponentQuery ComponentQuery} selectors that makes it
* easy to get references to key Components on your page. Example usage:
*
* refs: {
* main: '#mainTabPanel',
* loginButton: '#loginWindow button[action=login]',
*
* infoPanel: {
* selector: 'infopanel',
* xtype: 'infopanel',
* autoCreate: true
* }
* }
*
* The first two are simple ComponentQuery selectors, the third (infoPanel) also passes in the autoCreate and
* xtype options, which will first run the ComponentQuery to see if a Component matching that selector exists
* on the page. If not, it will automatically create one using the xtype provided:
*
* someControllerFunction: function() {
* //if the info panel didn't exist before, calling its getter will instantiate
* //it automatically and return the new instance
* this.getInfoPanel().show();
* }
*
* @accessor
*/
refs: {},
/**
* @cfg {Object} routes Provides a mapping of urls to Controller actions. Whenever the specified url is matched
* in the address bar, the specified Controller action is called. Example usage:
*
* routes: {
* 'login': 'showLogin',
* 'users/:id': 'showUserById'
* }
*
* The first route will match against http://myapp.com/#login and call the Controller's showLogin function. The
* second route contains a wildcard (':id') and will match all urls like http://myapp.com/#users/123, calling
* the showUserById function with the matched ID as the first argument.
*
* @accessor
*/
routes: {},
/**
* @cfg {Object} control Provides a mapping of Controller functions that should be called whenever certain
* Component events are fired. The Components can be specified using {@link Ext.ComponentQuery ComponentQuery}
* selectors or {@link #refs}. Example usage:
*
* control: {
* 'button[action=logout]': {
* tap: 'doLogout'
* },
* main: {
* activeitemchange: 'doUpdate'
* }
* }
*
* The first item uses a ComponentQuery selector to run the Controller's doLogout function whenever any Button
* with action=logout is tapped on. The second calls the Controller's doUpdate function whenever the
* activeitemchange event is fired by the Component referenced by our 'main' ref. In this case main is a tab
* panel (see {@link #refs} for how to set that reference up).
*
* @accessor
*/
control: {},
/**
* @cfg {Object} before Provides a mapping of Controller functions to filter functions that are run before them
* when dispatched to from a route. These are usually used to run pre-processing functions like authentication
* before a certain function is executed. They are only called when dispatching from a route. Example usage:
*
* Ext.define('MyApp.controller.Products', {
* config: {
* before: {
* editProduct: 'authenticate'
* },
*
* routes: {
* 'product/edit/:id': 'editProduct'
* }
* },
*
* //this is not directly because our before filter is called first
* editProduct: function() {
* //... performs the product editing logic
* },
*
* //this is run before editProduct
* authenticate: function(action) {
* MyApp.authenticate({
* success: function() {
* action.resume();
* },
* failure: function() {
* Ext.Msg.alert('Not Logged In', "You can't do that, you're not logged in");
* }
* });
* }
* });
*
* @accessor
*/
before: {},
/**
* @cfg {Ext.app.Application} application The Application instance this Controller is attached to. This is
* automatically provided when using the MVC architecture so should rarely need to be set directly.
* @accessor
*/
application: {},
/**
* @cfg {Array} stores The set of stores to load for this Application. Each store is expected to
* exist inside the *app/store* directory and define a class following the convention
* AppName.store.StoreName. For example, in the code below, the *AppName.store.Users* class will be loaded.
* Note that we are able to specify either the full class name (as with *AppName.store.Groups*) or just the
* final part of the class name and leave Application to automatically prepend *AppName.store.’* to each:
*
* stores: [
* 'Users',
* 'AppName.store.Groups',
* 'SomeCustomNamespace.store.Orders'
* ]
* @accessor
*/
stores: [],
/**
* @cfg {Array} models The set of models to load for this Application. Each model is expected to exist inside the
* *app/model* directory and define a class following the convention AppName.model.ModelName. For example, in the
* code below, the classes *AppName.model.User*, *AppName.model.Group* and *AppName.model.Product* will be loaded.
* Note that we are able to specify either the full class name (as with *AppName.model.Product*) or just the
* final part of the class name and leave Application to automatically prepend *AppName.model.* to each:
*
* models: [
* 'User',
* 'Group',
* 'AppName.model.Product',
* 'SomeCustomNamespace.model.Order'
* ]
* @accessor
*/
models: [],
/**
* @cfg {Array} views The set of views to load for this Application. Each view is expected to exist inside the
* *app/view* directory and define a class following the convention AppName.view.ViewName. For example, in the
* code below, the classes *AppName.view.Users*, *AppName.view.Groups* and *AppName.view.Products* will be loaded.
* Note that we are able to specify either the full class name (as with *AppName.view.Products*) or just the
* final part of the class name and leave Application to automatically prepend *AppName.view.* to each:
*
* views: [
* 'Users',
* 'Groups',
* 'AppName.view.Products',
* 'SomeCustomNamespace.view.Orders'
* ]
* @accessor
*/
views: []
},
/**
* Constructs a new Controller instance
*/
constructor: function(config) {
this.initConfig(config);
this.mixins.observable.constructor.call(this, config);
},
/**
* @cfg
* Called by the Controller's {@link #application} to initialize the Controller. This is always called before the
* {@link Ext.app.Application Application} launches, giving the Controller a chance to run any pre-launch logic.
* See also {@link #launch}, which is called after the {@link Ext.app.Application#launch Application's launch function}
*/
init: Ext.emptyFn,
/**
* @cfg
* Called by the Controller's {@link #application} immediately after the Application's own
* {@link Ext.app.Application#launch launch function} has been called. This is usually a good place to run any
* logic that has to run after the app UI is initialized. See also {@link #init}, which is called before the
* {@link Ext.app.Application#launch Application's launch function}.
*/
launch: Ext.emptyFn,
/**
* Convenient way to redirect to a new url. See {@link Ext.app.Application#redirectTo} for full usage information
*/
redirectTo: function(place) {
return this.getApplication().redirectTo(place);
},
/**
* @private
* Executes an Ext.app.Action by giving it the correct before filters and kicking off execution
*/
execute: function(action, skipFilters) {
action.setBeforeFilters(this.getBefore()[action.getAction()]);
action.execute();
},
/**
* @private
* Massages the before filters into an array of function references for each controller action
*/
applyBefore: function(before) {
var filters, name, length, i;
for (name in before) {
filters = Ext.Array.from(before[name]);
length = filters.length;
for (i = 0; i < length; i++) {
filters[i] = this[filters[i]];
}
before[name] = filters;
}
return before;
},
/**
* @private
*/
applyControl: function(config) {
this.control(config, this);
return config;
},
/**
* @private
*/
applyRefs: function(refs) {
if (Ext.isArray(refs)) {
Ext.Logger.deprecate("In Sencha Touch 2 the refs config accepts an object but you have passed it an array.");
}
this.ref(refs);
return refs;
},
/**
* @private
* Adds any routes specified in this Controller to the global Application router
*/
applyRoutes: function(routes) {
var app = this instanceof Ext.app.Application ? this : this.getApplication(),
router = app.getRouter(),
route, url, config;
for (url in routes) {
route = routes[url];
config = {
controller: this.$className
};
if (Ext.isString(route)) {
config.action = route;
} else {
Ext.apply(config, route);
}
router.connect(url, config);
}
return routes;
},
/**
* @private
* As a convenience developers can locally qualify store names (e.g. 'MyStore' vs
* 'MyApp.store.MyStore'). This just makes sure everything ends up fully qualified
*/
applyStores: function(stores) {
return this.getFullyQualified(stores, 'store');
},
/**
* @private
* As a convenience developers can locally qualify model names (e.g. 'MyModel' vs
* 'MyApp.model.MyModel'). This just makes sure everything ends up fully qualified
*/
applyModels: function(models) {
return this.getFullyQualified(models, 'model');
},
/**
* @private
* As a convenience developers can locally qualify view names (e.g. 'MyView' vs
* 'MyApp.view.MyView'). This just makes sure everything ends up fully qualified
*/
applyViews: function(views) {
return this.getFullyQualified(views, 'view');
},
/**
* @private
* Returns the fully qualified name for any class name variant. This is used to find the FQ name for the model,
* view, controller, store and profiles listed in a Controller or Application.
* @param {String[]} items The array of strings to get the FQ name for
* @param {String} namespace If the name happens to be an application class, add it to this namespace
* @return {String} The fully-qualified name of the class
*/
getFullyQualified: function(items, namespace) {
var length = items.length,
appName = this.getApplication().getName(),
name, i;
for (i = 0; i < length; i++) {
name = items[i];
//we check name === appName to allow MyApp.profile.MyApp to exist
if (Ext.isString(name) && (Ext.Loader.getPrefix(name) === "" || name === appName)) {
items[i] = appName + '.' + namespace + '.' + name;
}
}
return items;
},
/**
* @private
*/
control: function(selectors) {
this.getApplication().control(selectors, this);
},
/**
* @private
* 1.x-inspired ref implementation
*/
ref: function(refs) {
var refName, getterName, selector, info;
for (refName in refs) {
selector = refs[refName];
getterName = "get" + Ext.String.capitalize(refName);
if (!this[getterName]) {
if (Ext.isString(refs[refName])) {
info = {
ref: refName,
selector: selector
};
} else {
info = refs[refName];
}
this[getterName] = Ext.Function.pass(this.getRef, [refName, info], this);
}
this.references = this.references || [];
this.references.push(refName.toLowerCase());
}
},
/**
* @private
*/
getRef: function(ref, info, config) {
this.refCache = this.refCache || {};
info = info || {};
config = config || {};
Ext.apply(info, config);
if (info.forceCreate) {
return Ext.ComponentManager.create(info, 'component');
}
var me = this,
cached = me.refCache[ref];
if (!cached) {
me.refCache[ref] = cached = Ext.ComponentQuery.query(info.selector)[0];
if (!cached && info.autoCreate) {
me.refCache[ref] = cached = Ext.ComponentManager.create(info, 'component');
}
if (cached) {
cached.on('destroy', function() {
me.refCache[ref] = null;
});
}
}
return cached;
},
/**
* @private
*/
hasRef: function(ref) {
return this.references && this.references.indexOf(ref.toLowerCase()) !== -1;
},
}, function() {
});
/**
* @author Ed Spencer
* @private
*
* Manages the stack of {@link Ext.app.Action} instances that have been decoded, pushes new urls into the browser's
* location object and listens for changes in url, firing the {@link #change} event when a change is detected.
*
* This is tied to an {@link Ext.app.Application Application} instance. The Application performs all of the
* interactions with the History object, no additional integration should be required.
*/
Ext.define('Ext.app.History', {
mixins: ['Ext.mixin.Observable'],
/**
* @event change
* Fires when a change in browser url is detected
* @param {String} url The new url, after the hash (e.g. http://myapp.com/#someUrl returns 'someUrl')
*/
config: {
/**
* @cfg {Array} actions The stack of {@link Ext.app.Action action} instances that have occured so far
*/
actions: [],
/**
* @cfg {Boolean} updateUrl True to automatically update the browser's url when {@link #add} is called
*/
updateUrl: true,
/**
* @cfg {String} token The current token as read from the browser's location object
*/
token: ''
},
constructor: function(config) {
if (Ext.feature.has.History) {
window.addEventListener('hashchange', Ext.bind(this.detectStateChange, this));
}
else {
setInterval(Ext.bind(this.detectStateChange, this), 50);
}
this.initConfig(config);
},
/**
* Adds an {@link Ext.app.Action Action} to the stack, optionally updating the browser's url and firing the
* {@link #change} event.
* @param {Ext.app.Action} action The Action to add to the stack
* @param {Boolean} silent Cancels the firing of the {@link #change} event if true
*/
add: function(action, silent) {
this.getActions().push(Ext.factory(action, Ext.app.Action));
var url = action.getUrl();
if (this.getUpdateUrl()) {
// history.pushState({}, action.getTitle(), "#" + action.getUrl());
this.setToken(url);
window.location.hash = url;
}
if (silent !== true) {
this.fireEvent('change', url);
}
this.setToken(url);
},
/**
* @private
*/
back: function() {
this.getActions().pop().run();
},
/**
* @private
*/
applyToken: function(token) {
return token[0] == '#' ? token.substr(1) : token;
},
/**
* @private
*/
detectStateChange: function() {
var newToken = this.applyToken(window.location.hash),
oldToken = this.getToken();
if (newToken != oldToken) {
this.onStateChange();
this.setToken(newToken);
}
},
/**
* @private
*/
onStateChange: function() {
this.fireEvent('change', window.location.hash.substr(1));
}
});
/**
* @author Ed Spencer
*
* A Profile represents a range of devices that fall under a common category. For the vast majority of apps that use
* device profiles, the app defines a Phone profile and a Tablet profile. Doing this enables you to easily customize
* the experience for the different sized screens offered by those device types.
*
* Only one Profile can be active at a time, and each Profile defines a simple {@link #isActive} function that should
* return either true or false. The first Profile to return true from its isActive function is set as your Application's
* {@link Ext.app.Application#currentProfile current profile}.
*
* A Profile can define any number of {@link #models}, {@link #views}, {@link #controllers} and {@link #stores} which
* will be loaded if the Profile is activated. It can also define a {@link #launch} function that will be called after
* all of its dependencies have been loaded, just before the {@link Ext.app.Application#launch application launch}
* function is called.
*
* ## Sample Usage
*
* First you need to tell your Application about your Profile(s):
*
* Ext.application({
* name: 'MyApp',
* profiles: ['Phone', 'Tablet']
* });
*
* This will load app/profile/Phone.js and app/profile/Tablet.js. Here's how we might define the Phone profile:
*
* Ext.define('MyApp.profile.Phone', {
* extend: 'Ext.app.Profile',
*
* views: ['Main'],
*
* isActive: function() {
* return Ext.os.is.Phone;
* }
* });
*
* The isActive function returns true if we detect that we are running on a phone device. If that is the case the
* Application will set this Profile active and load the 'Main' view specified in the Profile's {@link #views} config.
*
* ## Class Specializations
*
* Because Profiles are specializations of an application, all of the models, views, controllers and stores defined
* in a Profile are expected to be namespaced under the name of the Profile. Here's an expanded form of the example
* above:
*
* Ext.define('MyApp.profile.Phone', {
* extend: 'Ext.app.Profile',
*
* views: ['Main'],
* controllers: ['Signup'],
* models: ['MyApp.model.Group'],
*
* isActive: function() {
* return Ext.os.is.Phone;
* }
* });
*
* In this case, the Profile is going to load *app/view/phone/Main.js*, *app/controller/phone/Signup.js* and
* *app/model/Group.js*. Notice that in each of the first two cases the name of the profile ('phone' in this case) was
* injected into the class names. In the third case we specified the full Model name (for Group) so the Profile name
* was not injected.
*
* For a fuller understanding of the ideas behind Profiles and how best to use them in your app, we suggest you read
* the <a href="#!/guide/profiles">device profiles guide</a>.
*
* @aside guide profiles
*/
Ext.define('Ext.app.Profile', {
mixins: {
observable: "Ext.mixin.Observable"
},
config: {
/**
* @cfg {String} namespace The namespace that this Profile's classes can be found in. Defaults to the lowercased
* Profile {@link #name}, for example a Profile called MyApp.profile.Phone will by default have a 'phone'
* namespace, which means that this Profile's additional models, stores, views and controllers will be loaded
* from the MyApp.model.phone.*, MyApp.store.phone.*, MyApp.view.phone.* and MyApp.controller.phone.* namespaces
* respectively.
* @accessor
*/
namespace: 'auto',
/**
* @cfg {String} name The name of this Profile. Defaults to the last section of the class name (e.g. a profile
* called MyApp.profile.Phone will default the name to 'Phone').
* @accessor
*/
name: 'auto',
/**
* @cfg {Array} controllers Any additional {@link Ext.app.Application#controllers Controllers} to load for this
* profile. Note that each item here will be prepended with the Profile namespace when loaded. Example usage:
*
* controllers: [
* 'Users',
* 'MyApp.controller.Products'
* ]
*
* This will load *MyApp.controller.tablet.Users* and *MyApp.controller.Products*.
* @accessor
*/
controllers: [],
/**
* @cfg {Array} models Any additional {@link Ext.app.Application#models Models} to load for this profile. Note
* that each item here will be prepended with the Profile namespace when loaded. Example usage:
*
* models: [
* 'Group',
* 'MyApp.model.User'
* ]
*
* This will load *MyApp.model.tablet.Group* and *MyApp.model.User*.
* @accessor
*/
models: [],
/**
* @cfg {Array} views Any additional {@link Ext.app.Application#views views} to load for this profile. Note
* that each item here will be prepended with the Profile namespace when loaded. Example usage:
*
* views: [
* 'Main',
* 'MyApp.view.Login'
* ]
*
* This will load *MyApp.view.tablet.Main* and *MyApp.view.Login*.
* @accessor
*/
views: [],
/**
* @cfg {Array} stores Any additional {@link Ext.app.Application#stores Stores} to load for this profile. Note
* that each item here will be prepended with the Profile namespace when loaded. Example usage:
*
* stores: [
* 'Users',
* 'MyApp.store.Products'
* ]
*
* This will load *MyApp.store.tablet.Users* and *MyApp.store.Products*.
* @accessor
*/
stores: [],
/**
* @cfg {Ext.app.Application} application The {@link Ext.app.Application Application} instance that this
* Profile is bound to. This is set automatically. Read only.
* @accessor
*/
application: null
},
/**
* Creates a new Profile instance
*/
constructor: function(config) {
this.initConfig(config);
this.mixins.observable.constructor.apply(this, arguments);
},
/**
* Determines whether or not this Profile is active on the device isActive is executed on. Should return true if
* this profile is meant to be active on this device, false otherwise. Each Profile should implement this function
* (the default implementation just returns false).
* @return {Boolean} True if this Profile should be activated on the device it is running on, false otherwise
*/
isActive: function() {
return false;
},
/**
* @method
* The launch function is called by the {@link Ext.app.Application Application} if this Profile's {@link #isActive}
* function returned true. This is typically the best place to run any profile-specific app launch code. Example
* usage:
*
* launch: function() {
* Ext.create('MyApp.view.tablet.Main');
* }
*/
launch: Ext.emptyFn,
/**
* @private
*/
applyNamespace: function(name) {
if (name == 'auto') {
name = this.getName();
}
return name.toLowerCase();
},
/**
* @private
*/
applyName: function(name) {
if (name == 'auto') {
var pieces = this.$className.split('.');
name = pieces[pieces.length - 1];
}
return name;
},
/**
* @private
* Computes the full class names of any specified model, view, controller and store dependencies, returns them in
* an object map for easy loading
*/
getDependencies: function() {
var allClasses = [],
format = Ext.String.format,
appName = this.getApplication().getName(),
namespace = this.getNamespace(),
map = {
model: this.getModels(),
view: this.getViews(),
controller: this.getControllers(),
store: this.getStores()
},
classType, classNames, fullyQualified;
for (classType in map) {
classNames = [];
Ext.each(map[classType], function(className) {
if (Ext.isString(className)) {
//we check name === appName to allow MyApp.profile.MyApp to exist
if (Ext.isString(className) && (Ext.Loader.getPrefix(className) === "" || className === appName)) {
className = appName + '.' + classType + '.' + namespace + '.' + className;
}
classNames.push(className);
allClasses.push(className);
}
}, this);
map[classType] = classNames;
}
map.all = allClasses;
return map;
}
});
/**
* @author Ed Spencer
*
* @aside guide apps_intro
* @aside guide first_app
* @aside video mvc-part-1
* @aside video mvc-part-2
*
* Ext.app.Application defines the set of {@link Ext.data.Model Models}, {@link Ext.app.Controller Controllers},
* {@link Ext.app.Profile Profiles}, {@link Ext.data.Store Stores} and {@link Ext.Component Views} that an application
* consists of. It automatically loads all of those dependencies and can optionally specify a {@link #launch} function
* that will be called when everthing is ready.
*
* Sample usage:
*
* Ext.application({
* name: 'MyApp',
*
* models: ['User', 'Group'],
* stores: ['Users'],
* controllers: ['Users'],
* views: ['Main', 'ShowUser'],
*
* launch: function() {
* Ext.create('MyApp.view.Main');
* }
* });
*
* Creating an Application instance is the only time in Sencha Touch 2 that we don't use Ext.create to create the new
* instance. Instead, the {@link Ext#application} function instantiates an Ext.app.Application internally,
* automatically loading the Ext.app.Application class if it is not present on the page already and hooking in to
* {@link Ext#onReady} before creating the instance itself. An alternative is to use Ext.create inside an Ext.onReady
* callback, but Ext.application is preferred.
*
* ## Dependencies
*
* Application follows a simple convention when it comes to specifying the controllers, views, models, stores and
* profiles it requires. By default it expects each of them to be found inside the *app/controller*, *app/view*,
* *app/model*, *app/store* and *app/profile* directories in your app - if you follow this convention you can just
* specify the last part of each class name and Application will figure out the rest for you:
*
* Ext.application({
* name: 'MyApp',
*
* controllers: ['Users'],
* models: ['User', 'Group'],
* stores: ['Users'],
* views: ['Main', 'ShowUser']
* });
*
* The example above will load 6 files:
*
* * app/model/User.js
* * app/model/Group.js
* * app/store/Users.js
* * app/controller/Users.js
* * app/view/Main.js
* * app/view/ShowUser.js
*
* ### Nested Dependencies
*
* For larger apps it's common to split the models, views and controllers into subfolders so keep the project
* organized. This is especially true of views - it's not unheard of for large apps to have over a hundred separate
* view classes so organizing them into folders can make maintenance much simpler.
*
* To specify dependencies in subfolders just use a period (".") to specify the folder:
*
* Ext.application({
* name: 'MyApp',
*
* controllers: ['Users', 'nested.MyController'],
* views: ['products.Show', 'products.Edit', 'user.Login']
* });
*
* In this case these 5 files will be loaded:
*
* * app/controller/Users.js
* * app/controller/nested/MyController.js
* * app/view/products/Show.js
* * app/view/products/Edit.js
* * app/view/user/Login.js
*
* Note that we can mix and match within each configuration here - for each model, view, controller, profile or store
* you can specify either just the final part of the class name (if you follow the directory conventions), or the full
* class name.
*
* ### External Dependencies
*
* Finally, we can specify application dependencies from outside our application by fully-qualifying the classes we
* want to load. A common use case for this is sharing authentication logic between multiple applications. Perhaps you
* have several apps that login via a common user database and you want to share that code between them. An easy way to
* do this is to create a folder alongside your app folder and then add its contents as dependencies for your app.
*
* For example, let's say our shared login code contains a login controller, a user model and a login form view. We
* want to use all of these in our application:
*
* Ext.Loader.setPath({
* 'Auth': 'Auth'
* });
*
* Ext.application({
* views: ['Auth.view.LoginForm', 'Welcome'],
* controllers: ['Auth.controller.Sessions', 'Main'],
* models: ['Auth.model.User']
* });
*
* This will load the following files:
*
* * Auth/view/LoginForm.js
* * Auth/controller/Sessions.js
* * Auth/model/User.js
* * app/view/Welcome.js
* * app/controller/Main.js
*
* The first three were loaded from outside our application, the last two from the application itself. Note how we can
* still mix and match application files and external
View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment